From c08b324d6a51e336d7f5b28c5d9360574c2b8ed1 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 15 Dec 2023 21:20:26 +0100 Subject: [PATCH] Refactor localization.js --- public/scripts/localization.js | 140 +++++++++++++++++++++------------ public/scripts/ui-main.js | 2 +- 2 files changed, 91 insertions(+), 51 deletions(-) diff --git a/public/scripts/localization.js b/public/scripts/localization.js index b696ef9..d81d3e3 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -1,40 +1,49 @@ class Localization { constructor() { + Localization.$htmlRoot = document.querySelector('html'); + Localization.defaultLocale = "en"; Localization.supportedLocales = ["ar", "ca", "de", "en", "es", "fr", "id", "it", "ja", "nb", "nl", "pt-BR", "ro", "ru", "tr", "zh-CN"]; Localization.supportedLocalesRtl = ["ar"]; Localization.translations = {}; - Localization.defaultTranslations = {}; + Localization.translationsDefaultLocale = {}; - Localization.systemLocale = Localization.getSupportedOrDefault(navigator.languages); + Localization.systemLocale = Localization.getSupportedOrDefaultLocales(navigator.languages); let storedLanguageCode = localStorage.getItem('language_code'); - Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode) + Localization.initialLocale = storedLanguageCode && Localization.localeIsSupported(storedLanguageCode) ? storedLanguageCode : Localization.systemLocale; } - static isSupported(locale) { + static localeIsSupported(locale) { return Localization.supportedLocales.indexOf(locale) > -1; } - static isRtlLanguage(locale) { + static localeIsRtl(locale) { return Localization.supportedLocalesRtl.indexOf(locale) > -1; } - static isCurrentLocaleRtl() { - return Localization.isRtlLanguage(Localization.locale); + static currentLocaleIsRtl() { + return Localization.localeIsRtl(Localization.locale); } - static getSupportedOrDefault(locales) { + static currentLocaleIsDefault() { + return Localization.locale === Localization.defaultLocale + } + + static getSupportedOrDefaultLocales(locales) { + // get generic locales not included in locales + // ["en-us", "de-CH", "fr"] --> ["en", "de"] let localesGeneric = locales .map(locale => locale.split("-")[0]) .filter(locale => locales.indexOf(locale) === -1); - return locales.find(Localization.isSupported) - || localesGeneric.find(Localization.isSupported) + // If there is no perfect match for browser locales, try generic locales first before resorting to the default locale + return locales.find(Localization.localeIsSupported) + || localesGeneric.find(Localization.localeIsSupported) || Localization.defaultLocale; } @@ -48,16 +57,14 @@ class Localization { await Localization.setLocale(locale) await Localization.translatePage(); - const htmlRootNode = document.querySelector('html'); - - if (Localization.isRtlLanguage(locale)) { - htmlRootNode.setAttribute('dir', 'rtl'); + if (Localization.localeIsRtl(locale)) { + Localization.$htmlRoot.setAttribute('dir', 'rtl'); } else { - htmlRootNode.removeAttribute('dir'); + Localization.$htmlRoot.removeAttribute('dir'); } - htmlRootNode.setAttribute('lang', locale); + Localization.$htmlRoot.setAttribute('lang', locale); console.log("Page successfully translated", @@ -111,75 +118,108 @@ class Localization { const key = element.getAttribute("data-i18n-key"); const attrs = element.getAttribute("data-i18n-attrs").split(" "); - for (let i in attrs) { - let attr = attrs[i]; + attrs.forEach(attr => { if (attr === "text") { element.innerText = Localization.getTranslation(key); } else { element.setAttribute(attr, Localization.getTranslation(key, attr)); } - } + }) } - static getTranslation(key, attr = null, data = {}, useDefault = false) { - const keys = key.split("."); - - let translationCandidates = useDefault - ? Localization.defaultTranslations - : Localization.translations; - + static getTranslationFromTranslationsObj(translationObj, key, attr) { let translation; - try { + const keys = key.split("."); + for (let i = 0; i < keys.length - 1; i++) { - translationCandidates = translationCandidates[keys[i]] + // iterate into translation object until last layer + translationObj = translationObj[keys[i]] } let lastKey = keys[keys.length - 1]; if (attr) lastKey += "_" + attr; - translation = translationCandidates[lastKey]; + translation = translationObj[lastKey]; - for (let j in data) { - if (translation.includes(`{{${j}}}`)) { - translation = translation.replace(`{{${j}}}`, data[j]); - } else { - console.warn(`Translation for your language ${Localization.locale.toUpperCase()} misses at least one data placeholder:`, key, attr, data); - Localization.logHelpCallKey(key); - Localization.logHelpCall(); - translation = ""; - break; - } - } } catch (e) { console.error(e); - translation = ""; } if (!translation) { - if (!useDefault) { - console.warn(`Missing translation entry for your language ${Localization.locale.toUpperCase()}. Using ${Localization.defaultLocale.toUpperCase()} instead.`, key, attr); - Localization.logHelpCallKey(key); - Localization.logHelpCall(); - translation = this.getTranslation(key, attr, data, true); + throw new Error(`Translation misses entry. Key: ${key} Attribute: ${attr}`); + } + + return translation; + } + + static addDataToTranslation(translation, data) { + for (let j in data) { + if (!translation.includes(`{{${j}}}`)) { + throw new Error(`Translation misses data placeholder: ${j}`); + } + // Add data to translation + translation = translation.replace(`{{${j}}}`, data[j]); + } + return translation; + } + + static getTranslation(key, attr = null, data = {}, useDefault = false) { + let translationObj = useDefault + ? Localization.translationsDefaultLocale + : Localization.translations; + + let translation; + + try { + translation = Localization.getTranslationFromTranslationsObj(translationObj, key, attr); + translation = Localization.addDataToTranslation(translation, data); + } + catch (e) { + // Log warnings and help calls + console.warn(e); + Localization.logTranslationMissingOrBroken(key, attr, data, useDefault); + Localization.logHelpCallKey(key, attr); + Localization.logHelpCall(); + + if (useDefault || Localization.currentLocaleIsDefault()) { + // Is default locale already + // Use empty string as translation + translation = "" } else { - console.warn("Missing translation in default language:", key, attr); - Localization.logHelpCall(); + // Is not default locale yet + // Get translation for default language with same arguments + console.log(`Using default language ${Localization.defaultLocale.toUpperCase()} instead.`); + translation = this.getTranslation(key, attr, data, true); } } return Localization.escapeHTML(translation); } + static logTranslationMissingOrBroken(key, attr, data, useDefault) { + let usedLocale = useDefault + ? Localization.defaultLocale.toUpperCase() + : Localization.locale.toUpperCase(); + + console.warn(`Missing or broken translation for language ${usedLocale}.\n`, 'key:', key, 'attr:', attr, 'data:', data); + } + static logHelpCall() { console.log("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/"); } - static logHelpCallKey(key) { - console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${Localization.locale.toLowerCase()}/?q=${key}`); + static logHelpCallKey(key, attr) { + let locale = Localization.locale.toLowerCase(); + + let keyComplete = !attr || attr === "text" + ? key + : `${key}_${attr}`; + + console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${locale}/?q=${keyComplete}`); } static escapeHTML(unsafeText) { diff --git a/public/scripts/ui-main.js b/public/scripts/ui-main.js index 409a29c..c7a27e5 100644 --- a/public/scripts/ui-main.js +++ b/public/scripts/ui-main.js @@ -132,7 +132,7 @@ class HeaderUI { this.$header.classList.remove('overflow-expanded'); - const rtlLocale = Localization.isCurrentLocaleRtl(); + const rtlLocale = Localization.currentLocaleIsRtl(); let icon; const $headerIconsShown = document.querySelectorAll('body > header:first-of-type > *:not([hidden])');