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' ;
2019-10-04 14:57:13 -04:00
import { SERVER , DEBUG } from 'utilities/constants' ;
import { get , pick _random , shallow _copy , deep _copy } 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
2019-10-04 14:57:13 -04:00
const STACK _SPLITTER = /\s*at\s+(.+?)\s+\((.+)\)$/ ,
SOURCE _SPLITTER = /^(.+):\/\/(.+?):(\d+:\d+)$/ ;
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 ;
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
}
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 ) => {
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-10-04 14:57:13 -04:00
this . capturing = this . settings . get ( 'i18n.debug.capture' ) ;
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' ) ;
}
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 ( ) {
return deep _copy ( Array . from ( this . captured . values ( ) ) ) ;
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 ( ) {
return this . _ . locale ;
}
set locale ( new _locale ) {
this . setLocale ( new _locale ) ;
}
2018-07-24 16:11:02 -04:00
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 ) ;
if ( ! store )
this . captured . set ( key , store = { key , phrase , hits : 0 , calls : [ ] } ) ;
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
}
if ( typeof ast === 'object' && ast . v )
out [ ast . v ] = shallow _copy ( get ( ast . v , options ) ) ;
}
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 ;
const out = ` ${ match [ 1 ] } ( ${ location [ 2 ] } : ${ location [ 3 ] } ) ` ;
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
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-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 ) ;
}
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 ) {
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 ;
}