import { LanguageResolver , LanguageTag , Locale } from '@phensley/cldr-core' ; import { getOther } from '../cldr' ; import { RBNFPacker } from './rbnf' ; import { RBNFCollector } from '../rbnf' ; const defaultContent = new Set ( ) ; const loadDefaultContent = ( ) => getOther ( ) . DefaultContent . forEach ( ( s ) => { const { tag } = Locale . resolve ( s ) ; defaultContent . add ( tag . expanded ( ) ) ; } ) ; const DELIMITER = '_' ; class Layer { readonly localeId : string ; readonly strings : string [ ] = [ ] ; readonly index : number [ ] = [ ] ; constructor ( readonly tag , readonly isDefault ) { this . localeId = tag . expanded ( ) ; } } const RE_QUOTE = /"/g ; /**
 * Join an array of strings together, escape double quotes, and UTF-8
 * encode the result.
 */ const join = ( strings ) : string => strings . join ( DELIMITER ) . replace ( RE_QUOTE , '"' ) ; /**
 * Builds a resource pack containing all strings across all regions for a
 * given language.
 *
 * The resulting format attempts to balance compactness with efficiency of
 * the decoding and string lookup at runtime.
 */ export class ResourcePack { private layers : { [ x ] : Layer [ ] ; } = { } ; private current ! : Layer ; private defaultLayer ? : LanguageTag ; private spellout : string = '{}' ; constructor ( private language , private version , private cldrVersion , rbnf ? ) { if ( rbnf ) { const packer = new RBNFPacker ( rbnf ) ; this . spellout = packer . pack ( language ) ; } } /**
     * Push a new language layer.
     */ push ( locale ) { if ( defaultContent . size === 0 ) { loadDefaultContent ( ) ; } const { tag } = locale ; const script = tag . script ( ) ; let layers = this . layers [ script ] ; if ( layers === undefined ) { layers = [ ] ; this . layers [ script ] = layers ; } const minimumTag = LanguageResolver . removeLikelySubtags ( tag ) ; if ( minimumTag . compact ( ) === minimumTag . language ( ) ) { this . defaultLayer = tag ; } const layerIsDefault = defaultContent . has ( tag . expanded ( ) ) ; const layer = new Layer ( tag , layerIsDefault ) ; this . current = layer ; layers . push ( layer ) ; } /**
     * Add a (possibly undefined) field value to the current layer.
     */ add ( raw ) { const value = typeof raw === 'string' ? raw : '' ; if ( value . indexOf ( DELIMITER ) !== - 1 ) { const id = this . current . localeId ; throw new Error ( `Resource pack delimiter character found in ${ id } string "${ value }"` ) ; } const strings = this . current . strings ; const offset = strings . length ; strings . push ( value ) ; return offset ; } render ( checksum ) { const scripts = [ ] ; Object . keys ( this . layers ) . forEach ( ( script ) => { const json = this . renderScript ( script ) ; scripts . push ( `"${ script }":${ json }` ) ; } ) ; if ( this . defaultLayer === undefined ) { throw new Error ( `No default layer found for ${ this . language }!` ) ; } return ( `{"version":"${ this . version }",` + `"cldr":"${ this . cldrVersion }",` + `"checksum":"${ checksum }",` + `"language":"${ this . language }",` + `"defaultTag":"${ this . defaultLayer }",` + `"scripts":{${ scripts . join ( ',' ) }},` + `"spellout":${ this . spellout }}` ) ; } /**
     * Render the pack to JSON.
     */ private renderScript ( script ) { const layers = this . layers [ script ] ; if ( layers === undefined ) { throw new Error ( `Attempt to render a non-existent script layer: ${ script }` ) ; } const len = layers . length ; if ( len === 0 ) { throw new Error ( 'Attempt to render an empty pack' ) ; } const base = this . selectBaseLayer ( layers ) ; const exceptions = [ ] ; const index = { } ; for ( let i = 0 ; i < layers . length ; i ++ ) { const curr = layers [ i ] ; for ( let j = 0 ; j < curr . strings . length ; j ++ ) { const str = curr . strings [ j ] ; if ( str === base . strings [ j ] ) { continue ; } curr . index . push ( j ) ; let offset = index [ str ] ; if ( typeof offset !== 'number' ) { offset = exceptions . length ; exceptions . push ( str ) ; index [ str ] = offset ; } curr . index . push ( offset ) ; } } let defaultRegion = '' ; const regions = layers . map ( ( curr ) => { const idx = curr . index . map ( ( n ) => n . toString ( 36 ) ) . join ( ' ' ) ; let id = curr . tag . region ( ) ; if ( curr . isDefault ) { defaultRegion = id ; } const variant = curr . tag . variant ( ) ; if ( variant !== '' ) { id += '-' + variant ; } return `"${ id }":"${ idx }"` ; } ) ; if ( defaultRegion === '' ) { const def = this . defaultLayer ; if ( def !== undefined && def . script ( ) === script && def . hasRegion ( ) ) { defaultRegion = def . region ( ) ; } else { throw new Error ( `Failed to set default region for ${ this . language }-${ script }` ) ; } } return ( `{"strings":"${ join ( base . strings ) }",` + `"exceptions":"${ join ( exceptions ) }",` + `"regions":{${ regions . join ( ',' ) }},` + `"defaultRegion":"${ defaultRegion }"}` ) ; } /**
     * Select the best base layers for all regions to inherit from.
     *
     * All strings are stored in the base layer, and each region stores its delta
     * from the base. To minimize overall size of the delta indexes we should select
     * a base layer that has the lowest distance to all locales.
     *
     * Here we compute the pairwise distance between all regions and pick the region
     * with the lowest overall distance. Based on a quick check this saves a total of
     * 350KB across all uncompressed resource files.
     *
     * TODO: select the base layer and then compute distances per-script.
     */ private selectBaseLayer ( layers ) { const distances = [ ] ; for ( const base of layers ) { const baseLen = base . strings . length ; let dist = 0 ; for ( const curr of layers ) { if ( curr . localeId === base . localeId ) { continue ; } const currLen = curr . strings . length ; if ( baseLen !== currLen ) { throw new Error ( `Severe error: string tables for ${ base . localeId } and` + `${ curr . localeId } lengths differ: ${ baseLen } !== ${ currLen }` ) ; } for ( let i = 0 ; i < curr . strings . length ; i ++ ) { if ( base . strings [ i ] !== curr . strings [ i ] ) { dist ++ ; } } } distances . push ( [ dist , base ] ) ; } distances . sort ( ( a , b ) => ( a [ 0 ] === b [ 0 ] ? 0 : a [ 0 ] < b [ 0 ] ? - 1 : 1 ) ) ; return distances [ 0 ] [ 1 ] ; } }