Implement Translator: i18n system

This commit is contained in:
Nolway 2021-12-08 01:34:50 +01:00 committed by Alexis Faizeau
parent 5f62894d56
commit 8a2767ef40
16 changed files with 393 additions and 47 deletions

View file

@ -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,
};

View file

@ -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

View file

@ -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);
}
});
});
}
}

View file

@ -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<LanguageFound> | undefined) => {
const files = fs.readdirSync(dirPath);
languages = languages || new Array<LanguageFound>();
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);

View file

@ -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<void> {
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();

View file

@ -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;
};

2
front/src/define-plugin.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
declare const FALLBACK_LANGUAGE_OBJECT: Object;
declare const LANGUAGES: Object;