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:
Aotumuri
2025-08-06 15:42:28 +09:00
committed by GitHub
parent 2524a4f00c
commit cbb504a304
4 changed files with 106 additions and 6 deletions
+2 -2
View File
@@ -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
View File
@@ -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;
}
};
/**