2017-11-13 01:23:39 -05:00
'use strict' ;
// ============================================================================
// Localization
// ============================================================================
2019-05-03 19:30:46 -04:00
import Parser from '@ffz/icu-msgparser' ;
2021-11-12 16:58:35 -05:00
import { DEBUG , SERVER } from 'utilities/constants' ;
2019-10-04 14:57:13 -04:00
import { get , pick _random , shallow _copy , deep _copy } from 'utilities/object' ;
2021-11-12 16:58:35 -05:00
import { getBuster } from 'utilities/time' ;
2017-11-13 01:23:39 -05:00
import Module from 'utilities/module' ;
2019-05-03 19:30:46 -04:00
import NewTransCore from 'utilities/translation-core' ;
2017-11-13 01:23:39 -05:00
2021-11-12 16:58:35 -05:00
const fetchJSON = ( url , options ) => fetch ( url , options ) . then ( r => r . ok ? r . json ( ) : null ) . catch ( ( ) => null ) ;
2019-10-04 14:57:13 -04:00
const STACK _SPLITTER = /\s*at\s+(.+?)\s+\((.+)\)$/ ,
2019-10-06 20:01:22 -04:00
SOURCE _SPLITTER = /^(.+):\/\/(.+?)(?:\?[a-zA-Z0-9]+)?:(\d+:\d+)$/ ;
2019-10-04 14:57:13 -04:00
const MAP _OPTIONS = {
filter ( line ) {
return line . includes ( '.frankerfacez.com' ) || line . includes ( 'localhost' ) ;
} ,
cacheGlobally : true
} ;
const BAD _FRAMES = [
'/src/i18n.js' ,
'/src/utilities/vue.js'
]
2019-05-03 19:30:46 -04:00
const FACES = [ '(・`ω´・)' , ';;w;;' , 'owo' , 'ono' , 'oAo' , 'oxo' , 'ovo;' , 'UwU' , '>w<' , '^w^' , '> w >' , 'v.v' ] ,
2018-04-15 17:19:22 -04:00
2019-06-20 15:15:54 -04:00
transformText = ( ast , fn ) => ast . map ( node => {
if ( typeof node === 'string' )
return fn ( node ) ;
else if ( typeof node === 'object' && node . o ) {
const out = Object . assign ( node , { o : { } } ) ;
for ( const key of Object . keys ( node . o ) )
out . o [ key ] = transformText ( node . o [ key ] , fn )
}
return node ;
} ) ,
2018-04-15 17:19:22 -04:00
owo = text => text
. replace ( /(?:r|l)/g , 'w' )
. replace ( /(?:R|L)/g , 'W' )
. replace ( /n([aeiou])/g , 'ny$1' )
. replace ( /N([aeiou])/g , 'Ny$1' )
. replace ( /N([AEIOU])/g , 'NY$1' )
. replace ( /ove/g , 'uv' )
. replace ( /!+/g , ` ${ pick _random ( FACES ) } ` ) ,
TRANSFORMATIONS = {
2019-05-03 19:30:46 -04:00
double : ( key , ast ) => [ ... ast , ' ' , ... ast ] ,
upper : ( key , ast ) => transformText ( ast , n => n . toUpperCase ( ) ) ,
lower : ( key , ast ) => transformText ( ast , n => n . toLowerCase ( ) ) ,
append _key : ( key , ast ) => [ ... ast , ` ( ${ key } ) ` ] ,
2019-05-31 16:05:50 -04:00
set _key : key => [ key ] ,
2019-05-03 19:30:46 -04:00
owo : ( key , ast ) => transformText ( ast , owo )
2018-04-15 17:19:22 -04:00
} ;
2017-11-13 01:23:39 -05:00
// ============================================================================
// TranslationManager
// ============================================================================
export class TranslationManager extends Module {
constructor ( ... args ) {
super ( ... args ) ;
this . inject ( 'settings' ) ;
2019-05-03 19:30:46 -04:00
this . parser = new Parser ;
2018-07-24 16:11:02 -04:00
this . _seen = new Set ;
2019-10-05 20:55:32 -04:00
this . availableLocales = [ 'en' ] ;
2021-11-12 16:58:35 -05:00
this . chunks = [ 'client' ] ;
2017-11-13 01:23:39 -05:00
this . localeData = {
2019-10-05 20:55:32 -04:00
en : { name : 'English' }
2017-11-13 01:23:39 -05:00
}
2019-10-05 20:55:32 -04:00
this . loadLocales ( ) ;
2019-10-06 20:01:22 -04:00
this . strings _loaded = false ;
this . new _strings = 0 ;
this . changed _strings = 0 ;
2019-10-04 14:57:13 -04:00
this . capturing = false ;
this . captured = new Map ;
this . settings . addUI ( 'i18n.debug.open' , {
path : 'Debugging > Localization >> Editing' ,
component : 'i18n-open' ,
force _seen : true
} ) ;
this . settings . add ( 'i18n.debug.capture' , {
default : null ,
process ( ctx , val ) {
if ( val === null )
return DEBUG ;
return val ;
} ,
ui : {
path : 'Debugging > Localization >> General' ,
title : 'Enable message capture.' ,
description : 'Capture all localized strings, including variables and call locations, for the purpose of reporting them to the backend. This is used to add new strings to the translation project. By default, message capture is enabled when running in development mode.' ,
component : 'setting-check-box' ,
force _seen : true
} ,
changed : val => {
this . capturing = val ;
}
} ) ;
2018-04-15 17:19:22 -04:00
this . settings . add ( 'i18n.debug.transform' , {
default : null ,
ui : {
path : 'Debugging > Localization >> General' ,
title : 'Transformation' ,
description : 'Transform all localized strings to test string coverage as well as length.' ,
component : 'setting-select-box' ,
data : [
{ value : null , title : 'Disabled' } ,
{ value : 'upper' , title : 'Upper Case' } ,
{ value : 'lower' , title : 'Lower Case' } ,
{ value : 'append_key' , title : 'Append Key' } ,
2019-05-03 19:30:46 -04:00
{ value : 'set_key' , title : 'Set to Key' } ,
2018-04-15 17:19:22 -04:00
{ value : 'double' , title : 'Double' } ,
{ value : 'owo' , title : "owo what's this" }
]
} ,
changed : val => {
this . _ . transformation = TRANSFORMATIONS [ val ] ;
2019-06-09 19:48:26 -04:00
this . emit ( ':transform' ) ;
this . emit ( ':update' ) ;
2018-04-15 17:19:22 -04:00
}
} ) ;
2017-11-13 01:23:39 -05:00
this . settings . add ( 'i18n.locale' , {
default : - 1 ,
process : ( ctx , val ) => {
2019-10-05 20:55:32 -04:00
if ( val === - 1 || typeof val !== 'string' )
val = ctx . get ( 'context.session.languageCode' ) || 'en' ;
if ( this . availableLocales . includes ( val ) )
return val ;
2020-07-16 17:21:51 -04:00
if ( val === 'no' && this . availableLocales . includes ( 'nb' ) )
return 'nb' ;
2019-10-05 20:55:32 -04:00
const idx = val . indexOf ( '-' ) ;
if ( idx === - 1 )
return 'en' ;
2017-11-13 01:23:39 -05:00
2019-10-05 20:55:32 -04:00
val = val . slice ( 0 , idx ) ;
2020-07-16 17:21:51 -04:00
return this . availableLocales . includes ( val ) ? val : 'en' ;
2017-11-13 01:23:39 -05:00
} ,
2019-10-05 20:55:32 -04:00
ui : {
2020-08-04 18:26:11 -04:00
path : 'Appearance > Localization >> General @{"sort":-100}' ,
2017-11-13 01:23:39 -05:00
title : 'Language' ,
2019-10-05 20:55:32 -04:00
description : ` FrankerFaceZ is lovingly translated by volunteers from our community. Thank you. If you're interested in helping to translate FrankerFaceZ, please [join our Discord](https://discord.gg/UrAkGhT) and ask about localization. ` ,
2017-11-13 01:23:39 -05:00
component : 'setting-select-box' ,
2019-10-06 20:01:22 -04:00
data : ( profile , val ) => this . getLocaleOptions ( val )
2017-11-13 01:23:39 -05:00
} ,
changed : val => this . locale = val
} ) ;
2020-08-04 18:26:11 -04:00
this . settings . add ( 'i18n.format.date' , {
default : 'default' ,
ui : {
path : 'Appearance > Localization >> Formatting' ,
title : 'Date Format' ,
2020-08-05 19:23:18 -04:00
description : 'The default date format. Custom date formats are formated using the [Day.js](https://day.js.org/docs/en/display/format) library.' ,
2020-08-04 18:26:11 -04:00
component : 'setting-combo-box' ,
2020-08-05 19:23:18 -04:00
extra : {
before : true ,
mode : 'date' ,
component : 'format-preview'
} ,
2020-08-04 18:26:11 -04:00
data : ( ) => {
const out = [ ] , now = new Date ;
for ( const [ key , fmt ] of Object . entries ( this . _ . formats . date ) ) {
out . push ( {
value : key , title : ` ${ this . formatDate ( now , key ) } ( ${ key } ) `
} )
}
return out ;
}
} ,
changed : val => {
this . _ . defaultDateFormat = val ;
this . emit ( ':update' )
}
} ) ;
this . settings . add ( 'i18n.format.time' , {
default : 'short' ,
ui : {
path : 'Appearance > Localization >> Formatting' ,
title : 'Time Format' ,
2020-08-05 19:23:18 -04:00
description : 'The default time format. Custom time formats are formated using the [Day.js](https://day.js.org/docs/en/display/format) library.' ,
2020-08-04 18:26:11 -04:00
component : 'setting-combo-box' ,
2020-08-05 19:23:18 -04:00
extra : {
before : true ,
mode : 'time' ,
component : 'format-preview'
} ,
2020-08-04 18:26:11 -04:00
data : ( ) => {
const out = [ ] , now = new Date ;
for ( const [ key , fmt ] of Object . entries ( this . _ . formats . time ) ) {
out . push ( {
value : key , title : ` ${ this . formatTime ( now , key ) } ( ${ key } ) `
} )
}
return out ;
}
} ,
changed : val => {
this . _ . defaultTimeFormat = val ;
this . emit ( ':update' )
}
} ) ;
this . settings . add ( 'i18n.format.datetime' , {
default : 'medium' ,
ui : {
path : 'Appearance > Localization >> Formatting' ,
title : 'Date-Time Format' ,
2020-08-05 19:23:18 -04:00
description : 'The default combined date-time format. Custom time formats are formated using the [Day.js](https://day.js.org/docs/en/display/format) library.' ,
2020-08-04 18:26:11 -04:00
component : 'setting-combo-box' ,
2020-08-05 19:23:18 -04:00
extra : {
before : true ,
mode : 'datetime' ,
component : 'format-preview'
} ,
2020-08-04 18:26:11 -04:00
data : ( ) => {
const out = [ ] , now = new Date ;
for ( const [ key , fmt ] of Object . entries ( this . _ . formats . datetime ) ) {
out . push ( {
value : key , title : ` ${ this . formatDateTime ( now , key ) } ( ${ key } ) `
} )
}
return out ;
}
} ,
changed : val => {
this . _ . defaultDateTimeFormat = val ;
this . emit ( ':update' )
}
} ) ;
2019-10-06 20:01:22 -04:00
}
getLocaleOptions ( val ) {
if ( val === undefined )
val = this . settings . get ( 'i18n.locale' ) ;
2020-02-09 15:10:12 -05:00
const normal _out = [ ] ,
joke _out = [ ] ;
for ( const locale of this . availableLocales ) {
const data = this . localeData [ locale ] ;
let title = data ? . native _name || data ? . name || locale ;
2019-10-06 20:01:22 -04:00
if ( data ? . coverage != null && data ? . coverage < 100 )
title = this . t ( 'i18n.locale-coverage' , '{name} ({coverage,number,percent} Complete)' , {
name : title ,
coverage : data . coverage / 100
} ) ;
2020-02-09 15:10:12 -05:00
const entry = {
selected : val === locale ,
value : locale ,
2019-10-06 20:01:22 -04:00
title
} ;
2020-02-09 15:10:12 -05:00
if ( data ? . joke )
joke _out . push ( entry ) ;
else
normal _out . push ( entry ) ;
}
normal _out . sort ( ( a , b ) => a . title . localeCompare ( b . title ) ) ;
joke _out . sort ( ( a , b ) => a . title . localeCompare ( b . title ) ) ;
2019-10-06 20:01:22 -04:00
2020-02-09 15:10:12 -05:00
let out = [ {
2019-10-06 20:01:22 -04:00
selected : val === - 1 ,
value : - 1 ,
i18n _key : 'setting.appearance.localization.general.language.twitch' ,
title : "Use Twitch's Language"
2020-02-09 15:10:12 -05:00
} ] ;
if ( normal _out . length ) {
out . push ( {
separator : true ,
i18n _key : 'setting.appearance.localization.general.language.languages' ,
title : 'Supported Languages'
} ) ;
out = out . concat ( normal _out ) ;
}
if ( joke _out . length ) {
out . push ( {
separator : true ,
i18n _key : 'setting.appearance.localization.general.language.joke' ,
title : 'Joke Languages'
} ) ;
out = out . concat ( joke _out ) ;
}
2017-11-13 01:23:39 -05:00
2019-10-06 20:01:22 -04:00
return out ;
2017-11-13 01:23:39 -05:00
}
onEnable ( ) {
2019-10-04 14:57:13 -04:00
this . capturing = this . settings . get ( 'i18n.debug.capture' ) ;
2019-10-06 20:01:22 -04:00
if ( this . capturing )
this . loadStrings ( ) ;
2019-10-04 14:57:13 -04:00
2019-05-03 19:30:46 -04:00
this . _ = new NewTransCore ( { //TranslationCore({
warn : ( ... args ) => this . log . warn ( ... args ) ,
2020-08-04 18:26:11 -04:00
defaultDateFormat : this . settings . get ( 'i18n.format.date' ) ,
defaultTimeFormat : this . settings . get ( 'i18n.format.time' ) ,
defaultDateTimeFormat : this . settings . get ( 'i18n.format.datetime' )
2018-07-13 14:32:12 -04:00
} ) ;
2017-11-13 01:23:39 -05:00
2018-07-24 16:11:02 -04:00
if ( window . BroadcastChannel ) {
const bc = this . _broadcaster = new BroadcastChannel ( 'ffz-i18n' ) ;
bc . addEventListener ( 'message' ,
this . _boundHandleMessage = this . handleMessage . bind ( this ) ) ;
}
2018-04-15 17:19:22 -04:00
this . _ . transformation = TRANSFORMATIONS [ this . settings . get ( 'i18n.debug.transform' ) ] ;
2017-11-13 01:23:39 -05:00
this . locale = this . settings . get ( 'i18n.locale' ) ;
}
2021-11-12 16:58:35 -05:00
async loadChunk ( name ) {
if ( this . chunks . includes ( name ) )
return [ ] ;
this . chunks . push ( name ) ;
const locale = this . _ . locale ;
const phrases = await this . loadLocale ( locale , name ) ;
const added = this . _ . extend ( phrases ) ;
if ( added . length ) {
this . log . info ( ` Loaded Chunk: ${ name } -- Phrases: ${ added . length } ` ) ;
this . emit ( ':loaded' , added ) ;
this . emit ( ':update' ) ;
}
return added ;
}
2019-10-04 14:57:13 -04:00
broadcast ( msg ) {
if ( this . _broadcaster )
this . _broadcaster . postMessage ( msg ) ;
2017-11-13 01:23:39 -05:00
}
2019-10-04 14:57:13 -04:00
getKeys ( ) {
2019-10-04 15:21:21 -04:00
const out = [ ] ;
for ( const entry of this . captured . values ( ) ) {
const thing = deep _copy ( entry ) ;
thing . translation = this . _ . phrases . get ( thing . key ) || thing . phrase ;
out . push ( thing ) ;
}
return out ;
2017-11-13 01:23:39 -05:00
}
2019-10-04 14:57:13 -04:00
requestKeys ( ) {
this . broadcast ( { type : 'request-keys' } ) ;
}
updatePhrase ( key , phrase ) {
this . broadcast ( {
type : 'update-key' ,
key ,
phrase
} ) ;
this . _ . extend ( {
[ key ] : phrase
} ) ;
this . emit ( ':loaded' , [ key ] ) ;
this . emit ( ':update' ) ;
}
2017-11-13 01:23:39 -05:00
2018-07-24 16:11:02 -04:00
handleMessage ( event ) {
const msg = event . data ;
2019-05-16 14:46:26 -04:00
if ( ! msg )
return ;
2019-10-04 14:57:13 -04:00
if ( msg . type === 'update-key' ) {
this . _ . extend ( {
[ msg . key ] : msg . phrase
} ) ;
this . emit ( ':loaded' , [ msg . key ] ) ;
this . emit ( ':update' ) ;
2018-07-24 16:11:02 -04:00
2019-10-04 14:57:13 -04:00
} else if ( msg . type === 'request-keys' )
this . broadcast ( {
type : 'keys' ,
data : Array . from ( this . captured . values ( ) )
} ) ;
else if ( msg . type === 'keys' && Array . isArray ( msg . data ) ) {
for ( const entry of msg . data ) {
// TODO: Merging logic.
this . captured . set ( entry . key , entry ) ;
}
this . emit ( ':got-keys' ) ;
2018-07-24 16:11:02 -04:00
}
2019-10-04 14:57:13 -04:00
}
2018-07-24 16:11:02 -04:00
2019-10-04 14:57:13 -04:00
openUI ( popout = true ) {
// Override the capturing state when we open the UI.
if ( ! this . capturing ) {
this . capturing = true ;
this . emit ( ':update' ) ;
}
const mod = this . resolve ( 'translation_ui' ) ;
if ( popout )
mod . openPopout ( ) ;
else
mod . enable ( ) ;
2018-07-24 16:11:02 -04:00
}
2019-10-04 14:57:13 -04:00
get locale ( ) {
2020-07-22 21:31:41 -04:00
return this . _ && this . _ . locale ;
2019-10-04 14:57:13 -04:00
}
set locale ( new _locale ) {
this . setLocale ( new _locale ) ;
}
2018-07-24 16:11:02 -04:00
2019-10-11 17:41:07 -04:00
async loadStrings ( ignore _loaded = false ) {
if ( this . strings _loaded && ! ignore _loaded )
2019-10-06 20:01:22 -04:00
return ;
if ( this . strings _loading )
return ;
this . strings _loading = true ;
2021-11-12 16:58:35 -05:00
const resp = await fetch ( ` ${ SERVER } /script/locale/strings.json?_= ${ getBuster ( 30 ) } ` ) ;
let strings ;
if ( ! resp . ok ) {
this . log . warn ( ` Error Loading Strings -- Status: ${ resp . status } ` ) ;
strings = [ ] ;
} else
strings = await resp . json ( ) ;
2019-10-06 20:01:22 -04:00
for ( const str of strings ) {
const key = str . id ;
let store = this . captured . get ( key ) ;
if ( ! store ) {
this . captured . set ( key , store = { key , phrase : str . default , hits : 0 , calls : [ ] } ) ;
if ( str . source ? . length )
store . calls . push ( str . source ) ;
}
if ( ! store . options && str . context ? . length )
try {
store . options = JSON . parse ( str . context ) ;
} catch ( err ) { /* no-op */ }
store . known = str . default ;
store . different = str . default !== store . phrase ;
}
this . new _strings = 0 ;
this . changed _strings = 0 ;
for ( const entry of this . captured . values ( ) ) {
if ( ! entry . known )
this . new _strings ++ ;
if ( entry . different )
this . changed _strings ++ ;
}
this . strings _loaded = true ;
this . strings _loading = false ;
this . log . info ( ` Loaded ${ strings . length } strings from the server. ` ) ;
this . emit ( ':strings-loaded' ) ;
this . emit ( ':new-strings' , this . new _strings ) ;
this . emit ( ':changed-strings' , this . changed _strings ) ;
}
2019-10-04 14:57:13 -04:00
see ( key , phrase , options ) {
if ( ! this . capturing )
return ;
let stack ;
2018-07-24 16:11:02 -04:00
try {
2019-10-04 14:57:13 -04:00
stack = new Error ( ) . stack ;
} catch ( err ) {
/* :thinking: */
try {
stack = err . stack ;
} catch ( err _again ) { /* aww */ }
}
let store = this . captured . get ( key ) ;
2019-10-06 20:01:22 -04:00
if ( ! store ) {
2019-10-04 14:57:13 -04:00
this . captured . set ( key , store = { key , phrase , hits : 0 , calls : [ ] } ) ;
2019-10-06 20:01:22 -04:00
if ( this . strings _loaded ) {
this . new _strings ++ ;
this . emit ( ':new-strings' , this . new _strings ) ;
}
}
2019-10-04 14:57:13 -04:00
2019-10-06 20:01:22 -04:00
if ( phrase !== store . phrase ) {
store . phrase = phrase ;
if ( store . known && phrase !== store . known && ! store . different ) {
store . different = true ;
this . changed _strings ++ ;
this . emit ( ':changed-strings' , this . changed _strings ) ;
}
}
2018-07-24 16:11:02 -04:00
2019-10-04 14:57:13 -04:00
store . options = this . pluckVariables ( key , options ) ;
store . hits ++ ;
2018-07-24 16:11:02 -04:00
2019-10-04 14:57:13 -04:00
if ( stack ) {
if ( this . mapStackTrace )
this . mapStackTrace ( stack , result => this . recordCall ( store , result ) , MAP _OPTIONS ) ;
else
import ( /* webpackChunkName: 'translation-ui' */ 'sourcemapped-stacktrace' ) . then ( mod => {
this . mapStackTrace = mod . mapStackTrace ;
this . mapStackTrace ( stack , result => this . recordCall ( store , result ) , MAP _OPTIONS ) ;
} ) ;
}
2018-07-24 16:11:02 -04:00
}
2019-10-04 14:57:13 -04:00
pluckVariables ( key , options ) {
const ast = this . _ . cache . get ( key ) ;
if ( ! ast )
return null ;
const out = { } ;
this . _doPluck ( ast , options , out ) ;
if ( Object . keys ( out ) . length )
return out ;
return null ;
2018-07-24 16:11:02 -04:00
}
2019-10-04 14:57:13 -04:00
_doPluck ( ast , options , out ) {
if ( Array . isArray ( ast ) ) {
for ( const val of ast )
this . _doPluck ( val , options , out ) ;
2018-07-24 16:11:02 -04:00
return ;
2019-10-04 14:57:13 -04:00
}
2021-02-17 02:08:21 -05:00
if ( typeof ast === 'object' && ast . v ) {
const val = get ( ast . v , options ) ;
// Skip React objects.
if ( val && val [ '$$typeof' ] )
return ;
out [ ast . v ] = shallow _copy ( val ) ;
}
2019-10-04 14:57:13 -04:00
}
2018-07-24 16:11:02 -04:00
2019-10-04 14:57:13 -04:00
recordCall ( store , stack ) { // eslint-disable-line class-methods-use-this
if ( ! Array . isArray ( stack ) )
return ;
for ( const line of stack ) {
const match = STACK _SPLITTER . exec ( line ) ;
if ( ! match )
continue ;
const location = SOURCE _SPLITTER . exec ( match [ 2 ] ) ;
if ( ! location || location [ 1 ] !== 'webpack' )
continue ;
const file = location [ 2 ] ;
if ( file . includes ( '/node_modules/' ) || BAD _FRAMES . includes ( file ) )
continue ;
2019-10-06 20:01:22 -04:00
let out ;
if ( match [ 1 ] === 'MainMenu.getSettingsTree' )
out = 'FFZ Control Center' ;
else {
let label = match [ 1 ] ;
2019-10-09 16:02:25 -04:00
if ( ( label === 'Proxy.render' || label . startsWith ( 'Proxy.push' ) ) && location [ 2 ] . includes ( '.vue' ) )
2019-10-06 20:01:22 -04:00
label = 'Vue Component' ;
out = ` ${ label } ( ${ location [ 2 ] } : ${ location [ 3 ] } ) ` ;
}
2019-10-04 14:57:13 -04:00
if ( ! store . calls . includes ( out ) )
store . calls . push ( out ) ;
return ;
}
2018-07-24 16:11:02 -04:00
}
2019-10-04 14:57:13 -04:00
2019-10-05 20:55:32 -04:00
async loadLocales ( ) {
2021-11-12 16:58:35 -05:00
const resp = await fetch ( ` ${ SERVER } /script/locale/locales.json?_= ${ getBuster ( 30 ) } ` ) ;
let data ;
2019-10-05 20:55:32 -04:00
if ( ! resp . ok ) {
this . log . warn ( ` Error Populating Locales -- Status: ${ resp . status } ` ) ;
2021-11-12 16:58:35 -05:00
} else
data = await resp . json ( ) ;
2019-10-05 20:55:32 -04:00
if ( ! Array . isArray ( data ) || ! data . length )
data = [ {
id : 'en' ,
name : 'English' ,
coverage : 100 ,
2021-11-12 16:58:35 -05:00
rtl : false ,
hashes : { }
2019-10-05 20:55:32 -04:00
} ] ;
this . localeData = { } ;
this . availableLocales = [ ] ;
for ( const locale of data ) {
const key = locale . id . toLowerCase ( ) ;
this . localeData [ key ] = locale ;
this . availableLocales . push ( key ) ;
}
this . emit ( ':locales-loaded' ) ;
}
2021-11-12 16:58:35 -05:00
async loadLocale ( locale , chunk = null ) {
2019-05-03 19:30:46 -04:00
if ( locale === 'en' )
2017-11-13 01:23:39 -05:00
return { } ;
2021-11-12 16:58:35 -05:00
const hashes = this . localeData [ locale ] ? . hashes ;
if ( ! hashes ) {
this . log . info ( ` Cannot Load Locale: ${ locale } ` ) ;
return { } ;
}
if ( ! chunk )
chunk = this . chunks ;
else if ( ! Array . isArray ( chunk ) )
chunk = [ chunk ] ;
const id = this . localeData [ locale ] . id ;
const promises = [ ] ;
for ( const chnk of chunk ) {
const hash = hashes [ chnk ] ;
if ( ! hash )
continue ;
promises . push ( fetchJSON ( ` https://cdn.frankerfacez.com/static/locale/ ${ id } / ${ chnk } . ${ hash } .json ` ) ) ;
}
const chunks = await Promise . all ( promises ) ;
const result = { } ;
2021-12-13 13:41:51 -05:00
let ignored = 0 ;
2021-11-12 16:58:35 -05:00
for ( const chunk of chunks ) {
if ( ! chunk )
continue ;
2021-12-13 13:41:51 -05:00
for ( const [ key , val ] of Object . entries ( chunk ) ) {
if ( typeof val === 'string' && val . length > 0 )
result [ key ] = val ;
else
ignored ++ ;
2017-11-13 01:23:39 -05:00
}
}
2021-12-13 13:41:51 -05:00
if ( ignored > 0 )
this . log . debug ( ` Ignored ${ ignored } invalid values while loading ${ locale } chunks. ` ) ;
return result ;
2017-11-13 01:23:39 -05:00
}
async setLocale ( new _locale ) {
const old _locale = this . _ . locale ;
if ( new _locale === old _locale )
return [ ] ;
2019-05-03 19:30:46 -04:00
await this . loadDayjsLocale ( new _locale ) ;
2017-11-13 01:23:39 -05:00
this . _ . locale = new _locale ;
this . _ . clear ( ) ;
this . log . info ( ` Changed Locale: ${ new _locale } -- Old: ${ old _locale } ` ) ;
this . emit ( ':changed' , new _locale , old _locale ) ;
this . emit ( ':update' ) ;
if ( new _locale === 'en' ) {
// All the built-in messages are English. We don't need special
// logic to load the translations.
this . emit ( ':loaded' , [ ] ) ;
return [ ] ;
}
2019-10-05 20:55:32 -04:00
const data = this . localeData [ new _locale ] ;
const phrases = await this . loadLocale ( data ? . id || new _locale ) ;
2017-11-13 01:23:39 -05:00
if ( this . _ . locale !== new _locale )
throw new Error ( 'locale has changed since we started loading' ) ;
const added = this . _ . extend ( phrases ) ;
if ( added . length ) {
this . log . info ( ` Loaded Locale: ${ new _locale } -- Phrases: ${ added . length } ` ) ;
this . emit ( ':loaded' , added ) ;
this . emit ( ':update' ) ;
}
return added ;
}
2019-05-03 19:30:46 -04:00
async loadDayjsLocale ( locale ) {
2021-11-12 16:58:35 -05:00
if ( locale === 'en' || locale === 'en-arrr' )
2019-05-03 19:30:46 -04:00
return ;
try {
await import (
/* webpackMode: 'lazy' */
/* webpackChunkName: 'i18n-[index]' */
2019-12-31 18:09:09 -05:00
` dayjs/locale/ ${ locale } .js `
2019-05-03 19:30:46 -04:00
) ;
} catch ( err ) {
this . log . warn ( ` Unable to load day.js locale data for locale " ${ locale } " ` , err ) ;
}
}
2017-11-13 01:23:39 -05:00
has ( key ) {
return this . _ . has ( key ) ;
}
2019-06-09 19:48:26 -04:00
formatNode ( ... args ) {
return this . _ . formatNode ( ... args ) ;
}
2019-05-03 19:30:46 -04:00
toLocaleString ( ... args ) {
return this . _ . toLocaleString ( ... args ) ;
}
2019-10-06 20:01:22 -04:00
toRelativeTime ( ... args ) {
return this . _ . formatRelativeTime ( ... args ) ;
2019-05-03 19:30:46 -04:00
}
2017-11-13 01:23:39 -05:00
formatNumber ( ... args ) {
return this . _ . formatNumber ( ... args ) ;
}
2020-07-26 17:50:14 -04:00
formatDuration ( ... args ) {
return this . _ . formatDuration ( ... args ) ;
}
2019-05-03 19:30:46 -04:00
formatDate ( ... args ) {
return this . _ . formatDate ( ... args )
}
formatTime ( ... args ) {
return this . _ . formatTime ( ... args )
}
formatDateTime ( ... args ) {
return this . _ . formatDateTime ( ... args )
}
2018-07-24 16:11:02 -04:00
t ( key , ... args ) {
2019-10-04 14:57:13 -04:00
this . see ( key , ... args ) ;
2018-07-24 16:11:02 -04:00
return this . _ . t ( key , ... args ) ;
2017-11-13 01:23:39 -05:00
}
2018-07-03 18:39:00 -04:00
2018-07-24 16:11:02 -04:00
tList ( key , ... args ) {
2019-10-04 14:57:13 -04:00
this . see ( key , ... args ) ;
2018-07-24 16:11:02 -04:00
return this . _ . tList ( key , ... args ) ;
2018-07-03 18:39:00 -04:00
}
2017-11-13 01:23:39 -05:00
}
// ============================================================================
// Transformations
// ============================================================================
const DOLLAR _REGEX = /\$/g ;
2019-06-19 20:57:14 -04:00
const REPLACE = String . prototype . replace ;
2018-07-03 18:39:00 -04:00
2017-11-13 01:23:39 -05:00
export function transformPhrase ( phrase , substitutions , locale , token _regex , formatters ) {
const is _array = Array . isArray ( phrase ) ;
if ( substitutions == null )
return is _array ? phrase [ 0 ] : phrase ;
let result = phrase ;
const options = typeof substitutions === 'number' ? { count : substitutions } : substitutions ;
if ( is _array )
2019-05-03 19:30:46 -04:00
result = result [ 0 ] ;
2017-11-13 01:23:39 -05:00
if ( typeof result === 'string' )
result = REPLACE . call ( result , token _regex , ( expr , arg , fmt ) => {
2018-04-28 17:56:03 -04:00
let val = get ( arg , options ) ;
if ( val == null )
2017-11-13 01:23:39 -05:00
return '' ;
const formatter = formatters [ fmt ] ;
if ( typeof formatter === 'function' )
val = formatter ( val , locale , options ) ;
else if ( typeof val === 'string' )
val = REPLACE . call ( val , DOLLAR _REGEX , '$$' ) ;
return val ;
} ) ;
return result ;
}