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' ;
2017-11-13 01:23:39 -05:00
import { SERVER } from 'utilities/constants' ;
2019-05-31 16:05:50 -04:00
import { get , pick _random , timeout } from 'utilities/object' ;
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
2018-04-15 17:19:22 -04:00
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-05-03 19:30:46 -04:00
transformText = ( ast , fn ) => {
return ast . map ( node => {
if ( typeof node === 'string' )
return fn ( node ) ;
2018-04-15 17:19:22 -04:00
2019-05-03 19:30:46 -04:00
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 )
}
2018-04-15 17:19:22 -04:00
2019-05-03 19:30:46 -04:00
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 ;
2017-11-13 01:23:39 -05:00
this . availableLocales = [ 'en' ] ; //, 'de', 'ja'];
this . localeData = {
2019-05-03 19:30:46 -04:00
en : { name : 'English' } / * ,
de : { name : 'Deutsch' } ,
ja : { name : '日本語' } * /
2017-11-13 01:23:39 -05:00
}
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 ] ;
this . emit ( ':update' )
}
} ) ;
2017-11-13 01:23:39 -05:00
this . settings . add ( 'i18n.locale' , {
default : - 1 ,
process : ( ctx , val ) => {
if ( val === - 1 )
val = ctx . get ( 'context.session.languageCode' ) ;
return this . availableLocales . includes ( val ) ? val : 'en'
} ,
_ui : {
path : 'Appearance > Localization >> General' ,
title : 'Language' ,
// description: '',
component : 'setting-select-box' ,
data : ( profile , val ) => [ {
selected : val === - 1 ,
value : - 1 ,
i18n _key : 'setting.appearance.localization.general.language.twitch' ,
title : "Use Twitch's Language"
} ] . concat ( this . availableLocales . map ( l => ( {
selected : val === l ,
value : l ,
title : this . localeData [ l ] . name
} ) ) )
} ,
changed : val => this . locale = val
} ) ;
}
onEnable ( ) {
2019-05-03 19:30:46 -04:00
this . _ = new NewTransCore ( { //TranslationCore({
warn : ( ... args ) => this . log . warn ( ... args ) ,
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' ) ;
}
get locale ( ) {
return this . _ . locale ;
}
set locale ( new _locale ) {
this . setLocale ( new _locale ) ;
}
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 ;
2018-07-24 16:11:02 -04:00
if ( msg . type === 'seen' )
this . see ( msg . key , true ) ;
else if ( msg . type === 'request-keys' ) {
this . broadcast ( { type : 'keys' , keys : Array . from ( this . _seen ) } )
}
else if ( msg . type === 'keys' )
this . emit ( ':receive-keys' , msg . keys ) ;
}
async getKeys ( ) {
this . broadcast ( { type : 'request-keys' } ) ;
let data ;
try {
data = await timeout ( this . waitFor ( ':receive-keys' ) , 100 ) ;
} catch ( err ) { /* no-op */ }
if ( data )
for ( const val of data )
this . _seen . add ( val ) ;
return this . _seen ;
}
broadcast ( msg ) {
if ( this . _broadcaster )
this . _broadcaster . postMessage ( msg )
}
see ( key , from _broadcast = false ) {
if ( this . _seen . has ( key ) )
return ;
this . _seen . add ( key ) ;
this . emit ( ':seen' , key ) ;
if ( ! from _broadcast )
this . broadcast ( { type : 'seen' , key } ) ;
}
2017-11-13 01:23:39 -05:00
async loadLocale ( locale ) {
2019-05-03 19:30:46 -04:00
if ( locale === 'en' )
2017-11-13 01:23:39 -05:00
return { } ;
2019-05-03 22:36:26 -04:00
/ * i f ( l o c a l e = = = ' d e ' )
2017-11-13 01:23:39 -05:00
return {
site : {
menu _button : 'FrankerFaceZ Leitstelle'
} ,
player : {
reset _button : 'Doppelklicken, um den Player zurückzusetzen'
} ,
setting : {
reset : 'Zurücksetzen' ,
appearance : {
_ : 'Aussehen' ,
description : 'Personalisieren Sie das Aussehen von Twitch. Ändern Sie das Farbschema und die Schriften und stimmen Sie das Layout so ab, dass Sie ein optimales Erlebnis erleben.<br><br>(Yes, this is Google Translate still.)' ,
localization : {
_ : 'Lokalisierung' ,
general : {
language : {
_ : 'Sprache' ,
twitch : "Verwenden Sie Twitch's Sprache"
}
} ,
dates _and _times : {
_ : 'Termine und Zeiten' ,
allow _relative _times : {
_ : 'Relative Zeiten zulassen' ,
description : 'Wenn dies aktiviert ist, zeigt FrankerFaceZ einige Male in einem relativen Format an. <br>Beispiel: vor 3 Stunden'
}
}
} ,
layout : 'Layout' ,
theme : 'Thema'
} ,
profiles : {
_ : 'Profile' ,
active : 'Dieses Profil ist aktiv.' ,
inactive : {
_ : 'Dieses Profil ist nicht aktiv.' ,
description : 'Dieses Profil stimmt nicht mit dem aktuellen Kontext überein und ist momentan nicht aktiv, so dass Sie keine Änderungen sehen, die Sie hier bei Twitch vorgenommen haben.'
} ,
configure : 'Konfigurieren' ,
default : {
_ : 'Standard Profil' ,
description : 'Einstellungen, die überall auf Twitch angewendet werden.'
} ,
moderation : {
_ : 'Mäßigung' ,
description : 'Einstellungen, die gelten, wenn Sie ein Moderator des aktuellen Kanals sind.'
}
} ,
add _ons : {
_ : 'Erweiterung'
} ,
2019-05-03 22:36:26 -04:00
'inherited-from' : 'Vererbt von: {title}' ,
'overridden-by' : 'Überschrieben von: {title}'
2017-11-13 01:23:39 -05:00
} ,
'main-menu' : {
search : 'Sucheinstellungen' ,
about : {
_ : 'Über' ,
news : 'Nachrichten' ,
support : 'Unterstützung'
}
}
}
if ( locale === 'ja' )
return {
greeting : 'こんにちは' ,
site : {
menu _button : 'FrankerFaceZコントロールセンター'
} ,
setting : {
appearance : {
_ : '外観' ,
localization : '局地化' ,
layout : '設計' ,
theme : '題材'
}
} ,
'main-menu' : {
search : '検索設定' ,
2019-05-03 22:36:26 -04:00
version : 'バージョン{version}' ,
2017-11-13 01:23:39 -05:00
about : {
_ : '約' ,
news : '便り' ,
support : '対応'
}
}
2019-05-03 22:36:26 -04:00
} * /
2017-11-13 01:23:39 -05:00
const resp = await fetch ( ` ${ SERVER } /script/i18n/ ${ locale } .json ` ) ;
if ( ! resp . ok ) {
if ( resp . status === 404 ) {
this . log . info ( ` Cannot Load Locale: ${ locale } ` ) ;
return { } ;
}
this . log . warn ( ` Cannot Load Locale: ${ locale } -- Status: ${ resp . status } ` ) ;
throw new Error ( ` http error ${ resp . status } loading phrases ` ) ;
}
return resp . json ( ) ;
}
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 [ ] ;
}
const phrases = await this . loadLocale ( new _locale ) ;
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 ) {
if ( locale === 'en' )
return ;
try {
await import (
/* webpackMode: 'lazy' */
/* webpackChunkName: 'i18n-[index]' */
` dayjs/locale/ ${ locale } `
) ;
} 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-05-03 19:30:46 -04:00
toLocaleString ( ... args ) {
return this . _ . toLocaleString ( ... args ) ;
}
toHumanTime ( ... args ) {
return this . _ . formatHumanTime ( ... args ) ;
}
2017-11-13 01:23:39 -05:00
formatNumber ( ... args ) {
return this . _ . formatNumber ( ... 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 ) {
this . see ( key ) ;
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 ) {
this . see ( key ) ;
return this . _ . tList ( key , ... args ) ;
2018-07-03 18:39:00 -04:00
}
2017-11-13 01:23:39 -05:00
}
// ============================================================================
// TranslationCore
// ============================================================================
2019-05-03 19:30:46 -04:00
const REPLACE = String . prototype . replace ; / * ,
2017-11-13 01:23:39 -05:00
SPLIT = String . prototype . split ;
const DEFAULT _FORMATTERS = {
en _plural : n => n !== 1 ? 's' : '' ,
number : ( n , locale ) => n . toLocaleString ( locale )
}
export default class TranslationCore {
constructor ( options ) {
options = options || { } ;
this . warn = options . warn ;
this . phrases = new Map ;
this . extend ( options . phrases ) ;
this . locale = options . locale || 'en' ;
this . defaultLocale = options . defaultLocale || this . locale ;
2018-04-15 17:19:22 -04:00
this . transformation = null ;
2017-11-13 01:23:39 -05:00
const allowMissing = options . allowMissing ? transformPhrase : null ;
this . onMissingKey = typeof options . onMissingKey === 'function' ? options . onMissingKey : allowMissing ;
this . transformPhrase = typeof options . transformPhrase === 'function' ? options . transformPhrase : transformPhrase ;
2018-07-03 18:39:00 -04:00
this . transformList = typeof options . transformList === 'function' ? options . transformList : transformList ;
2019-05-03 19:30:46 -04:00
this . delimiter = options . delimiter || /\s*\|\|\|\|\s/ ;
2017-11-13 01:23:39 -05:00
this . tokenRegex = options . tokenRegex || /%\{(.*?)(?:\|(.*?))?\}/g ;
this . formatters = Object . assign ( { } , DEFAULT _FORMATTERS , options . formatters || { } ) ;
}
formatNumber ( value ) {
return value . toLocaleString ( this . locale ) ;
}
extend ( phrases , prefix ) {
const added = [ ] ;
for ( const key in phrases )
if ( has ( phrases , key ) ) {
let phrase = phrases [ key ] ;
const pref _key = prefix ? key === '_' ? prefix : ` ${ prefix } . ${ key } ` : key ;
if ( typeof phrase === 'object' )
added . push ( ... this . extend ( phrase , pref _key ) ) ;
else {
if ( typeof phrase === 'string' && phrase . indexOf ( this . delimiter ) !== - 1 )
phrase = SPLIT . call ( phrase , this . delimiter ) ;
this . phrases . set ( pref _key , phrase ) ;
added . push ( pref _key ) ;
}
}
return added ;
}
unset ( phrases , prefix ) {
if ( typeof phrases === 'string' )
phrases = [ phrases ] ;
const keys = Array . isArray ( phrases ) ? phrases : Object . keys ( phrases ) ;
for ( const key of keys ) {
const pref _key = prefix ? ` ${ prefix } . ${ key } ` : key ;
const phrase = phrases [ key ] ;
if ( typeof phrase === 'object' )
this . unset ( phrase , pref _key ) ;
else
this . phrases . delete ( pref _key ) ;
}
}
has ( key ) {
return this . phrases . has ( key ) ;
}
set ( key , phrase ) {
if ( typeof phrase === 'string' && phrase . indexOf ( this . delimiter ) !== - 1 )
phrase = SPLIT . call ( phrase , this . delimiter ) ;
this . phrases . set ( key , phrase ) ;
}
clear ( ) {
this . phrases . clear ( ) ;
}
replace ( phrases ) {
this . clear ( ) ;
this . extend ( phrases ) ;
}
2018-07-03 18:39:00 -04:00
preT ( key , phrase , options , use _default ) {
2017-11-13 01:23:39 -05:00
const opts = options == null ? { } : options ;
let p , locale ;
if ( use _default ) {
p = phrase ;
locale = this . defaultLocale ;
} else if ( key === undefined && phrase ) {
p = phrase ;
locale = this . defaultLocale ;
if ( this . warn )
this . warn ( ` Translation key not generated with phrase " ${ phrase } " ` ) ;
} else if ( this . phrases . has ( key ) ) {
p = this . phrases . get ( key ) ;
locale = this . locale ;
} else if ( phrase ) {
if ( this . warn && this . locale !== this . defaultLocale )
this . warn ( ` Missing translation for key " ${ key } " in locale " ${ this . locale } " ` ) ;
p = phrase ;
locale = this . defaultLocale ;
} else if ( this . onMissingKey )
return this . onMissingKey ( key , opts , this . locale , this . tokenRegex , this . formatters ) ;
else {
if ( this . warn )
this . warn ( ` Missing translation for key " ${ key } " in locale " ${ this . locale } " ` ) ;
return key ;
}
2018-04-15 17:19:22 -04:00
if ( this . transformation )
p = this . transformation ( key , p , opts , locale , this . tokenRegex ) ;
2018-07-03 18:39:00 -04:00
return [ p , opts , locale ] ;
}
t ( key , phrase , options , use _default ) {
const [ p , opts , locale ] = this . preT ( key , phrase , options , use _default ) ;
2017-11-13 01:23:39 -05:00
return this . transformPhrase ( p , opts , locale , this . tokenRegex , this . formatters ) ;
}
2018-07-03 18:39:00 -04:00
tList ( key , phrase , options , use _default ) {
const [ p , opts , locale ] = this . preT ( key , phrase , options , use _default ) ;
return this . transformList ( p , opts , locale , this . tokenRegex , this . formatters ) ;
}
2019-05-03 19:30:46 -04:00
} * /
2017-11-13 01:23:39 -05:00
// ============================================================================
// Transformations
// ============================================================================
const DOLLAR _REGEX = /\$/g ;
2018-07-03 18:39:00 -04:00
export function transformList ( phrase , substitutions , locale , token _regex , formatters ) {
const is _array = Array . isArray ( phrase ) ;
if ( substitutions == null )
return is _array ? phrase [ 0 ] : phrase ;
let p = phrase ;
const options = typeof substitutions === 'number' ? { count : substitutions } : substitutions ;
if ( is _array )
2019-05-03 19:30:46 -04:00
p = p [ 0 ] ;
2018-07-03 18:39:00 -04:00
const result = [ ] ;
token _regex . lastIndex = 0 ;
let idx = 0 , match ;
while ( ( match = token _regex . exec ( p ) ) ) {
const nix = match . index ,
arg = match [ 1 ] ,
fmt = match [ 2 ] ;
if ( nix !== idx )
result . push ( p . slice ( idx , nix ) ) ;
let val = get ( arg , options ) ;
if ( val != null ) {
const formatter = formatters [ fmt ] ;
if ( typeof formatter === 'function' )
val = formatter ( val , locale , options ) ;
else if ( typeof val === 'string' )
val = REPLACE . call ( val , DOLLAR _REGEX , '$$' ) ;
result . push ( val ) ;
}
idx = nix + match [ 0 ] . length ;
}
if ( idx < p . length )
result . push ( p . slice ( idx ) ) ;
return result ;
}
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 ;
}