From 5d5b54cfc43934c268738d2145e4e8866f5ef315 Mon Sep 17 00:00:00 2001 From: reverse Date: Sat, 20 Jun 2026 16:28:25 +0000 Subject: [PATCH] config: optional defaults.lang to pin a default UI language When set (and the user hasn't explicitly chosen one), it overrides browser detection after the runtime config loads. Omitted = auto-detect as before. --- public/sw.js | 2 +- src/config.ts | 1 + src/i18n/index.ts | 12 ++++++++++++ src/main.tsx | 4 +++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/public/sw.js b/public/sw.js index a0cc3cc..5e404f0 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,7 +1,7 @@ // Tchatou service worker — installable PWA + offline app shell. // Scope: /app/. Only handles same-origin /app/ GETs; the IRC websocket and all // API calls (cloudflare, change_password, upload) pass straight through. -const CACHE = 'tchatou-v50'; +const CACHE = 'tchatou-v51'; const SHELL = ['/app/', '/app/index.html', '/app/favicon.svg', '/app/orbit-icon.svg', '/app/manifest.webmanifest']; self.addEventListener('install', (e) => { diff --git a/src/config.ts b/src/config.ts index c8e8421..bb9b410 100644 --- a/src/config.ts +++ b/src/config.ts @@ -39,6 +39,7 @@ export interface AppConfig { sound: boolean; // blip on mention / PM hideJoinQuit: boolean; // hide join/part/quit noise clock24: boolean; // 24h timestamps + lang?: string; // force a default UI language (e.g. 'en'); omit to auto-detect from the browser }; /** Feature switches — turn whole features off for a deployment. */ features: { diff --git a/src/i18n/index.ts b/src/i18n/index.ts index c7165ea..07144aa 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -65,6 +65,18 @@ void i18n.use(initReactI18next).init({ document.documentElement.lang = i18n.language; +// Apply a deployment's default UI language (config.defaults.lang) — but only for +// users who haven't explicitly picked one (setLang writes KEY). Called after the +// runtime config loads, so it can override the browser-detected language. +export function applyConfigDefaultLang(code?: string): void { + if (!code) return; + if (localStorage.getItem(KEY)) return; // explicit user choice — honour it + if (!CODES.includes(code)) return; // unknown language — ignore + if (i18n.language === code) return; + void i18n.changeLanguage(code); + document.documentElement.lang = code; +} + export function getLang(): string { return i18n.language; } diff --git a/src/main.tsx b/src/main.tsx index d6c2b79..fc0c7b2 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,9 +2,10 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { applyTheme, getTheme } from './ui/theme.ts' import './i18n' +import { applyConfigDefaultLang } from './i18n' import './index.css' import { initViewport } from './ui/viewport.ts' -import { loadConfig } from './config.ts' +import { loadConfig, getConfig } from './config.ts' // Track the visual viewport so the layout shrinks above the on-screen keyboard. initViewport() @@ -14,6 +15,7 @@ initViewport() // a rebuild. loadConfig().then(async () => { applyTheme(getTheme()) // re-apply now that config (default theme) is loaded + applyConfigDefaultLang(getConfig().defaults.lang) // honour a config-pinned default language const { default: App } = await import('./App.tsx') // also creates the store // Plugin subsystem: publish window.Orbit, bridge app/IRC events onto the bus, // then load operator-listed plugins from config. After the store exists,