Merge pull request #2436 from SillyTavern/translation-improvements

Translation improvements
This commit is contained in:
Cohee 2024-06-29 22:38:27 +03:00 committed by GitHub
commit 175a91f979
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 99 additions and 18 deletions

View File

@ -209,7 +209,7 @@ import {
instruct_presets,
selectContextPreset,
} from './scripts/instruct-mode.js';
import { initLocales } from './scripts/i18n.js';
import { initLocales, t, translate } from './scripts/i18n.js';
import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, getTokenizerModel, initTokenizers, saveTokenCache } from './scripts/tokenizers.js';
import {
user_avatar,
@ -7826,6 +7826,8 @@ window['SillyTavern'].getContext = function () {
messageFormatting: messageFormatting,
shouldSendOnEnter: shouldSendOnEnter,
isMobile: isMobile,
t: t,
translate: translate,
tags: tags,
tagMap: tag_map,
menuType: menu_type,

View File

@ -9,6 +9,71 @@ const langs = await fetch('/locales/lang.json').then(response => response.json()
// eslint-disable-next-line prefer-const
var localeData = await getLocaleData(localeFile);
/**
* An observer that will check if any new i18n elements are added to the document
* @type {MutationObserver}
*/
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE && node instanceof Element) {
if (node.hasAttribute('data-i18n')) {
translateElement(node);
}
node.querySelectorAll('[data-i18n]').forEach(element => {
translateElement(element);
});
}
});
if (mutation.attributeName === 'data-i18n' && mutation.target instanceof Element) {
translateElement(mutation.target);
}
});
});
/**
* Translates a template string with named arguments
*
* Uses the template literal with all values replaced by index placeholder for translation key.
*
* @example
* ```js
* toastr.warn(t`Tag ${tagName} not found.`);
* ```
* Should be translated in the translation files as:
* ```
* Tag ${0} not found. -> Tag ${0} nicht gefunden.
* ```
*
* @param {TemplateStringsArray} strings - Template strings array
* @param {...any} values - Values for placeholders in the template string
* @returns {string} Translated and formatted string
*/
export function t(strings, ...values) {
let str = strings.reduce((result, string, i) => result + string + (values[i] !== undefined ? `\${${i}}` : ''), '');
let translatedStr = translate(str);
// Replace indexed placeholders with actual values
return translatedStr.replace(/\$\{(\d+)\}/g, (match, index) => values[index]);
}
/**
* Translates a given key or text
*
* If the translation is based on a key, that one is used to find a possible translation in the translation file.
* The original text still has to be provided, as that is the default value being returned if no translation is found.
*
* For in-code text translation on a format string, using the template literal `t` is preferred.
*
* @param {string} text - The text to translate
* @param {string?} key - The key to use for translation. If not provided, text is used as the key.
* @returns {string} - The translated text
*/
export function translate(text, key = null) {
const translationKey = key || text;
return localeData?.[translationKey] || text;
}
/**
* Fetches the locale data for the given language.
* @param {string} language Language code
@ -40,6 +105,29 @@ function findLang(language) {
return supportedLang;
}
/**
* Translates a given element based on its data-i18n attribute.
* @param {Element} element The element to translate
*/
function translateElement(element) {
const keys = element.getAttribute('data-i18n').split(';'); // Multi-key entries are ; delimited
for (const key of keys) {
const attributeMatch = key.match(/\[(\S+)\](.+)/); // [attribute]key
if (attributeMatch) { // attribute-tagged key
const localizedValue = localeData?.[attributeMatch[2]];
if (localizedValue || localizedValue === '') {
element.setAttribute(attributeMatch[1], localizedValue);
}
} else { // No attribute tag, treat as 'text'
const localizedValue = localeData?.[key];
if (localizedValue || localizedValue === '') {
element.textContent = localizedValue;
}
}
}
}
async function getMissingTranslations() {
const missingData = [];
@ -103,22 +191,7 @@ export function applyLocale(root = document) {
//find all the elements with `data-i18n` attribute
$root.find('[data-i18n]').each(function () {
//read the translation from the language data
const keys = $(this).data('i18n').split(';'); // Multi-key entries are ; delimited
for (const key of keys) {
const attributeMatch = key.match(/\[(\S+)\](.+)/); // [attribute]key
if (attributeMatch) { // attribute-tagged key
const localizedValue = localeData?.[attributeMatch[2]];
if (localizedValue || localizedValue == '') {
$(this).attr(attributeMatch[1], localizedValue);
}
} else { // No attribute tag, treat as 'text'
const localizedValue = localeData?.[key];
if (localizedValue || localizedValue == '') {
$(this).text(localizedValue);
}
}
}
translateElement(this);
});
if (root !== document) {
@ -126,7 +199,6 @@ export function applyLocale(root = document) {
}
}
function addLanguagesToDropdown() {
const uiLanguageSelects = $('#ui_language_select, #onboarding_ui_language_select');
for (const langObj of langs) { // Set the value to the language code
@ -159,6 +231,13 @@ export function initLocales() {
location.reload();
});
observer.observe(document, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['data-i18n'],
});
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);
registerDebugFunction('applyLocale', 'Apply locale', 'Reapplies the currently selected locale to the page.', applyLocale);
}