From e1fa10fef7acc946c5ebb0cf49f7aee00c4bb0e8 Mon Sep 17 00:00:00 2001 From: Mejans <61360811+Mejans@users.noreply.github.com> Date: Thu, 26 Aug 2021 20:37:49 +0200 Subject: [PATCH 01/12] Language files To display in various languages --- static/lang/en.json | 15 +++++++++++++++ static/lang/fr.json | 15 +++++++++++++++ static/lang/oc.json | 15 +++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 static/lang/en.json create mode 100644 static/lang/fr.json create mode 100644 static/lang/oc.json diff --git a/static/lang/en.json b/static/lang/en.json new file mode 100644 index 00000000..c27921db --- /dev/null +++ b/static/lang/en.json @@ -0,0 +1,15 @@ +{ + "site": { + "title": "Galène", + "by": "by", + "locale": "Language:" + }, + "groups": { + "public": "Public groups", + "group": "Group:" + }, + "input": { + "value": "Join" + } + +} diff --git a/static/lang/fr.json b/static/lang/fr.json new file mode 100644 index 00000000..dde3a9c1 --- /dev/null +++ b/static/lang/fr.json @@ -0,0 +1,15 @@ +{ + "site": { + "title": "Galène", + "by": "par", + "locale": "Langue :" + }, + "groups": { + "public": "Groupes publics", + "group": "Groupe :" + }, + "input": { + "value": "Rejoindre" + } + +} diff --git a/static/lang/oc.json b/static/lang/oc.json new file mode 100644 index 00000000..aa059fb5 --- /dev/null +++ b/static/lang/oc.json @@ -0,0 +1,15 @@ +{ + "site": { + "title": "Galène", + "by": "per", + "locale": "Lenga :" + }, + "groups": { + "public": "Grops publics", + "group": "Grop :" + }, + "input": { + "value": "Participar" + } + +} From ff50ff1195ced6a5cdfd0d863510642a048071ba Mon Sep 17 00:00:00 2001 From: Mejans <61360811+Mejans@users.noreply.github.com> Date: Thu, 26 Aug 2021 20:39:18 +0200 Subject: [PATCH 02/12] Adds Simple-Translator Adapted from https://github.com/andreasremdt/simple-translator per Andreas Remdt (https://andreasremdt.com) --- static/scripts/index.js | 29 ++++ static/scripts/translator.js | 316 +++++++++++++++++++++++++++++++++++ static/scripts/utils.js | 64 +++++++ 3 files changed, 409 insertions(+) create mode 100644 static/scripts/index.js create mode 100644 static/scripts/translator.js create mode 100644 static/scripts/utils.js diff --git a/static/scripts/index.js b/static/scripts/index.js new file mode 100644 index 00000000..dac30be7 --- /dev/null +++ b/static/scripts/index.js @@ -0,0 +1,29 @@ +// The below provided options are default. +var translator = new Translator({ + defaultLanguage: "en", + detectLanguage: true, + selector: "[data-i18n]", + debug: false, + registerGlobally: "__", + persist: false, + persistKey: "preferred_language", + filesLocation: "/lang" +}); + +translator.fetch(["en", "oc", "fr"]).then(() => { + // Calling `translatePageTo()` without any parameters + // will translate to the default language. + translator.translatePageTo(); + registerLanguageToggle(); +}); + +function registerLanguageToggle() { + var select = document.querySelector("select"); + + select.addEventListener("change", evt => { + var language = evt.target.value; + translator.translatePageTo(language); + }); +}; +document.getElementById(translator.currentLanguage).selected = true; + diff --git a/static/scripts/translator.js b/static/scripts/translator.js new file mode 100644 index 00000000..d5bb47d3 --- /dev/null +++ b/static/scripts/translator.js @@ -0,0 +1,316 @@ +/** + * simple-translator + * A small JavaScript library to translate webpages into different languages. + * https://github.com/andreasremdt/simple-translator + * + * Author: Andreas Remdt (https://andreasremdt.com) + * License: MIT (https://mit-license.org/) + */ +class Translator { + /** + * Initialize the Translator by providing options. + * + * @param {Object} options + */ + constructor(options = {}) { + + if (typeof options != 'object' || Array.isArray(options)) { + this.debug('INVALID_OPTIONS', options); + options = {}; + } + + this.languages = new Map(); + this.config = Object.assign(Translator.defaultConfig, options); + + const { debug, registerGlobally, detectLanguage } = this.config; + + + if (registerGlobally) { + this._globalObject[registerGlobally] = this.translateForKey.bind(this); + } + + if (detectLanguage && this._env == 'browser') { + this._detectLanguage(); + } + } + + /** + * Return the global object, depending on the environment. + * If the script is executed in a browser, return the window object, + * otherwise, in Node.js, return the global object. + * + * @return {Object} + */ + get _globalObject() { + if (this._env == 'browser') { + return window; + } + + return global; + } + + /** + * Check and return the environment in which the script is executed. + * + * @return {String} The environment + */ + get _env() { + if (typeof window != 'undefined') { + return 'browser'; + } else if (typeof module !== 'undefined' && module.exports) { + return 'node'; + } + + return 'browser'; + } + + /** + * Detect the users preferred language. If the language is stored in + * localStorage due to a previous interaction, use it. + * If no localStorage entry has been found, use the default browser language. + */ + _detectLanguage() { + const inMemory = localStorage.getItem(this.config.persistKey); + + if (inMemory) { + this.config.defaultLanguage = inMemory; + } else { + const lang = navigator.languages + ? navigator.languages[0] + : navigator.language; + + this.config.defaultLanguage = lang.substr(0, 2); + } + } + + /** + * Get a translated value from a JSON by providing a key. Additionally, + * the target language can be specified as the second parameter. + * + * @param {String} key + * @param {String} toLanguage + * @return {String} + */ + _getValueFromJSON(key, toLanguage) { + const json = this.languages.get(toLanguage); + + return key.split('.').reduce((obj, i) => (obj ? obj[i] : null), json); + } + + /** + * Replace a given DOM nodes' attribute values (by default innerHTML) with + * the translated text. + * + * @param {HTMLElement} element + * @param {String} toLanguage + */ + _replace(element, toLanguage) { + const keys = element.getAttribute('data-i18n')?.split(/\s/g); + const attributes = element?.getAttribute('data-i18n-attr')?.split(/\s/g); + + + keys.forEach((key, index) => { + const text = this._getValueFromJSON(key, toLanguage); + const attr = attributes ? attributes[index] : 'innerHTML'; + + if (text) { + if (attr == 'innerHTML') { + element[attr] = text; + } else { + element.setAttribute(attr, text); + } + } + }); + } + + /** + * Translate all DOM nodes that match the given selector into the + * specified target language. + * + * @param {String} toLanguage The target language + */ + translatePageTo(toLanguage = this.config.defaultLanguage) { + + const elements = + typeof this.config.selector == 'string' + ? Array.from(document.querySelectorAll(this.config.selector)) + : this.config.selector; + + if (elements.length && elements.length > 0) { + elements.forEach((element) => this._replace(element, toLanguage)); + } else if (elements.length == undefined) { + this._replace(elements, toLanguage); + } + + this._currentLanguage = toLanguage; + document.documentElement.lang = toLanguage; + + if (this.config.persist) { + localStorage.setItem(this.config.persistKey, toLanguage); + } + } + + /** + * Translate a given key into the specified language if it exists + * in the translation file. If not or if the language hasn't been added yet, + * the return value is `null`. + * + * @param {String} key The key from the language file to translate + * @param {String} toLanguage The target language + * @return {(String|null)} + */ + translateForKey(key, toLanguage = this.config.defaultLanguage) { + + return text; + } + + /** + * Add a translation resource to the Translator object. The language + * can then be used to translate single keys or the entire page. + * + * @param {String} language The target language to add + * @param {String} json The language resource file as JSON + * @return {Object} Translator instance + */ + add(language, json) { + + + this.languages.set(language, json); + + return this; + } + + /** + * Remove a translation resource from the Translator object. The language + * won't be available afterwards. + * + * @param {String} language The target language to remove + * @return {Object} Translator instance + */ + remove(language) { + + this.languages.delete(language); + + return this; + } + + /** + * Fetch a translation resource from the web server. It can either fetch + * a single resource or an array of resources. After all resources are fetched, + * return a Promise. + * If the optional, second parameter is set to true, the fetched translations + * will be added to the Translator object. + * + * @param {String|Array} sources The files to fetch + * @param {Boolean} save Save the translation to the Translator object + * @return {(Promise|null)} + */ + fetch(sources, save = true) { + + if (!Array.isArray(sources)) { + sources = [sources]; + } + + const urls = sources.map((source) => { + const filename = source.replace(/\.json$/, '').replace(/^\//, ''); + const path = this.config.filesLocation.replace(/\/$/, ''); + + return `${path}/${filename}.json`; + }); + + if (this._env == 'browser') { + return Promise.all(urls.map((url) => fetch(url))) + .then((responses) => + Promise.all( + responses.map((response) => { + if (response.ok) { + return response.json(); + } + + }) + ) + ) + .then((languageFiles) => { + // If a file could not be fetched, it will be `undefined` and filtered out. + languageFiles = languageFiles.filter((file) => file); + + if (save) { + languageFiles.forEach((file, index) => { + this.add(sources[index], file); + }); + } + + return languageFiles.length > 1 ? languageFiles : languageFiles[0]; + }); + } else if (this._env == 'node') { + return new Promise((resolve) => { + const languageFiles = []; + + urls.forEach((url, index) => { + try { + const json = JSON.parse( + require('fs').readFileSync(process.cwd() + url, 'utf-8') + ); + + if (save) { + this.add(sources[index], json); + } + + languageFiles.push(json); + } catch (err) { + + } + }); + + resolve(languageFiles.length > 1 ? languageFiles : languageFiles[0]); + }); + } + } + + /** + * Sets the default language of the translator instance. + * + * @param {String} language + * @return {void} + */ + setDefaultLanguage(language) { + + this.config.defaultLanguage = language; + } + + /** + * Return the currently selected language. + * + * @return {String} + */ + get currentLanguage() { + return this._currentLanguage || this.config.defaultLanguage; + } + + /** + * Returns the current default language; + * + * @return {String} + */ + get defaultLanguage() { + return this.config.defaultLanguage; + } + + /** + * Return the default config object whose keys can be overriden + * by the user's config passed to the constructor. + * + * @return {Object} + */ + static get defaultConfig() { + return { + defaultLanguage: 'en', + detectLanguage: true, + selector: '[data-i18n]', + registerGlobally: '__', + persist: false, + persistKey: 'preferred_language', + filesLocation: '/i18n', + }; + } +} diff --git a/static/scripts/utils.js b/static/scripts/utils.js new file mode 100644 index 00000000..a52b2a31 --- /dev/null +++ b/static/scripts/utils.js @@ -0,0 +1,64 @@ +const CONSOLE_MESSAGES = { + INVALID_PARAM_LANGUAGE: (param) => + `Invalid parameter for \`language\` provided. Expected a string, but got ${typeof param}.`, + INVALID_PARAM_JSON: (param) => + `Invalid parameter for \`json\` provided. Expected an object, but got ${typeof param}.`, + EMPTY_PARAM_LANGUAGE: () => + `The parameter for \`language\` can't be an empty string.`, + EMPTY_PARAM_JSON: () => + `The parameter for \`json\` must have at least one key/value pair.`, + INVALID_PARAM_KEY: (param) => + `Invalid parameter for \`key\` provided. Expected a string, but got ${typeof param}.`, + NO_LANGUAGE_REGISTERED: (language) => + `No translation for language "${language}" has been added, yet. Make sure to register that language using the \`.add()\` method first.`, + TRANSLATION_NOT_FOUND: (key, language) => + `No translation found for key "${key}" in language "${language}". Is there a key/value in your translation file?`, + INVALID_PARAMETER_SOURCES: (param) => + `Invalid parameter for \`sources\` provided. Expected either a string or an array, but got ${typeof param}.`, + FETCH_ERROR: (response) => + `Could not fetch "${response.url}": ${response.status} (${response.statusText})`, + INVALID_ENVIRONMENT: () => + `You are trying to execute the method \`translatePageTo()\`, which is only available in the browser. Your environment is most likely Node.js`, + MODULE_NOT_FOUND: (message) => message, + MISMATCHING_ATTRIBUTES: (keys, attributes, element) => + `The attributes "data-i18n" and "data-i18n-attr" must contain the same number of keys. + +Values in \`data-i18n\`: (${keys.length}) \`${keys.join(' ')}\` +Values in \`data-i18n-attr\`: (${attributes.length}) \`${attributes.join(' ')}\` + +The HTML element is: +${element.outerHTML}`, + INVALID_OPTIONS: (param) => + `Invalid config passed to the \`Translator\` constructor. Expected an object, but got ${typeof param}. Using default config instead.`, +}; + +/** + * + * @param {Boolean} isEnabled + * @return {Function} + */ +export function logger(isEnabled) { + return function log(code, ...args) { + if (isEnabled) { + try { + const message = CONSOLE_MESSAGES[code]; + throw new TypeError(message ? message(...args) : 'Unhandled Error'); + } catch (ex) { + const line = ex.stack.split(/\n/g)[1]; + const [method, filepath] = line.split(/@/); + + console.error(`${ex.message} + +This error happened in the method \`${method}\` from: \`${filepath}\`. + +If you don't want to see these error messages, turn off debugging by passing \`{ debug: false }\` to the constructor. + +Error code: ${code} + +Check out the documentation for more details about the API: +https://github.com/andreasremdt/simple-translator#usage + `); + } + } + }; +} From 0e5c1fa4fe8d41e8ef8b9294344235e304649e11 Mon Sep 17 00:00:00 2001 From: Mejans <61360811+Mejans@users.noreply.github.com> Date: Thu, 26 Aug 2021 20:41:31 +0200 Subject: [PATCH 03/12] Update with i10n --- static/index.html | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/static/index.html b/static/index.html index e5d9d7f7..1c1225f1 100644 --- a/static/index.html +++ b/static/index.html @@ -1,7 +1,7 @@ - Galène + Galène @@ -13,25 +13,36 @@
-

Galène

+

Galène

- + -
+
-

Public groups

+

Public groups

+ + + From 9ab2b6cbfc51b6d64a1e43de1d5a99737990139b Mon Sep 17 00:00:00 2001 From: Mejans <61360811+Mejans@users.noreply.github.com> Date: Thu, 26 Aug 2021 20:44:08 +0200 Subject: [PATCH 04/12] Update index.html --- static/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/index.html b/static/index.html index 1c1225f1..11cc51d0 100644 --- a/static/index.html +++ b/static/index.html @@ -16,7 +16,7 @@

Galène

- +
From f325bd49f6edd1e6e6bb5d482691f136a097ab25 Mon Sep 17 00:00:00 2001 From: Mejans <61360811+Mejans@users.noreply.github.com> Date: Sat, 28 Aug 2021 10:55:21 +0200 Subject: [PATCH 05/12] Update index.html --- static/index.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/static/index.html b/static/index.html index 11cc51d0..0e80e5f4 100644 --- a/static/index.html +++ b/static/index.html @@ -1,5 +1,5 @@ - + Galène @@ -31,12 +31,12 @@

Public groups

-

Galène by

+

Galène by

From 0d50157c6f2cb11f9787cd907751ad2c4c653ae2 Mon Sep 17 00:00:00 2001 From: Mejans <61360811+Mejans@users.noreply.github.com> Date: Sat, 28 Aug 2021 10:56:10 +0200 Subject: [PATCH 06/12] Update en.json --- static/lang/en.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/static/lang/en.json b/static/lang/en.json index c27921db..e7332123 100644 --- a/static/lang/en.json +++ b/static/lang/en.json @@ -10,6 +10,8 @@ }, "input": { "value": "Join" - } - + }, + "html": { + "lang": "en" + } } From ebd60a006abf41193d9cb8c2c720e54fd94a16ef Mon Sep 17 00:00:00 2001 From: Mejans <61360811+Mejans@users.noreply.github.com> Date: Sat, 28 Aug 2021 10:56:38 +0200 Subject: [PATCH 07/12] Update fr.json --- static/lang/fr.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/static/lang/fr.json b/static/lang/fr.json index dde3a9c1..259496be 100644 --- a/static/lang/fr.json +++ b/static/lang/fr.json @@ -10,6 +10,9 @@ }, "input": { "value": "Rejoindre" + }, + }, + "html": { + "lang": "fr" } - } From f1a4930345f65ea1965f9c634b54b3d21c060c9f Mon Sep 17 00:00:00 2001 From: Mejans <61360811+Mejans@users.noreply.github.com> Date: Sat, 28 Aug 2021 10:57:03 +0200 Subject: [PATCH 08/12] Update oc.json --- static/lang/oc.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/static/lang/oc.json b/static/lang/oc.json index aa059fb5..67a4ef1c 100644 --- a/static/lang/oc.json +++ b/static/lang/oc.json @@ -10,6 +10,8 @@ }, "input": { "value": "Participar" - } - + }, + "html": { + "lang": "oc" + } } From f07abd5915e976baf07ce83005fe942b741a7d3c Mon Sep 17 00:00:00 2001 From: Mejans <61360811+Mejans@users.noreply.github.com> Date: Sat, 28 Aug 2021 10:57:27 +0200 Subject: [PATCH 09/12] Update fr.json --- static/lang/fr.json | 1 - 1 file changed, 1 deletion(-) diff --git a/static/lang/fr.json b/static/lang/fr.json index 259496be..2db24753 100644 --- a/static/lang/fr.json +++ b/static/lang/fr.json @@ -11,7 +11,6 @@ "input": { "value": "Rejoindre" }, - }, "html": { "lang": "fr" } From 0d6aa7a6e3aa2886224bda80add05bb7b0e88041 Mon Sep 17 00:00:00 2001 From: Mejans <61360811+Mejans@users.noreply.github.com> Date: Sat, 28 Aug 2021 11:04:45 +0200 Subject: [PATCH 10/12] Update en.json --- static/lang/en.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/static/lang/en.json b/static/lang/en.json index e7332123..ec70e444 100644 --- a/static/lang/en.json +++ b/static/lang/en.json @@ -13,5 +13,11 @@ }, "html": { "lang": "en" - } + }, + "404": { + "title": "Page not Found", + "notfound": " Page not Found", + "text": " We can't find the page you're looking for.", + "back": "Back to home" + } } From 213dc7342bd0145895b42e80673fd9ab5c10109f Mon Sep 17 00:00:00 2001 From: Mejans <61360811+Mejans@users.noreply.github.com> Date: Sat, 28 Aug 2021 11:06:35 +0200 Subject: [PATCH 11/12] Update fr.json --- static/lang/fr.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/static/lang/fr.json b/static/lang/fr.json index 2db24753..725df6c3 100644 --- a/static/lang/fr.json +++ b/static/lang/fr.json @@ -13,5 +13,11 @@ }, "html": { "lang": "fr" + }, + "404": { + "title": "Page non trouvée", + "notfound": " Page non trouvée", + "text": " Nous ne trouvons pas la page que vous cherchez.", + "back": "Retour à l’accueil" } } From a441594ceecdd917eb32a18cbb3f3d5c526f71d4 Mon Sep 17 00:00:00 2001 From: Mejans <61360811+Mejans@users.noreply.github.com> Date: Sat, 28 Aug 2021 11:07:39 +0200 Subject: [PATCH 12/12] Update oc.json --- static/lang/oc.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/static/lang/oc.json b/static/lang/oc.json index 67a4ef1c..e0cc2cba 100644 --- a/static/lang/oc.json +++ b/static/lang/oc.json @@ -13,5 +13,11 @@ }, "html": { "lang": "oc" - } + }, + "404": { + "title": "Pagina pas trobada", + "notfound": " Pagina pas trobada", + "text": " Podèm pas trobar la pagina que cercatz.", + "back": "Tornar a l’acuèlh" + } }