Merge pull request #3576 from SillyTavern/debug-track-dynamic-translations Debug function to track missing dynamic translation fields (`t` and `translate` calls)

20b5c962dbdc03e6c7e46855f66f780de0f3fd38

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
2 files changed, +62 -12Ignore whitespace
public/scripts/i18n.js+61 -11
@@ -9,6 +9,9 @@ var langs;
99// eslint-disable-next-line prefer-const
1010var localeData;
1111
12+/** @type {Set<string>|null} Array of translations keys if they should be tracked - if not tracked then null */
13+let trackMissingDynamicTranslate = null;
14+
1215export const getCurrentLocale = () => localeFile;
1316
1417/**
@@ -97,6 +100,13 @@ export function t(strings, ...values) {
97100 */
98101export function translate(text, key = null) {
99102 const translationKey = key || text;
103+ if (translationKey === null || translationKey === undefined) {
104+ console.trace('WARN: No translation key provided');
105+ return '';
106+ }
107+ if (trackMissingDynamicTranslate && localeData && !Object.hasOwn(localeData, translationKey)) {
108+ trackMissingDynamicTranslate.add(translationKey);
109+ }
100110 return localeData?.[translationKey] || text;
101111}
102112
@@ -153,13 +163,26 @@ function translateElement(element) {
153163 }
154164}
155165
166+/**
167+ * Checks if the given locale is supported and not English.
168+ * @param {string} [locale=null] The locale to check (defaults to the current locale)
169+ * @returns {boolean} True if the locale is not English and supported
170+ */
171+function isSupportedNonEnglish(locale = null) {
172+ const lang = locale || localeFile;
173+ return lang && lang != 'en' && findLang(lang);
174+}
156175
157176async function getMissingTranslations() {
177+ /** @type {Array<{key: string, language: string, value: string}>} */
158178 const missingData = [];
159179
180+ if (trackMissingDynamicTranslate) {
181+ missingData.push(...Array.from(trackMissingDynamicTranslate).map(key => ({ key, language: localeFile, value: key })));
182+ }
183+
160184 // Determine locales to search for untranslated strings
161185 const isNotSupportedlangsToProcess = !isSupportedNonEnglish() ? [findLang(localeFile)] : langs;
162- const langsToProcess = (isNotSupported || localeFile == 'en') ? langs : [findLang(localeFile)];
163186
164187 for (const language of langsToProcess) {
165188 const localeData = await getLocaleData(language.lang);
@@ -170,7 +193,7 @@ async function getMissingTranslations() {
170193 if (attributeMatch) { // attribute-tagged key
171194 const localizedValue = localeData?.[attributeMatch[2]];
172195 if (!localizedValue) {
173196 missingData.push({ key, language: language.lang, value: String($(this).attr(attributeMatch[1])) });
174197 }
175198 } else { // No attribute tag, treat as 'text'
176199 const localizedValue = localeData?.[key];
@@ -194,17 +217,19 @@ async function getMissingTranslations() {
194217 uniqueMissingData.sort((a, b) => a.language.localeCompare(b.language) || a.key.localeCompare(b.key));
195218
196219 // Map to { language: { key: value } }
197- let missingDataMap = {};
220+ const missingDataMap = Object.fromEntries(uniqueMissingData.map(({ key, value }) => [key, value]));
198- for (const { key, value } of uniqueMissingData) {
199- if (!missingDataMap) {
200- missingDataMap = {};
201- }
202- missingDataMap[key] = value;
203- }
204221
222+ console.log(`Missing Translations (${uniqueMissingData.length}):`);
205223 console.table(uniqueMissingData);
224+ console.log(`Full map of missing data (${Object.keys(missingDataMap).length}):`);
206225 console.log(missingDataMap);
207226
227+ if (trackMissingDynamicTranslate) {
228+ const trackMissingDynamicTranslateMap = Object.fromEntries(Array.from(trackMissingDynamicTranslate).map(key => [key, key]));
229+ console.log(`Dynamic translations missing (${Object.keys(trackMissingDynamicTranslateMap).length}):`);
230+ console.log(trackMissingDynamicTranslateMap);
231+ }
232+
208233 toastr.success(`Found ${uniqueMissingData.length} missing translations. See browser console for details.`);
209234}
210235
@@ -266,6 +291,31 @@ export async function initLocales() {
266291 attributeFilter: ['data-i18n'],
267292 });
268293
269- registerDebugFunction('getMissingTranslations', 'Get missing translations', 'Detects missing localization data in the current locale and dumps the data into the browser console. If the current locale is English, searches all other locales.', getMissingTranslations);
294+ if (localStorage.getItem('trackDynamicTranslate') === 'true' && isSupportedNonEnglish()) {
295+ trackMissingDynamicTranslate = new Set();
296+ }
297+
298+ registerDebugFunction('getMissingTranslations', 'Get missing translations',
299+ 'Detects missing localization data in the current locale and dumps the data into the browser console. ' +
300+ 'If the current locale is English, searches all other locales.',
301+ getMissingTranslations);
302+ registerDebugFunction('trackDynamicTranslate', 'Track dynamic translation',
303+ 'Toggles tracking of dynamic translations, which will be dumped into the missing translations translations too. ' +
304+ 'This includes things translated via the t`...` function and translate(). It will only track strings translated <b>after</b> this is toggled on, '
305+ + 'and when they actually pop up, so refreshing the page and opening popups, etc, is needed. Will only track if the current locale is not English.',
306+ () => {
307+ const isTracking = localStorage.getItem('trackDynamicTranslate') !== 'true';
308+ localStorage.setItem('trackDynamicTranslate', isTracking ? 'true' : 'false');
309+ if (isTracking && isSupportedNonEnglish()) {
310+ trackMissingDynamicTranslate = new Set();
311+ toastr.success('Dynamic translation tracking enabled.');
312+ } else if (isTracking) {
313+ trackMissingDynamicTranslate = null;
314+ toastr.warning('Dynamic translation tracking enabled, but will not be tracked with locale English.');
315+ } else {
316+ trackMissingDynamicTranslate = null;
317+ toastr.info('Dynamic translation tracking disabled.');
318+ }
319+ });
270320 registerDebugFunction('applyLocale', 'Apply locale', 'Reapplies the currently selected locale to the page.', applyLocale);
271321}
public/scripts/reasoning.js+1 -1
@@ -493,7 +493,7 @@ export class ReasoningHandler {
493493 data = null;
494494 }
495495
496496 if (this.type && this.type !== ReasoningType.Model) {
497497 title += ` [${translate(this.type)}]`;
498498 title = title.trim();
499499 }