mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-09 15:50:53 +00:00
4.12.5
* Added: Localization Message Capture. Message capture is a developer feature intended to make it easier to add new strings to the localization project. * Added: Translation Tester. A tool to allow translators to test translations directly in the app before submitting them to the localization project. * Fixed: Modified emotes not appearing correctly in tool-tips.
This commit is contained in:
parent
ebb954e6c1
commit
f4c989561e
25 changed files with 521 additions and 82 deletions
224
src/i18n.js
224
src/i18n.js
|
@ -6,12 +6,27 @@
|
|||
|
||||
import Parser from '@ffz/icu-msgparser';
|
||||
|
||||
import {SERVER} from 'utilities/constants';
|
||||
import {get, pick_random, timeout} from 'utilities/object';
|
||||
import {SERVER, DEBUG} from 'utilities/constants';
|
||||
import {get, pick_random, shallow_copy, deep_copy} from 'utilities/object';
|
||||
import Module from 'utilities/module';
|
||||
|
||||
import NewTransCore from 'utilities/translation-core';
|
||||
|
||||
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'
|
||||
]
|
||||
|
||||
const FACES = ['(・`ω´・)', ';;w;;', 'owo', 'ono', 'oAo', 'oxo', 'ovo;', 'UwU', '>w<', '^w^', '> w >', 'v.v'],
|
||||
|
||||
transformText = (ast, fn) => ast.map(node => {
|
||||
|
@ -68,6 +83,33 @@ export class TranslationManager extends Module {
|
|||
ja: { name: '日本語' }*/
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('i18n.debug.transform', {
|
||||
default: null,
|
||||
|
@ -128,6 +170,8 @@ export class TranslationManager extends Module {
|
|||
}
|
||||
|
||||
onEnable() {
|
||||
this.capturing = this.settings.get('i18n.debug.capture');
|
||||
|
||||
this._ = new NewTransCore({ //TranslationCore({
|
||||
warn: (...args) => this.log.warn(...args),
|
||||
});
|
||||
|
@ -142,6 +186,79 @@ export class TranslationManager extends Module {
|
|||
this.locale = this.settings.get('i18n.locale');
|
||||
}
|
||||
|
||||
broadcast(msg) {
|
||||
if ( this._broadcaster )
|
||||
this._broadcaster.postMessage(msg);
|
||||
}
|
||||
|
||||
getKeys() {
|
||||
return deep_copy(Array.from(this.captured.values()));
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
handleMessage(event) {
|
||||
const msg = event.data;
|
||||
if ( ! msg )
|
||||
return;
|
||||
|
||||
if ( msg.type === 'update-key' ) {
|
||||
this._.extend({
|
||||
[msg.key]: msg.phrase
|
||||
});
|
||||
|
||||
this.emit(':loaded', [msg.key]);
|
||||
this.emit(':update');
|
||||
|
||||
} 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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
get locale() {
|
||||
return this._.locale;
|
||||
}
|
||||
|
@ -151,57 +268,92 @@ export class TranslationManager extends Module {
|
|||
}
|
||||
|
||||
|
||||
handleMessage(event) {
|
||||
const msg = event.data;
|
||||
if ( ! msg )
|
||||
see(key, phrase, options) {
|
||||
if ( ! this.capturing )
|
||||
return;
|
||||
|
||||
if ( msg.type === 'seen' )
|
||||
this.see(msg.key, true);
|
||||
|
||||
else if ( msg.type === 'request-keys' ) {
|
||||
this.broadcast({type: 'keys', keys: Array.from(this._seen)})
|
||||
let stack;
|
||||
try {
|
||||
stack = new Error().stack;
|
||||
} catch(err) {
|
||||
/* :thinking: */
|
||||
try {
|
||||
stack = err.stack;
|
||||
} catch(err_again) { /* aww */ }
|
||||
}
|
||||
|
||||
else if ( msg.type === 'keys' )
|
||||
this.emit(':receive-keys', msg.keys);
|
||||
let store = this.captured.get(key);
|
||||
if ( ! store )
|
||||
this.captured.set(key, store = {key, phrase, hits: 0, calls: []});
|
||||
|
||||
|
||||
store.options = this.pluckVariables(key, options);
|
||||
store.hits++;
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async getKeys() {
|
||||
this.broadcast({type: 'request-keys'});
|
||||
pluckVariables(key, options) {
|
||||
const ast = this._.cache.get(key);
|
||||
if ( ! ast )
|
||||
return null;
|
||||
|
||||
let data;
|
||||
const out = {};
|
||||
this._doPluck(ast, options, out);
|
||||
if ( Object.keys(out).length )
|
||||
return out;
|
||||
|
||||
try {
|
||||
data = await timeout(this.waitFor(':receive-keys'), 100);
|
||||
} catch(err) { /* no-op */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( data )
|
||||
for(const val of data)
|
||||
this._seen.add(val);
|
||||
_doPluck(ast, options, out) {
|
||||
if ( Array.isArray(ast) ) {
|
||||
for(const val of ast)
|
||||
this._doPluck(val, options, out);
|
||||
|
||||
return this._seen;
|
||||
return;
|
||||
}
|
||||
|
||||
if ( typeof ast === 'object' && ast.v )
|
||||
out[ast.v] = shallow_copy(get(ast.v, options));
|
||||
}
|
||||
|
||||
|
||||
broadcast(msg) {
|
||||
if ( this._broadcaster )
|
||||
this._broadcaster.postMessage(msg)
|
||||
}
|
||||
|
||||
|
||||
see(key, from_broadcast = false) {
|
||||
if ( this._seen.has(key) )
|
||||
recordCall(store, stack) { // eslint-disable-line class-methods-use-this
|
||||
if ( ! Array.isArray(stack) )
|
||||
return;
|
||||
|
||||
this._seen.add(key);
|
||||
this.emit(':seen', key);
|
||||
for(const line of stack) {
|
||||
const match = STACK_SPLITTER.exec(line);
|
||||
if ( ! match )
|
||||
continue;
|
||||
|
||||
if ( ! from_broadcast )
|
||||
this.broadcast({type: 'seen', key});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async loadLocale(locale) {
|
||||
if ( locale === 'en' )
|
||||
return {};
|
||||
|
@ -414,12 +566,12 @@ export class TranslationManager extends Module {
|
|||
}
|
||||
|
||||
t(key, ...args) {
|
||||
this.see(key);
|
||||
this.see(key, ...args);
|
||||
return this._.t(key, ...args);
|
||||
}
|
||||
|
||||
tList(key, ...args) {
|
||||
this.see(key);
|
||||
this.see(key, ...args);
|
||||
return this._.tList(key, ...args);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue