'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';
import {has} from 'utilities/object';
import Module from 'utilities/module';
// ============================================================================
// 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: '日本語' }
}
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)
});*/
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;
}
async loadLocale(locale) {
/*if ( locale === 'en' )
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.
(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.
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;
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;
}
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) => {
if ( ! has(options, arg) )
return '';
let val = options[arg];
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);
}