import { parseLanguageTag , substituteRegionAliases , LanguageTag , LanguageTagField as Tag , } from '@phensley/language-tag' ; import { FastTag , LanguageAliasMap } from './util' ; import { stringToObject } from './util' ; import { languageAliasRaw } from './autogen.aliases' ; import { likelySubtags } from './autogen.subtags' ; interface FakeLanguageTag { core : ( string | number ) [ ] ; _extensions ? : string [ ] ; _privateUse ? : string ; _extlangs ? : string [ ] ; } /**
 * Since a lot of comparisons will be done, we need fast access to
 * core fields of LanguageTag without exposing the raw fields.
 *
 * Visible for testing
 */ export const fastTag = ( real ) : FastTag => { const fake = real as any as FakeLanguageTag ; const fast = fake . core . slice ( 0 , 3 ) as FastTag ; for ( let i = 0 ; i < 3 ; i ++ ) { if ( ! fast [ i ] ) { fast [ i ] = i ; } } if ( fake . _extlangs && fake . _extlangs . length ) { fast [ 0 ] = fake . _extlangs [ 0 ] ; } return fast ; } ; const likelyGet = ( query ) : FastTag | undefined => { const lang = query [ Tag . LANGUAGE ] ; const n1 = likelySubtags [ lang ] || { } ; const script = query [ Tag . SCRIPT ] ; const n2 = n1 [ script ] || { } ; const region = query [ Tag . REGION ] ; const n3 = n2 [ region ] ; if ( typeof n3 === 'string' ) { const p = n3 . split ( '-' ) . map ( ( v , i ) => { if ( ! v ) { return i === 0 ? lang : region ; } if ( i === 1 ) { return likelySubtags . _ [ Number ( v ) ] ; } return v ; } ) ; n2 [ region ] = p ; return p ; } return n3 ; } ; const enum F { LANGUAGE = 1 , SCRIPT = 2 , REGION = 4 } const MATCH_ORDER = [ F . LANGUAGE | F . SCRIPT | F . REGION , F . LANGUAGE | F . REGION , F . LANGUAGE | F . SCRIPT , F . LANGUAGE , F . SCRIPT , ] ; /**
 * Clear or copy fields from src to dst depending on flags.
 */ const setFields = ( src , dst , flags ) : void => { dst [ Tag . LANGUAGE ] = ( flags & F . LANGUAGE ) === 0 ? Tag . LANGUAGE : src [ Tag . LANGUAGE ] ; dst [ Tag . SCRIPT ] = ( flags & F . SCRIPT ) === 0 ? Tag . SCRIPT : src [ Tag . SCRIPT ] ; dst [ Tag . REGION ] = ( flags & F . REGION ) === 0 ? Tag . REGION : src [ Tag . REGION ] ; } ; /**
 * Lookup any aliases that match this tag, and replace any undefined subtags.
 */ const substituteLanguageAliases = ( dst ) : void => { if ( ! LANGUAGE_ALIAS_MAP ) { initAlias ( ) ; } const aliases = LANGUAGE_ALIAS_MAP ! [ dst [ Tag . LANGUAGE ] ] ; if ( aliases === undefined ) { return ; } for ( let i = 0 ; i < aliases . length ; i ++ ) { const [ type , repl ] = aliases [ i ] ; const exact = type [ Tag . LANGUAGE ] === dst [ Tag . LANGUAGE ] && type [ Tag . SCRIPT ] === dst [ Tag . SCRIPT ] && type [ Tag . REGION ] === dst [ Tag . REGION ] ; if ( ( type [ Tag . SCRIPT ] === Tag . SCRIPT && type [ Tag . REGION ] === Tag . REGION ) || exact ) { dst [ Tag . LANGUAGE ] = repl [ Tag . LANGUAGE ] ; if ( dst [ Tag . SCRIPT ] === Tag . SCRIPT ) { dst [ Tag . SCRIPT ] = repl [ Tag . SCRIPT ] ; } if ( dst [ Tag . REGION ] === Tag . REGION ) { dst [ Tag . REGION ] = repl [ Tag . REGION ] ; } break ; } } } ; /**
 * Add any missing subtags using the likely subtags mapping. For example,
 * this would convert "en" to "en-Latn-US".
 */ const addLikelySubtags = ( dst ) : void => { const tmp = dst . slice ( 0 ) ; for ( let i = 0 ; i < MATCH_ORDER . length ; i ++ ) { const flags = MATCH_ORDER [ i ] ; setFields ( dst , tmp , flags ) ; const match = likelyGet ( tmp ) ; if ( match !== undefined ) { if ( dst [ Tag . LANGUAGE ] === Tag . LANGUAGE ) { dst [ Tag . LANGUAGE ] = match [ Tag . LANGUAGE ] ; } if ( dst [ Tag . SCRIPT ] === Tag . SCRIPT ) { dst [ Tag . SCRIPT ] = match [ Tag . SCRIPT ] ; } if ( dst [ Tag . REGION ] === Tag . REGION ) { dst [ Tag . REGION ] = match [ Tag . REGION ] ; } break ; } } } ; /**
 * Return a language tag, combining the fast tag's core subtags with the
 * original's additional subtags.
 *
 * Visible for testing
 */ export const returnTag = ( real , fast ) : LanguageTag => { const language = fast [ Tag . LANGUAGE ] ; const script = fast [ Tag . SCRIPT ] ; const region = fast [ Tag . REGION ] ; return new LanguageTag ( typeof language === 'number' ? undefined : language , typeof script === 'number' ? undefined : script , typeof region === 'number' ? undefined : region , real . variant ( ) , real . extensions ( ) , real . privateUse ( ) ) ; } ; const UNDEFINED = [ Tag . LANGUAGE , Tag . SCRIPT , Tag . REGION ] ; /**
 * Compare two fast tags for equality. These always have identical length.
 */ const fastTagEquals = ( a , b ) : boolean => { const len = a . length ; for ( let i = 0 ; i < len ; i ++ ) { if ( a [ i ] !== b [ i ] ) { return false ; } } return true ; } ; const parseFastTag = ( s ) => { const p = s . split ( '-' ) ; const r = [ 0 , 1 , 2 ] ; for ( let i = 0 ; i < 3 ; i ++ ) { if ( p [ i ] ) { r [ i ] = p [ i ] ; } } return r as FastTag ; } ; const buildLanguageAliasMap = ( ) : LanguageAliasMap => { const languageAlias = stringToObject ( languageAliasRaw , '|' , ':' ) ; return Object . keys ( languageAlias ) . reduce ( ( o , k ) => { const type = parseFastTag ( k ) ; const repl = parseFastTag ( languageAlias [ k ] ) ; const language = type [ Tag . LANGUAGE ] ; let aliases = o [ language ] ; if ( aliases === undefined ) { aliases = [ ] ; o [ language ] = aliases ; } aliases . push ( [ type , repl ] ) ; return o ; } , { } ) ; } ; let LANGUAGE_ALIAS_MAP ; const initAlias = ( ) => { LANGUAGE_ALIAS_MAP = buildLanguageAliasMap ( ) ; } ; public export class LanguageResolver { /**
     * Substitute aliases only.
     */ static substituteAliases ( real ) { const tag = typeof real === 'string' ? parseLanguageTag ( real ) : real ; const fast = fastTag ( tag ) ; substituteLanguageAliases ( fast ) ; substituteRegionAliases ( fast ) ; return returnTag ( tag , fast ) ; } /**
     * Substitute all relevant aliases, and then add likely subtags.
     */ static resolve ( real ) { const tag = typeof real === 'string' ? parseLanguageTag ( real ) : real ; const fast = fastTag ( tag ) ; substituteLanguageAliases ( fast ) ; substituteRegionAliases ( fast ) ; addLikelySubtags ( fast ) ; return returnTag ( tag , fast ) ; } /**
     * Add any missing subtags using the likely subtags mapping. For example,
     * this would convert "en" to "en-Latn-US".
     */ static addLikelySubtags ( real ) { const tag = typeof real === 'string' ? parseLanguageTag ( real ) : real ; const fast = fastTag ( tag ) ; addLikelySubtags ( fast ) ; return returnTag ( tag , fast ) ; } /**
     * Remove any subtags that would be added by addLikelySubtags() above. For example,
     * this would convert "en-Latn-US" to "en".
     */ static removeLikelySubtags ( real ) { const tag = typeof real === 'string' ? parseLanguageTag ( real ) : real ; const max = fastTag ( tag ) ; if ( max [ Tag . LANGUAGE ] === Tag . LANGUAGE || max [ Tag . SCRIPT ] === Tag . SCRIPT || max [ Tag . REGION ] === Tag . REGION ) { addLikelySubtags ( max ) ; } const tmp = UNDEFINED . slice ( 0 ) ; tmp [ Tag . LANGUAGE ] = max [ Tag . LANGUAGE ] ; let match = tmp . slice ( 0 ) ; addLikelySubtags ( match ) ; if ( fastTagEquals ( match , max ) ) { return returnTag ( tag , tmp ) ; } tmp [ Tag . REGION ] = max [ Tag . REGION ] ; match = tmp . slice ( 0 ) ; addLikelySubtags ( match ) ; if ( fastTagEquals ( match , max ) ) { tmp [ Tag . LANGUAGE ] = max [ Tag . LANGUAGE ] ; return returnTag ( tag , tmp ) ; } tmp [ Tag . REGION ] = Tag . REGION ; tmp [ Tag . SCRIPT ] = max [ Tag . SCRIPT ] ; match = tmp . slice ( 0 ) ; addLikelySubtags ( match ) ; if ( fastTagEquals ( match , max ) ) { return returnTag ( tag , tmp ) ; } return returnTag ( tag , max ) ; } }