From 8a2767ef4041c67bce58ff7aa7865823262cc6bd Mon Sep 17 00:00:00 2001 From: Nolway Date: Wed, 8 Dec 2021 01:34:50 +0100 Subject: [PATCH 01/14] Implement Translator: i18n system --- front/.eslintrc.js | 5 +- front/dist/resources/translations/.gitignore | 1 + front/package.json | 1 + front/src/Enum/EnvironmentVariable.ts | 2 + front/src/Phaser/Game/GameManager.ts | 14 +- front/src/Phaser/Login/EntryScene.ts | 73 ++++---- front/src/Translator/TranslationCompiler.ts | 72 ++++++++ front/src/Translator/Translator.ts | 170 +++++++++++++++++++ front/src/Utils/Cookies.ts | 20 +++ front/src/define-plugin.d.ts | 2 + front/translations/en-US/index.en-US.json | 5 + front/translations/en-US/test.en-US.json | 5 + front/translations/fr-FR/index.fr-FR.json | 5 + front/translations/fr-FR/test.fr-FR.json | 5 + front/webpack.config.ts | 39 ++++- front/yarn.lock | 21 ++- 16 files changed, 393 insertions(+), 47 deletions(-) create mode 100644 front/dist/resources/translations/.gitignore create mode 100644 front/src/Translator/TranslationCompiler.ts create mode 100644 front/src/Translator/Translator.ts create mode 100644 front/src/Utils/Cookies.ts create mode 100644 front/src/define-plugin.d.ts create mode 100644 front/translations/en-US/index.en-US.json create mode 100644 front/translations/en-US/test.en-US.json create mode 100644 front/translations/fr-FR/index.fr-FR.json create mode 100644 front/translations/fr-FR/test.fr-FR.json diff --git a/front/.eslintrc.js b/front/.eslintrc.js index ed94b3b2..fa57ebf4 100644 --- a/front/.eslintrc.js +++ b/front/.eslintrc.js @@ -8,7 +8,7 @@ module.exports = { "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking" + "plugin:@typescript-eslint/recommended-requiring-type-checking", ], "globals": { "Atomics": "readonly", @@ -23,7 +23,7 @@ module.exports = { }, "plugins": [ "@typescript-eslint", - "svelte3" + "svelte3", ], "overrides": [ { @@ -33,6 +33,7 @@ module.exports = { ], "rules": { "no-unused-vars": "off", + "eol-last": ["error", "always"], "@typescript-eslint/no-explicit-any": "error", "no-throw-literal": "error", // TODO: remove those ignored rules and write a stronger code! diff --git a/front/dist/resources/translations/.gitignore b/front/dist/resources/translations/.gitignore new file mode 100644 index 00000000..a6c57f5f --- /dev/null +++ b/front/dist/resources/translations/.gitignore @@ -0,0 +1 @@ +*.json diff --git a/front/package.json b/front/package.json index 4a4e78f6..c7b819bd 100644 --- a/front/package.json +++ b/front/package.json @@ -21,6 +21,7 @@ "html-webpack-plugin": "^5.3.1", "jasmine": "^3.5.0", "lint-staged": "^11.0.0", + "merge-jsons-webpack-plugin": "^2.0.1", "mini-css-extract-plugin": "^1.6.0", "node-polyfill-webpack-plugin": "^1.1.2", "npm-run-all": "^4.1.5", diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index 76b4c8af..07d4ff8e 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -25,6 +25,7 @@ export const POSTHOG_API_KEY: string = (process.env.POSTHOG_API_KEY as string) | export const POSTHOG_URL = process.env.POSTHOG_URL || undefined; export const DISABLE_ANONYMOUS: boolean = process.env.DISABLE_ANONYMOUS === "true"; export const OPID_LOGIN_SCREEN_PROVIDER = process.env.OPID_LOGIN_SCREEN_PROVIDER; +const FALLBACK_LANGUAGE: string = process.env.FALLBACK_LANGUAGE || "en-US"; export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600; @@ -44,4 +45,5 @@ export { TURN_PASSWORD, JITSI_URL, JITSI_PRIVATE_MODE, + FALLBACK_LANGUAGE, }; diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index e4f4ea1c..f023a7aa 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,14 +1,14 @@ -import { GameScene } from "./GameScene"; +import { get } from "svelte/store"; import { connectionManager } from "../../Connexion/ConnectionManager"; +import { localUserStore } from "../../Connexion/LocalUserStore"; import type { Room } from "../../Connexion/Room"; +import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore"; +import { requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore"; +import { menuIconVisiblilityStore } from "../../Stores/MenuStore"; +import { EnableCameraSceneName } from "../Login/EnableCameraScene"; import { LoginSceneName } from "../Login/LoginScene"; import { SelectCharacterSceneName } from "../Login/SelectCharacterScene"; -import { EnableCameraSceneName } from "../Login/EnableCameraScene"; -import { localUserStore } from "../../Connexion/LocalUserStore"; -import { get } from "svelte/store"; -import { requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore"; -import { helpCameraSettingsVisibleStore } from "../../Stores/HelpCameraSettingsStore"; -import { menuIconVisiblilityStore } from "../../Stores/MenuStore"; +import { GameScene } from "./GameScene"; /** * This class should be responsible for any scene starting/stopping diff --git a/front/src/Phaser/Login/EntryScene.ts b/front/src/Phaser/Login/EntryScene.ts index 3fb2e6b5..d75ead03 100644 --- a/front/src/Phaser/Login/EntryScene.ts +++ b/front/src/Phaser/Login/EntryScene.ts @@ -4,6 +4,7 @@ import { ErrorScene, ErrorSceneName } from "../Reconnecting/ErrorScene"; import { WAError } from "../Reconnecting/WAError"; import { waScaleManager } from "../Services/WaScaleManager"; import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene"; +import { translator } from "../../Translator/Translator"; export const EntrySceneName = "EntryScene"; @@ -12,6 +13,7 @@ export const EntrySceneName = "EntryScene"; * and to route to the next correct scene. */ export class EntryScene extends Scene { + constructor() { super({ key: EntrySceneName, @@ -24,41 +26,50 @@ export class EntryScene extends Scene { // Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap this.load.bitmapFont(ReconnectingTextures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml"); this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 }); + translator.loadCurrentLanguageFile(this.load); } create() { - gameManager - .init(this.scene) - .then((nextSceneName) => { - // Let's rescale before starting the game - // We can do it at this stage. - waScaleManager.applyNewSize(); - this.scene.start(nextSceneName); + translator + .loadCurrentLanguageObject(this.cache) + .catch((e: unknown) => { + console.error("Error during language loading!", e); + throw e; }) - .catch((err) => { - if (err.response && err.response.status == 404) { - ErrorScene.showError( - new WAError( - "Access link incorrect", - "Could not find map. Please check your access link.", - "If you want more information, you may contact administrator or contact us at: hello@workadventu.re" - ), - this.scene - ); - } else if (err.response && err.response.status == 403) { - ErrorScene.showError( - new WAError( - "Connection rejected", - "You cannot join the World. Try again later" + - (err.response.data ? ". \n\r \n\r" + `${err.response.data}` : "") + - ".", - "If you want more information, you may contact administrator or contact us at: hello@workadventu.re" - ), - this.scene - ); - } else { - ErrorScene.showError(err, this.scene); - } + .finally(() => { + gameManager + .init(this.scene) + .then((nextSceneName) => { + // Let's rescale before starting the game + // We can do it at this stage. + waScaleManager.applyNewSize(); + this.scene.start(nextSceneName); + }) + .catch((err) => { + if (err.response && err.response.status == 404) { + ErrorScene.showError( + new WAError( + "Access link incorrect", + "Could not find map. Please check your access link.", + "If you want more information, you may contact administrator or contact us at: hello@workadventu.re" + ), + this.scene + ); + } else if (err.response && err.response.status == 403) { + ErrorScene.showError( + new WAError( + "Connection rejected", + "You cannot join the World. Try again later" + + (err.response.data ? ". \n\r \n\r" + `${err.response.data}` : "") + + ".", + "If you want more information, you may contact administrator or contact us at: hello@workadventu.re" + ), + this.scene + ); + } else { + ErrorScene.showError(err, this.scene); + } + }); }); } } diff --git a/front/src/Translator/TranslationCompiler.ts b/front/src/Translator/TranslationCompiler.ts new file mode 100644 index 00000000..6b43ba2c --- /dev/null +++ b/front/src/Translator/TranslationCompiler.ts @@ -0,0 +1,72 @@ +import fs from "fs"; + +const translationsBasePath = "./translations"; +const fallbackLanguage = process.env.FALLBACK_LANGUAGE || "en-US"; + +export type LanguageFound = { + id: string; + default: boolean; +}; + +const getAllLanguagesByFiles = (dirPath: string, languages: Array | undefined) => { + const files = fs.readdirSync(dirPath); + languages = languages || new Array(); + + files.forEach(function (file) { + if (fs.statSync(dirPath + "/" + file).isDirectory()) { + languages = getAllLanguagesByFiles(dirPath + "/" + file, languages); + } else { + const parts = file.split("."); + + if (parts.length !== 3 || parts[0] !== "index" || parts[2] !== "json") { + return; + } + + const rawData = fs.readFileSync(dirPath + "/" + file, "utf-8"); + const languageObject = JSON.parse(rawData); + + languages?.push({ + id: parts[1], + default: languageObject.default !== undefined && languageObject.default, + }); + } + }); + + return languages; +}; + +const getFallbackLanguageObject = (dirPath: string, languageObject: Object | undefined) => { + const files = fs.readdirSync(dirPath); + languageObject = languageObject || {}; + + files.forEach(function (file) { + if (fs.statSync(dirPath + "/" + file).isDirectory()) { + languageObject = getFallbackLanguageObject(dirPath + "/" + file, languageObject); + } else { + const parts = file.split("."); + + if (parts.length !== 3 || parts[1] !== fallbackLanguage || parts[2] !== "json") { + return; + } + + const rawData = fs.readFileSync(dirPath + "/" + file, "utf-8"); + languageObject = { ...languageObject, ...JSON.parse(rawData) }; + } + }); + + return languageObject; +}; + +const languagesToObject = () => { + const object: { [key: string]: boolean } = {}; + + languages.forEach((language) => { + object[language.id] = false; + }); + + return object; +}; + +export const languages = getAllLanguagesByFiles(translationsBasePath, undefined); +export const languagesObject = languagesToObject(); +export const fallbackLanguageObject = getFallbackLanguageObject(translationsBasePath, undefined); diff --git a/front/src/Translator/Translator.ts b/front/src/Translator/Translator.ts new file mode 100644 index 00000000..7e02ac53 --- /dev/null +++ b/front/src/Translator/Translator.ts @@ -0,0 +1,170 @@ +import { FALLBACK_LANGUAGE } from "../Enum/EnvironmentVariable"; +import { getCookie } from "../Utils/Cookies"; + +export type Language = { + language: string; + country: string; +}; + +type LanguageObject = { + [key: string]: string | LanguageObject; +}; + +class Translator { + public readonly fallbackLanguage: Language = this.getLanguageByString(FALLBACK_LANGUAGE) || { + language: "en", + country: "US", + }; + + private readonly fallbackLanguageObject: LanguageObject = FALLBACK_LANGUAGE_OBJECT as LanguageObject; + + private currentLanguage: Language; + private currentLanguageObject: LanguageObject; + + public constructor() { + this.currentLanguage = this.fallbackLanguage; + this.currentLanguageObject = this.fallbackLanguageObject; + + this.defineCurrentLanguage(); + } + + public getLanguageByString(languageString: string): Language | undefined { + const parts = languageString.split("-"); + if (parts.length !== 2 || parts[0].length !== 2 || parts[1].length !== 2) { + console.error(`Language string "${languageString}" do not respect RFC 5646 with language and country code`); + return undefined; + } + + return { + language: parts[0].toLowerCase(), + country: parts[1].toUpperCase(), + }; + } + + public getStringByLanguage(language: Language): string | undefined { + return `${language.language}-${language.country}`; + } + + public loadCurrentLanguageFile(pluginLoader: Phaser.Loader.LoaderPlugin) { + const languageString = this.getStringByLanguage(this.currentLanguage); + pluginLoader.json({ + key: `language-${languageString}`, + url: `resources/translations/${languageString}.json`, + }); + } + + public loadCurrentLanguageObject(cacheManager: Phaser.Cache.CacheManager): Promise { + return new Promise((resolve, reject) => { + const languageObject: Object = cacheManager.json.get( + `language-${this.getStringByLanguage(this.currentLanguage)}` + ); + + if (!languageObject) { + return reject(); + } + + this.currentLanguageObject = languageObject as LanguageObject; + return resolve(); + }); + } + + public getLanguageWithoutCountry(languageString: string): Language | undefined { + if (languageString.length !== 2) { + return undefined; + } + + let languageFound = undefined; + + const languages: { [key: string]: boolean } = LANGUAGES as { [key: string]: boolean }; + + for (const language in languages) { + if (language.startsWith(languageString) && languages[language]) { + languageFound = this.getLanguageByString(language); + break; + } + } + + return languageFound; + } + + private defineCurrentLanguage() { + const navigatorLanguage: string | undefined = navigator.language; + const cookieLanguage = getCookie("language"); + let currentLanguage = undefined; + + if (cookieLanguage && typeof cookieLanguage === "string") { + const cookieLanguageObject = this.getLanguageByString(cookieLanguage); + if (cookieLanguageObject) { + currentLanguage = cookieLanguageObject; + } + } + + if (!currentLanguage && navigatorLanguage) { + const navigatorLanguageObject = + navigator.language.length === 2 + ? this.getLanguageWithoutCountry(navigatorLanguage) + : this.getLanguageByString(navigatorLanguage); + if (navigatorLanguageObject) { + currentLanguage = navigatorLanguageObject; + } + } + + if (!currentLanguage || currentLanguage === this.fallbackLanguage) { + return; + } + + this.currentLanguage = currentLanguage; + } + + private getObjectValueByPath(path: string, object: LanguageObject): string | undefined { + const paths = path.split("."); + let currentValue: LanguageObject | string = object; + + for (const path of paths) { + if (typeof currentValue === "string" || currentValue[path] === undefined) { + return undefined; + } + + currentValue = currentValue[path]; + } + + if (typeof currentValue !== "string") { + return undefined; + } + + return currentValue; + } + + private formatStringWithParams(string: string, params: { [key: string]: string | number }): string { + let formattedString = string; + + for (const param in params) { + const regex = `/{{\\s*\\${param}\\s*}}/g`; + formattedString = formattedString.replace(new RegExp(regex), params[param].toString()); + } + + return formattedString; + } + + public _(key: string, params?: { [key: string]: string | number }): string { + const currentLanguageValue = this.getObjectValueByPath(key, this.currentLanguageObject); + + if (currentLanguageValue) { + return params ? this.formatStringWithParams(currentLanguageValue, params) : currentLanguageValue; + } + + console.warn(`"${key}" key cannot be found in ${this.getStringByLanguage(this.currentLanguage)} language`); + + const fallbackLanguageValue = this.getObjectValueByPath(key, this.fallbackLanguageObject); + + if (fallbackLanguageValue) { + return params ? this.formatStringWithParams(fallbackLanguageValue, params) : fallbackLanguageValue; + } + + console.warn(`"${key}" key cannot be found in ${this.getStringByLanguage(this.fallbackLanguage)} fallback language`); + + return key; + } +} + +export const translator = new Translator(); diff --git a/front/src/Utils/Cookies.ts b/front/src/Utils/Cookies.ts new file mode 100644 index 00000000..3ca418c2 --- /dev/null +++ b/front/src/Utils/Cookies.ts @@ -0,0 +1,20 @@ +export const setCookie = (name: string, value: unknown, days: number) => { + let expires = ""; + if (days) { + const date = new Date(); + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); + expires = "; expires=" + date.toUTCString(); + } + document.cookie = name + "=" + (value || "") + expires + "; path=/"; +}; + +export const getCookie = (name: string): unknown | undefined => { + const nameEquals = name + "="; + const ca = document.cookie.split(";"); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) == " ") c = c.substring(1, c.length); + if (c.indexOf(nameEquals) == 0) return c.substring(nameEquals.length, c.length); + } + return undefined; +}; diff --git a/front/src/define-plugin.d.ts b/front/src/define-plugin.d.ts new file mode 100644 index 00000000..679cf2e0 --- /dev/null +++ b/front/src/define-plugin.d.ts @@ -0,0 +1,2 @@ +declare const FALLBACK_LANGUAGE_OBJECT: Object; +declare const LANGUAGES: Object; diff --git a/front/translations/en-US/index.en-US.json b/front/translations/en-US/index.en-US.json new file mode 100644 index 00000000..8de1fc09 --- /dev/null +++ b/front/translations/en-US/index.en-US.json @@ -0,0 +1,5 @@ +{ + "language": "English", + "country": "United States", + "default": true +} diff --git a/front/translations/en-US/test.en-US.json b/front/translations/en-US/test.en-US.json new file mode 100644 index 00000000..5bc56b94 --- /dev/null +++ b/front/translations/en-US/test.en-US.json @@ -0,0 +1,5 @@ +{ + "test": { + "nolway": "Too mutch cofee" + } +} diff --git a/front/translations/fr-FR/index.fr-FR.json b/front/translations/fr-FR/index.fr-FR.json new file mode 100644 index 00000000..b741e3bd --- /dev/null +++ b/front/translations/fr-FR/index.fr-FR.json @@ -0,0 +1,5 @@ +{ + "language": "Français", + "country": "France", + "default": true +} diff --git a/front/translations/fr-FR/test.fr-FR.json b/front/translations/fr-FR/test.fr-FR.json new file mode 100644 index 00000000..ab428962 --- /dev/null +++ b/front/translations/fr-FR/test.fr-FR.json @@ -0,0 +1,5 @@ +{ + "test": { + "nolway": "Trop de café" + } +} diff --git a/front/webpack.config.ts b/front/webpack.config.ts index 77ad92bd..b407adb9 100644 --- a/front/webpack.config.ts +++ b/front/webpack.config.ts @@ -1,12 +1,16 @@ -import type { Configuration } from "webpack"; -import type WebpackDevServer from "webpack-dev-server"; -import path from "path"; -import webpack from "webpack"; +import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin"; import HtmlWebpackPlugin from "html-webpack-plugin"; import MiniCssExtractPlugin from "mini-css-extract-plugin"; -import sveltePreprocess from "svelte-preprocess"; -import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin"; import NodePolyfillPlugin from "node-polyfill-webpack-plugin"; +import path from "path"; +import sveltePreprocess from "svelte-preprocess"; +import type { Configuration } from "webpack"; +import webpack from "webpack"; +import type WebpackDevServer from "webpack-dev-server"; +import type { LanguageFound } from "./src/Translator/TranslationCompiler"; +import { fallbackLanguageObject, languages, languagesObject } from "./src/Translator/TranslationCompiler"; + +const MergeJsonWebpackPlugin = require("merge-jsons-webpack-plugin"); const mode = process.env.NODE_ENV ?? "development"; const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS; @@ -141,6 +145,11 @@ module.exports = { filename: "fonts/[name][ext]", }, }, + { + test: /\.json$/, + exclude: /node_modules/, + type: "asset", + }, ], }, resolve: { @@ -210,6 +219,24 @@ module.exports = { NODE_ENV: mode, DISABLE_ANONYMOUS: false, OPID_LOGIN_SCREEN_PROVIDER: null, + FALLBACK_LANGUAGE: null, + }), + new webpack.DefinePlugin({ + FALLBACK_LANGUAGE_OBJECT: JSON.stringify(fallbackLanguageObject), + LANGUAGES: JSON.stringify(languagesObject), + }), + new MergeJsonWebpackPlugin({ + output: { + groupBy: languages.map((language: LanguageFound) => { + return { + pattern: `./translations/**/*.${language.id}.json`, + fileName: `./resources/translations/${language.id}.json` + }; + }) + }, + globOptions: { + nosort: true, + }, }), ], } as Configuration & WebpackDevServer.Configuration; diff --git a/front/yarn.lock b/front/yarn.lock index d2ac31b3..d6768844 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -2776,6 +2776,18 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== +glob@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" + integrity sha1-gFIR3wT6rxxjo2ADBs31reULLsg= + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.0.3, glob@^7.1.3, glob@^7.1.6: version "7.1.7" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" @@ -3860,6 +3872,13 @@ merge-descriptors@1.0.1: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= +merge-jsons-webpack-plugin@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/merge-jsons-webpack-plugin/-/merge-jsons-webpack-plugin-2.0.1.tgz#f2975ce0f734171331d42eee62d63329031800b4" + integrity sha512-8GP8rpOX3HSFsm7Gx+b3OAQR7yhgeAQvMqcZOJ+/cQIrqdak1c42a2T2vyeee8pzGPBf7pMLumthPh4CHgv2BA== + dependencies: + glob "7.1.1" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -3961,7 +3980,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -minimatch@^3.0.4: +minimatch@^3.0.2, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== From 77f8426788394ec80493d9644391eeb93d3b1f0f Mon Sep 17 00:00:00 2001 From: Nolway Date: Wed, 8 Dec 2021 20:12:18 +0100 Subject: [PATCH 02/14] Add translator documentation --- docs/maps/api-player.md | 17 ++++ front/src/Translator/Translator.ts | 64 ++++++++++++++- front/translations/how-to-translate.md | 103 +++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 front/translations/how-to-translate.md diff --git a/docs/maps/api-player.md b/docs/maps/api-player.md index d9a89bd1..04dd5613 100644 --- a/docs/maps/api-player.md +++ b/docs/maps/api-player.md @@ -36,6 +36,23 @@ WA.onInit().then(() => { }) ``` +### Get the player language + +``` +WA.player.language: string; +``` + +The current language of player is available from the `WA.player.language` property. + +{.alert.alert-info} +You need to wait for the end of the initialization before accessing `WA.player.language` + +```typescript +WA.onInit().then(() => { + console.log('Player language: ', WA.player.language); +}) +``` + ### Get the tags of the player ``` diff --git a/front/src/Translator/Translator.ts b/front/src/Translator/Translator.ts index 7e02ac53..57a9e26e 100644 --- a/front/src/Translator/Translator.ts +++ b/front/src/Translator/Translator.ts @@ -10,6 +10,10 @@ type LanguageObject = { [key: string]: string | LanguageObject; }; +type TranslationParams = { + [key: string]: string | number +}; + class Translator { public readonly fallbackLanguage: Language = this.getLanguageByString(FALLBACK_LANGUAGE) || { language: "en", @@ -18,7 +22,14 @@ class Translator { private readonly fallbackLanguageObject: LanguageObject = FALLBACK_LANGUAGE_OBJECT as LanguageObject; + /** + * Current language + */ private currentLanguage: Language; + + /** + * Contain all translation keys of current language + */ private currentLanguageObject: LanguageObject; public constructor() { @@ -28,6 +39,11 @@ class Translator { this.defineCurrentLanguage(); } + /** + * Get language object from string who respect the RFC 5646 + * @param {string} languageString RFC 5646 formatted string + * @returns {Language|undefined} Language object who represent the languageString + */ public getLanguageByString(languageString: string): Language | undefined { const parts = languageString.split("-"); if (parts.length !== 2 || parts[0].length !== 2 || parts[1].length !== 2) { @@ -41,10 +57,19 @@ class Translator { }; } + /** + * Get a string who respect the RFC 5646 by a language object + * @param {Language} language A language object + * @returns {string|undefined} String who represent the language object + */ public getStringByLanguage(language: Language): string | undefined { return `${language.language}-${language.country}`; } + /** + * Add the current language file loading into Phaser loader queue + * @param {Phaser.Loader.LoaderPlugin} pluginLoader Phaser LoaderPLugin + */ public loadCurrentLanguageFile(pluginLoader: Phaser.Loader.LoaderPlugin) { const languageString = this.getStringByLanguage(this.currentLanguage); pluginLoader.json({ @@ -53,6 +78,11 @@ class Translator { }); } + /** + * Get from the Phase cache the current language object and promise to load it + * @param {Phaser.Cache.CacheManager} cacheManager Phaser CacheManager + * @returns {Promise} Load current language promise + */ public loadCurrentLanguageObject(cacheManager: Phaser.Cache.CacheManager): Promise { return new Promise((resolve, reject) => { const languageObject: Object = cacheManager.json.get( @@ -68,6 +98,11 @@ class Translator { }); } + /** + * Get the language for RFC 5646 2 char string from availables languages + * @param {string} languageString Language RFC 5646 string + * @returns {Language|undefined} Language object who represent the languageString + */ public getLanguageWithoutCountry(languageString: string): Language | undefined { if (languageString.length !== 2) { return undefined; @@ -87,6 +122,9 @@ class Translator { return languageFound; } + /** + * Define the current language by the navigator or a cookie + */ private defineCurrentLanguage() { const navigatorLanguage: string | undefined = navigator.language; const cookieLanguage = getCookie("language"); @@ -116,8 +154,14 @@ class Translator { this.currentLanguage = currentLanguage; } - private getObjectValueByPath(path: string, object: LanguageObject): string | undefined { - const paths = path.split("."); + /** + * Get value on object by property path + * @param {string} key Translation key + * @param {LanguageObject} object Language object + * @returns {string|undefined} Found translation by path + */ + private getObjectValueByPath(key: string, object: LanguageObject): string | undefined { + const paths = key.split("."); let currentValue: LanguageObject | string = object; for (const path of paths) { @@ -135,7 +179,13 @@ class Translator { return currentValue; } - private formatStringWithParams(string: string, params: { [key: string]: string | number }): string { + /** + * Replace {{ }} tags on a string by the params values + * @param {string} string Translation string + * @param {{ [key: string]: string | number }} params Tags to replace by value + * @returns {string} Formatted string + */ + private formatStringWithParams(string: string, params: TranslationParams): string { let formattedString = string; for (const param in params) { @@ -146,7 +196,13 @@ class Translator { return formattedString; } - public _(key: string, params?: { [key: string]: string | number }): string { + /** + * Get translation by a key and formatted with params by {{ }} tag + * @param {string} key Translation key + * @param {{ [key: string]: string | number }} params Tags to replace by value + * @returns {string} Translation formatted + */ + public _(key: string, params?: TranslationParams): string { const currentLanguageValue = this.getObjectValueByPath(key, this.currentLanguageObject); if (currentLanguageValue) { diff --git a/front/translations/how-to-translate.md b/front/translations/how-to-translate.md new file mode 100644 index 00000000..1586c17e --- /dev/null +++ b/front/translations/how-to-translate.md @@ -0,0 +1,103 @@ +# How to translate WorkAdventure + +## How the translation files work + +In the translations folder, all json files of the form [namespace].[language code].json are interpreted to create languages by the language code in the file name. + +The only mandatory file that is the entry point is the index.[language code].json which must contain the properties language, country and default so that the language can be taken into account. + +Example: +```json +{ + "language": "English", # Language that will be used + "country": "United States", # Country specification (e.g. British English and American English are not the same thing) + "default": true # In some cases WorkAdventure only knows the language, not the country language of the browser. In this case it takes the language with the default property at true. +} +``` + +It does not matter where the file is placed in this folder. However, we have set up the following architecture in order to have a simple reading: + +- translations/ + - [language code]/ + - [namepace].[language code].json + +Example: +- translations/ + - en-US/ + - index.en-US.json + - main-menu.en-US.json + - chat.en-US.json + - fr-FR/ + - index.fr-FR.json + - main-menu.fr-FR.json + - chat.fr-FR.json + +## Add a new language + +It is very easy to add a new language! + +First, in the translations folder create a new folder with the language code as name (the language code according to [RFC 5646](https://datatracker.ietf.org/doc/html/rfc5646)). + +In the previously created folder, add a file named as index.[language code].json with the following content: + +```json +{ + "language": "Language Name", + "country": "Country Name", + "default": true +} +``` + +See the section above (*How the translation files work*) for more information on how this works. + +BE CAREFUL if your language has variants by country don't forget to give attention to the default language used. If several languages are default the first one in the list will be used. + +## Add a new key + +### Add a simple key +The keys are searched by a path through the properties of the sub-objects and it is therefore advisable to write your translation as a JavaScript object. + +Please use [kebab-case](https://en.wikipedia.org/wiki/Letter_case#Kebab_case) to name your keys! + +Example: + +```json +{ + "messages": { + "coffe-machine":{ + "start": "Coffe machine has been started!" + } + } +} +``` + +In the code you can use it like this: + +```js +translator._('messages.coffe-machine.start'); +``` + +### Add a key with parameters + +You can also use parameters to make the translation dynamic. +Use the tag {{ [parameter name] }} to apply your parameters in the translations + +Example: + +```json +{ + "messages": { + "coffe-machine":{ + "player-start": "{{ playerName }} started the coffee machine!" + } + } +} +``` + +In the code you can use it like this: + +```js +translator._('messages.coffe-machine.player-start', { + playerName: "John" +}); +``` \ No newline at end of file From 8286cdd41d54814c232b1cf5959bbb7e004ec12d Mon Sep 17 00:00:00 2001 From: Nolway Date: Thu, 9 Dec 2021 01:31:18 +0100 Subject: [PATCH 03/14] Add WA.player.language in the API --- front/src/Api/Events/GameStateEvent.ts | 1 + front/src/Api/iframe/player.ts | 15 +++++++++++++++ front/src/Phaser/Game/GameScene.ts | 4 ++++ front/src/Translator/Translator.ts | 10 +++++++++- front/src/iframe_api.ts | 3 ++- 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/front/src/Api/Events/GameStateEvent.ts b/front/src/Api/Events/GameStateEvent.ts index 6d20ac9e..80c07e5a 100644 --- a/front/src/Api/Events/GameStateEvent.ts +++ b/front/src/Api/Events/GameStateEvent.ts @@ -5,6 +5,7 @@ export const isGameStateEvent = new tg.IsInterface() roomId: tg.isString, mapUrl: tg.isString, nickname: tg.isString, + language: tg.isUnion(tg.isString, tg.isUndefined), uuid: tg.isUnion(tg.isString, tg.isUndefined), startLayerName: tg.isUnion(tg.isString, tg.isNull), tags: tg.isArray(tg.isString), diff --git a/front/src/Api/iframe/player.ts b/front/src/Api/iframe/player.ts index 0c71ae33..281cb295 100644 --- a/front/src/Api/iframe/player.ts +++ b/front/src/Api/iframe/player.ts @@ -13,6 +13,12 @@ export const setPlayerName = (name: string) => { playerName = name; }; +let playerLanguage: string | undefined; + +export const setPlayerLanguage = (language: string | undefined) => { + playerLanguage = language; +}; + let tags: string[] | undefined; export const setTags = (_tags: string[]) => { @@ -61,6 +67,15 @@ export class WorkadventurePlayerCommands extends IframeApiContribution { setPlayerName(gameState.nickname); + setPlayerLanguage(gameState.language); setRoomId(gameState.roomId); setMapURL(gameState.mapUrl); setTags(gameState.tags); From 41ef9fd49fcd316cdd2999ece60dd6c8aed37356 Mon Sep 17 00:00:00 2001 From: Nolway Date: Sat, 18 Dec 2021 22:18:35 +0100 Subject: [PATCH 04/14] Replace cookie by local storage to store language --- front/src/Phaser/Game/GameScene.ts | 6 ++---- front/src/Phaser/Login/EntryScene.ts | 1 - front/src/Translator/Translator.ts | 15 +++++++-------- front/src/Utils/Cookies.ts | 20 -------------------- front/translations/how-to-translate.md | 2 +- 5 files changed, 10 insertions(+), 34 deletions(-) delete mode 100644 front/src/Utils/Cookies.ts diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index c7147993..33e0a862 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -76,7 +76,7 @@ import { userIsAdminStore } from "../../Stores/GameStore"; import { contactPageStore } from "../../Stores/MenuStore"; import type { WasCameraUpdatedEvent } from "../../Api/Events/WasCameraUpdatedEvent"; import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore"; -import { Translator } from "../../Translator/Translator"; +import { translator } from "../../Translator/Translator"; import EVENT_TYPE = Phaser.Scenes.Events; import Texture = Phaser.Textures.Texture; @@ -212,7 +212,6 @@ export class GameScene extends DirtyScene { private loader: Loader; private lastCameraEvent: WasCameraUpdatedEvent | undefined; private firstCameraUpdateSent: boolean = false; - private translator: Translator; constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) { super({ @@ -232,7 +231,6 @@ export class GameScene extends DirtyScene { this.connectionAnswerPromiseResolve = resolve; }); this.loader = new Loader(this); - this.translator = Translator.getInstance(); } //hook preload scene @@ -1324,7 +1322,7 @@ ${escapedMessage} startLayerName: this.startPositionCalculator.startLayerName, uuid: localUserStore.getLocalUser()?.uuid, nickname: this.playerName, - language: Translator.getStringByLanguage(this.translator.getCurrentLanguage()), + language: translator.getStringByLanguage(translator.getCurrentLanguage()), roomId: this.roomUrl, tags: this.connection ? this.connection.getAllTags() : [], variables: this.sharedVariablesManager.variables, diff --git a/front/src/Phaser/Login/EntryScene.ts b/front/src/Phaser/Login/EntryScene.ts index d75ead03..37b77048 100644 --- a/front/src/Phaser/Login/EntryScene.ts +++ b/front/src/Phaser/Login/EntryScene.ts @@ -34,7 +34,6 @@ export class EntryScene extends Scene { .loadCurrentLanguageObject(this.cache) .catch((e: unknown) => { console.error("Error during language loading!", e); - throw e; }) .finally(() => { gameManager diff --git a/front/src/Translator/Translator.ts b/front/src/Translator/Translator.ts index b1e0bdf4..35785e95 100644 --- a/front/src/Translator/Translator.ts +++ b/front/src/Translator/Translator.ts @@ -1,5 +1,4 @@ import { FALLBACK_LANGUAGE } from "../Enum/EnvironmentVariable"; -import { getCookie } from "../Utils/Cookies"; export type Language = { language: string; @@ -90,7 +89,7 @@ class Translator { ); if (!languageObject) { - return reject(); + return reject(new Error("Language not found in cache")); } this.currentLanguageObject = languageObject as LanguageObject; @@ -135,13 +134,13 @@ class Translator { */ private defineCurrentLanguage() { const navigatorLanguage: string | undefined = navigator.language; - const cookieLanguage = getCookie("language"); + const localStorageLanguage = localStorage.getItem("language"); let currentLanguage = undefined; - if (cookieLanguage && typeof cookieLanguage === "string") { - const cookieLanguageObject = this.getLanguageByString(cookieLanguage); - if (cookieLanguageObject) { - currentLanguage = cookieLanguageObject; + if (localStorageLanguage && typeof localStorageLanguage === "string") { + const localStorageLanguageObject = this.getLanguageByString(localStorageLanguage); + if (localStorageLanguageObject) { + currentLanguage = localStorageLanguageObject; } } @@ -207,7 +206,7 @@ class Translator { /** * Get translation by a key and formatted with params by {{ }} tag * @param {string} key Translation key - * @param {{ [key: string]: string | number }} params Tags to replace by value + * @param {TranslationParams} params Tags to replace by value * @returns {string} Translation formatted */ public _(key: string, params?: TranslationParams): string { diff --git a/front/src/Utils/Cookies.ts b/front/src/Utils/Cookies.ts deleted file mode 100644 index 3ca418c2..00000000 --- a/front/src/Utils/Cookies.ts +++ /dev/null @@ -1,20 +0,0 @@ -export const setCookie = (name: string, value: unknown, days: number) => { - let expires = ""; - if (days) { - const date = new Date(); - date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); - expires = "; expires=" + date.toUTCString(); - } - document.cookie = name + "=" + (value || "") + expires + "; path=/"; -}; - -export const getCookie = (name: string): unknown | undefined => { - const nameEquals = name + "="; - const ca = document.cookie.split(";"); - for (let i = 0; i < ca.length; i++) { - let c = ca[i]; - while (c.charAt(0) == " ") c = c.substring(1, c.length); - if (c.indexOf(nameEquals) == 0) return c.substring(nameEquals.length, c.length); - } - return undefined; -}; diff --git a/front/translations/how-to-translate.md b/front/translations/how-to-translate.md index 1586c17e..72c35c49 100644 --- a/front/translations/how-to-translate.md +++ b/front/translations/how-to-translate.md @@ -4,7 +4,7 @@ In the translations folder, all json files of the form [namespace].[language code].json are interpreted to create languages by the language code in the file name. -The only mandatory file that is the entry point is the index.[language code].json which must contain the properties language, country and default so that the language can be taken into account. +The only mandatory file (the entry point) is the `index.[language code].json` which must contain the properties language, country and default so that the language can be taken into account. Example: ```json From 31b92da6ce7a89b004bbd084fbcdf98c8177e66b Mon Sep 17 00:00:00 2001 From: Nolway Date: Sat, 18 Dec 2021 22:32:15 +0100 Subject: [PATCH 05/14] Move translate documentation --- CONTRIBUTING.md | 24 +++++++++++-------- .../dev}/how-to-translate.md | 6 +++-- 2 files changed, 18 insertions(+), 12 deletions(-) rename {front/translations => docs/dev}/how-to-translate.md (82%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b3361333..2d3f5d0b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,13 +1,13 @@ # Contributing to WorkAdventure -Are you looking to help on WorkAdventure? Awesome, feel welcome and read the following sections in order to know how to +Are you looking to help on WorkAdventure? Awesome, feel welcome and read the following sections in order to know how to ask questions and how to work on something. ## Contributions we are seeking We love to receive contributions from our community — you! -There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, +There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, submitting bug reports and feature requests or writing code which can be incorporated into WorkAdventure itself. ## Contributing external resources @@ -16,7 +16,7 @@ You can share your work on maps / articles / videos related to WorkAdventure on ## Developer documentation -Documentation targeted at developers can be found in the [`/docs/dev`](docs/dev/) +Documentation targeted at developers can be found in the [`/docs/dev`](docs/dev/) ## Using the issue tracker @@ -34,11 +34,11 @@ Finally, you can come and talk to the WorkAdventure core team... on WorkAdventur ## Pull requests -Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope +Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope and avoid containing unrelated commits. -Please ask first before embarking on any significant pull request (e.g. implementing features, refactoring code), -otherwise you risk spending a lot of time working on something that the project's developers might not want to merge +Please ask first before embarking on any significant pull request (e.g. implementing features, refactoring code), +otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project. You can ask us on [Discord](https://discord.gg/YGtngdh9gt) or in the [GitHub issues](https://github.com/thecodingmachine/workadventure/issues). @@ -54,7 +54,7 @@ $ yarn install $ yarn run prepare ``` -If you don't have the precommit hook installed (or if you committed code before installing the precommit hook), you will need +If you don't have the precommit hook installed (or if you committed code before installing the precommit hook), you will need to run code linting manually: ```console @@ -72,7 +72,7 @@ Nevertheless, if your code can be unit tested, please provide a unit test (we us If you are providing a new feature, you should setup a test map in the `maps/tests` directory. The test map should contain some description text describing how to test the feature. -* if the features is meant to be manually tested, you should modify the `maps/tests/index.html` file to add a reference +* if the features is meant to be manually tested, you should modify the `maps/tests/index.html` file to add a reference to your newly created test map * if the features can be automatically tested, please provide a testcafe test @@ -90,8 +90,8 @@ $ npm run test ``` Note: If your tests fail on a Javascript error in "sockjs", this is due to the -Webpack live reload. The Webpack live reload feature is conflicting with testcafe. This is why we recommend starting -WorkAdventure with the `LIVE_RELOAD=0` environment variable. +Webpack live reload. The Webpack live reload feature is conflicting with testcafe. This is why we recommend starting +WorkAdventure with the `LIVE_RELOAD=0` environment variable. End-to-end tests can take a while to run. To run only one test, use: @@ -107,3 +107,7 @@ $ LIVE_RELOAD=0 docker-compose up -d # Wait 2-3 minutes for the environment to start, then: $ PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up ``` + +### A bad wording or a missing language + +If you notice a translation error or missing language you can help us by following the [how to translate](docs/dev/how-to-translate.md) documentation. diff --git a/front/translations/how-to-translate.md b/docs/dev/how-to-translate.md similarity index 82% rename from front/translations/how-to-translate.md rename to docs/dev/how-to-translate.md index 72c35c49..86fd5129 100644 --- a/front/translations/how-to-translate.md +++ b/docs/dev/how-to-translate.md @@ -2,9 +2,9 @@ ## How the translation files work -In the translations folder, all json files of the form [namespace].[language code].json are interpreted to create languages by the language code in the file name. +In the `front/translations` folder, all json files of the form `[namespace].[language code].json` are interpreted to create languages by the language code in the file name. -The only mandatory file (the entry point) is the `index.[language code].json` which must contain the properties language, country and default so that the language can be taken into account. +The only mandatory file (the entry point) is the `index.[language code].json` which must contain the properties `language`, `country` and `default` so that the language can be taken into account. Example: ```json @@ -32,6 +32,8 @@ Example: - main-menu.fr-FR.json - chat.fr-FR.json +If a key isn't found then it will be searched in the fallback language and if it isn't found then the key will be returned. By default the fallback language is `en-US` but you can set another one with the `FALLBACK_LANGUAGE` environment variable. + ## Add a new language It is very easy to add a new language! From bd01a35cc6f47e8d26c3b1394dabf584892ae696 Mon Sep 17 00:00:00 2001 From: Nolway Date: Sun, 19 Dec 2021 16:01:51 +0100 Subject: [PATCH 06/14] Add en-US translations --- .../AudioManager/AudioManager.svelte | 3 +- front/src/Components/Chat/Chat.svelte | 3 +- .../Components/Chat/ChatMessageForm.svelte | 3 +- front/src/Components/Chat/ChatSubMenu.svelte | 9 +- .../CustomCharacterScene.svelte | 14 ++- .../EnableCamera/EnableCameraScene.svelte | 5 +- .../Components/FollowMenu/FollowMenu.svelte | 50 +++++--- .../HelpCameraSettingsPopup.svelte | 16 +-- front/src/Components/Login/LoginScene.svelte | 13 +- .../Components/Menu/AboutRoomSubMenu.svelte | 25 ++-- .../Components/Menu/AudioGlobalMessage.svelte | 5 +- .../src/Components/Menu/ContactSubMenu.svelte | 24 ++-- .../Menu/GlobalMessagesSubMenu.svelte | 11 +- front/src/Components/Menu/GuestSubMenu.svelte | 14 ++- front/src/Components/Menu/Menu.svelte | 15 ++- front/src/Components/Menu/MenuIcon.svelte | 29 ++++- .../src/Components/Menu/ProfileSubMenu.svelte | 19 +-- .../Components/Menu/SettingsSubMenu.svelte | 67 +++++++--- .../Components/Menu/TextGlobalMessage.svelte | 3 +- front/src/Components/MyCamera.svelte | 3 +- .../Components/ReportMenu/BlockSubMenu.svelte | 7 +- .../Components/ReportMenu/ReportMenu.svelte | 9 +- .../ReportMenu/ReportSubMenu.svelte | 13 +- .../SelectCompanionScene.svelte | 7 +- .../Components/TypeMessage/BanMessage.svelte | 4 +- front/src/Components/UI/AudioPlaying.svelte | 3 +- .../src/Components/VisitCard/VisitCard.svelte | 4 +- .../WarningContainer/WarningContainer.svelte | 11 +- .../SelectCharacterScene.svelte | 7 +- front/src/Phaser/Login/EntryScene.ts | 17 ++- .../Phaser/Reconnecting/ReconnectingScene.ts | 3 +- front/src/Translator/Translator.ts | 4 +- front/src/WebRtc/MediaManager.ts | 5 +- front/translations/en-US/audio.en-US.json | 8 ++ front/translations/en-US/camera.en-US.json | 19 +++ front/translations/en-US/chat.en-US.json | 10 ++ front/translations/en-US/companion.en-US.json | 9 ++ .../en-US/custom-character.en-US.json | 16 +++ front/translations/en-US/error.en-US.json | 14 +++ front/translations/en-US/follow.en-US.json | 25 ++++ front/translations/en-US/login.en-US.json | 12 ++ front/translations/en-US/menu.en-US.json | 118 ++++++++++++++++++ front/translations/en-US/report.en-US.json | 23 ++++ front/translations/en-US/test.en-US.json | 5 - front/translations/en-US/warning.en-US.json | 13 ++ 45 files changed, 539 insertions(+), 158 deletions(-) create mode 100644 front/translations/en-US/audio.en-US.json create mode 100644 front/translations/en-US/camera.en-US.json create mode 100644 front/translations/en-US/chat.en-US.json create mode 100644 front/translations/en-US/companion.en-US.json create mode 100644 front/translations/en-US/custom-character.en-US.json create mode 100644 front/translations/en-US/error.en-US.json create mode 100644 front/translations/en-US/follow.en-US.json create mode 100644 front/translations/en-US/login.en-US.json create mode 100644 front/translations/en-US/menu.en-US.json create mode 100644 front/translations/en-US/report.en-US.json delete mode 100644 front/translations/en-US/test.en-US.json create mode 100644 front/translations/en-US/warning.en-US.json diff --git a/front/src/Components/AudioManager/AudioManager.svelte b/front/src/Components/AudioManager/AudioManager.svelte index b62d8fbe..5bf02fc9 100644 --- a/front/src/Components/AudioManager/AudioManager.svelte +++ b/front/src/Components/AudioManager/AudioManager.svelte @@ -5,6 +5,7 @@ import { get } from "svelte/store"; import type { Unsubscriber } from "svelte/store"; import { onDestroy, onMount } from "svelte"; + import { translator } from "../../Translator/Translator"; let HTMLAudioPlayer: HTMLAudioElement; let audioPlayerVolumeIcon: HTMLElement; @@ -144,7 +145,7 @@
diff --git a/front/src/Components/Chat/Chat.svelte b/front/src/Components/Chat/Chat.svelte index 6827dde4..2e60e43e 100644 --- a/front/src/Components/Chat/Chat.svelte +++ b/front/src/Components/Chat/Chat.svelte @@ -5,6 +5,7 @@ import ChatElement from "./ChatElement.svelte"; import { afterUpdate, beforeUpdate, onMount } from "svelte"; import { HtmlUtils } from "../../WebRtc/HtmlUtils"; + import { translator } from "../../Translator/Translator"; let listDom: HTMLElement; let chatWindowElement: HTMLElement; @@ -45,7 +46,7 @@

×

    -
  • Here is your chat history:

  • +
  • {translator._("chat.intro")}

  • {#each $chatMessagesStore as message, i}
  • {/each} diff --git a/front/src/Components/Chat/ChatMessageForm.svelte b/front/src/Components/Chat/ChatMessageForm.svelte index d57eaf5c..6853d669 100644 --- a/front/src/Components/Chat/ChatMessageForm.svelte +++ b/front/src/Components/Chat/ChatMessageForm.svelte @@ -1,5 +1,6 @@
      -
    • -
    • +
    • + +
    • +