Move lang-selector into a lit element. This is required so we can import the lang json files using webpack file hashing. We need file hashing to bust the cache.

This commit is contained in:
Evan
2025-03-28 10:57:30 -07:00
parent d8fe41de7a
commit 5c258bfca6
4 changed files with 208 additions and 128 deletions
+180
View File
@@ -0,0 +1,180 @@
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
// Import language files
import enTranslations from "../../resources/lang/en.json";
import bgTranslations from "../../resources/lang/bg.json";
import jaTranslations from "../../resources/lang/ja.json";
import frTranslations from "../../resources/lang/fr.json";
import nlTranslations from "../../resources/lang/nl.json";
import deTranslations from "../../resources/lang/de.json";
import esTranslations from "../../resources/lang/es.json";
const translations = {
en: enTranslations,
bg: bgTranslations,
ja: jaTranslations,
fr: frTranslations,
nl: nlTranslations,
de: deTranslations,
es: esTranslations,
};
@customElement("lang-selector")
export class LangSelector extends LitElement {
@state() public translations: any = {};
@state() private defaultTranslations: any = {};
@state() private currentLang: string = "en";
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.initializeLanguage();
}
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;
this.applyTranslation(this.translations);
}
private async loadLanguage(lang: string): Promise<any> {
try {
const translation = translations[lang as keyof typeof translations];
if (!translation) throw new Error(`Language file not found: ${lang}`);
return translation;
} catch (error) {
console.error("🚨 Translation load error:", error);
return {};
}
}
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",
];
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(`Missing translation key: ${key}`);
}
});
components.forEach((tagName) => {
const el = document.querySelector(tagName) as any;
if (el && typeof el.requestUpdate === "function") {
el.requestUpdate();
} else {
console.warn(
`requestUpdate() not available on <${tagName}> or element not found.`,
);
}
});
}
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 async changeLanguage(lang: string) {
localStorage.setItem("lang", lang);
this.translations = await this.loadLanguage(lang);
this.currentLang = lang;
this.applyTranslation(this.translations);
}
render() {
return html`
<select
@change=${(e: Event) =>
this.changeLanguage((e.target as HTMLSelectElement).value)}
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"
>
<option value="en" ?selected=${this.currentLang === "en"}>
English
</option>
<option value="bg" ?selected=${this.currentLang === "bg"}>
Български
</option>
<option value="ja" ?selected=${this.currentLang === "ja"}>
日本語
</option>
<option value="fr" ?selected=${this.currentLang === "fr"}>
Français
</option>
<option value="nl" ?selected=${this.currentLang === "nl"}>
Nederlands
</option>
<option value="de" ?selected=${this.currentLang === "de"}>
Deutsch
</option>
<option value="es" ?selected=${this.currentLang === "es"}>
Español
</option>
</select>
`;
}
}
+9
View File
@@ -26,6 +26,8 @@ import { GameType } from "../core/game/Game";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import GoogleAdElement from "./GoogleAdElement";
import { GameConfig, GameInfo, GameRecord } from "../core/Schemas";
import "./LangSelector";
import { LangSelector } from "./LangSelector";
export interface JoinLobbyEvent {
// Multiplayer games only have gameID, gameConfig is not known until game starts.
@@ -51,6 +53,13 @@ class Client {
constructor() {}
initialize(): void {
const langSelector = document.querySelector(
"lang-selector",
) as LangSelector;
if (!langSelector) {
consolex.warn("Lang selector element not found");
}
this.flagInput = document.querySelector("flag-input") as FlagInput;
if (!this.flagInput) {
consolex.warn("Flag input element not found");
+18 -20
View File
@@ -1,3 +1,5 @@
import { LangSelector } from "./LangSelector";
export function renderTroops(troops: number): string {
return renderNumber(troops / 10);
}
@@ -71,29 +73,25 @@ export function generateCryptoRandomUUID(): string {
);
}
export function translateText(
// Re-export translateText from LangSelector
export const translateText = (
key: string,
params: Record<string, string | number> = {},
): string {
const keys = key.split(".");
let text: any = (window as any).translations;
for (const k of keys) {
text = text?.[k];
if (!text) break;
): string => {
const langSelector = document.querySelector("lang-selector") as LangSelector;
if (!langSelector) {
console.warn("LangSelector not found in DOM");
return key;
}
if (!text && (window as any).defaultTranslations) {
text = (window as any).defaultTranslations;
for (const k of keys) {
text = text?.[k];
if (!text) return key;
}
// Wait for translations to be loaded
if (
!langSelector.translations ||
Object.keys(langSelector.translations).length === 0
) {
console.warn("Translations not loaded yet");
return key;
}
for (const [param, value] of Object.entries(params)) {
text = text.replace(`{${param}}`, String(value));
}
return text;
}
return langSelector.translateText(key, params);
};
+1 -108
View File
@@ -260,18 +260,7 @@
secondary
></o-button>
<div class="container__row">
<select
id="lang-selector"
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"
>
<option value="en">English</option>
<option value="bg">Български</option>
<option value="ja">日本語</option>
<option value="fr">Français</option>
<option value="nl">Nederlands</option>
<option value="de">Deutsch</option>
<option value="es">Español</option>
</select>
<lang-selector class="w-full"></lang-selector>
</div>
</div>
</main>
@@ -359,102 +348,6 @@
});
});
</script>
<script>
document.addEventListener("DOMContentLoaded", async function () {
const locale = new Intl.Locale(navigator.language);
const defaultLang = locale.language;
const userLang = localStorage.getItem("lang") || defaultLang;
async function loadLanguage(lang) {
try {
const response = await fetch(`/lang/${lang}.json`);
if (!response.ok)
throw new Error(`Language file not found: ${lang}`);
return await response.json();
} catch (error) {
console.error("🚨 Translation load error:", error);
return {};
}
}
function applyTranslation(translations) {
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",
];
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 && window.defaultTranslations) {
let fallback = window.defaultTranslations;
for (const k of keys) {
fallback = fallback?.[k];
if (!fallback) break;
}
text = fallback;
}
if (text) {
element.innerHTML = text;
} else {
console.warn(`Missing translation key: ${key}`);
}
});
components.forEach((tagName) => {
const el = document.querySelector(tagName);
if (el && typeof el.requestUpdate === "function") {
el.requestUpdate();
} else {
console.warn(
`requestUpdate() not available on <${tagName}> or element not found.`,
);
}
});
}
async function changeLanguage(lang) {
// console.log(`Changing language to: ${lang}`);
localStorage.setItem("lang", lang);
const translations = await loadLanguage(lang);
window.translations = translations;
applyTranslation(translations);
}
const defaultTranslations = await loadLanguage("en");
window.defaultTranslations = defaultTranslations;
const translations = await loadLanguage(userLang);
window.translations = translations;
applyTranslation(translations);
const langSelector = document.getElementById("lang-selector");
if (langSelector) {
langSelector.value = userLang;
}
document
.getElementById("lang-selector")
.addEventListener("change", function (event) {
changeLanguage(event.target.value);
});
});
</script>
<!-- Analytics -->
<script