2017-11-13 01:23:39 -05:00
'use strict' ;
// ============================================================================
// Localization
// This is based on Polyglot, but with some changes to avoid dependencies on
// additional libraries and with support for Vue.
// ============================================================================
import { SERVER } from 'utilities/constants' ;
2018-04-28 17:56:03 -04:00
import { get , pick _random , has } from 'utilities/object' ;
2017-11-13 01:23:39 -05:00
import Module from 'utilities/module' ;
2018-04-15 17:19:22 -04:00
const FACES = [ '(・`ω´・)' , ';;w;;' , 'owo' , 'UwU' , '>w<' , '^w^' ] ,
format _text = ( phrase , token _regex , formatter ) => {
const out = [ ] ;
let i = 0 , match ;
token _regex . lastIndex = 0 ;
while ( ( match = token _regex . exec ( phrase ) ) ) {
if ( match . index !== i )
out . push ( formatter ( phrase . slice ( i , match . index ) ) )
out . push ( match [ 0 ] ) ;
i = match . index + match [ 0 ] . length ;
}
if ( i < phrase . length )
out . push ( formatter ( phrase . slice ( i ) ) ) ;
return out . join ( '' )
} ,
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 = {
double : ( key , text ) =>
` ${ text } ${ text } ` ,
upper : ( key , text , opts , locale , token _regex ) =>
format _text ( text , token _regex , t => t . toUpperCase ( ) ) ,
lower : ( key , text , opts , locale , token _regex ) =>
format _text ( text , token _regex , t => t . toLowerCase ( ) ) ,
append _key : ( key , text ) => ` ${ text } ( ${ key } ) ` ,
owo : ( key , text , opts , locale , token _regex ) =>
format _text ( text , token _regex , t => owo ( t ) )
} ;
2017-11-13 01:23:39 -05:00
// ============================================================================
// TranslationManager
// ============================================================================
export class TranslationManager extends Module {
constructor ( ... args ) {
super ( ... args ) ;
this . inject ( 'settings' ) ;
this . availableLocales = [ 'en' ] ; //, 'de', 'ja'];
this . localeData = {
en : { name : 'English' } ,
//de: { name: 'Deutsch' },
//ja: { name: '日本語' }
}
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' } ,
{ 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 ( ) {
this . _ = new TranslationCore ; / * ( {
awarn : ( ... args ) => this . log . info ( ... args )
} ) ; * /
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 ) ;
}
toLocaleString ( thing ) {
if ( thing && thing . toLocaleString )
return thing . toLocaleString ( this . _ . locale ) ;
return thing ;
}
2018-04-06 21:12:12 -04:00
toHumanTime ( duration , factor = 1 ) {
// TODO: Make this better. Make all time handling better in fact.
duration = Math . floor ( duration ) ;
const years = Math . floor ( ( duration * factor ) / 31536000 ) / factor ;
if ( years >= 1 )
return this . t ( 'human-time.years' , '%{count} year%{count|en_plural}' , years ) ;
const days = Math . floor ( ( duration %= 31536000 ) / 86400 ) ;
if ( days >= 1 )
return this . t ( 'human-time.days' , '%{count} day%{count|en_plural}' , days ) ;
const hours = Math . floor ( ( duration %= 86400 ) / 3600 ) ;
if ( hours >= 1 )
return this . t ( 'human-time.hours' , '%{count} hour%{count|en_plural}' , hours ) ;
const minutes = Math . floor ( ( duration %= 3600 ) / 60 ) ;
if ( minutes >= 1 )
return this . t ( 'human-time.minutes' , '%{count} minute%{count|en_plural}' , minutes ) ;
const seconds = duration % 60 ;
if ( seconds >= 1 )
return this . t ( 'human-time.seconds' , '%{count} second%{count|en_plural}' , seconds ) ;
return this . t ( 'human-time.none' , 'less than a second' ) ;
}
2017-11-13 01:23:39 -05:00
async loadLocale ( locale ) {
/ * i f ( l o c a l e = = = ' e n ' )
return { } ;
if ( locale === 'de' )
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'
} ,
'inherited-from' : 'Vererbt von: %{title}' ,
'overridden-by' : 'Überschrieben von: %{title}'
} ,
'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 : '検索設定' ,
version : 'バージョン%{version}' ,
about : {
_ : '約' ,
news : '便り' ,
support : '対応'
}
}
} * /
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 [ ] ;
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 ;
}
has ( key ) {
return this . _ . has ( key ) ;
}
formatNumber ( ... args ) {
return this . _ . formatNumber ( ... args ) ;
}
t ( ... args ) {
return this . _ . t ( ... args ) ;
}
}
// ============================================================================
// TranslationCore
// ============================================================================
const REPLACE = String . prototype . replace ,
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 ;
this . delimiter = options . delimiter || /\s*\|\|\|\|\s*/ ;
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 ) ;
}
t ( key , phrase , options , use _default ) {
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 ) ;
2017-11-13 01:23:39 -05:00
return this . transformPhrase ( p , opts , locale , this . tokenRegex , this . formatters ) ;
}
}
// ============================================================================
// Transformations
// ============================================================================
const DOLLAR _REGEX = /\$/g ;
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 )
result = result [ pluralTypeIndex (
locale || 'en' ,
has ( options , 'count' ) ? options . count : 1
) ] || result [ 0 ] ;
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 ;
}
// ============================================================================
// Plural Nonsense
// ============================================================================
const PLURAL _TYPE _TO _LANG = {
arabic : [ 'ar' ] ,
chinese : [ 'fa' , 'id' , 'ja' , 'ko' , 'lo' , 'ms' , 'th' , 'tr' , 'zh' ] ,
german : [ 'da' , 'de' , 'en' , 'es' , 'es' , 'fi' , 'el' , 'he' , 'hu' , 'it' , 'nl' , 'no' , 'pt' , 'sv' ] ,
french : [ 'fr' , 'tl' , 'pt-br' ] ,
russian : [ 'hr' , 'ru' , 'lt' ] ,
czech : [ 'cs' , 'sk' ] ,
polish : [ 'pl' ] ,
icelandic : [ 'is' ]
} ;
const PLURAL _LANG _TO _TYPE = { } ;
for ( const type in PLURAL _TYPE _TO _LANG ) // eslint-disable-line guard-for-in
for ( const lang of PLURAL _TYPE _TO _LANG [ type ] )
PLURAL _LANG _TO _TYPE [ lang ] = type ;
const PLURAL _TYPES = {
arabic : n => {
if ( n < 3 ) return n ;
const n1 = n % 100 ;
if ( n1 >= 3 && n1 <= 10 ) return 3 ;
return n1 >= 11 ? 4 : 5 ;
} ,
chinese : ( ) => 0 ,
german : n => n !== 1 ? 1 : 0 ,
french : n => n > 1 ? 1 : 0 ,
russian : n => {
const n1 = n % 10 , n2 = n % 100 ;
if ( n1 === 1 && n2 !== 11 ) return 0 ;
return n1 >= 2 && n1 <= 4 && ( n2 < 10 || n2 >= 20 ) ? 1 : 2 ;
} ,
czech : n => {
if ( n === 1 ) return 0 ;
return n >= 2 && n <= 4 ? 1 : 2 ;
} ,
polish : n => {
if ( n === 1 ) return 0 ;
const n1 = n % 10 , n2 = n % 100 ;
return n1 >= 2 && n1 <= 4 && ( n2 < 10 || n2 >= 20 ) ? 1 : 2 ;
} ,
icelandic : n => n % 10 !== 1 || n % 100 === 11 ? 1 : 0
} ;
export function pluralTypeIndex ( locale , n ) {
let type = PLURAL _LANG _TO _TYPE [ locale ] ;
if ( ! type ) {
const idx = locale . indexOf ( '-' ) ;
if ( idx !== - 1 )
type = PLURAL _LANG _TO _TYPE [ locale . slice ( 0 , idx ) ]
}
return PLURAL _TYPES [ type || 'german' ] ( n ) ;
}