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;
9// eslint-disable-next-line prefer-const9// eslint-disable-next-line prefer-const
10var localeData;10var localeData;
1111
12/** @type {Set<string>|null} Array of translations keys if they should be tracked - if not tracked then null */
13let trackMissingDynamicTranslate = null;
14
12export const getCurrentLocale = () => localeFile;15export const getCurrentLocale = () => localeFile;
1316
14/**17/**
@@ -97,6 +100,13 @@ export function t(strings, ...values) {
97 */100 */
98export function translate(text, key = null) {101export function translate(text, key = null) {
99 const translationKey = key || text;102 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 }
100 return localeData?.[translationKey] || text;110 return localeData?.[translationKey] || text;
101}111}
102112
@@ -153,13 +163,26 @@ function translateElement(element) {
153 }163 }
154}164}
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 */
171function isSupportedNonEnglish(locale = null) {
172 const lang = locale || localeFile;
173 return lang && lang != 'en' && findLang(lang);
174}
156175
157async function getMissingTranslations() {176async function getMissingTranslations() {
177 /** @type {Array<{key: string, language: string, value: string}>} */
158 const missingData = [];178 const missingData = [];
159179
180 if (trackMissingDynamicTranslate) {
181 missingData.push(...Array.from(trackMissingDynamicTranslate).map(key => ({ key, language: localeFile, value: key })));
182 }
183
160 // Determine locales to search for untranslated strings184 // Determine locales to search for untranslated strings
161 const isNotSupported = !findLang(localeFile);185 const langsToProcess = isSupportedNonEnglish() ? [findLang(localeFile)] : langs;
162 const langsToProcess = (isNotSupported || localeFile == 'en') ? langs : [findLang(localeFile)];
163186
164 for (const language of langsToProcess) {187 for (const language of langsToProcess) {
165 const localeData = await getLocaleData(language.lang);188 const localeData = await getLocaleData(language.lang);
@@ -170,7 +193,7 @@ async function getMissingTranslations() {
170 if (attributeMatch) { // attribute-tagged key193 if (attributeMatch) { // attribute-tagged key
171 const localizedValue = localeData?.[attributeMatch[2]];194 const localizedValue = localeData?.[attributeMatch[2]];
172 if (!localizedValue) {195 if (!localizedValue) {
173 missingData.push({ key, language: language.lang, value: $(this).attr(attributeMatch[1]) });196 missingData.push({ key, language: language.lang, value: String($(this).attr(attributeMatch[1])) });
174 }197 }
175 } else { // No attribute tag, treat as 'text'198 } else { // No attribute tag, treat as 'text'
176 const localizedValue = localeData?.[key];199 const localizedValue = localeData?.[key];
@@ -194,17 +217,19 @@ async function getMissingTranslations() {
194 uniqueMissingData.sort((a, b) => a.language.localeCompare(b.language) || a.key.localeCompare(b.key));217 uniqueMissingData.sort((a, b) => a.language.localeCompare(b.language) || a.key.localeCompare(b.key));
195218
196 // Map to { language: { key: value } }219 // 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}):`);
205 console.table(uniqueMissingData);223 console.table(uniqueMissingData);
224 console.log(`Full map of missing data (${Object.keys(missingDataMap).length}):`);
206 console.log(missingDataMap);225 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
208 toastr.success(`Found ${uniqueMissingData.length} missing translations. See browser console for details.`);233 toastr.success(`Found ${uniqueMissingData.length} missing translations. See browser console for details.`);
209}234}
210235
@@ -266,6 +291,31 @@ export async function initLocales() {
266 attributeFilter: ['data-i18n'],291 attributeFilter: ['data-i18n'],
267 });292 });
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 });
270 registerDebugFunction('applyLocale', 'Apply locale', 'Reapplies the currently selected locale to the page.', applyLocale);320 registerDebugFunction('applyLocale', 'Apply locale', 'Reapplies the currently selected locale to the page.', applyLocale);
271}321}
public/scripts/reasoning.js+1 -1
@@ -493,7 +493,7 @@ export class ReasoningHandler {
493 data = null;493 data = null;
494 }494 }
495495
496 if (this.type !== ReasoningType.Model) {496 if (this.type && this.type !== ReasoningType.Model) {
497 title += ` [${translate(this.type)}]`;497 title += ` [${translate(this.type)}]`;
498 title = title.trim();498 title = title.trim();
499 }499 }