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