mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-04 01:58:31 +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
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "frankerfacez",
|
"name": "frankerfacez",
|
||||||
"version": "4.12.3",
|
"version": "4.12.5",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -7475,6 +7475,21 @@
|
||||||
"integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
|
"integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"sourcemapped-stacktrace": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/sourcemapped-stacktrace/-/sourcemapped-stacktrace-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-O0pcWjJqzQFVsisPlPXuNawJHHg9N9UgpJ/aDmvi9+vnS3x1C0NhwkVFzzZ1VN0Xo+bekyweoqYvBw5ZBKiNnQ==",
|
||||||
|
"requires": {
|
||||||
|
"source-map": "0.5.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"source-map": {
|
||||||
|
"version": "0.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
|
||||||
|
"integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"spdx-correct": {
|
"spdx-correct": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "frankerfacez",
|
"name": "frankerfacez",
|
||||||
"author": "Dan Salvato LLC",
|
"author": "Dan Salvato LLC",
|
||||||
"version": "4.12.4",
|
"version": "4.12.5",
|
||||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -79,6 +79,7 @@
|
||||||
"react": "^16.4.1",
|
"react": "^16.4.1",
|
||||||
"safe-regex": "^2.0.2",
|
"safe-regex": "^2.0.2",
|
||||||
"sortablejs": "^1.10.0-rc3",
|
"sortablejs": "^1.10.0-rc3",
|
||||||
|
"sourcemapped-stacktrace": "^1.1.11",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-clickaway": "^2.2.2",
|
"vue-clickaway": "^2.2.2",
|
||||||
"vue-color": "^2.4.6",
|
"vue-color": "^2.4.6",
|
||||||
|
|
230
src/i18n.js
230
src/i18n.js
|
@ -6,12 +6,27 @@
|
||||||
|
|
||||||
import Parser from '@ffz/icu-msgparser';
|
import Parser from '@ffz/icu-msgparser';
|
||||||
|
|
||||||
import {SERVER} from 'utilities/constants';
|
import {SERVER, DEBUG} from 'utilities/constants';
|
||||||
import {get, pick_random, timeout} from 'utilities/object';
|
import {get, pick_random, shallow_copy, deep_copy} from 'utilities/object';
|
||||||
import Module from 'utilities/module';
|
import Module from 'utilities/module';
|
||||||
|
|
||||||
import NewTransCore from 'utilities/translation-core';
|
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'],
|
const FACES = ['(・`ω´・)', ';;w;;', 'owo', 'ono', 'oAo', 'oxo', 'ovo;', 'UwU', '>w<', '^w^', '> w >', 'v.v'],
|
||||||
|
|
||||||
transformText = (ast, fn) => ast.map(node => {
|
transformText = (ast, fn) => ast.map(node => {
|
||||||
|
@ -68,6 +83,33 @@ export class TranslationManager extends Module {
|
||||||
ja: { name: '日本語' }*/
|
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', {
|
this.settings.add('i18n.debug.transform', {
|
||||||
default: null,
|
default: null,
|
||||||
|
@ -128,6 +170,8 @@ export class TranslationManager extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnable() {
|
onEnable() {
|
||||||
|
this.capturing = this.settings.get('i18n.debug.capture');
|
||||||
|
|
||||||
this._ = new NewTransCore({ //TranslationCore({
|
this._ = new NewTransCore({ //TranslationCore({
|
||||||
warn: (...args) => this.log.warn(...args),
|
warn: (...args) => this.log.warn(...args),
|
||||||
});
|
});
|
||||||
|
@ -142,6 +186,79 @@ export class TranslationManager extends Module {
|
||||||
this.locale = this.settings.get('i18n.locale');
|
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() {
|
get locale() {
|
||||||
return this._.locale;
|
return this._.locale;
|
||||||
}
|
}
|
||||||
|
@ -151,56 +268,91 @@ export class TranslationManager extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleMessage(event) {
|
see(key, phrase, options) {
|
||||||
const msg = event.data;
|
if ( ! this.capturing )
|
||||||
if ( ! msg )
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( msg.type === 'seen' )
|
let stack;
|
||||||
this.see(msg.key, true);
|
|
||||||
|
|
||||||
else if ( msg.type === 'request-keys' ) {
|
|
||||||
this.broadcast({type: 'keys', keys: Array.from(this._seen)})
|
|
||||||
}
|
|
||||||
|
|
||||||
else if ( msg.type === 'keys' )
|
|
||||||
this.emit(':receive-keys', msg.keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async getKeys() {
|
|
||||||
this.broadcast({type: 'request-keys'});
|
|
||||||
|
|
||||||
let data;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
data = await timeout(this.waitFor(':receive-keys'), 100);
|
stack = new Error().stack;
|
||||||
} catch(err) { /* no-op */ }
|
} catch(err) {
|
||||||
|
/* :thinking: */
|
||||||
|
try {
|
||||||
|
stack = err.stack;
|
||||||
|
} catch(err_again) { /* aww */ }
|
||||||
|
}
|
||||||
|
|
||||||
if ( data )
|
let store = this.captured.get(key);
|
||||||
for(const val of data)
|
if ( ! store )
|
||||||
this._seen.add(val);
|
this.captured.set(key, store = {key, phrase, hits: 0, calls: []});
|
||||||
|
|
||||||
return this._seen;
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
broadcast(msg) {
|
pluckVariables(key, options) {
|
||||||
if ( this._broadcaster )
|
const ast = this._.cache.get(key);
|
||||||
this._broadcaster.postMessage(msg)
|
if ( ! ast )
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const out = {};
|
||||||
|
this._doPluck(ast, options, out);
|
||||||
|
if ( Object.keys(out).length )
|
||||||
|
return out;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_doPluck(ast, options, out) {
|
||||||
|
if ( Array.isArray(ast) ) {
|
||||||
|
for(const val of ast)
|
||||||
|
this._doPluck(val, options, out);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( typeof ast === 'object' && ast.v )
|
||||||
|
out[ast.v] = shallow_copy(get(ast.v, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
see(key, from_broadcast = false) {
|
recordCall(store, stack) { // eslint-disable-line class-methods-use-this
|
||||||
if ( this._seen.has(key) )
|
if ( ! Array.isArray(stack) )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this._seen.add(key);
|
for(const line of stack) {
|
||||||
this.emit(':seen', key);
|
const match = STACK_SPLITTER.exec(line);
|
||||||
|
if ( ! match )
|
||||||
|
continue;
|
||||||
|
|
||||||
if ( ! from_broadcast )
|
const location = SOURCE_SPLITTER.exec(match[2]);
|
||||||
this.broadcast({type: 'seen', key});
|
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) {
|
async loadLocale(locale) {
|
||||||
if ( locale === 'en' )
|
if ( locale === 'en' )
|
||||||
|
@ -414,12 +566,12 @@ export class TranslationManager extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
t(key, ...args) {
|
t(key, ...args) {
|
||||||
this.see(key);
|
this.see(key, ...args);
|
||||||
return this._.t(key, ...args);
|
return this._.t(key, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
tList(key, ...args) {
|
tList(key, ...args) {
|
||||||
this.see(key);
|
this.see(key, ...args);
|
||||||
return this._.tList(key, ...args);
|
return this._.tList(key, ...args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1028,7 +1028,7 @@ export const AddonEmotes = {
|
||||||
const set_id = this.emotes.getTwitchEmoteSet(emote_id, tip.rerender),
|
const set_id = this.emotes.getTwitchEmoteSet(emote_id, tip.rerender),
|
||||||
emote_set = set_id != null && this.emotes.getTwitchSetChannel(set_id, tip.rerender);
|
emote_set = set_id != null && this.emotes.getTwitchSetChannel(set_id, tip.rerender);
|
||||||
|
|
||||||
preview = `//static-cdn.jtvnw.net/emoticons/v1/${emote_id}/3.0?_=preview`;
|
preview = `//static-cdn.jtvnw.net/emoticons/v1/${ds.id}/3.0?_=preview`;
|
||||||
fav_source = 'twitch';
|
fav_source = 'twitch';
|
||||||
|
|
||||||
if ( emote_set ) {
|
if ( emote_set ) {
|
||||||
|
|
39
src/modules/main_menu/components/i18n-open.vue
Normal file
39
src/modules/main_menu/components/i18n-open.vue
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<template lang="html">
|
||||||
|
<div>
|
||||||
|
<div class="tw-mg-b-1">
|
||||||
|
<markdown :source="t('i18n.interest', md)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="tw-button"
|
||||||
|
@click="open"
|
||||||
|
>
|
||||||
|
<span class="tw-button__icon tw-button__icon--left">
|
||||||
|
<figure class="ffz-i-language" />
|
||||||
|
</span>
|
||||||
|
<span class="tw-button__text">
|
||||||
|
{{ t('i18n.ui.open', 'Open Translation Tester') }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import I18N_MD from '../i18n.md';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
md: I18N_MD
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
open() {
|
||||||
|
window.FrankerFaceZ.get().resolve('i18n').openUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
|
@ -1,13 +1,13 @@
|
||||||
<template lang="html">
|
<template lang="html">
|
||||||
<div v-if="item.contents" :class="classes">
|
<div v-if="item.contents" :class="classes">
|
||||||
<header v-if="! item.no_header">
|
<header v-if="! item.no_header">
|
||||||
{{ t(item.i18n_key, item.title, item) }}
|
{{ t(item.i18n_key, item.title) }}
|
||||||
</header>
|
</header>
|
||||||
<section
|
<section
|
||||||
v-if="item.description"
|
v-if="item.description"
|
||||||
class="tw-pd-b-1"
|
class="tw-pd-b-1"
|
||||||
>
|
>
|
||||||
<markdown :source="t(item.desc_i18n_key, item.description, item)" />
|
<markdown :source="t(item.desc_i18n_key, item.description)" />
|
||||||
</section>
|
</section>
|
||||||
<div
|
<div
|
||||||
v-for="i in item.contents"
|
v-for="i in item.contents"
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
<div class="ffz--menu-page">
|
<div class="ffz--menu-page">
|
||||||
<header class="tw-mg-b-1">
|
<header class="tw-mg-b-1">
|
||||||
<span v-for="i in breadcrumbs" :key="i.full_key">
|
<span v-for="i in breadcrumbs" :key="i.full_key">
|
||||||
<a v-if="i !== item" href="#" @click="$emit('change-item', i, false)">{{ t(i.i18n_key, i.title, i) }}</a>
|
<a v-if="i !== item" href="#" @click="$emit('change-item', i, false)">{{ t(i.i18n_key, i.title) }}</a>
|
||||||
<strong v-if="i === item">{{ t(i.i18n_key, i.title, i) }}</strong>
|
<strong v-if="i === item">{{ t(i.i18n_key, i.title) }}</strong>
|
||||||
<template v-if="i !== item">» </template>
|
<template v-if="i !== item">» </template>
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
v-if="item.description"
|
v-if="item.description"
|
||||||
class="tw-border-t tw-pd-y-1"
|
class="tw-border-t tw-pd-y-1"
|
||||||
>
|
>
|
||||||
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description, item)" />
|
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description)" />
|
||||||
</section>
|
</section>
|
||||||
<template v-if="! item.contents || ! item.contents.length">
|
<template v-if="! item.contents || ! item.contents.length">
|
||||||
<ul class="tw-border-t tw-pd-y-1">
|
<ul class="tw-border-t tw-pd-y-1">
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
class="tw-pd-x-1"
|
class="tw-pd-x-1"
|
||||||
>
|
>
|
||||||
<a href="#" @click="$emit('change-item', i, false)">
|
<a href="#" @click="$emit('change-item', i, false)">
|
||||||
{{ t(i.i18n_key, i.title, i) }}
|
{{ t(i.i18n_key, i.title) }}
|
||||||
<span v-if="i.unseen" class="tw-pill">{{ i.unseen }}</span>
|
<span v-if="i.unseen" class="tw-pill">{{ i.unseen }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -32,10 +32,10 @@
|
||||||
class="arrow"
|
class="arrow"
|
||||||
/>
|
/>
|
||||||
<span class="tw-flex-grow-1">
|
<span class="tw-flex-grow-1">
|
||||||
{{ t(item.i18n_key, item.title, item) }}
|
{{ t(item.i18n_key, item.title) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="item.pill" class="tw-pill">
|
<span v-if="item.pill" class="tw-pill">
|
||||||
{{ item.pill_i18n_key ? t(item.pill_i18n_key, item.pill, item) : item.pill }}
|
{{ item.pill_i18n_key ? t(item.pill_i18n_key, item.pill) : item.pill }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="item.unseen" class="tw-pill">
|
<span v-else-if="item.unseen" class="tw-pill">
|
||||||
{{ item.unseen }}
|
{{ item.unseen }}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
>
|
>
|
||||||
|
|
||||||
<label :for="item.full_key" class="tw-checkbox__label">
|
<label :for="item.full_key" class="tw-checkbox__label">
|
||||||
{{ t(item.i18n_key, item.title, item) }}
|
{{ t(item.i18n_key, item.title) }}
|
||||||
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
|
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
class="tw-c-text-alt-2"
|
class="tw-c-text-alt-2"
|
||||||
style="padding-left:2.2rem"
|
style="padding-left:2.2rem"
|
||||||
>
|
>
|
||||||
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description, item)" />
|
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description)" />
|
||||||
</section>
|
</section>
|
||||||
<section
|
<section
|
||||||
v-if="item.extra"
|
v-if="item.extra"
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
>
|
>
|
||||||
<div class="tw-flex tw-align-items-center">
|
<div class="tw-flex tw-align-items-center">
|
||||||
<label :for="item.full_key">
|
<label :for="item.full_key">
|
||||||
{{ t(item.i18n_key, item.title, item) }}
|
{{ t(item.i18n_key, item.title) }}
|
||||||
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
|
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
v-if="item.description"
|
v-if="item.description"
|
||||||
class="tw-c-text-alt-2"
|
class="tw-c-text-alt-2"
|
||||||
>
|
>
|
||||||
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description, item)" />
|
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description)" />
|
||||||
</section>
|
</section>
|
||||||
<section v-if="item.extra">
|
<section v-if="item.extra">
|
||||||
<component :is="item.extra.component" :context="context" :item="item" />
|
<component :is="item.extra.component" :context="context" :item="item" />
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
>
|
>
|
||||||
<div class="tw-flex tw-align-items-start">
|
<div class="tw-flex tw-align-items-start">
|
||||||
<label :for="item.full_key" class="tw-mg-y-05">
|
<label :for="item.full_key" class="tw-mg-y-05">
|
||||||
{{ t(item.i18n_key, item.title, item) }}
|
{{ t(item.i18n_key, item.title) }}
|
||||||
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
|
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
v-if="item.description"
|
v-if="item.description"
|
||||||
class="tw-c-text-alt-2"
|
class="tw-c-text-alt-2"
|
||||||
>
|
>
|
||||||
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description, item)" />
|
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description)" />
|
||||||
</section>
|
</section>
|
||||||
<section v-if="item.extra">
|
<section v-if="item.extra">
|
||||||
<component :is="item.extra.component" :context="context" :item="item" />
|
<component :is="item.extra.component" :context="context" :item="item" />
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template lang="html">
|
<template lang="html">
|
||||||
<div class="ffz--widget ffz--hotkey-input">
|
<div class="ffz--widget ffz--hotkey-input">
|
||||||
<label :for="item.full_key">
|
<label :for="item.full_key">
|
||||||
{{ t(item.i18n_key, item.title, item) }}
|
{{ t(item.i18n_key, item.title) }}
|
||||||
</label>
|
</label>
|
||||||
<div class="tw-relative">
|
<div class="tw-relative">
|
||||||
<div class="tw-input__icon-group tw-input__icon-group--right">
|
<div class="tw-input__icon-group tw-input__icon-group--right">
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
v-if="item.description"
|
v-if="item.description"
|
||||||
class="tw-c-text-alt-2"
|
class="tw-c-text-alt-2"
|
||||||
>
|
>
|
||||||
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description, item)" />
|
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description)" />
|
||||||
</section>
|
</section>
|
||||||
<section v-if="item.extra">
|
<section v-if="item.extra">
|
||||||
<component :is="item.extra.component" :context="context" :item="item" />
|
<component :is="item.extra.component" :context="context" :item="item" />
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<template lang="html">
|
<template lang="html">
|
||||||
<div class="tw-input">
|
<div class="tw-input">
|
||||||
<header>
|
<header>
|
||||||
{{ t(item.i18n_key, item.title, item) }}
|
{{ t(item.i18n_key, item.title) }}
|
||||||
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
|
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
|
||||||
</header>
|
</header>
|
||||||
<section
|
<section
|
||||||
v-if="item.description"
|
v-if="item.description"
|
||||||
class="tw-c-text-alt-2"
|
class="tw-c-text-alt-2"
|
||||||
>
|
>
|
||||||
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description, item)" />
|
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description)" />
|
||||||
</section>
|
</section>
|
||||||
<section v-if="item.extra">
|
<section v-if="item.extra">
|
||||||
<component :is="item.extra.component" :context="context" :item="item" />
|
<component :is="item.extra.component" :context="context" :item="item" />
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
>
|
>
|
||||||
<div class="tw-flex tw-align-items-center">
|
<div class="tw-flex tw-align-items-center">
|
||||||
<label :for="item.full_key">
|
<label :for="item.full_key">
|
||||||
{{ t(item.i18n_key, item.title, item) }}
|
{{ t(item.i18n_key, item.title) }}
|
||||||
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
|
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
v-if="item.description"
|
v-if="item.description"
|
||||||
class="tw-c-text-alt-2"
|
class="tw-c-text-alt-2"
|
||||||
>
|
>
|
||||||
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description, item)" />
|
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description)" />
|
||||||
</section>
|
</section>
|
||||||
<section v-if="item.extra">
|
<section v-if="item.extra">
|
||||||
<component :is="item.extra.component" :context="context" :item="item" />
|
<component :is="item.extra.component" :context="context" :item="item" />
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
>
|
>
|
||||||
<div class="tw-flex tw-align-items-center">
|
<div class="tw-flex tw-align-items-center">
|
||||||
<label :for="item.full_key">
|
<label :for="item.full_key">
|
||||||
{{ t(item.i18n_key, item.title, item) }}
|
{{ t(item.i18n_key, item.title) }}
|
||||||
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
|
<span v-if="unseen" class="tw-pill">{{ t('setting.new', 'New') }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
v-if="item.description"
|
v-if="item.description"
|
||||||
class="tw-c-text-alt-2"
|
class="tw-c-text-alt-2"
|
||||||
>
|
>
|
||||||
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description, item)" />
|
<markdown :source="t(item.desc_i18n_key || `${item.i18n_key}.description`, item.description)" />
|
||||||
</section>
|
</section>
|
||||||
<section v-if="item.extra">
|
<section v-if="item.extra">
|
||||||
<component :is="item.extra.component" :context="context" :item="item" />
|
<component :is="item.extra.component" :context="context" :item="item" />
|
||||||
|
|
3
src/modules/main_menu/i18n.md
Normal file
3
src/modules/main_menu/i18n.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
Interested in helping out with the localization efforts? Please [join our Discord](https://discord.gg/UrAkGhT) and join the chat in the localization channel.
|
||||||
|
|
||||||
|
This tool allows translators to test their strings in real-time in the app. That way you can be sure everything looks right before submitting your translations.
|
|
@ -364,7 +364,7 @@ export default class MainMenu extends Module {
|
||||||
|
|
||||||
for(const [setting_key, def] of token.settings)
|
for(const [setting_key, def] of token.settings)
|
||||||
if ( def.ui ) { //} && def.ui.title ) {
|
if ( def.ui ) { //} && def.ui.title ) {
|
||||||
const i18n_key = `${token.i18n_key}.${def.ui.key}`
|
const i18n_key = def.ui.i18n_key ? def.ui.i18n_key : setting_key ? `setting.entry.${setting_key}` : def.ui.key ? `${token.i18n_key}.${def.ui.key}` : token.i18n_key;
|
||||||
const tok = Object.assign({
|
const tok = Object.assign({
|
||||||
i18n_key,
|
i18n_key,
|
||||||
desc_i18n_key: `${i18n_key}.description`,
|
desc_i18n_key: `${i18n_key}.description`,
|
||||||
|
@ -387,17 +387,17 @@ export default class MainMenu extends Module {
|
||||||
|
|
||||||
let terms = [
|
let terms = [
|
||||||
setting_key,
|
setting_key,
|
||||||
this.i18n.t(tok.i18n_key, tok.title, tok, true)
|
this.i18n.t(tok.i18n_key, tok.title, null, true)
|
||||||
];
|
];
|
||||||
|
|
||||||
if ( have_locale && this.i18n.has(tok.i18n_key) )
|
if ( have_locale && this.i18n.has(tok.i18n_key) )
|
||||||
terms.push(this.i18n.t(tok.i18n_key, tok.title, tok));
|
terms.push(this.i18n.t(tok.i18n_key, tok.title, null));
|
||||||
|
|
||||||
if ( tok.description ) {
|
if ( tok.description ) {
|
||||||
terms.push(this.i18n.t(tok.desc_i18n_key, tok.description, tok, true));
|
terms.push(this.i18n.t(tok.desc_i18n_key, tok.description, null, true));
|
||||||
|
|
||||||
if ( have_locale && this.i18n.has(tok.desc_i18n_key) )
|
if ( have_locale && this.i18n.has(tok.desc_i18n_key) )
|
||||||
terms.push(this.i18n.t(tok.desc_i18n_key, tok.description, tok));
|
terms.push(this.i18n.t(tok.desc_i18n_key, tok.description, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( tok.getExtraTerms )
|
if ( tok.getExtraTerms )
|
||||||
|
@ -425,20 +425,20 @@ export default class MainMenu extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! token.search_terms ) {
|
if ( ! token.search_terms ) {
|
||||||
const formatted = this.i18n.t(token.i18n_key, token.title, token, true);
|
const formatted = this.i18n.t(token.i18n_key, token.title, null, true);
|
||||||
let terms = [token.key];
|
let terms = [token.key];
|
||||||
|
|
||||||
if ( formatted && formatted.localeCompare(token.key, undefined, {sensitivity: 'base'}) )
|
if ( formatted && formatted.localeCompare(token.key, undefined, {sensitivity: 'base'}) )
|
||||||
terms.push(formatted);
|
terms.push(formatted);
|
||||||
|
|
||||||
if ( have_locale && this.i18n.has(token.i18n_key) )
|
if ( have_locale && this.i18n.has(token.i18n_key) )
|
||||||
terms.push(this.i18n.t(token.i18n_key, token.title, token));
|
terms.push(this.i18n.t(token.i18n_key, token.title, null));
|
||||||
|
|
||||||
if ( token.description ) {
|
if ( token.description ) {
|
||||||
terms.push(this.i18n.t(token.desc_i18n_key, token.description, token, true));
|
terms.push(this.i18n.t(token.desc_i18n_key, token.description, null, true));
|
||||||
|
|
||||||
if ( have_locale && this.i18n.has(token.desc_i18n_key) )
|
if ( have_locale && this.i18n.has(token.desc_i18n_key) )
|
||||||
terms.push(this.i18n.t(token.desc_i18n_key, token.description, token));
|
terms.push(this.i18n.t(token.desc_i18n_key, token.description, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
terms = terms.map(format_term);
|
terms = terms.map(format_term);
|
||||||
|
|
103
src/modules/translation_ui/components/i18n-entry.vue
Normal file
103
src/modules/translation_ui/components/i18n-entry.vue
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
<template lang="html">
|
||||||
|
<div class="ffz--i18n-entry tw-pd-x-1 tw-pd-y-05 tw-border-b">
|
||||||
|
<div class="tw-flex tw-full-width">
|
||||||
|
<div class="ffz--i18n-sub-entry tw-mg-r-05 tw-flex-grow-1 tw-c-text-alt tw-mg-b-2">
|
||||||
|
<div class="tw-font-size-7 tw-c-text-alt-2 tw-pd-b-05 tw-strong tw-upcase tw-ellipsis" :title="entry.key">
|
||||||
|
{{ entry.key }}
|
||||||
|
</div>
|
||||||
|
<code>{{ entry.phrase }}</code>
|
||||||
|
</div>
|
||||||
|
<div class="ffz--i18n-sub-entry tw-flex-grow-1">
|
||||||
|
<textarea
|
||||||
|
v-model="value"
|
||||||
|
:class="{'tw-textarea--error': ! valid}"
|
||||||
|
class="tw-block tw-font-size-6 tw-full-width tw-full-height tw-textarea"
|
||||||
|
@input="onInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="ffz--i18n-sub-entry tw-mg-l-05 tw-flex-grow-1">
|
||||||
|
<div v-if="error">
|
||||||
|
<div class="tw-strong">{{ t('i18n.ui.error', 'Error') }}</div>
|
||||||
|
<code class="tw-font-size-7 tw-c-text-alt-2">{{ error }}</code>
|
||||||
|
</div>
|
||||||
|
<div v-if="source">
|
||||||
|
<div class="tw-strong">{{ t('i18n.ui.source', 'Source') }}</div>
|
||||||
|
<code class="tw-font-size-7 tw-c-text-alt-2">{{ source }}</code>
|
||||||
|
</div>
|
||||||
|
<div v-if="context">
|
||||||
|
<div class="tw-strong">{{ t('i18n.ui.context', 'Context') }}</div>
|
||||||
|
<code class="tw-font-size-7 tw-c-text-alt-2">{{ context }}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import Parser from '@ffz/icu-msgparser';
|
||||||
|
import { debounce } from 'utilities/object';
|
||||||
|
|
||||||
|
const parser = new Parser();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['entry'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
value: this.entry.phrase,
|
||||||
|
valid: true,
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
source() {
|
||||||
|
const calls = this.entry.calls;
|
||||||
|
if ( ! Array.isArray(calls) || ! calls.length )
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return calls.join('\n');
|
||||||
|
},
|
||||||
|
|
||||||
|
context() {
|
||||||
|
const opts = this.entry.options;
|
||||||
|
if ( ! opts || typeof opts !== 'object' )
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const lines = [];
|
||||||
|
for(const [key, val] of Object.entries(opts))
|
||||||
|
lines.push(`${key}: ${JSON.stringify(val)}`);
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.validate();
|
||||||
|
this.onInput = debounce(this.onInput, 250);
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onInput() {
|
||||||
|
this.validate();
|
||||||
|
if ( this.valid )
|
||||||
|
this.$emit('update', this.value);
|
||||||
|
},
|
||||||
|
|
||||||
|
validate() {
|
||||||
|
try {
|
||||||
|
parser.parse(this.value);
|
||||||
|
} catch(err) {
|
||||||
|
this.error = err;
|
||||||
|
this.valid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.error = null;
|
||||||
|
this.valid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
|
@ -4,7 +4,7 @@
|
||||||
class="ffz-dialog tw-elevation-3 tw-c-background-alt tw-c-text-base tw-border tw-flex tw-flex-nowrap tw-flex-column"
|
class="ffz-dialog tw-elevation-3 tw-c-background-alt tw-c-text-base tw-border tw-flex tw-flex-nowrap tw-flex-column"
|
||||||
>
|
>
|
||||||
<header class="tw-c-background-base tw-full-width tw-align-items-center tw-flex tw-flex-nowrap" @dblclick="resize">
|
<header class="tw-c-background-base tw-full-width tw-align-items-center tw-flex tw-flex-nowrap" @dblclick="resize">
|
||||||
<h3 class="ffz-i-zreknarf ffz-i-pd-1">{{ t('i18n.ui.title', 'Translation Editor') }}</h3>
|
<h3 class="ffz-i-zreknarf ffz-i-pd-1">{{ t('i18n.ui.title', 'Translation Tester') }}</h3>
|
||||||
<div class="tw-flex-grow-1 tw-pd-x-2">
|
<div class="tw-flex-grow-1 tw-pd-x-2">
|
||||||
<div class="tw-search-input">
|
<div class="tw-search-input">
|
||||||
<label for="ffz-main-menu.search" class="tw-hide-accessible">{{ t('i18n.ui.search', 'Search Strings') }}</label>
|
<label for="ffz-main-menu.search" class="tw-hide-accessible">{{ t('i18n.ui.search', 'Search Strings') }}</label>
|
||||||
|
@ -26,6 +26,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="tw-button-icon tw-mg-x-05 tw-relative tw-tooltip-wrapper" @click="requestKeys">
|
||||||
|
<span class="tw-button-icon__icon">
|
||||||
|
<figure class="ffz-i-arrows-cw" />
|
||||||
|
</span>
|
||||||
|
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
|
||||||
|
{{ t('i18n.ui.refresh', 'Refresh Strings') }}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
<button v-if="!maximized && !exclusive" class="tw-button-icon tw-mg-x-05" @click="faded = ! faded">
|
<button v-if="!maximized && !exclusive" class="tw-button-icon tw-mg-x-05" @click="faded = ! faded">
|
||||||
<span class="tw-button-icon__icon">
|
<span class="tw-button-icon__icon">
|
||||||
<figure :class="faded ? 'ffz-i-eye-off' : 'ffz-i-eye'" />
|
<figure :class="faded ? 'ffz-i-eye-off' : 'ffz-i-eye'" />
|
||||||
|
@ -51,9 +59,14 @@
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<section class="tw-border-t tw-full-height tw-full-width tw-flex tw-overflow-hidden">
|
<section class="tw-border-t tw-full-height tw-full-width tw-flex tw-overflow-hidden">
|
||||||
<div v-for="(key, idx) of phrases" :key="idx" class="tw-block tw-mg-1">
|
<simplebar classes="tw-flex-grow-1">
|
||||||
{{ key }}
|
<i18n-entry
|
||||||
</div>
|
v-for="phrase in filtered"
|
||||||
|
:key="phrase.key"
|
||||||
|
:entry="phrase"
|
||||||
|
@update="update(phrase.key, $event)"
|
||||||
|
/>
|
||||||
|
</simplebar>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -70,6 +83,21 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
filter() {
|
filter() {
|
||||||
return this.query.toLowerCase()
|
return this.query.toLowerCase()
|
||||||
|
},
|
||||||
|
|
||||||
|
filtered() {
|
||||||
|
if ( ! this.query || ! this.query.length )
|
||||||
|
return this.phrases;
|
||||||
|
|
||||||
|
return this.phrases.filter(entry => {
|
||||||
|
if ( entry.key.toLowerCase().includes(this.query) )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if ( entry.phrase.toLowerCase().includes(this.query) )
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -79,6 +107,13 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.requestKeys();
|
||||||
|
this.grabKeys();
|
||||||
|
|
||||||
|
this.listen('i18n:got-keys', this.grabKeys, this);
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.updateDrag();
|
this.updateDrag();
|
||||||
|
|
||||||
|
@ -93,9 +128,22 @@ export default {
|
||||||
window.removeEventListener('resize', this._on_resize);
|
window.removeEventListener('resize', this._on_resize);
|
||||||
this._on_resize = null;
|
this._on_resize = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.unlisten('i18n:got-keys', this.grabKeys, this);
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
grabKeys() {
|
||||||
|
this.phrases = this.getKeys();
|
||||||
|
this.phrases.sort((a, b) => {
|
||||||
|
return a.key.localeCompare(b.key)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
update(key, phrase) {
|
||||||
|
this.updatePhrase(key, phrase);
|
||||||
|
},
|
||||||
|
|
||||||
updateDrag() {
|
updateDrag() {
|
||||||
if ( this.maximized )
|
if ( this.maximized )
|
||||||
this.destroyDrag();
|
this.destroyDrag();
|
||||||
|
|
|
@ -70,15 +70,13 @@ export default class TranslationUI extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async buildDialog() {
|
buildDialog() {
|
||||||
if ( this._dialog )
|
if ( this._dialog )
|
||||||
return this._dialog;
|
return this._dialog;
|
||||||
|
|
||||||
const data = await this.getData();
|
|
||||||
|
|
||||||
this._vue = new this.vue.Vue({
|
this._vue = new this.vue.Vue({
|
||||||
el: createElement('div'),
|
el: createElement('div'),
|
||||||
render: h => h('translation-ui', data)
|
render: h => h('translation-ui', this.getData())
|
||||||
});
|
});
|
||||||
|
|
||||||
return this._dialog = this._vue.$el;
|
return this._dialog = this._vue.$el;
|
||||||
|
@ -92,15 +90,21 @@ export default class TranslationUI extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async getData() {
|
getData() {
|
||||||
return {
|
return {
|
||||||
|
phrases: this.i18n.getKeys(),
|
||||||
query: '',
|
query: '',
|
||||||
|
|
||||||
faded: false,
|
faded: false,
|
||||||
maximized: this.dialog.maximized,
|
maximized: this.dialog.maximized,
|
||||||
exclusive: this.dialog.exclusive,
|
exclusive: this.dialog.exclusive,
|
||||||
|
|
||||||
phrases: Array.from(await this.i18n.getKeys()),
|
getKeys: () => this.i18n.getKeys(),
|
||||||
|
requestKeys: () => this.i18n.requestKeys(),
|
||||||
|
updatePhrase: (key, phrase) => this.i18n.updatePhrase(key, phrase),
|
||||||
|
|
||||||
|
listen: (event, fn, ctx) => this.on(event, fn, ctx),
|
||||||
|
unlisten: (event, fn, ctx) => this.off(event, fn, ctx),
|
||||||
|
|
||||||
resize: e => ! this.dialog.exclusive && this.dialog.toggleSize(e),
|
resize: e => ! this.dialog.exclusive && this.dialog.toggleSize(e),
|
||||||
close: e => ! this.dialog.exclusive && this.dialog.toggleVisible(e),
|
close: e => ! this.dialog.exclusive && this.dialog.toggleVisible(e),
|
||||||
|
|
|
@ -454,6 +454,9 @@ export default class SettingsManager extends Module {
|
||||||
parse_path(ui.path) :
|
parse_path(ui.path) :
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
|
if ( ! ui.key && key )
|
||||||
|
ui.key = key;
|
||||||
|
|
||||||
if ( ! ui.key && ui.title )
|
if ( ! ui.key && ui.title )
|
||||||
ui.key = ui.title.toSnakeCase();
|
ui.key = ui.title.toSnakeCase();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
|
.search-result-card,
|
||||||
.tw-avatar {
|
.tw-avatar {
|
||||||
--border-radius-rounded: 0 !important;
|
--border-radius-rounded: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.player-streaminfo__picture img[src] {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.bits-leaderboard-medal .tw-avatar {
|
.bits-leaderboard-medal .tw-avatar {
|
||||||
--border-radius-rounded: 9000px !important;
|
--border-radius-rounded: 9000px !important;
|
||||||
}
|
}
|
|
@ -29,7 +29,7 @@
|
||||||
class="tab tw-pd-y-05 tw-pd-x-1"
|
class="tab tw-pd-y-05 tw-pd-x-1"
|
||||||
@click="select(idx)"
|
@click="select(idx)"
|
||||||
>
|
>
|
||||||
{{ t(i.i18n_key, i.title, i) }}
|
{{ t(i.i18n_key, i.title) }}
|
||||||
<span v-if="i.unseen > 0" class="tw-pill">{{ i.unseen }}</span>
|
<span v-if="i.unseen > 0" class="tw-pill">{{ i.unseen }}</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
aria-expanded="true"
|
aria-expanded="true"
|
||||||
>
|
>
|
||||||
<section v-if="tab.description" class="tw-pd-b-1">
|
<section v-if="tab.description" class="tw-pd-b-1">
|
||||||
{{ t(tab.desc_i18n_key, tab.description, tab) }}
|
{{ t(tab.desc_i18n_key, tab.description) }}
|
||||||
</section>
|
</section>
|
||||||
<div
|
<div
|
||||||
v-for="i in tab.contents"
|
v-for="i in tab.contents"
|
||||||
|
|
|
@ -349,6 +349,58 @@ export function get(path, object) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy an object so that it can be safely serialized. If an object
|
||||||
|
* is not serializable, such as a promise, returns null.
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @param {*} object The thing to copy.
|
||||||
|
* @param {Number} [depth=2] The maximum depth to explore the object.
|
||||||
|
* @param {Set} [seen=null] A Set of seen objects. Internal use only.
|
||||||
|
* @returns {Object} The copy to safely store or use.
|
||||||
|
*/
|
||||||
|
export function shallow_copy(object, depth = 2, seen = null) {
|
||||||
|
if ( object == null )
|
||||||
|
return object;
|
||||||
|
|
||||||
|
if ( object instanceof Promise || typeof object === 'function' )
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if ( typeof object !== 'object' )
|
||||||
|
return object;
|
||||||
|
|
||||||
|
if ( depth === 0 )
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if ( ! seen )
|
||||||
|
seen = new Set;
|
||||||
|
|
||||||
|
seen.add(object);
|
||||||
|
|
||||||
|
if ( Array.isArray(object) ) {
|
||||||
|
const out = [];
|
||||||
|
for(const val of object) {
|
||||||
|
if ( seen.has(val) )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
out.push(shallow_copy(val, depth - 1, new Set(seen)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = {};
|
||||||
|
for(const [key, val] of Object.entries(object) ) {
|
||||||
|
if ( seen.has(val) )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
out[key] = shallow_copy(val, depth - 1, new Set(seen));
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function deep_copy(object, seen) {
|
export function deep_copy(object, seen) {
|
||||||
if ( object === null )
|
if ( object === null )
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -27,6 +27,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ffz--i18n-entry {
|
||||||
|
&:nth-child(2n+1) {
|
||||||
|
background-color: var(--color-background-alt-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ffz--i18n-sub-entry {
|
||||||
|
width: 33%;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
textarea.tw-input {
|
textarea.tw-input {
|
||||||
height: unset;
|
height: unset;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue