From 3ad32cf8ea9b441c4e32759c8513fcde028c720c Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Wed, 11 Feb 2026 10:47:06 +0700 Subject: [PATCH 1/4] 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 3/4] 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 4/4] 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); } }