mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-03 10:50:47 +00:00
Add language metadata and enhance language validation tests (#2748)
Resolves #2739 ## Description: Introduce language metadata handling and refactor existing language checks to validate the existence of language JSON and corresponding SVG files. Add tests to ensure the integrity of the new metadata structure and its references. The lang field is intentionally kept in each language file. This is because the files are frequently regenerated by Crowdin, and the field also serves as a hint for management and maintenance. ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: aotumuri --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
+60
-96
@@ -2,40 +2,15 @@ import { LitElement, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import "./LanguageModal";
|
||||
|
||||
import ar from "../../resources/lang/ar.json";
|
||||
import bg from "../../resources/lang/bg.json";
|
||||
import bn from "../../resources/lang/bn.json";
|
||||
import cs from "../../resources/lang/cs.json";
|
||||
import da from "../../resources/lang/da.json";
|
||||
import de from "../../resources/lang/de.json";
|
||||
import el from "../../resources/lang/el.json";
|
||||
import en from "../../resources/lang/en.json";
|
||||
import eo from "../../resources/lang/eo.json";
|
||||
import es from "../../resources/lang/es.json";
|
||||
import fa from "../../resources/lang/fa.json";
|
||||
import fi from "../../resources/lang/fi.json";
|
||||
import fr from "../../resources/lang/fr.json";
|
||||
import gl from "../../resources/lang/gl.json";
|
||||
import he from "../../resources/lang/he.json";
|
||||
import hi from "../../resources/lang/hi.json";
|
||||
import hu from "../../resources/lang/hu.json";
|
||||
import it from "../../resources/lang/it.json";
|
||||
import ja from "../../resources/lang/ja.json";
|
||||
import ko from "../../resources/lang/ko.json";
|
||||
import mk from "../../resources/lang/mk.json";
|
||||
import nl from "../../resources/lang/nl.json";
|
||||
import pl from "../../resources/lang/pl.json";
|
||||
import pt_BR from "../../resources/lang/pt-BR.json";
|
||||
import pt_PT from "../../resources/lang/pt-PT.json";
|
||||
import ru from "../../resources/lang/ru.json";
|
||||
import sh from "../../resources/lang/sh.json";
|
||||
import sk from "../../resources/lang/sk.json";
|
||||
import sl from "../../resources/lang/sl.json";
|
||||
import sv_SE from "../../resources/lang/sv-SE.json";
|
||||
import tp from "../../resources/lang/tp.json";
|
||||
import tr from "../../resources/lang/tr.json";
|
||||
import uk from "../../resources/lang/uk.json";
|
||||
import zh_CN from "../../resources/lang/zh-CN.json";
|
||||
import metadata from "../../resources/lang/metadata.json";
|
||||
|
||||
type LanguageMetadata = {
|
||||
code: string;
|
||||
native: string;
|
||||
en: string;
|
||||
svg: string;
|
||||
};
|
||||
|
||||
@customElement("lang-selector")
|
||||
export class LangSelector extends LitElement {
|
||||
@@ -47,43 +22,8 @@ export class LangSelector extends LitElement {
|
||||
@state() private debugMode: boolean = false;
|
||||
|
||||
private debugKeyPressed: boolean = false;
|
||||
|
||||
private languageMap: Record<string, any> = {
|
||||
ar,
|
||||
bg,
|
||||
bn,
|
||||
de,
|
||||
el,
|
||||
en,
|
||||
es,
|
||||
eo,
|
||||
fr,
|
||||
it,
|
||||
hi,
|
||||
hu,
|
||||
ja,
|
||||
nl,
|
||||
pl,
|
||||
"pt-PT": pt_PT,
|
||||
"pt-BR": pt_BR,
|
||||
ru,
|
||||
sh,
|
||||
tr,
|
||||
tp,
|
||||
uk,
|
||||
cs,
|
||||
he,
|
||||
da,
|
||||
fa,
|
||||
fi,
|
||||
"sv-SE": sv_SE,
|
||||
"zh-CN": zh_CN,
|
||||
ko,
|
||||
mk,
|
||||
gl,
|
||||
sl,
|
||||
sk,
|
||||
};
|
||||
private languageMetadata: LanguageMetadata[] = metadata;
|
||||
private languageCache = new Map<string, Record<string, string>>();
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
@@ -106,10 +46,12 @@ export class LangSelector extends LitElement {
|
||||
|
||||
private getClosestSupportedLang(lang: string): string {
|
||||
if (!lang) return "en";
|
||||
if (lang in this.languageMap) return lang;
|
||||
if (lang === "debug") return "debug";
|
||||
const supported = new Set(this.languageMetadata.map((entry) => entry.code));
|
||||
if (supported.has(lang)) return lang;
|
||||
|
||||
const base = lang.slice(0, 2);
|
||||
const candidates = Object.keys(this.languageMap).filter((key) =>
|
||||
const candidates = Array.from(supported).filter((key) =>
|
||||
key.startsWith(base),
|
||||
);
|
||||
if (candidates.length > 0) {
|
||||
@@ -125,41 +67,53 @@ export class LangSelector extends LitElement {
|
||||
const savedLang = localStorage.getItem("lang");
|
||||
const userLang = this.getClosestSupportedLang(savedLang ?? browserLocale);
|
||||
|
||||
this.defaultTranslations = this.loadLanguage("en");
|
||||
this.translations = this.loadLanguage(userLang);
|
||||
const [defaultTranslations, translations] = await Promise.all([
|
||||
this.loadLanguage("en"),
|
||||
this.loadLanguage(userLang),
|
||||
]);
|
||||
|
||||
this.defaultTranslations = defaultTranslations;
|
||||
this.translations = translations;
|
||||
this.currentLang = userLang;
|
||||
|
||||
await this.loadLanguageList();
|
||||
this.applyTranslation();
|
||||
}
|
||||
|
||||
private loadLanguage(lang: string): Record<string, string> {
|
||||
const language = this.languageMap[lang] ?? {};
|
||||
const flat = flattenTranslations(language);
|
||||
return flat;
|
||||
private async loadLanguage(lang: string): Promise<Record<string, string>> {
|
||||
if (!lang) return {};
|
||||
const cached = this.languageCache.get(lang);
|
||||
if (cached) return cached;
|
||||
|
||||
if (lang === "en") {
|
||||
const flat = flattenTranslations(en);
|
||||
this.languageCache.set(lang, flat);
|
||||
return flat;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/lang/${encodeURIComponent(lang)}.json`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch language ${lang}: ${response.status}`);
|
||||
}
|
||||
const language = (await response.json()) as Record<string, any>;
|
||||
const flat = flattenTranslations(language);
|
||||
this.languageCache.set(lang, flat);
|
||||
return flat;
|
||||
} catch (err) {
|
||||
console.error(`Failed to load language ${lang}:`, err);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
private async loadLanguageList() {
|
||||
try {
|
||||
const data = this.languageMap;
|
||||
let list: any[] = [];
|
||||
|
||||
const browserLang = new Intl.Locale(navigator.language).language;
|
||||
|
||||
for (const langCode of Object.keys(data)) {
|
||||
const langData = data[langCode].lang;
|
||||
if (!langData) continue;
|
||||
|
||||
list.push({
|
||||
code: langData.lang_code ?? langCode,
|
||||
native: langData.native ?? langCode,
|
||||
en: langData.en ?? langCode,
|
||||
svg: langData.svg ?? langCode,
|
||||
});
|
||||
}
|
||||
|
||||
let debugLang: any = null;
|
||||
if (this.debugKeyPressed) {
|
||||
if (this.debugKeyPressed || this.currentLang === "debug") {
|
||||
debugLang = {
|
||||
code: "debug",
|
||||
native: "Debug",
|
||||
@@ -169,6 +123,16 @@ export class LangSelector extends LitElement {
|
||||
this.debugMode = true;
|
||||
}
|
||||
|
||||
for (const langData of this.languageMetadata) {
|
||||
if (langData.code === "debug" && !debugLang) continue;
|
||||
list.push({
|
||||
code: langData.code,
|
||||
native: langData.native,
|
||||
en: langData.en,
|
||||
svg: langData.svg,
|
||||
});
|
||||
}
|
||||
|
||||
const currentLangEntry = list.find((l) => l.code === this.currentLang);
|
||||
const browserLangEntry =
|
||||
browserLang !== this.currentLang && browserLang !== "en"
|
||||
@@ -202,9 +166,9 @@ export class LangSelector extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private changeLanguage(lang: string) {
|
||||
private async changeLanguage(lang: string) {
|
||||
localStorage.setItem("lang", lang);
|
||||
this.translations = this.loadLanguage(lang);
|
||||
this.translations = await this.loadLanguage(lang);
|
||||
this.currentLang = lang;
|
||||
this.applyTranslation();
|
||||
this.showModal = false;
|
||||
@@ -277,10 +241,10 @@ export class LangSelector extends LitElement {
|
||||
return text;
|
||||
}
|
||||
|
||||
private openModal() {
|
||||
private async openModal() {
|
||||
this.debugMode = this.debugKeyPressed;
|
||||
this.showModal = true;
|
||||
this.loadLanguageList();
|
||||
await this.loadLanguageList();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
Reference in New Issue
Block a user