diff --git a/package-lock.json b/package-lock.json index 4841bbed..da8caa03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "frankerfacez", - "version": "4.12.5", + "version": "4.13.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -7902,6 +7902,11 @@ } } }, + "text-diff": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/text-diff/-/text-diff-1.0.1.tgz", + "integrity": "sha1-bBBZBUNeM3hXN1ydL2ymPkU/9WU=" + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index a76fc294..61aa61c8 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.13.0", + "version": "4.13.1", "description": "FrankerFaceZ is a Twitch enhancement suite.", "license": "Apache-2.0", "scripts": { @@ -80,6 +80,7 @@ "safe-regex": "^2.0.2", "sortablejs": "^1.10.0-rc3", "sourcemapped-stacktrace": "^1.1.11", + "text-diff": "^1.0.1", "vue": "^2.6.10", "vue-clickaway": "^2.2.2", "vue-color": "^2.4.6", diff --git a/src/addons.js b/src/addons.js index cc29460d..a12dcf60 100644 --- a/src/addons.js +++ b/src/addons.js @@ -141,9 +141,9 @@ export default class AddonManager extends Module { const old = this.addons[addon.id]; this.addons[addon.id] = addon; - addon.name_i18n = addon.name_i18n || `addon.${addon.id}.name`; + /*addon.name_i18n = addon.name_i18n || `addon.${addon.id}.name`; addon.short_name_i18n = addon.short_name_i18n || `addon.${addon.id}.short_name`; - addon.author_i18n = addon.author_i18n || `addon.${addon.id}.author`; + addon.author_i18n = addon.author_i18n || `addon.${addon.id}.author`;*/ addon.dev = is_dev; addon.requires = addon.requires || []; diff --git a/src/i18n.js b/src/i18n.js index 04459f21..00b29e06 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -12,8 +12,10 @@ import Module from 'utilities/module'; import NewTransCore from 'utilities/translation-core'; +const API_SERVER = 'https://api-test.frankerfacez.com'; + const STACK_SPLITTER = /\s*at\s+(.+?)\s+\((.+)\)$/, - SOURCE_SPLITTER = /^(.+):\/\/(.+?):(\d+:\d+)$/; + SOURCE_SPLITTER = /^(.+):\/\/(.+?)(?:\?[a-zA-Z0-9]+)?:(\d+:\d+)$/; const MAP_OPTIONS = { filter(line) { @@ -83,6 +85,9 @@ export class TranslationManager extends Module { this.loadLocales(); + this.strings_loaded = false; + this.new_strings = 0; + this.changed_strings = 0; this.capturing = false; this.captured = new Map; @@ -160,48 +165,54 @@ export class TranslationManager extends Module { description: `FrankerFaceZ is lovingly translated by volunteers from our community. Thank you. If you're interested in helping to translate FrankerFaceZ, please [join our Discord](https://discord.gg/UrAkGhT) and ask about localization.`, component: 'setting-select-box', - data: (profile, val) => { - const out = this.availableLocales.map(l => { - const data = this.localeData[l]; - let title = data?.native_name; - if ( ! title ) - title = data?.name || l; - - if ( data?.coverage != null && data?.coverage < 100 ) - title = this.t('i18n.locale-coverage', '{name} ({coverage,number,percent} Complete)', { - name: title, - coverage: data.coverage / 100 - }); - - return { - selected: val === l, - value: l, - title - }; - }); - - out.sort((a, b) => { - return a.title.localeCompare(b.title) - }); - - out.unshift({ - selected: val === -1, - value: -1, - i18n_key: 'setting.appearance.localization.general.language.twitch', - title: "Use Twitch's Language" - }); - - return out; - } + data: (profile, val) => this.getLocaleOptions(val) }, changed: val => this.locale = val }); + } + getLocaleOptions(val) { + if( val === undefined ) + val = this.settings.get('i18n.locale'); + + const out = this.availableLocales.map(l => { + const data = this.localeData[l]; + let title = data?.native_name; + if ( ! title ) + title = data?.name || l; + + if ( data?.coverage != null && data?.coverage < 100 ) + title = this.t('i18n.locale-coverage', '{name} ({coverage,number,percent} Complete)', { + name: title, + coverage: data.coverage / 100 + }); + + return { + selected: val === l, + value: l, + title + }; + }); + + out.sort((a, b) => { + return a.title.localeCompare(b.title) + }); + + out.unshift({ + selected: val === -1, + value: -1, + i18n_key: 'setting.appearance.localization.general.language.twitch', + title: "Use Twitch's Language" + }); + + return out; } onEnable() { this.capturing = this.settings.get('i18n.debug.capture'); + if ( this.capturing ) + this.loadStrings(); this._ = new NewTransCore({ //TranslationCore({ warn: (...args) => this.log.warn(...args), @@ -306,6 +317,80 @@ export class TranslationManager extends Module { } + async loadStrings() { + if ( this.strings_loaded ) + return; + + if ( this.strings_loading ) + return; + + this.strings_loading = true; + + const loadPage = async page => { + const resp = await fetch(`${API_SERVER}/v2/i18n/strings?page=${page}`); + if ( ! resp.ok ) { + this.log.warn(`Error Loading Strings -- Status: ${resp.status}`); + return { + next: false, + strings: [] + }; + } + + const data = await resp.json(); + return { + next: data?.pages > page, + strings: data?.strings || [] + } + } + + let page = 1; + let next = true; + let strings = []; + + while(next) { + const data = await loadPage(page++); // eslint-disable-line no-await-in-loop + strings = strings.concat(data.strings); + next = data.next; + } + + for(const str of strings) { + const key = str.id; + let store = this.captured.get(key); + if ( ! store ) { + this.captured.set(key, store = {key, phrase: str.default, hits: 0, calls: []}); + if ( str.source?.length ) + store.calls.push(str.source); + } + + if ( ! store.options && str.context?.length ) + try { + store.options = JSON.parse(str.context); + } catch(err) { /* no-op */ } + + store.known = str.default; + store.different = str.default !== store.phrase; + } + + this.new_strings = 0; + this.changed_strings = 0; + + for(const entry of this.captured.values()) { + if ( ! entry.known ) + this.new_strings++; + if ( entry.different ) + this.changed_strings++; + } + + this.strings_loaded = true; + this.strings_loading = false; + + this.log.info(`Loaded ${strings.length} strings from the server.`); + this.emit(':strings-loaded'); + this.emit(':new-strings', this.new_strings); + this.emit(':changed-strings', this.changed_strings); + } + + see(key, phrase, options) { if ( ! this.capturing ) return; @@ -321,9 +406,22 @@ export class TranslationManager extends Module { } let store = this.captured.get(key); - if ( ! store ) + if ( ! store ) { this.captured.set(key, store = {key, phrase, hits: 0, calls: []}); + if ( this.strings_loaded ) { + this.new_strings++; + this.emit(':new-strings', this.new_strings); + } + } + if ( phrase !== store.phrase ) { + store.phrase = phrase; + if ( store.known && phrase !== store.known && ! store.different ) { + store.different = true; + this.changed_strings++; + this.emit(':changed-strings', this.changed_strings); + } + } store.options = this.pluckVariables(key, options); store.hits++; @@ -383,7 +481,17 @@ export class TranslationManager extends Module { if ( file.includes('/node_modules/') || BAD_FRAMES.includes(file) ) continue; - const out = `${match[1]} (${location[2]}:${location[3]})`; + let out; + if ( match[1] === 'MainMenu.getSettingsTree' ) + out = 'FFZ Control Center'; + else { + let label = match[1]; + if ( label === 'Proxy.render' && location[2].includes('.vue') ) + label = 'Vue Component'; + + out = `${label} (${location[2]}:${location[3]})`; + } + if ( ! store.calls.includes(out) ) store.calls.push(out); @@ -393,7 +501,7 @@ export class TranslationManager extends Module { async loadLocales() { - const resp = await fetch(`https://api-test.frankerfacez.com/v2/i18n/locales`); + const resp = await fetch(`${API_SERVER}/v2/i18n/locales`); if ( ! resp.ok ) { this.log.warn(`Error Populating Locales -- Status: ${resp.status}`); throw new Error(`http error ${resp.status} loading locales`) @@ -425,7 +533,7 @@ export class TranslationManager extends Module { if ( locale === 'en' ) return {}; - const resp = await fetch(`https://api-test.frankerfacez.com/v2/i18n/locale/${locale}`); + const resp = await fetch(`${API_SERVER}/v2/i18n/locale/${locale}`); if ( ! resp.ok ) { if ( resp.status === 404 ) { this.log.info(`Cannot Load Locale: ${locale}`); @@ -503,8 +611,8 @@ export class TranslationManager extends Module { return this._.toLocaleString(...args); } - toHumanTime(...args) { - return this._.formatHumanTime(...args); + toRelativeTime(...args) { + return this._.formatRelativeTime(...args); } formatNumber(...args) { diff --git a/src/modules/chat/actions/types.jsx b/src/modules/chat/actions/types.jsx index 083ff22f..6ee481b0 100644 --- a/src/modules/chat/actions/types.jsx +++ b/src/modules/chat/actions/types.jsx @@ -242,7 +242,7 @@ export const unban = { title: 'Unban User', tooltip(data) { - return this.i18n.t('chat.actions.unban', 'Unban {user.login}', {user: data.user}); + return this.i18n.t('chat.actions.unban.tooltip', 'Unban {user.login}', {user: data.user}); }, click(event, data) { diff --git a/src/modules/chat/rich_providers.js b/src/modules/chat/rich_providers.js index 39e39145..0c63f844 100644 --- a/src/modules/chat/rich_providers.js +++ b/src/modules/chat/rich_providers.js @@ -194,7 +194,7 @@ export const Videos = { image: video.previewThumbnailURL, title: video.title, desc_1, - desc_2: this.i18n.t('video.desc.2', '{length,duration} — {views,number} Views - {date}', { + desc_2: this.i18n.t('video.desc.2', '{length,duration} — {views,number} Views - {date,datetime}', { length: video.lengthSeconds, views: video.viewCount, date: video.publishedAt diff --git a/src/modules/main_menu/components/addon.vue b/src/modules/main_menu/components/addon.vue index cb14c074..ee248edf 100644 --- a/src/modules/main_menu/components/addon.vue +++ b/src/modules/main_menu/components/addon.vue @@ -24,10 +24,10 @@
{{ entry.phrase }}
+ {{ bit[1] }}
{{ error }}
{{ source }}
+ {{ context }}
+ {{ context_str }}
{{ error }}
+ {{ preview }}
+