1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 12:55:55 +00:00
FrankerFaceZ/src/i18n.js
2017-11-13 01:23:39 -05:00

489 lines
No EOL
12 KiB
JavaScript

'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.<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;
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);
}