diff --git a/_includes/2020/templates/Head.php b/_includes/2020/templates/Head.php index 53e0bd70..e9993854 100644 --- a/_includes/2020/templates/Head.php +++ b/_includes/2020/templates/Head.php @@ -66,9 +66,13 @@ static function render($fields = []) { Util::cdn('js/kmlive.js') ); - foreach($fields->js as $jsFile) { ?> + foreach($fields->js as $jsFile) { + if (str_contains($jsFile, '/js/i18n/') || str_contains($jsFile, 'search.js')) { ?> + + - + ", + "crowdinContext": "More pages" + } +} diff --git a/cdn/dev/js/i18n/es.json b/cdn/dev/js/i18n/es.json new file mode 100644 index 00000000..441345b7 --- /dev/null +++ b/cdn/dev/js/i18n/es.json @@ -0,0 +1,54 @@ +{ + "resultOne": { + "text": "resultado", + "crowdinContext": "1 keyboard result found" + }, + "resultMore": { + "text": "resultados", + "crowdinContext": "More than 1 keyboard results found" + }, + "pageNumberOfTotalPages": { + "text": "página {pageNumber} de {totalPages}.", + "crowdinContext": "Summary of how many pages of keyboard results" + }, + "keyboardSearchTitle": { + "text": "- Búsqueda por teclado", + "crowdinContext": "title" + }, + "obsoleteKeyboards": { + "text": "Teclados obsoletos", + "crowdinContext": "Separator for obsolete keyboards" + }, + "monthlyDownloadZero": { + "text": "No hay descargas recientes", + "crowdinContext": "0 monthly downloads" + }, + "monthlyDownloadOne": { + "text": "descargas mensual", + "crowdinContext": "1 monthly download" + }, + "monthlyDownloadMore": { + "text": "descargas mensuales", + "crowdinContext": "More than 1 monthly download" + }, + "notUnicode": { + "text": "Nota: No es un teclado Unicode", + "crowdinContext": "Disclaimer for legacy non-Unicode keyboard" + }, + "designedForPlatform": { + "text": "Diseñado para {platform}", + "crowdinContext": "Designed for {OS platform}" + }, + "noMatchesFoundForKeyboard": { + "text": "No se encontraron coincidencias para '{keyboard}'", + "crowdinContext": "No keyboards found for search" + }, + "previousPager": { + "text": "< Anteriores", + "crowdinContext": "Previous pages" + }, + "nextPager": { + "text": "Más >", + "crowdinContext": "More pages" + } +} diff --git a/cdn/dev/js/i18n/fr.json b/cdn/dev/js/i18n/fr.json new file mode 100644 index 00000000..fd81dbb1 --- /dev/null +++ b/cdn/dev/js/i18n/fr.json @@ -0,0 +1,54 @@ +{ + "resultOne": { + "text": "résultat", + "crowdinContext": "1 keyboard result found" + }, + "resultMore": { + "text": "résultats", + "crowdinContext": "More than 1 keyboard results found" + }, + "pageNumberOfTotalPages": { + "text": "page {pageNumber} sur {totalPages}.", + "crowdinContext": "Summary of how many pages of keyboard results" + }, + "keyboardSearchTitle": { + "text": "- Recherche au clavier", + "crowdinContext": "title" + }, + "obsoleteKeyboards": { + "text": "Claviers obsolètes", + "crowdinContext": "Separator for obsolete keyboards" + }, + "monthlyDownloadZero": { + "text": "Aucun téléchargement récent", + "crowdinContext": "0 monthly downloads" + }, + "monthlyDownloadOne": { + "text": "téléchargement mensuel", + "crowdinContext": "1 monthly download" + }, + "monthlyDownloadMore": { + "text": "téléchargements mensuels", + "crowdinContext": "More than 1 monthly download" + }, + "notUnicode": { + "text": "Remarque: Ce n'est pas un clavier Unicode.", + "crowdinContext": "Disclaimer for legacy non-Unicode keyboard" + }, + "designedForPlatform": { + "text": "Conçu pour {platform}", + "crowdinContext": "Designed for {OS platform}" + }, + "noMatchesFoundForKeyboard": { + "text": "Aucun résultat trouvé pour '{keyboard}'", + "crowdinContext": "No keyboards found for search" + }, + "previousPager": { + "text": "< Précédentes", + "crowdinContext": "Previous pages" + }, + "nextPager": { + "text": "Plus >", + "crowdinContext": "More pages" + } +} diff --git a/cdn/dev/js/i18n/i18n.js b/cdn/dev/js/i18n/i18n.js new file mode 100644 index 00000000..fe820216 --- /dev/null +++ b/cdn/dev/js/i18n/i18n.js @@ -0,0 +1,104 @@ +/** + * Keyman is copyright (c) SIL Global. MIT License + * + * Vanilla JS for localizing keyboard search strings without a framework + * Reference: https://medium.com/@mihura.ian/translations-in-vanilla-javascript-c942c2095170 + */ +import translationEN from './en.json' with { type: 'json' }; +import translationES from './es.json' with { type: 'json' }; +import translationFR from './fr.json' with { type: 'json' }; + +const translations = { + "en": translationEN, +}; + +/** + * Load translation for a language if not already added + * @param {String} lang + */ +function loadTranslation(lang) { + if (!translations.hasOwnProperty(lang)) { + switch(lang) { + case 'es' : + translations['es'] = translationES; + break; + case 'fr' : + translations['fr'] = translationFR; + break; + default: + } + } +} + +/** + * Navigates inside `obj` with `path` string, + * + * Usage: + * objNavigate({a: {b: 123}}, "a.b") // returns 123 + * + * Fails silently. + * @param {obj} obj + * @param {String} path to navigate into obj + * @returns String or undefined if variable is not found. + */ +function objNavigate(obj, path){ + var aPath = path.split('.'); + try { + return aPath.reduce((a, v) => a[v].text, obj); + } catch { + return; + } +}; + +/** + * Interpolates variables wrapped with `{}` in `str` with variables in `obj` + * It will replace what it can, and leave the rest untouched + * + * Usage: + * + * named variables: + * strObjInterpolation("I'm {age} years old!", { age: 29 }); + * + * ordered variables + * strObjInterpolation("The {0} says {1}, {1}, {1}!", ['cow', 'moo']); + */ +function strObjInterpolation(str, obj){ + obj = obj || []; + str = str ? str.toString() : ''; + return str.replace( + /{([^{}]*)}/g, + (a, b) => { + const r = obj[b]; + return typeof r === 'string' || typeof r === 'number' ? r : a; + }, + ); +}; + +/** + * Determine the display UI language for the keyboard search + * Navigate the translation JSON + * @param {string} key + * @param {obj} interpolations for optional formatted parameters + * @returns localized string + */ +export default function t(key, interpolations) { + // embed_lang set by session.php + var language = embed_lang ?? "en"; + + loadTranslation(language); + + if (!translations[language]) { + // Langage is missing, so fallback to "en" + console.warn(`i18n for language: '${language}' missing, fallback to 'en'`); + language = "en"; + } + + if (!translations[language].hasOwnProperty(key)) { + // key is missing for current language + console.warn(`key '${key}' missing in '${language}' translations`); + return ''; + } + + const value = objNavigate(translations[language], key); + return strObjInterpolation(value, interpolations); +} diff --git a/cdn/dev/keyboard-search/search.js b/cdn/dev/keyboard-search/search.js index e4242fb9..ff814b99 100644 --- a/cdn/dev/keyboard-search/search.js +++ b/cdn/dev/keyboard-search/search.js @@ -1,3 +1,5 @@ +import t from '../js/i18n/i18n.js'; + // Polyfill for String.prototype.includes if (!String.prototype.includes) { @@ -258,25 +260,25 @@ function process_response(q, obsolete, res) { var deprecatedElement = null; $('