mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 06:12:19 +00:00
607e5b5ff0
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors
This is commit pack
This PR refactors and improves the language selection experience:
• Centralizes all language-related logic in LangSelector.ts &
LanguageModal.ts
• Redesigns the language selection UI for better UX across devices
• Adds new translations and supports more languages
Changes .w.
• Language selection is now handled entirely inside LangSelector.ts &
LanguageModal.ts
• Prevents background scrolling when open
• Highlights the current language at the top
• Always shows English second
• Shows browser language third (if different from current)
• All other languages are sorted alphabetically by English name
• Debug option is shown at the end when pressing D
• The language list is scrollable when it exceeds screen height
Supported Languages
["en", "ja", "fr", "bg", "nl", "ru", "ua", "de"]
Added Translation Keys
```
"lang": {
"en": "English",
"native": "English",
"svg": "xx"
},
"map": {
"map": "Map"
},
"game_starting_modal": {
"title": "Game is Starting...",
"desc": "Preparing for the lobby to start. Please wait."
},
"difficulty": {
"difficulty": "Difficulty"
}
```
## Please put your Discord username so you can be contacted if a bug or
regression is found:
MLS Representative
- aotumuri
Translation collaborator
- Nikola123 (He was a very big help from setting up the translation site
to adding the json. Thank you so much!)
I don't have permission from my collaborators to display their names
here, so I'll put the discord link here
https://discord.com/channels/1284581928254701718/1352553113612980224/1352553113612980224
- tryout33
Collaborators from other servers.
- CCC Group (This is not Culture Convenience Club. Think of it like a
server where developers of various games are playing.)
- People who fixed the UI and found bugs.
meow02952 (discord id) <- This person also gave me a code suggestion.
Thanks!
moon_spear (discord id)
ww_what_ww (discord id)
Azuna (he doesn't have discord account)
- People who corrected translations, etc.
_kyoyume_ (discord id)
_ultrasuper_ (discord id)
grueg (he doesn't have discord account)
# If I forgot to include your name, or if you’d like your name to be
added, please let me know via Gmail or Discord.
---------
Co-authored-by: Duwibi <86431918+Duwibi@users.noreply.github.com>
280 lines
7.5 KiB
TypeScript
280 lines
7.5 KiB
TypeScript
import { LitElement, html } from "lit";
|
|
import { customElement, state } from "lit/decorators.js";
|
|
import "./LanguageModal";
|
|
|
|
import bg from "../../resources/lang/bg.json";
|
|
import de from "../../resources/lang/de.json";
|
|
import en from "../../resources/lang/en.json";
|
|
import es from "../../resources/lang/es.json";
|
|
import fr from "../../resources/lang/fr.json";
|
|
import ja from "../../resources/lang/ja.json";
|
|
import nl from "../../resources/lang/nl.json";
|
|
import pl from "../../resources/lang/pl.json";
|
|
import ru from "../../resources/lang/ru.json";
|
|
import uk from "../../resources/lang/uk.json";
|
|
|
|
@customElement("lang-selector")
|
|
export class LangSelector extends LitElement {
|
|
@state() public translations: any = {};
|
|
@state() private defaultTranslations: any = {};
|
|
@state() private currentLang: string = "en";
|
|
@state() private languageList: any[] = [];
|
|
@state() private showModal: boolean = false;
|
|
@state() private debugMode: boolean = false;
|
|
|
|
private dKeyPressed: boolean = false;
|
|
|
|
private languageMap: Record<string, any> = {
|
|
bg,
|
|
de,
|
|
en,
|
|
es,
|
|
fr,
|
|
ja,
|
|
nl,
|
|
pl,
|
|
ru,
|
|
uk,
|
|
};
|
|
|
|
createRenderRoot() {
|
|
return this; // Use Light DOM if you prefer this
|
|
}
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
this.setupDebugKey();
|
|
this.initializeLanguage();
|
|
}
|
|
|
|
private setupDebugKey() {
|
|
window.addEventListener("keydown", (e) => {
|
|
if (e.key.toLowerCase() === "t") this.dKeyPressed = true;
|
|
});
|
|
window.addEventListener("keyup", (e) => {
|
|
if (e.key.toLowerCase() === "t") this.dKeyPressed = false;
|
|
});
|
|
}
|
|
|
|
private async initializeLanguage() {
|
|
const locale = new Intl.Locale(navigator.language);
|
|
const defaultLang = locale.language;
|
|
const userLang = localStorage.getItem("lang") || defaultLang;
|
|
|
|
this.defaultTranslations = await this.loadLanguage("en");
|
|
this.translations = await this.loadLanguage(userLang);
|
|
this.currentLang = userLang;
|
|
|
|
await this.loadLanguageList();
|
|
this.applyTranslation(this.translations);
|
|
}
|
|
|
|
private async loadLanguage(lang: string): Promise<any> {
|
|
return Promise.resolve(this.languageMap[lang] || {});
|
|
}
|
|
|
|
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.dKeyPressed) {
|
|
debugLang = {
|
|
code: "debug",
|
|
native: "Debug",
|
|
en: "Debug",
|
|
svg: "xx",
|
|
};
|
|
this.debugMode = true;
|
|
}
|
|
|
|
const currentLangEntry = list.find((l) => l.code === this.currentLang);
|
|
const browserLangEntry =
|
|
browserLang !== this.currentLang && browserLang !== "en"
|
|
? list.find((l) => l.code === browserLang)
|
|
: undefined;
|
|
const englishEntry =
|
|
this.currentLang !== "en"
|
|
? list.find((l) => l.code === "en")
|
|
: undefined;
|
|
|
|
list = list.filter(
|
|
(l) =>
|
|
l.code !== this.currentLang &&
|
|
l.code !== browserLang &&
|
|
l.code !== "en" &&
|
|
l.code !== "debug",
|
|
);
|
|
|
|
list.sort((a, b) => a.en.localeCompare(b.en));
|
|
|
|
const finalList: any[] = [];
|
|
if (currentLangEntry) finalList.push(currentLangEntry);
|
|
if (englishEntry) finalList.push(englishEntry);
|
|
if (browserLangEntry) finalList.push(browserLangEntry);
|
|
finalList.push(...list);
|
|
if (debugLang) finalList.push(debugLang);
|
|
|
|
this.languageList = finalList;
|
|
} catch (err) {
|
|
console.error("Failed to load language list:", err);
|
|
}
|
|
}
|
|
|
|
private async changeLanguage(lang: string) {
|
|
localStorage.setItem("lang", lang);
|
|
this.translations = await this.loadLanguage(lang);
|
|
this.currentLang = lang;
|
|
this.applyTranslation(this.translations);
|
|
this.showModal = false;
|
|
}
|
|
|
|
private applyTranslation(translations: any) {
|
|
const components = [
|
|
"single-player-modal",
|
|
"host-lobby-modal",
|
|
"join-private-lobby-modal",
|
|
"emoji-table",
|
|
"leader-board",
|
|
"build-menu",
|
|
"win-modal",
|
|
"game-starting-modal",
|
|
"top-bar",
|
|
"player-panel",
|
|
"help-modal",
|
|
"username-input",
|
|
"public-lobby",
|
|
"o-modal",
|
|
"o-button",
|
|
];
|
|
|
|
document.title = translations.main?.title || document.title;
|
|
|
|
document.querySelectorAll("[data-i18n]").forEach((element) => {
|
|
const key = element.getAttribute("data-i18n");
|
|
const keys = key?.split(".") || [];
|
|
let text = translations;
|
|
|
|
for (const k of keys) {
|
|
text = text?.[k];
|
|
if (!text) break;
|
|
}
|
|
|
|
if (!text && this.defaultTranslations) {
|
|
let fallback = this.defaultTranslations;
|
|
for (const k of keys) {
|
|
fallback = fallback?.[k];
|
|
if (!fallback) break;
|
|
}
|
|
text = fallback;
|
|
}
|
|
|
|
if (text) {
|
|
element.innerHTML = text;
|
|
} else {
|
|
console.warn(`Translation key not found: ${key}`);
|
|
}
|
|
});
|
|
|
|
components.forEach((tag) => {
|
|
document.querySelectorAll(tag).forEach((el) => {
|
|
if (typeof (el as any).requestUpdate === "function") {
|
|
(el as any).requestUpdate();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
public translateText(
|
|
key: string,
|
|
params: Record<string, string | number> = {},
|
|
): string {
|
|
const keys = key.split(".");
|
|
let text: any = this.translations;
|
|
|
|
for (const k of keys) {
|
|
text = text?.[k];
|
|
if (!text) break;
|
|
}
|
|
|
|
if (!text && this.defaultTranslations) {
|
|
text = this.defaultTranslations;
|
|
for (const k of keys) {
|
|
text = text?.[k];
|
|
if (!text) return key;
|
|
}
|
|
}
|
|
|
|
for (const [param, value] of Object.entries(params)) {
|
|
text = text.replace(`{${param}}`, String(value));
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
private openModal() {
|
|
this.debugMode = this.dKeyPressed;
|
|
this.showModal = true;
|
|
this.loadLanguageList();
|
|
}
|
|
|
|
render() {
|
|
const currentLang =
|
|
this.languageList.find((l) => l.code === this.currentLang) ??
|
|
(this.currentLang === "debug"
|
|
? {
|
|
code: "debug",
|
|
native: "Debug",
|
|
en: "Debug",
|
|
svg: "xx",
|
|
}
|
|
: {
|
|
native: "English",
|
|
en: "English",
|
|
svg: "xx",
|
|
});
|
|
|
|
return html`
|
|
<div class="container__row">
|
|
<button
|
|
id="lang-selector"
|
|
@click=${this.openModal}
|
|
class="text-center appearance-none w-full bg-blue-100 hover:bg-blue-200 text-blue-900 p-3 sm:p-4 lg:p-5 font-medium text-sm sm:text-base lg:text-lg rounded-md border-none cursor-pointer transition-colors duration-300 flex items-center gap-2 justify-center"
|
|
>
|
|
<img
|
|
id="lang-flag"
|
|
class="w-6 h-4"
|
|
src="/flags/${currentLang.svg}.svg"
|
|
alt="flag"
|
|
/>
|
|
<span id="lang-name">${currentLang.native} (${currentLang.en})</span>
|
|
</button>
|
|
</div>
|
|
|
|
<language-modal
|
|
.visible=${this.showModal}
|
|
.languageList=${this.languageList}
|
|
.currentLang=${this.currentLang}
|
|
@language-selected=${(e: CustomEvent) =>
|
|
this.changeLanguage(e.detail.lang)}
|
|
@close-modal=${() => (this.showModal = false)}
|
|
></language-modal>
|
|
`;
|
|
}
|
|
}
|