mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 14:20:45 +00:00
Add basic ICU message format support for translations (#1645)
## Description: This pull request adds support for ICU (Intl MessageFormat) syntax in the translation system. Existing translation files may need to be updated to fully leverage ICU features. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: DISCORD_USERNAME
This commit is contained in:
Generated
+64
-1
@@ -23,6 +23,7 @@
|
||||
"express": "^4.21.1",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"fastpriorityqueue": "^0.7.5",
|
||||
"intl-messageformat": "^10.7.16",
|
||||
"ip-anonymize": "^0.1.0",
|
||||
"jose": "^6.0.10",
|
||||
"js-yaml": "^4.1.0",
|
||||
@@ -3623,6 +3624,57 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
|
||||
"integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/fast-memoize": "2.2.7",
|
||||
"@formatjs/intl-localematcher": "0.6.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/fast-memoize": {
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
|
||||
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/icu-messageformat-parser": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz",
|
||||
"integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "2.3.4",
|
||||
"@formatjs/icu-skeleton-parser": "1.8.14",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/icu-skeleton-parser": {
|
||||
"version": "1.8.14",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz",
|
||||
"integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "2.3.4",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz",
|
||||
"integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -10353,7 +10405,6 @@
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
|
||||
"integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
@@ -12763,6 +12814,18 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/intl-messageformat": {
|
||||
"version": "10.7.16",
|
||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz",
|
||||
"integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "2.3.4",
|
||||
"@formatjs/fast-memoize": "2.2.7",
|
||||
"@formatjs/icu-messageformat-parser": "2.11.2",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-anonymize": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-anonymize/-/ip-anonymize-0.1.0.tgz",
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
"express": "^4.21.1",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"fastpriorityqueue": "^0.7.5",
|
||||
"intl-messageformat": "^10.7.16",
|
||||
"ip-anonymize": "^0.1.0",
|
||||
"jose": "^6.0.10",
|
||||
"js-yaml": "^4.1.0",
|
||||
|
||||
@@ -34,8 +34,8 @@ import zh_CN from "../../resources/lang/zh-CN.json";
|
||||
@customElement("lang-selector")
|
||||
export class LangSelector extends LitElement {
|
||||
@state() public translations: Record<string, string> | undefined;
|
||||
@state() private defaultTranslations: Record<string, string> | undefined;
|
||||
@state() private currentLang: string = "en";
|
||||
@state() public defaultTranslations: Record<string, string> | undefined;
|
||||
@state() public currentLang: string = "en";
|
||||
@state() private languageList: any[] = [];
|
||||
@state() private showModal: boolean = false;
|
||||
@state() private debugMode: boolean = false;
|
||||
|
||||
+39
-3
@@ -1,3 +1,4 @@
|
||||
import IntlMessageFormat from "intl-messageformat";
|
||||
import { MessageType } from "../core/game/Game";
|
||||
import { LangSelector } from "./LangSelector";
|
||||
|
||||
@@ -78,18 +79,20 @@ export function generateCryptoRandomUUID(): string {
|
||||
);
|
||||
}
|
||||
|
||||
// Re-export translateText from LangSelector
|
||||
export const translateText = (
|
||||
key: string,
|
||||
params: Record<string, string | number> = {},
|
||||
): string => {
|
||||
const self = translateText as any;
|
||||
self.formatterCache ??= new Map();
|
||||
self.lastLang ??= null;
|
||||
|
||||
const langSelector = document.querySelector("lang-selector") as LangSelector;
|
||||
if (!langSelector) {
|
||||
console.warn("LangSelector not found in DOM");
|
||||
return key;
|
||||
}
|
||||
|
||||
// Wait for translations to be loaded
|
||||
if (
|
||||
!langSelector.translations ||
|
||||
Object.keys(langSelector.translations).length === 0
|
||||
@@ -97,7 +100,40 @@ export const translateText = (
|
||||
return key;
|
||||
}
|
||||
|
||||
return langSelector.translateText(key, params);
|
||||
if (self.lastLang !== langSelector.currentLang) {
|
||||
self.formatterCache.clear();
|
||||
self.lastLang = langSelector.currentLang;
|
||||
}
|
||||
|
||||
let message = langSelector.translations[key];
|
||||
|
||||
if (!message && langSelector.defaultTranslations) {
|
||||
const defaultTranslations = langSelector.defaultTranslations;
|
||||
if (defaultTranslations && defaultTranslations[key]) {
|
||||
message = defaultTranslations[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (!message) return key;
|
||||
|
||||
try {
|
||||
const locale =
|
||||
!langSelector.translations[key] && langSelector.currentLang !== "en"
|
||||
? "en"
|
||||
: langSelector.currentLang;
|
||||
const cacheKey = `${key}:${locale}:${message}`;
|
||||
let formatter = self.formatterCache.get(cacheKey);
|
||||
|
||||
if (!formatter) {
|
||||
formatter = new IntlMessageFormat(message, locale);
|
||||
self.formatterCache.set(cacheKey, formatter);
|
||||
}
|
||||
|
||||
return formatter.format(params) as string;
|
||||
} catch (e) {
|
||||
console.warn("ICU format error", e);
|
||||
return message;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user