From 5c258bfca67f6c4b0f22c7bcf337dfa10c0698ab Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 28 Mar 2025 10:57:30 -0700 Subject: [PATCH] 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. --- src/client/LangSelector.ts | 180 +++++++++++++++++++++++++++++++++++++ src/client/Main.ts | 9 ++ src/client/Utils.ts | 38 ++++---- src/client/index.html | 109 +--------------------- 4 files changed, 208 insertions(+), 128 deletions(-) create mode 100644 src/client/LangSelector.ts diff --git a/src/client/LangSelector.ts b/src/client/LangSelector.ts new file mode 100644 index 000000000..22fb15e31 --- /dev/null +++ b/src/client/LangSelector.ts @@ -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 { + 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 { + 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` + + `; + } +} diff --git a/src/client/Main.ts b/src/client/Main.ts index 48354c68d..3b46e88b5 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -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"); diff --git a/src/client/Utils.ts b/src/client/Utils.ts index 88ab5726e..0522fe539 100644 --- a/src/client/Utils.ts +++ b/src/client/Utils.ts @@ -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 { - 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); +}; diff --git a/src/client/index.html b/src/client/index.html index 8a03739ad..9b592059a 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -260,18 +260,7 @@ secondary >
- +
@@ -359,102 +348,6 @@ }); }); -