diff --git a/static/index.html b/static/index.html
index e5d9d7f7..0e80e5f4 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
+
+
+
diff --git a/static/lang/en.json b/static/lang/en.json
new file mode 100644
index 00000000..ec70e444
--- /dev/null
+++ b/static/lang/en.json
@@ -0,0 +1,23 @@
+{
+ "site": {
+ "title": "Galène",
+ "by": "by",
+ "locale": "Language:"
+ },
+ "groups": {
+ "public": "Public groups",
+ "group": "Group:"
+ },
+ "input": {
+ "value": "Join"
+ },
+ "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"
+ }
+}
diff --git a/static/lang/fr.json b/static/lang/fr.json
new file mode 100644
index 00000000..725df6c3
--- /dev/null
+++ b/static/lang/fr.json
@@ -0,0 +1,23 @@
+{
+ "site": {
+ "title": "Galène",
+ "by": "par",
+ "locale": "Langue :"
+ },
+ "groups": {
+ "public": "Groupes publics",
+ "group": "Groupe :"
+ },
+ "input": {
+ "value": "Rejoindre"
+ },
+ "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"
+ }
+}
diff --git a/static/lang/oc.json b/static/lang/oc.json
new file mode 100644
index 00000000..e0cc2cba
--- /dev/null
+++ b/static/lang/oc.json
@@ -0,0 +1,23 @@
+{
+ "site": {
+ "title": "Galène",
+ "by": "per",
+ "locale": "Lenga :"
+ },
+ "groups": {
+ "public": "Grops publics",
+ "group": "Grop :"
+ },
+ "input": {
+ "value": "Participar"
+ },
+ "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"
+ }
+}
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
+ `);
+ }
+ }
+ };
+}