import { parseLanguageTag , LanguageTag } from '@phensley/language-tag' ; import { LanguageResolver , Locale } from '@phensley/locale' ; import { getDistance , DEFAULT_THRESHOLD , MAX_DISTANCE } from './distance' ; import { paradigmLocales } from './autogen.distance' ; const TAG_SEP = /[,\s]+/g ; const U = undefined ; const numberCmp = ( a , b ) => ( a === b ? 0 : a < b ? - 1 : 1 ) ; class Entry implements Locale { readonly compact : string ; constructor ( readonly id , readonly tag ) { this . compact = tag . compact ( ) ; } } type LangTag = Locale | LanguageTag | string ; /**
 * Flatten and split the string or array into a list of matcher entries.
 *
 * Visible for testing.
 */ export const parse = ( locales = [ ] , options = { } ) : Entry [ ] => { let raw ; if ( typeof locales === 'string' ) { raw = locales . split ( TAG_SEP ) ; } else { raw = locales . reduce ( ( a , e ) : LangTag [ ] => { if ( typeof e === 'string' ) { const tmp = e . split ( TAG_SEP ) ; return a . concat ( tmp ) ; } if ( ( e as Locale ) . tag instanceof LanguageTag ) { a . push ( e as Locale ) ; } else if ( e instanceof LanguageTag ) { a . push ( e ) ; } return a ; } , [ ] as LangTag [ ] ) ; } const resolve = options . resolve !== false ; const result = [ ] ; const len = raw . length ; for ( let i = 0 ; i < len ; i ++ ) { const e = raw [ i ] ; let id ; let tag ; if ( e instanceof LanguageTag ) { tag = e ; id = tag . compact ( ) ; } else if ( typeof e === 'string' ) { id = e . trim ( ) ; tag = parseLanguageTag ( id ) ; } else { tag = e . tag ; id = e . id ; result . push ( new Entry ( e . id , resolve ? LanguageResolver . substituteAliases ( e . tag ) : e . tag ) ) ; continue ; } const l = tag . hasLanguage ( ) ; const s = tag . hasScript ( ) ; const r = tag . hasRegion ( ) ; if ( l && s && r ) { result . push ( new Entry ( id , resolve ? LanguageResolver . substituteAliases ( tag ) : tag ) ) ; } else if ( l || s || r ) { result . push ( new Entry ( id , resolve ? LanguageResolver . resolve ( tag ) : tag ) ) ; } else { result . push ( new Entry ( id , new LanguageTag ( undefined , undefined , undefined , undefined , tag . extensions ( ) , tag . privateUse ( ) ) ) ) ; } } return result ; } ; public export interface LocaleMatch { /**
     * The locale that was matched.
     */ locale : Locale ; /**
     * Distance of the match from the desired locale.
     */ distance : number ; } public export interface LocaleMatcherOptions { /**
     * Resolve language tags. (default true)
     */ resolve ? : boolean ; } /**
 * Sort the supported locale entries. The result will have the following order:
 *
 *  First: default locale
 *   Next: all paradigm locales
 *   Last: all other locales
 *
 * Visible for testing.
 */ export const sortEntries = ( d ) => ( a , b ) : number => { if ( a . tag === d . tag ) { return - 1 ; } if ( b . tag === d . tag ) { return 1 ; } const pa = paradigmLocales [ a . compact ] ; const pb = paradigmLocales [ b . compact ] ; if ( pa !== undefined ) { return pb === U ? - 1 : numberCmp ( pa , pb ) ; } else if ( pb !== undefined ) { return 1 ; } return 0 ; } ; public export class LocaleMatcher { private supported : Entry [ ] ; private count : number ; private default : Entry ; private exactMap : { [ x ] : Entry [ ] ; } = { } ; constructor ( supportedLocales , options = { } ) { this . supported = parse ( supportedLocales , options ) ; this . count = this . supported . length ; if ( ! this . count ) { throw new Error ( 'LocaleMatcher expects at least one supported locale' ) ; } this . default = this . supported [ 0 ] ; this . supported . sort ( sortEntries ( this . default ) ) ; this . supported . forEach ( ( locale ) => { const key = locale . compact ; let bundles = this . exactMap [ key ] ; if ( bundles === U ) { bundles = [ locale ] ; this . exactMap [ key ] = bundles ; } else { bundles . push ( locale ) ; } } ) ; } /**
     * Find the desired locale that is the closed match to a supported locale, within
     * the given threshold. Any matches whose distance is greater than or equal to the
     * threshold will be treated as having maximum distance.
     */ match ( desiredLocales , threshold = DEFAULT_THRESHOLD ) { const desireds = parse ( desiredLocales ) ; const len = desireds . length ; let bestDistance = MAX_DISTANCE ; let bestMatch = undefined ; let bestDesired = len === 0 ? this . default : desireds [ 0 ] ; for ( let i = 0 ; i < len ; i ++ ) { const desired = desireds [ i ] ; const exact = this . exactMap [ desired . compact ] ; if ( exact !== undefined ) { bestMatch = exact [ 0 ] ; bestDistance = 0 ; bestDesired = desired ; break ; } for ( let j = 0 ; j < this . count ; j ++ ) { const supported = this . supported [ j ] ; const distance = getDistance ( desired . tag , supported . tag , threshold ) ; if ( distance < bestDistance ) { bestDistance = distance ; bestMatch = supported ; bestDesired = desired ; } } } const extensions = bestDesired . tag . extensions ( ) ; const privateUse = bestDesired . tag . privateUse ( ) ; const { id , tag } = bestMatch === U ? this . default : bestMatch ; const result = new LanguageTag ( tag . language ( ) , tag . script ( ) , tag . region ( ) , tag . variant ( ) , extensions , privateUse ) ; return { locale : { id , tag : result } , distance : bestMatch === U ? MAX_DISTANCE : bestDistance , } ; } }