From 3ad32cf8ea9b441c4e32759c8513fcde028c720c Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Wed, 11 Feb 2026 10:47:06 +0700 Subject: [PATCH 01/19] diy for localizing JS search strings --- cdn/dev/js/i18n/i18n.js | 74 +++++++++++++++++++++++++++++++ cdn/dev/js/i18n/translations.js | 33 ++++++++++++++ cdn/dev/keyboard-search/search.js | 22 ++++----- keyboards/index.php | 3 ++ 4 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 cdn/dev/js/i18n/i18n.js create mode 100644 cdn/dev/js/i18n/translations.js diff --git a/cdn/dev/js/i18n/i18n.js b/cdn/dev/js/i18n/i18n.js new file mode 100644 index 00000000..8c62609e --- /dev/null +++ b/cdn/dev/js/i18n/i18n.js @@ -0,0 +1,74 @@ +/** + * Vanilla JS for localizing keyboard search strings without a framework + * Reference: https://medium.com/@mihura.ian/translations-in-vanilla-javascript-c942c2095170 + */ + + +/** + * Navigates inside `obj` with `path` string, + * + * Usage: + * objNavigate({a: {b: 123}}, "a.b") // returns 123 + * + * Returns undefined if variable is not found. + * Fails silently. + */ +function objNavigate(obj, path){ + aPath = path.split('.'); + try { + return aPath.reduce((a, v) => a[v], 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 + */ +function t(key, interpolations) { + // embed_lang set by session.php + var language = embed_lang ?? "en"; + + 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); +} \ No newline at end of file diff --git a/cdn/dev/js/i18n/translations.js b/cdn/dev/js/i18n/translations.js new file mode 100644 index 00000000..fc787dfb --- /dev/null +++ b/cdn/dev/js/i18n/translations.js @@ -0,0 +1,33 @@ +// TODO: auto-generate this from JSON files + +const translations = { + "en" : { + "resultOne": "result", + "resultMore": "results", + "pageNumberOfTotalPages": "page {pageNumber} of {totalPages}.", + "keyboardSearchTitle": "- Keyboard search", + "obsoleteKeyboards": "Obsolete keyboards", + "monthlyDownloadZero": "No recent downloads", + "monthlyDownloadOne": "monthly download", + "monthlyDownloadMore": "monthly downloads", + "notUnicode": "Note: Not a Unicode keyboard", + "designedForPlatform": "Designed for", + "previousPager": "< Previous", + "nextPager": "Next >" + + }, + "es" : { + "resultOne": "resultado", + "resultMore": "resultados", + "pageNumberOfTotalPages": "página {pageNumber} de {totalPages}.", + "keyboardSearchTitle": "- Búsqueda por teclado", + "obsoleteKeyboards": "Teclados obsoletos", + "monthlyDownloadZero": "No hay descargas recientes", + "monthlyDownloadMore": "descargas mensual", + "monthlyDownloadMore": "descargas mensuales", + "notUnicode": "Nota: No es un teclado Unicode", + "designedForPlatform": "Diseñado para", + "previousPager": "< Anteriores", + "nextPager": "Más >" + } +} \ No newline at end of file diff --git a/cdn/dev/keyboard-search/search.js b/cdn/dev/keyboard-search/search.js index e4242fb9..ec2253f5 100644 --- a/cdn/dev/keyboard-search/search.js +++ b/cdn/dev/keyboard-search/search.js @@ -258,18 +258,18 @@ function process_response(q, obsolete, res) { var deprecatedElement = null; $('
').text( - res.context.totalRows + (res.context.totalRows == 1 ? ' result' : ' results') + - (res.context.totalPages < 2 ? '' : '; page '+res.context.pageNumber + ' of '+res.context.totalPages+'.') + res.context.totalRows + ' ' + (res.context.totalRows == 1 ? t('resultOne') : t('resultMore') )+ ' ' + + (res.context.totalPages < 2 ? '' : t('pageNumberOfTotalPages', {pageNumber: res.context.pageNumber, totalPages: res.context.totalPages})) ).appendTo(resultsElement); - document.title = q + ' - Keyboard search'; + document.title = q + ' ' + t('keyboardSearchTitle'); res.keyboards.forEach(function(kbd) { if(isKeyboardObsolete(kbd) && !deprecatedElement) { // TODO: make title change depending on whether deprecated keyboards are shown or hidden deprecatedElement = $( - '

Obsolete keyboards

'); + '

' + t('obsoleteKeyboards') + '

'); resultsElement.append(deprecatedElement); } @@ -296,14 +296,14 @@ function process_response(q, obsolete, res) { if(kbd.isDedicatedLandingPage) { // We won't show the downloads text } else if(kbd.match.downloads == 0) - $('.downloads', k).text('No recent downloads'); + $('.downloads', k).text(t('monthlyDownloadZero')); else if(kbd.match.downloads == 1) - $('.downloads', k).text(kbd.match.downloads+' monthly download'); + $('.downloads', k).text(kbd.match.downloads+' ' + t('monthlyDownloadOne')); else - $('.downloads', k).text(kbd.match.downloads+' monthly downloads'); + $('.downloads', k).text(kbd.match.downloads+' ' + t('monthlyDownloadMore')); if(!kbd.encodings.toString().match(/unicode/)) { - $('.encoding', k).text('Note: Not a Unicode keyboard'); + $('.encoding', k).text(t('notUnicode')); } $('.id', k).text(kbd.id); @@ -331,7 +331,7 @@ function process_response(q, obsolete, res) { // icon-ios // icon-linux // icon-windows - var img = $('').attr('src', '/cdn/dev/keyboard-search/icon-'+i+'.png').attr('title', 'Designed for '+i); + var img = $('').attr('src', '/cdn/dev/keyboard-search/icon-'+i+'.png').attr('title', t('designedForPlatform')+' '+i); $('.platforms', k).append(img); } } @@ -358,7 +358,7 @@ function buildPager(res, q, obsolete) { } } - appendPager(pager, '< Previous', res.context.pageNumber-1); + appendPager(pager, t('previousPager'), res.context.pageNumber-1); if(res.context.pageNumber > 5) { $('...').appendTo(pager); } @@ -368,7 +368,7 @@ function buildPager(res, q, obsolete) { if(res.context.pageNumber < res.context.totalPages - 4) { $('...').appendTo(pager); } - appendPager(pager, 'Next >', res.context.pageNumber+1); + appendPager(pager, t('nextPager'), res.context.pageNumber+1); return pager; } diff --git a/keyboards/index.php b/keyboards/index.php index ebe6455b..2165ce15 100644 --- a/keyboards/index.php +++ b/keyboards/index.php @@ -19,6 +19,8 @@ function _m($id, ...$args) { return Locale::m(LOCALE_KEYBOARDS, $id, ...$args 'language' => isset($_SESSION['lang']) ? $_SESSION['lang'] : 'en', 'css' => [Util::cdn('css/template.css'), Util::cdn('keyboard-search/search.css')], 'js' => [Util::cdn('keyboard-search/jquery.mark.js'), Util::cdn('keyboard-search/dedicated-landing-pages.js'), + Util::cdn('js/i18n/translations.js'), + Util::cdn('js/i18n/i18n.js'), Util::cdn('keyboard-search/search.js')] ]; @@ -39,6 +41,7 @@ function _m($id, ...$args) { return Locale::m(LOCALE_KEYBOARDS, $id, ...$args + - + " +} diff --git a/cdn/dev/js/i18n/es.json b/cdn/dev/js/i18n/es.json new file mode 100644 index 00000000..8d3ba0e6 --- /dev/null +++ b/cdn/dev/js/i18n/es.json @@ -0,0 +1,14 @@ +{ + "resultOne": "resultado", + "resultMore": "resultados", + "pageNumberOfTotalPages": "página {pageNumber} de {totalPages}.", + "keyboardSearchTitle": "- Búsqueda por teclado", + "obsoleteKeyboards": "Teclados obsoletos", + "monthlyDownloadZero": "No hay descargas recientes", + "monthlyDownloadOne": "descargas mensual", + "monthlyDownloadMore": "descargas mensuales", + "notUnicode": "Nota: No es un teclado Unicode", + "designedForPlatform": "Diseñado para", + "previousPager": "< Anteriores", + "nextPager": "Más >" +} \ No newline at end of file diff --git a/cdn/dev/js/i18n/fr.json b/cdn/dev/js/i18n/fr.json new file mode 100644 index 00000000..39ed3713 --- /dev/null +++ b/cdn/dev/js/i18n/fr.json @@ -0,0 +1,14 @@ +{ + "resultOne": "résultat", + "resultMore": "résultats", + "pageNumberOfTotalPages": "page {pageNumber} sur {totalPages}.", + "keyboardSearchTitle": "- Recherche au clavier", + "obsoleteKeyboards": "Claviers obsolètes", + "monthlyDownloadZero": "Aucun téléchargement récent", + "monthlyDownloadOne": "téléchargement mensuel", + "monthlyDownloadMore": "téléchargements mensuels", + "notUnicode": "Remarque : Ce n’est pas un clavier Unicode.", + "designedForPlatform": "Conçu pour", + "previousPager": "< Précédentes", + "nextPager": "Plus >" +} \ No newline at end of file diff --git a/cdn/dev/js/i18n/i18n.js b/cdn/dev/js/i18n/i18n.js index 8c62609e..588e528e 100644 --- a/cdn/dev/js/i18n/i18n.js +++ b/cdn/dev/js/i18n/i18n.js @@ -2,6 +2,15 @@ * 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, + "es": translationES, + "fr": translationFR +}; /** @@ -10,11 +19,13 @@ * Usage: * objNavigate({a: {b: 123}}, "a.b") // returns 123 * - * Returns undefined if variable is not found. * 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){ - aPath = path.split('.'); + var aPath = path.split('.'); try { return aPath.reduce((a, v) => a[v], obj); } catch { @@ -53,7 +64,7 @@ function strObjInterpolation(str, obj){ * @param {obj} interpolations for optional formatted parameters * @returns localized string */ -function t(key, interpolations) { +export default function t(key, interpolations) { // embed_lang set by session.php var language = embed_lang ?? "en"; diff --git a/cdn/dev/js/i18n/translations.js b/cdn/dev/js/i18n/translations.js deleted file mode 100644 index fc787dfb..00000000 --- a/cdn/dev/js/i18n/translations.js +++ /dev/null @@ -1,33 +0,0 @@ -// TODO: auto-generate this from JSON files - -const translations = { - "en" : { - "resultOne": "result", - "resultMore": "results", - "pageNumberOfTotalPages": "page {pageNumber} of {totalPages}.", - "keyboardSearchTitle": "- Keyboard search", - "obsoleteKeyboards": "Obsolete keyboards", - "monthlyDownloadZero": "No recent downloads", - "monthlyDownloadOne": "monthly download", - "monthlyDownloadMore": "monthly downloads", - "notUnicode": "Note: Not a Unicode keyboard", - "designedForPlatform": "Designed for", - "previousPager": "< Previous", - "nextPager": "Next >" - - }, - "es" : { - "resultOne": "resultado", - "resultMore": "resultados", - "pageNumberOfTotalPages": "página {pageNumber} de {totalPages}.", - "keyboardSearchTitle": "- Búsqueda por teclado", - "obsoleteKeyboards": "Teclados obsoletos", - "monthlyDownloadZero": "No hay descargas recientes", - "monthlyDownloadMore": "descargas mensual", - "monthlyDownloadMore": "descargas mensuales", - "notUnicode": "Nota: No es un teclado Unicode", - "designedForPlatform": "Diseñado para", - "previousPager": "< Anteriores", - "nextPager": "Más >" - } -} \ No newline at end of file diff --git a/cdn/dev/keyboard-search/search.js b/cdn/dev/keyboard-search/search.js index ec2253f5..a5ee0e7d 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) { @@ -273,10 +275,10 @@ function process_response(q, obsolete, res) { resultsElement.append(deprecatedElement); } - $keyboardClass = kbd.isDedicatedLandingPage ? 'keyboard keyboardLandingPage' : 'keyboard'; + var keyboardClass = kbd.isDedicatedLandingPage ? 'keyboard keyboardLandingPage' : 'keyboard'; var k = $( - "
"+ + "
"+ "
"+ "
"+ "
"+ @@ -384,7 +386,7 @@ function goToPage(event, q, page) { return false; } -function do_search() { +export function do_search() { document.f.page.value = 1; search(true); return false; // always return false from search box diff --git a/keyboards/index.php b/keyboards/index.php index 2165ce15..3ff54b5c 100644 --- a/keyboards/index.php +++ b/keyboards/index.php @@ -19,7 +19,6 @@ function _m($id, ...$args) { return Locale::m(LOCALE_KEYBOARDS, $id, ...$args 'language' => isset($_SESSION['lang']) ? $_SESSION['lang'] : 'en', 'css' => [Util::cdn('css/template.css'), Util::cdn('keyboard-search/search.css')], 'js' => [Util::cdn('keyboard-search/jquery.mark.js'), Util::cdn('keyboard-search/dedicated-landing-pages.js'), - Util::cdn('js/i18n/translations.js'), Util::cdn('js/i18n/i18n.js'), Util::cdn('keyboard-search/search.js')] ]; From ef90f138ccd373fab8c4c4ce4e2e02cbca1efa8a Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Thu, 12 Feb 2026 10:01:27 +0700 Subject: [PATCH 03/19] refactor: Add crowdinContext to JSON files --- cdn/dev/js/i18n/en.json | 60 ++++++++++++++++++++++++------ cdn/dev/js/i18n/es.json | 62 ++++++++++++++++++++++++------- cdn/dev/js/i18n/fr.json | 62 ++++++++++++++++++++++++------- cdn/dev/js/i18n/i18n.js | 27 ++++++++++++-- cdn/dev/keyboard-search/search.js | 2 +- 5 files changed, 170 insertions(+), 43 deletions(-) diff --git a/cdn/dev/js/i18n/en.json b/cdn/dev/js/i18n/en.json index 114bb0db..0b359216 100644 --- a/cdn/dev/js/i18n/en.json +++ b/cdn/dev/js/i18n/en.json @@ -1,14 +1,50 @@ { - "resultOne": "result", - "resultMore": "results", - "pageNumberOfTotalPages": "page {pageNumber} of {totalPages}.", - "keyboardSearchTitle": "- Keyboard search", - "obsoleteKeyboards": "Obsolete keyboards", - "monthlyDownloadZero": "No recent downloads", - "monthlyDownloadOne": "monthly download", - "monthlyDownloadMore": "monthly downloads", - "notUnicode": "Note: Not a Unicode keyboard", - "designedForPlatform": "Designed for", - "previousPager": "< Previous", - "nextPager": "Next >" + "resultOne": { + "text": "result", + "crowdinContext": "1 keyboard result found" + }, + "resultMore": { + "text": "results", + "crowdinContext": "More than 1 keyboard results found" + }, + "pageNumberOfTotalPages": { + "text": "page {pageNumber} of {totalPages}.", + "crowdinContext": "Summary of how many pages of keyboard results" + }, + "keyboardSearchTitle": { + "text": "- Keyboard search", + "crowdinContext": "title" + }, + "obsoleteKeyboards": { + "text": "Obsolete keyboards", + "crowdinContext": "Separator for obsolete keyboards" + }, + "monthlyDownloadZero": { + "text": "No recent downloads", + "crowdinContext": "0 monthly downloads" + }, + "monthlyDownloadOne": { + "text": "monthly download", + "crowdinContext": "1 monthly download" + }, + "monthlyDownloadMore": { + "text": "monthly downloads", + "crowdinContext": "More than 1 monthly download" + }, + "notUnicode": { + "text": "Note: Not a Unicode keyboard", + "crowdinContext": "Disclaimer for legacy non-Unicode keyboard" + }, + "designedForPlatform": { + "text": "Designed for {platform}", + "crowdinContext": "Designed for {OS platform}" + }, + "previousPager": { + "text": "< Previous", + "crowdinContext": "Previous pages" + }, + "nextPager": { + "text": "Next >", + "crowdinContext": "More pages" + } } diff --git a/cdn/dev/js/i18n/es.json b/cdn/dev/js/i18n/es.json index 8d3ba0e6..8863c4dc 100644 --- a/cdn/dev/js/i18n/es.json +++ b/cdn/dev/js/i18n/es.json @@ -1,14 +1,50 @@ { - "resultOne": "resultado", - "resultMore": "resultados", - "pageNumberOfTotalPages": "página {pageNumber} de {totalPages}.", - "keyboardSearchTitle": "- Búsqueda por teclado", - "obsoleteKeyboards": "Teclados obsoletos", - "monthlyDownloadZero": "No hay descargas recientes", - "monthlyDownloadOne": "descargas mensual", - "monthlyDownloadMore": "descargas mensuales", - "notUnicode": "Nota: No es un teclado Unicode", - "designedForPlatform": "Diseñado para", - "previousPager": "< Anteriores", - "nextPager": "Más >" -} \ No newline at end of file + "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}" + }, + "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 index 39ed3713..396a25f4 100644 --- a/cdn/dev/js/i18n/fr.json +++ b/cdn/dev/js/i18n/fr.json @@ -1,14 +1,50 @@ { - "resultOne": "résultat", - "resultMore": "résultats", - "pageNumberOfTotalPages": "page {pageNumber} sur {totalPages}.", - "keyboardSearchTitle": "- Recherche au clavier", - "obsoleteKeyboards": "Claviers obsolètes", - "monthlyDownloadZero": "Aucun téléchargement récent", - "monthlyDownloadOne": "téléchargement mensuel", - "monthlyDownloadMore": "téléchargements mensuels", - "notUnicode": "Remarque : Ce n’est pas un clavier Unicode.", - "designedForPlatform": "Conçu pour", - "previousPager": "< Précédentes", - "nextPager": "Plus >" -} \ No newline at end of file + "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}" + }, + "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 index 588e528e..fe820216 100644 --- a/cdn/dev/js/i18n/i18n.js +++ b/cdn/dev/js/i18n/i18n.js @@ -1,4 +1,6 @@ /** + * 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 */ @@ -8,10 +10,25 @@ import translationFR from './fr.json' with { type: 'json' }; const translations = { "en": translationEN, - "es": translationES, - "fr": translationFR }; +/** + * 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, @@ -27,7 +44,7 @@ const translations = { function objNavigate(obj, path){ var aPath = path.split('.'); try { - return aPath.reduce((a, v) => a[v], obj); + return aPath.reduce((a, v) => a[v].text, obj); } catch { return; } @@ -68,6 +85,8 @@ 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'`); @@ -82,4 +101,4 @@ export default function t(key, interpolations) { const value = objNavigate(translations[language], key); return strObjInterpolation(value, interpolations); -} \ No newline at end of file +} diff --git a/cdn/dev/keyboard-search/search.js b/cdn/dev/keyboard-search/search.js index a5ee0e7d..eefa82f6 100644 --- a/cdn/dev/keyboard-search/search.js +++ b/cdn/dev/keyboard-search/search.js @@ -333,7 +333,7 @@ function process_response(q, obsolete, res) { // icon-ios // icon-linux // icon-windows - var img = $('').attr('src', '/cdn/dev/keyboard-search/icon-'+i+'.png').attr('title', t('designedForPlatform')+' '+i); + var img = $('').attr('src', '/cdn/dev/keyboard-search/icon-'+i+'.png').attr('title', t('designedForPlatform', {platform: i})); $('.platforms', k).append(img); } } From 71e1823afe7ff827f433bfeff26fbae6a0b9a76d Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Thu, 12 Feb 2026 14:16:41 +0700 Subject: [PATCH 04/19] fix: Add string for no keyboard found --- cdn/dev/js/i18n/en.json | 4 ++++ cdn/dev/js/i18n/es.json | 4 ++++ cdn/dev/js/i18n/fr.json | 4 ++++ cdn/dev/keyboard-search/search.js | 2 +- 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cdn/dev/js/i18n/en.json b/cdn/dev/js/i18n/en.json index 0b359216..e4e89ef7 100644 --- a/cdn/dev/js/i18n/en.json +++ b/cdn/dev/js/i18n/en.json @@ -39,6 +39,10 @@ "text": "Designed for {platform}", "crowdinContext": "Designed for {OS platform}" }, + "noMatchesFoundForKeyboard": { + "text": "No matches found for '{keyboard}'", + "crowdinContext": "No keyboards found for search" + }, "previousPager": { "text": "< Previous", "crowdinContext": "Previous pages" diff --git a/cdn/dev/js/i18n/es.json b/cdn/dev/js/i18n/es.json index 8863c4dc..441345b7 100644 --- a/cdn/dev/js/i18n/es.json +++ b/cdn/dev/js/i18n/es.json @@ -39,6 +39,10 @@ "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" diff --git a/cdn/dev/js/i18n/fr.json b/cdn/dev/js/i18n/fr.json index 396a25f4..fd81dbb1 100644 --- a/cdn/dev/js/i18n/fr.json +++ b/cdn/dev/js/i18n/fr.json @@ -39,6 +39,10 @@ "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" diff --git a/cdn/dev/keyboard-search/search.js b/cdn/dev/keyboard-search/search.js index eefa82f6..ff814b99 100644 --- a/cdn/dev/keyboard-search/search.js +++ b/cdn/dev/keyboard-search/search.js @@ -346,7 +346,7 @@ function process_response(q, obsolete, res) { buildPager(res, q, obsolete).appendTo(resultsElement); } } else { - $('

').addClass('red').text("No matches found for '"+qq+"'").appendTo(resultsElement); + $('

').addClass('red').text(t('noMatchesFoundForKeyboard', {keyboard: qq})).appendTo(resultsElement); } } From a35c18e573dbde0ee2afb47dcd1e14c2a0af6d59 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Fri, 13 Feb 2026 14:10:28 +0700 Subject: [PATCH 05/19] wrap i18n in a class --- cdn/dev/js/i18n/i18n.js | 104 ---------------------------- cdn/dev/js/i18n/i18n.mjs | 111 ++++++++++++++++++++++++++++++ cdn/dev/keyboard-search/search.js | 4 +- keyboards/index.php | 2 +- 4 files changed, 115 insertions(+), 106 deletions(-) delete mode 100644 cdn/dev/js/i18n/i18n.js create mode 100644 cdn/dev/js/i18n/i18n.mjs diff --git a/cdn/dev/js/i18n/i18n.js b/cdn/dev/js/i18n/i18n.js deleted file mode 100644 index fe820216..00000000 --- a/cdn/dev/js/i18n/i18n.js +++ /dev/null @@ -1,104 +0,0 @@ -/** - * 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/js/i18n/i18n.mjs b/cdn/dev/js/i18n/i18n.mjs new file mode 100644 index 00000000..bb198195 --- /dev/null +++ b/cdn/dev/js/i18n/i18n.mjs @@ -0,0 +1,111 @@ +/** + * 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' }; + +export class I18n { + + static translations = { + "en": translationEN, + }; + + constructor() {} + + + /** + * Load translation for a language if not already added + * @param {String} lang + */ + static loadTranslation(lang) { + if (!I18n.translations.hasOwnProperty(lang)) { + switch(lang) { + case 'es' : + I18n.translations['es'] = translationES; + break; + case 'fr' : + I18n.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. + */ + static 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']); + */ + static 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 + */ + static t(key, interpolations) { + // embed_lang set by session.php + var language = embed_lang ?? "en"; + + I18n.loadTranslation(language); + + if (!I18n.translations[language]) { + // Langage is missing, so fallback to "en" + console.warn(`i18n for language: '${language}' missing, fallback to 'en'`); + language = "en"; + } + + if (!I18n.translations[language].hasOwnProperty(key)) { + // key is missing for current language + console.warn(`key '${key}' missing in '${language}' translations`); + return ''; + } + + const value = I18n.objNavigate(I18n.translations[language], key); + return I18n.strObjInterpolation(value, interpolations); + } + +} diff --git a/cdn/dev/keyboard-search/search.js b/cdn/dev/keyboard-search/search.js index ff814b99..cea7232a 100644 --- a/cdn/dev/keyboard-search/search.js +++ b/cdn/dev/keyboard-search/search.js @@ -1,4 +1,4 @@ -import t from '../js/i18n/i18n.js'; +import { I18n } from '../js/i18n/i18n.mjs'; // Polyfill for String.prototype.includes @@ -14,6 +14,8 @@ if (!String.prototype.includes) { }; } +const t = I18n.t; + ///////////////////////////// if(typeof embed_query == 'undefined') { diff --git a/keyboards/index.php b/keyboards/index.php index 3ff54b5c..f7f2c928 100644 --- a/keyboards/index.php +++ b/keyboards/index.php @@ -19,7 +19,7 @@ function _m($id, ...$args) { return Locale::m(LOCALE_KEYBOARDS, $id, ...$args 'language' => isset($_SESSION['lang']) ? $_SESSION['lang'] : 'en', 'css' => [Util::cdn('css/template.css'), Util::cdn('keyboard-search/search.css')], 'js' => [Util::cdn('keyboard-search/jquery.mark.js'), Util::cdn('keyboard-search/dedicated-landing-pages.js'), - Util::cdn('js/i18n/i18n.js'), + Util::cdn('js/i18n/i18n.mjs'), Util::cdn('keyboard-search/search.js')] ]; From e8691199c4039b2099d4f1c37a81d8bc29fae4a0 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Mon, 16 Feb 2026 07:58:15 +0700 Subject: [PATCH 06/19] Apply suggestions from code review Co-authored-by: Marc Durdin --- cdn/dev/keyboard-search/search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdn/dev/keyboard-search/search.js b/cdn/dev/keyboard-search/search.js index cea7232a..d615cc89 100644 --- a/cdn/dev/keyboard-search/search.js +++ b/cdn/dev/keyboard-search/search.js @@ -277,7 +277,7 @@ function process_response(q, obsolete, res) { resultsElement.append(deprecatedElement); } - var keyboardClass = kbd.isDedicatedLandingPage ? 'keyboard keyboardLandingPage' : 'keyboard'; + const keyboardClass = kbd.isDedicatedLandingPage ? 'keyboard keyboardLandingPage' : 'keyboard'; var k = $( "
"+ From e87862c9342a187ddbf01e5135ea0589f05c2bc3 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Mon, 16 Feb 2026 08:10:01 +0700 Subject: [PATCH 07/19] rename search.js --> search.mjs --- _includes/2020/templates/Head.php | 10 ++++------ _legacy/keyboards/index.php | 2 +- cdn/dev/keyboard-search/{search.js => search.mjs} | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) rename cdn/dev/keyboard-search/{search.js => search.mjs} (98%) diff --git a/_includes/2020/templates/Head.php b/_includes/2020/templates/Head.php index e9993854..96ac70ae 100644 --- a/_includes/2020/templates/Head.php +++ b/_includes/2020/templates/Head.php @@ -67,12 +67,10 @@ static function render($fields = []) { ); foreach($fields->js as $jsFile) { - if (str_contains($jsFile, '/js/i18n/') || str_contains($jsFile, 'search.js')) { ?> - - - - + $jsFileType = str_ends_with($jsFile, '.mjs') ? "type='module'" : ""; + echo ""; + } + ?> Loading...

- \ No newline at end of file + \ No newline at end of file diff --git a/cdn/dev/keyboard-search/search.js b/cdn/dev/keyboard-search/search.mjs similarity index 98% rename from cdn/dev/keyboard-search/search.js rename to cdn/dev/keyboard-search/search.mjs index d615cc89..0e301bca 100644 --- a/cdn/dev/keyboard-search/search.js +++ b/cdn/dev/keyboard-search/search.mjs @@ -89,7 +89,7 @@ function wrapSearch(localCounter, updateHistory) { return false; } - var base = location.protocol+'//api.'+location.host; // this works on test sites as well as live, assuming we use the host pattern "keyman.com[.localhost]" + var base = 'https://api.keyman.com'; // Don't commit to production - location.protocol+'//api.'+location.host; // this works on test sites as well as live, assuming we use the host pattern "keyman.com[.localhost]" var url = base+'/search/2.0?p='+page+'&q='+encodeURIComponent(stripCommonWords(q)); if(embed) { From c9a7bfb25967e2963a792a38800c1433b009b166 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Mon, 16 Feb 2026 08:16:56 +0700 Subject: [PATCH 08/19] revert api line --- cdn/dev/keyboard-search/search.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdn/dev/keyboard-search/search.mjs b/cdn/dev/keyboard-search/search.mjs index 0e301bca..d615cc89 100644 --- a/cdn/dev/keyboard-search/search.mjs +++ b/cdn/dev/keyboard-search/search.mjs @@ -89,7 +89,7 @@ function wrapSearch(localCounter, updateHistory) { return false; } - var base = 'https://api.keyman.com'; // Don't commit to production - location.protocol+'//api.'+location.host; // this works on test sites as well as live, assuming we use the host pattern "keyman.com[.localhost]" + var base = location.protocol+'//api.'+location.host; // this works on test sites as well as live, assuming we use the host pattern "keyman.com[.localhost]" var url = base+'/search/2.0?p='+page+'&q='+encodeURIComponent(stripCommonWords(q)); if(embed) { From 0b7dc5d60916668d546eea93a2017f65891b79f0 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Mon, 16 Feb 2026 09:18:24 +0700 Subject: [PATCH 09/19] Fix objNavigate nesting --- cdn/dev/js/i18n/i18n.mjs | 4 ++-- keyboards/index.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cdn/dev/js/i18n/i18n.mjs b/cdn/dev/js/i18n/i18n.mjs index bb198195..4026f4af 100644 --- a/cdn/dev/js/i18n/i18n.mjs +++ b/cdn/dev/js/i18n/i18n.mjs @@ -39,7 +39,7 @@ export class I18n { * Navigates inside `obj` with `path` string, * * Usage: - * objNavigate({a: {b: 123}}, "a.b") // returns 123 + * objNavigate({a: {b: {c: {text:123}}}}, "a.b.c") // returns 123 * * Fails silently. * @param {obj} obj @@ -49,7 +49,7 @@ export class I18n { static objNavigate(obj, path){ var aPath = path.split('.'); try { - return aPath.reduce((a, v) => a[v].text, obj); + return aPath.reduce((a, v) => a[v], obj).text; } catch { return; } diff --git a/keyboards/index.php b/keyboards/index.php index f7f2c928..3c20595d 100644 --- a/keyboards/index.php +++ b/keyboards/index.php @@ -20,7 +20,7 @@ function _m($id, ...$args) { return Locale::m(LOCALE_KEYBOARDS, $id, ...$args 'css' => [Util::cdn('css/template.css'), Util::cdn('keyboard-search/search.css')], 'js' => [Util::cdn('keyboard-search/jquery.mark.js'), Util::cdn('keyboard-search/dedicated-landing-pages.js'), Util::cdn('js/i18n/i18n.mjs'), - Util::cdn('keyboard-search/search.js')] + Util::cdn('keyboard-search/search.mjs')] ]; if($embed != 'none') { From f525485021ef5dd9d1617e9dd751a637f64a7b06 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Mon, 16 Feb 2026 09:27:44 +0700 Subject: [PATCH 10/19] chore: cleanup TODO pagination strings now handled by search.mjs --- _includes/locale/strings/keyboards/details/en.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/_includes/locale/strings/keyboards/details/en.php b/_includes/locale/strings/keyboards/details/en.php index e6e7c872..be11fe69 100644 --- a/_includes/locale/strings/keyboards/details/en.php +++ b/_includes/locale/strings/keyboards/details/en.php @@ -134,9 +134,6 @@ "new_search" => "New search", - ## TODO: Previous/Next pagination handled in search.js - - ## Errors # Failed to load keyboard package [ID] From 2caf7cfdd816dc5dd7c81ae2396e5f04711d4baf Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Tue, 17 Feb 2026 10:44:25 +0700 Subject: [PATCH 11/19] revert legacy/ keyboard search --- _legacy/keyboards/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_legacy/keyboards/index.php b/_legacy/keyboards/index.php index 094aee0e..71b0c732 100644 --- a/_legacy/keyboards/index.php +++ b/_legacy/keyboards/index.php @@ -39,4 +39,4 @@

Loading...

- \ No newline at end of file + \ No newline at end of file From 8088ca2ada22ec09e5a6b370b27e277f48054999 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Mon, 16 Feb 2026 14:16:51 +0700 Subject: [PATCH 12/19] start adding namespace to strings --- cdn/dev/js/i18n/i18n.mjs | 141 ++++++++++++++++++++++----- cdn/dev/js/i18n/{ => search}/en.json | 0 cdn/dev/js/i18n/{ => search}/es.json | 0 cdn/dev/js/i18n/{ => search}/fr.json | 0 cdn/dev/keyboard-search/search.mjs | 42 ++++---- 5 files changed, 138 insertions(+), 45 deletions(-) rename cdn/dev/js/i18n/{ => search}/en.json (100%) rename cdn/dev/js/i18n/{ => search}/es.json (100%) rename cdn/dev/js/i18n/{ => search}/fr.json (100%) diff --git a/cdn/dev/js/i18n/i18n.mjs b/cdn/dev/js/i18n/i18n.mjs index 4026f4af..30cbb1c6 100644 --- a/cdn/dev/js/i18n/i18n.mjs +++ b/cdn/dev/js/i18n/i18n.mjs @@ -4,35 +4,104 @@ * 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' }; export class I18n { - static translations = { - "en": translationEN, - }; + static DEFAULT_LOCALE = 'en'; + + // Array of the supported locales + static currentLocales = []; + + static currentDomain = ''; - constructor() {} + // strings is an array of domains. + // Each domain is an array of locales + // Each locale is an object? with loaded flag and array of strings + static strings = []; /** - * Load translation for a language if not already added - * @param {String} lang + * Set the current locales, with an array of fallbacks, ending in 'en' + * @param {locale} The new current locale */ - static loadTranslation(lang) { - if (!I18n.translations.hasOwnProperty(lang)) { - switch(lang) { - case 'es' : - I18n.translations['es'] = translationES; - break; - case 'fr' : - I18n.translations['fr'] = translationFR; - break; - default: - } + static setLocale(locale) { + // Clea current locales + I18n.currentLocales = []; + + if (!locale) { + I18n.currentLocales = I18n.calculateFallbackLocales(locale); } + + // Push default ballback locale to the end + I18n.currentLocales.push(I18n.DEFAULT_LOCALE); + } + + /** + * Load the strings for the given domain + * @param {string} domain + */ + static loadDomain(domain) { + if (!I18n.strings[domain]) { + I18n.strings[domain] = []; + } + I18n.strings[domain]['en'] = { + strings: [], + loaded: false + } + } + + /** + * Defines a global variable for page locale strings and also + * tells locale system that current page uses locales + * @param $domain - + * @param $id - folder containing locale strings, relative to /cdn/dev/js/i18n + */ + static async definePageLocale(domain, id) { + I18n.currentDomain = domain; + if (!I18n.strings.hasOwnProperty(id)) { + I18n.strings[id] = []; + } + await I18n.loadStrings(domain, 'en'); + } + + /** + * Given a locale, return an array of fallback locales + * For example: es-ES --> [es, es-ES] + * TODO: Use an existing fallback algorthim like + * https://cldr.unicode.org/development/development-process/design-proposals/language-distance-data + * @param $locale - the locale to determine fallback locales + * @return array of fallback locales + */ + static calculateFallbackLocales(locale) { + // Start with the given locale + var fallback = [locale]; + + // Support other fallbacks such as es-419 -> es + var parts = locale.split('-'); + for (var i = parts.length-1; i > 0; i--) { + var lastPosition = locale.lastIndexOf(parts[i]) - 1; + // Insert language tag substring to head + fallback.unshift(locale.substr(0, lastPosition)); + } + + return fallback; + } + + /** + * Dynamically load translation for a language if not already added + * @param {String} lang + */ + static async loadStrings(domain, lang) { + var currentLocaleFilename = `./${domain}/${lang}.json`; + I18n.currentDomain = domain; + + const jsModule = await import(currentLocaleFilename, { + with: { type: 'json'} + }); + I18n.strings[I18n.currentDomain][lang] = { + strings: jsModule.default, + loaded: true + }; } /** @@ -82,29 +151,47 @@ export class I18n { /** * Determine the display UI language for the keyboard search * Navigate the translation JSON + * @param {string} domain of the localized strings * @param {string} key * @param {obj} interpolations for optional formatted parameters * @returns localized string */ - static t(key, interpolations) { + static async t(domain, key, interpolations) { + // Load the domain if it doesn't exist + if (!I18n.strings[domain]) { + loadDomain(domain); + } + // embed_lang set by session.php var language = embed_lang ?? "en"; + if (I18n.currentDomain) { + if (!I18n.strings[domain][language]) { + var obj = { + strings: {}, + loaded: false + }; + I18n.strings[domain][language] = obj; + } + if (!I18n.strings[domain][language].loaded) { + // Will set -> loaded = true + await I18n.loadStrings(domain, language); + } + } - I18n.loadTranslation(language); - - if (!I18n.translations[language]) { + if (!I18n.strings[I18n.currentDomain][language]) { // Langage is missing, so fallback to "en" console.warn(`i18n for language: '${language}' missing, fallback to 'en'`); language = "en"; } - if (!I18n.translations[language].hasOwnProperty(key)) { + if (!I18n.strings[I18n.currentDomain][language].strings[key]) { // key is missing for current language - console.warn(`key '${key}' missing in '${language}' translations`); + console.warn(`key '${key}' missing in '${language}' strings`); return ''; } - const value = I18n.objNavigate(I18n.translations[language], key); + const value = I18n.objNavigate(I18n.strings[I18n.currentDomain][language].strings, key); + //console.info(`language: ${language}, key: ${key}, value: ${value}`); return I18n.strObjInterpolation(value, interpolations); } diff --git a/cdn/dev/js/i18n/en.json b/cdn/dev/js/i18n/search/en.json similarity index 100% rename from cdn/dev/js/i18n/en.json rename to cdn/dev/js/i18n/search/en.json diff --git a/cdn/dev/js/i18n/es.json b/cdn/dev/js/i18n/search/es.json similarity index 100% rename from cdn/dev/js/i18n/es.json rename to cdn/dev/js/i18n/search/es.json diff --git a/cdn/dev/js/i18n/fr.json b/cdn/dev/js/i18n/search/fr.json similarity index 100% rename from cdn/dev/js/i18n/fr.json rename to cdn/dev/js/i18n/search/fr.json diff --git a/cdn/dev/keyboard-search/search.mjs b/cdn/dev/keyboard-search/search.mjs index d615cc89..636e9e35 100644 --- a/cdn/dev/keyboard-search/search.mjs +++ b/cdn/dev/keyboard-search/search.mjs @@ -14,7 +14,12 @@ if (!String.prototype.includes) { }; } -const t = I18n.t; +await I18n.definePageLocale('search', 'search'); + +const t = async (key, interpolations) => { + const v = await I18n.t('search', key, interpolations); + return v; +} ///////////////////////////// @@ -233,7 +238,7 @@ function process_page_match(q) { return result; } -function process_response(q, obsolete, res) { +async function process_response(q, obsolete, res) { var resultsElement = $('#search-results'); res = JSON.parse(res); resultsElement.empty(); @@ -262,18 +267,18 @@ function process_response(q, obsolete, res) { var deprecatedElement = null; $('
').text( - res.context.totalRows + ' ' + (res.context.totalRows == 1 ? t('resultOne') : t('resultMore') )+ ' ' + - (res.context.totalPages < 2 ? '' : t('pageNumberOfTotalPages', {pageNumber: res.context.pageNumber, totalPages: res.context.totalPages})) + res.context.totalRows + ' ' + (res.context.totalRows == 1 ? await t('resultOne') : await t('resultMore') )+ ' ' + + (res.context.totalPages < 2 ? '' : await t('pageNumberOfTotalPages', {pageNumber: res.context.pageNumber, totalPages: res.context.totalPages})) ).appendTo(resultsElement); - document.title = q + ' ' + t('keyboardSearchTitle'); + document.title = q + ' ' + await t('keyboardSearchTitle'); - res.keyboards.forEach(function(kbd) { + for (const kbd of res.keyboards) { if(isKeyboardObsolete(kbd) && !deprecatedElement) { // TODO: make title change depending on whether deprecated keyboards are shown or hidden deprecatedElement = $( - '

' + t('obsoleteKeyboards') + '

'); + '

' + await t('obsoleteKeyboards') + '

'); resultsElement.append(deprecatedElement); } @@ -300,14 +305,14 @@ function process_response(q, obsolete, res) { if(kbd.isDedicatedLandingPage) { // We won't show the downloads text } else if(kbd.match.downloads == 0) - $('.downloads', k).text(t('monthlyDownloadZero')); + $('.downloads', k).text(t('search', 'monthlyDownloadZero')); else if(kbd.match.downloads == 1) - $('.downloads', k).text(kbd.match.downloads+' ' + t('monthlyDownloadOne')); + $('.downloads', k).text(kbd.match.downloads+' ' + await('monthlyDownloadOne')); else - $('.downloads', k).text(kbd.match.downloads+' ' + t('monthlyDownloadMore')); + $('.downloads', k).text(kbd.match.downloads+' ' + await('monthlyDownloadMore')); if(!kbd.encodings.toString().match(/unicode/)) { - $('.encoding', k).text(t('notUnicode')); + $('.encoding', k).text(t('search', 'notUnicode')); } $('.id', k).text(kbd.id); @@ -335,24 +340,25 @@ function process_response(q, obsolete, res) { // icon-ios // icon-linux // icon-windows - var img = $('').attr('src', '/cdn/dev/keyboard-search/icon-'+i+'.png').attr('title', t('designedForPlatform', {platform: i})); + var img = $('').attr('src', '/cdn/dev/keyboard-search/icon-'+i+'.png').attr('title', await('designedForPlatform', {platform: i})); $('.platforms', k).append(img); } } } $('.platforms', k).text(); (deprecatedElement ? deprecatedElement : resultsElement).append(k); - }); + }; if(res.context.totalPages > 1) { - buildPager(res, q, obsolete).appendTo(resultsElement); + const p = await buildPager(res, q, obsolete); + p.appendTo(resultsElement); } } else { - $('

').addClass('red').text(t('noMatchesFoundForKeyboard', {keyboard: qq})).appendTo(resultsElement); + $('

').addClass('red').text(t('search', 'noMatchesFoundForKeyboard', {keyboard: qq})).appendTo(resultsElement); } } -function buildPager(res, q, obsolete) { +async function buildPager(res, q, obsolete) { var pager = $('
'); function appendPager(pager, text, page) { if(page != res.context.pageNumber && page > 0 && page <= res.context.totalPages) { @@ -362,7 +368,7 @@ function buildPager(res, q, obsolete) { } } - appendPager(pager, t('previousPager'), res.context.pageNumber-1); + appendPager(pager, await('previousPager'), res.context.pageNumber-1); if(res.context.pageNumber > 5) { $('...').appendTo(pager); } @@ -372,7 +378,7 @@ function buildPager(res, q, obsolete) { if(res.context.pageNumber < res.context.totalPages - 4) { $('...').appendTo(pager); } - appendPager(pager, t('nextPager'), res.context.pageNumber+1); + appendPager(pager, await('nextPager'), res.context.pageNumber+1); return pager; } From 33c7964ade7c178d708c75bcec55b910e8b979cb Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Tue, 17 Feb 2026 14:35:43 +0700 Subject: [PATCH 13/19] collapse json files * Remove crowdinContext Also fix some missing `await t()` syntax --- cdn/dev/js/i18n/i18n.mjs | 4 +- cdn/dev/js/i18n/search/en.json | 65 ++++++------------------------ cdn/dev/js/i18n/search/es.json | 65 ++++++------------------------ cdn/dev/js/i18n/search/fr.json | 65 ++++++------------------------ cdn/dev/keyboard-search/search.mjs | 8 ++-- 5 files changed, 45 insertions(+), 162 deletions(-) diff --git a/cdn/dev/js/i18n/i18n.mjs b/cdn/dev/js/i18n/i18n.mjs index 30cbb1c6..3cdd9caa 100644 --- a/cdn/dev/js/i18n/i18n.mjs +++ b/cdn/dev/js/i18n/i18n.mjs @@ -108,7 +108,7 @@ export class I18n { * Navigates inside `obj` with `path` string, * * Usage: - * objNavigate({a: {b: {c: {text:123}}}}, "a.b.c") // returns 123 + * objNavigate({a: {b: {c: 123}}}, "a.b.c") // returns 123 * * Fails silently. * @param {obj} obj @@ -118,7 +118,7 @@ export class I18n { static objNavigate(obj, path){ var aPath = path.split('.'); try { - return aPath.reduce((a, v) => a[v], obj).text; + return aPath.reduce((a, v) => a[v], obj); } catch { return; } diff --git a/cdn/dev/js/i18n/search/en.json b/cdn/dev/js/i18n/search/en.json index e4e89ef7..c2bcd24a 100644 --- a/cdn/dev/js/i18n/search/en.json +++ b/cdn/dev/js/i18n/search/en.json @@ -1,54 +1,15 @@ { - "resultOne": { - "text": "result", - "crowdinContext": "1 keyboard result found" - }, - "resultMore": { - "text": "results", - "crowdinContext": "More than 1 keyboard results found" - }, - "pageNumberOfTotalPages": { - "text": "page {pageNumber} of {totalPages}.", - "crowdinContext": "Summary of how many pages of keyboard results" - }, - "keyboardSearchTitle": { - "text": "- Keyboard search", - "crowdinContext": "title" - }, - "obsoleteKeyboards": { - "text": "Obsolete keyboards", - "crowdinContext": "Separator for obsolete keyboards" - }, - "monthlyDownloadZero": { - "text": "No recent downloads", - "crowdinContext": "0 monthly downloads" - }, - "monthlyDownloadOne": { - "text": "monthly download", - "crowdinContext": "1 monthly download" - }, - "monthlyDownloadMore": { - "text": "monthly downloads", - "crowdinContext": "More than 1 monthly download" - }, - "notUnicode": { - "text": "Note: Not a Unicode keyboard", - "crowdinContext": "Disclaimer for legacy non-Unicode keyboard" - }, - "designedForPlatform": { - "text": "Designed for {platform}", - "crowdinContext": "Designed for {OS platform}" - }, - "noMatchesFoundForKeyboard": { - "text": "No matches found for '{keyboard}'", - "crowdinContext": "No keyboards found for search" - }, - "previousPager": { - "text": "< Previous", - "crowdinContext": "Previous pages" - }, - "nextPager": { - "text": "Next >", - "crowdinContext": "More pages" - } + "resultOne": "result", + "resultMore": "results", + "pageNumberOfTotalPages": "page {pageNumber} of {totalPages}.", + "keyboardSearchTitle": "- Keyboard search", + "obsoleteKeyboards": "Obsolete keyboards", + "monthlyDownloadZero": "No recent downloads", + "monthlyDownloadOne": "monthly download", + "monthlyDownloadMore": "monthly downloads", + "notUnicode": "Note: Not a Unicode keyboard", + "designedForPlatform": "Designed for {platform}", + "noMatchesFoundForKeyboard": "No matches found for '{keyboard}'", + "previousPager": "< Previous", + "nextPager": "Next >" } diff --git a/cdn/dev/js/i18n/search/es.json b/cdn/dev/js/i18n/search/es.json index 441345b7..57ae2dcd 100644 --- a/cdn/dev/js/i18n/search/es.json +++ b/cdn/dev/js/i18n/search/es.json @@ -1,54 +1,15 @@ { - "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" - } + "resultOne": "resultado", + "resultMore": "resultados", + "pageNumberOfTotalPages": "página {pageNumber} de {totalPages}.", + "keyboardSearchTitle": "- Búsqueda por teclado", + "obsoleteKeyboards": "Teclados obsoletos", + "monthlyDownloadZero": "No hay descargas recientes", + "monthlyDownloadOne": "descargas mensual", + "monthlyDownloadMore": "descargas mensuales", + "notUnicode": "Nota: No es un teclado Unicode", + "designedForPlatform": "Diseñado para {platform}", + "noMatchesFoundForKeyboard": "No se encontraron coincidencias para '{keyboard}'", + "previousPager": "< Anteriores", + "nextPager": "Más >" } diff --git a/cdn/dev/js/i18n/search/fr.json b/cdn/dev/js/i18n/search/fr.json index fd81dbb1..880a0f99 100644 --- a/cdn/dev/js/i18n/search/fr.json +++ b/cdn/dev/js/i18n/search/fr.json @@ -1,54 +1,15 @@ { - "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" - } + "resultOne": "résultat", + "resultMore": "résultats", + "pageNumberOfTotalPages": "page {pageNumber} sur {totalPages}.", + "keyboardSearchTitle": "- Recherche au clavier", + "obsoleteKeyboards": "Claviers obsolètes", + "monthlyDownloadZero": "Aucun téléchargement récent", + "monthlyDownloadOne": "téléchargement mensuel", + "monthlyDownloadMore": "téléchargements mensuels", + "notUnicode": "Remarque: Ce n'est pas un clavier Unicode.", + "designedForPlatform": "Conçu pour {platform}", + "noMatchesFoundForKeyboard": "Aucun résultat trouvé pour '{keyboard}'", + "previousPager": "< Précédentes", + "nextPager": "Plus >" } diff --git a/cdn/dev/keyboard-search/search.mjs b/cdn/dev/keyboard-search/search.mjs index 636e9e35..945b2c08 100644 --- a/cdn/dev/keyboard-search/search.mjs +++ b/cdn/dev/keyboard-search/search.mjs @@ -305,14 +305,14 @@ async function process_response(q, obsolete, res) { if(kbd.isDedicatedLandingPage) { // We won't show the downloads text } else if(kbd.match.downloads == 0) - $('.downloads', k).text(t('search', 'monthlyDownloadZero')); + $('.downloads', k).text(await t('search', 'monthlyDownloadZero')); else if(kbd.match.downloads == 1) $('.downloads', k).text(kbd.match.downloads+' ' + await('monthlyDownloadOne')); else $('.downloads', k).text(kbd.match.downloads+' ' + await('monthlyDownloadMore')); if(!kbd.encodings.toString().match(/unicode/)) { - $('.encoding', k).text(t('search', 'notUnicode')); + $('.encoding', k).text(await t('notUnicode')); } $('.id', k).text(kbd.id); @@ -340,7 +340,7 @@ async function process_response(q, obsolete, res) { // icon-ios // icon-linux // icon-windows - var img = $('').attr('src', '/cdn/dev/keyboard-search/icon-'+i+'.png').attr('title', await('designedForPlatform', {platform: i})); + var img = $('').attr('src', '/cdn/dev/keyboard-search/icon-'+i+'.png').attr('title', await t('designedForPlatform', {platform: i})); $('.platforms', k).append(img); } } @@ -354,7 +354,7 @@ async function process_response(q, obsolete, res) { p.appendTo(resultsElement); } } else { - $('

').addClass('red').text(t('search', 'noMatchesFoundForKeyboard', {keyboard: qq})).appendTo(resultsElement); + $('

').addClass('red').text(await t('noMatchesFoundForKeyboard', {keyboard: qq})).appendTo(resultsElement); } } From 84a08d012b73aec756935288e39dbacef6e8ff20 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Tue, 17 Feb 2026 15:57:59 +0700 Subject: [PATCH 14/19] fix: Handle fallback to 'en' strings --- cdn/dev/js/i18n/i18n.mjs | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/cdn/dev/js/i18n/i18n.mjs b/cdn/dev/js/i18n/i18n.mjs index 3cdd9caa..4c293f38 100644 --- a/cdn/dev/js/i18n/i18n.mjs +++ b/cdn/dev/js/i18n/i18n.mjs @@ -44,7 +44,7 @@ export class I18n { if (!I18n.strings[domain]) { I18n.strings[domain] = []; } - I18n.strings[domain]['en'] = { + I18n.strings[domain][I18n.DEFAULT_LOCALE] = { strings: [], loaded: false } @@ -61,7 +61,7 @@ export class I18n { if (!I18n.strings.hasOwnProperty(id)) { I18n.strings[id] = []; } - await I18n.loadStrings(domain, 'en'); + await I18n.loadStrings(domain, I18n.DEFAULT_LOCALE); } /** @@ -95,13 +95,18 @@ export class I18n { var currentLocaleFilename = `./${domain}/${lang}.json`; I18n.currentDomain = domain; - const jsModule = await import(currentLocaleFilename, { - with: { type: 'json'} - }); - I18n.strings[I18n.currentDomain][lang] = { - strings: jsModule.default, - loaded: true - }; + try { + const jsModule = await import(currentLocaleFilename, { + with: { type: 'json'} + }); + I18n.strings[I18n.currentDomain][lang] = { + strings: jsModule.default, + loaded: true + }; + } catch (ex) { + // JSON localization file doesn't exist. Log to sentry? + //console.warn(`${domain}/${lang}.json doesn't exist. Not loading...`); + } } /** @@ -178,16 +183,11 @@ export class I18n { } } - if (!I18n.strings[I18n.currentDomain][language]) { - // Langage is missing, so fallback to "en" - console.warn(`i18n for language: '${language}' missing, fallback to 'en'`); - language = "en"; - } - - if (!I18n.strings[I18n.currentDomain][language].strings[key]) { - // key is missing for current language - console.warn(`key '${key}' missing in '${language}' strings`); - return ''; + if (!I18n.strings[I18n.currentDomain][language] || !I18n.strings[I18n.currentDomain][language].strings[key]) { + // Langage or key is missing, so fallback to "en" + // Log to Sentry? + // console.warn(`i18n for language: '${language}' or key ${key} missing, fallback to 'en'`); + language = I18n.DEFAULT_LOCALE; } const value = I18n.objNavigate(I18n.strings[I18n.currentDomain][language].strings, key); From 1caf14e9cab84a4dc25f4399418821de945ab9af Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Wed, 18 Feb 2026 06:31:27 +0700 Subject: [PATCH 15/19] fix: cleanup fallbacks --- cdn/dev/js/i18n/i18n.mjs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cdn/dev/js/i18n/i18n.mjs b/cdn/dev/js/i18n/i18n.mjs index 4c293f38..cdffaddc 100644 --- a/cdn/dev/js/i18n/i18n.mjs +++ b/cdn/dev/js/i18n/i18n.mjs @@ -157,7 +157,7 @@ export class I18n { * Determine the display UI language for the keyboard search * Navigate the translation JSON * @param {string} domain of the localized strings - * @param {string} key + * @param {string} key for the string * @param {obj} interpolations for optional formatted parameters * @returns localized string */ @@ -168,7 +168,7 @@ export class I18n { } // embed_lang set by session.php - var language = embed_lang ?? "en"; + var language = embed_lang ?? I18n.DEFAULT_LOCALE; if (I18n.currentDomain) { if (!I18n.strings[domain][language]) { var obj = { @@ -191,7 +191,10 @@ export class I18n { } const value = I18n.objNavigate(I18n.strings[I18n.currentDomain][language].strings, key); - //console.info(`language: ${language}, key: ${key}, value: ${value}`); + if (!value) { + // Warn if string doesn't exist + console.log(`Missing '${I18n.currentDomain}/${language}.json' string for '${key}'`); + } return I18n.strObjInterpolation(value, interpolations); } From b5e93b280744893d97631bea04411d4e0d169d52 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Wed, 18 Feb 2026 11:14:47 +0700 Subject: [PATCH 16/19] fix monthly download strings --- cdn/dev/js/i18n/i18n.mjs | 2 +- cdn/dev/keyboard-search/search.mjs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cdn/dev/js/i18n/i18n.mjs b/cdn/dev/js/i18n/i18n.mjs index cdffaddc..336e6da1 100644 --- a/cdn/dev/js/i18n/i18n.mjs +++ b/cdn/dev/js/i18n/i18n.mjs @@ -186,7 +186,7 @@ export class I18n { if (!I18n.strings[I18n.currentDomain][language] || !I18n.strings[I18n.currentDomain][language].strings[key]) { // Langage or key is missing, so fallback to "en" // Log to Sentry? - // console.warn(`i18n for language: '${language}' or key ${key} missing, fallback to 'en'`); + // console.warn(`i18n for language: '${language}' for '${key}' missing, fallback to 'en'`); language = I18n.DEFAULT_LOCALE; } diff --git a/cdn/dev/keyboard-search/search.mjs b/cdn/dev/keyboard-search/search.mjs index 945b2c08..45db34c1 100644 --- a/cdn/dev/keyboard-search/search.mjs +++ b/cdn/dev/keyboard-search/search.mjs @@ -305,11 +305,11 @@ async function process_response(q, obsolete, res) { if(kbd.isDedicatedLandingPage) { // We won't show the downloads text } else if(kbd.match.downloads == 0) - $('.downloads', k).text(await t('search', 'monthlyDownloadZero')); + $('.downloads', k).text(await t('monthlyDownloadZero')); else if(kbd.match.downloads == 1) - $('.downloads', k).text(kbd.match.downloads+' ' + await('monthlyDownloadOne')); + $('.downloads', k).text(kbd.match.downloads+' ' + await t('monthlyDownloadOne')); else - $('.downloads', k).text(kbd.match.downloads+' ' + await('monthlyDownloadMore')); + $('.downloads', k).text(kbd.match.downloads+' ' + await t('monthlyDownloadMore')); if(!kbd.encodings.toString().match(/unicode/)) { $('.encoding', k).text(await t('notUnicode')); From a88d5cbf6236e30bca36497d320f989599e3935f Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Wed, 18 Feb 2026 15:26:36 +0700 Subject: [PATCH 17/19] Add reviewed Spanish strings --- cdn/dev/js/i18n/search/es.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cdn/dev/js/i18n/search/es.json b/cdn/dev/js/i18n/search/es.json index 57ae2dcd..70a65bbd 100644 --- a/cdn/dev/js/i18n/search/es.json +++ b/cdn/dev/js/i18n/search/es.json @@ -5,11 +5,11 @@ "keyboardSearchTitle": "- Búsqueda por teclado", "obsoleteKeyboards": "Teclados obsoletos", "monthlyDownloadZero": "No hay descargas recientes", - "monthlyDownloadOne": "descargas mensual", + "monthlyDownloadOne": "descarga mensual", "monthlyDownloadMore": "descargas mensuales", "notUnicode": "Nota: No es un teclado Unicode", "designedForPlatform": "Diseñado para {platform}", "noMatchesFoundForKeyboard": "No se encontraron coincidencias para '{keyboard}'", - "previousPager": "< Anteriores", - "nextPager": "Más >" + "previousPager": "< Anterior", + "nextPager": "Siguente >" } From 4821955172a3d188a8ad9e817d26e23d19a9bc59 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Thu, 19 Feb 2026 07:08:25 +0700 Subject: [PATCH 18/19] fix: Update crowdin.yml to handle *.json files --- crowdin.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crowdin.yml b/crowdin.yml index 20c8a423..06ca6f21 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -27,6 +27,20 @@ files: # Keyboard search files + # JS files + - source: /cdn/dev/js/i18n/search/en.json + dest: /js/search/en.json + translation: /cdn/dev/js/i18n/search/%locale%.json + languages_mapping: + locale: + # Canonical locales as needed + es-ES: es + de: de + fr: fr + km: km + + # PHP files + - source: /_includes/locale/strings/keyboards/en.php dest: /keyboards/keyboards/en.php translation: /_includes/locale/strings/keyboards/%locale%.php From 70ef0337ea4a7672d96b94945b4817e40ecbdc6b Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Thu, 19 Feb 2026 10:14:42 +0700 Subject: [PATCH 19/19] fix paging strings again --- cdn/dev/js/i18n/i18n.mjs | 2 +- cdn/dev/keyboard-search/search.mjs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cdn/dev/js/i18n/i18n.mjs b/cdn/dev/js/i18n/i18n.mjs index 336e6da1..3fdf4eed 100644 --- a/cdn/dev/js/i18n/i18n.mjs +++ b/cdn/dev/js/i18n/i18n.mjs @@ -25,7 +25,7 @@ export class I18n { * @param {locale} The new current locale */ static setLocale(locale) { - // Clea current locales + // Clean current locales I18n.currentLocales = []; if (!locale) { diff --git a/cdn/dev/keyboard-search/search.mjs b/cdn/dev/keyboard-search/search.mjs index 45db34c1..94bf0ab0 100644 --- a/cdn/dev/keyboard-search/search.mjs +++ b/cdn/dev/keyboard-search/search.mjs @@ -368,7 +368,7 @@ async function buildPager(res, q, obsolete) { } } - appendPager(pager, await('previousPager'), res.context.pageNumber-1); + appendPager(pager, await t('previousPager'), res.context.pageNumber-1); if(res.context.pageNumber > 5) { $('...').appendTo(pager); } @@ -378,7 +378,7 @@ async function buildPager(res, q, obsolete) { if(res.context.pageNumber < res.context.totalPages - 4) { $('...').appendTo(pager); } - appendPager(pager, await('nextPager'), res.context.pageNumber+1); + appendPager(pager, await t('nextPager'), res.context.pageNumber+1); return pager; }