1import { resolveLocale } from "@t3tools/client-runtime/locales";
2import { getTranslationsSnapshot } from "gt-react";
3
4import {
5 getClientSettings,
6 subscribeClientSettings,
7 updateClientSettings,
8} from "../hooks/useSettings";
9import { initializeI18n } from "./initializeI18n";
10import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from "./locales";
11
12type LocaleTranslations = Awaited<ReturnType<typeof getTranslationsSnapshot>>;
13
14export interface I18nSnapshot {
15 readonly locale: string;
16 readonly translations: LocaleTranslations;
17}
18
19const loadedTranslations: LocaleTranslations = { [DEFAULT_LOCALE]: {} };
20const pendingLoads = new Map<string, Promise<void>>();
21const listeners = new Set<() => void>();
22let snapshot: I18nSnapshot = { locale: DEFAULT_LOCALE, translations: { ...loadedTranslations } };
23let requestedLocale = DEFAULT_LOCALE;
24
25function loadLocale(locale: string): Promise<void> {
26 if (locale in loadedTranslations) return Promise.resolve();
27 let load = pendingLoads.get(locale);
28 if (!load) {
29 // GT resolves a failed load to an empty catalog. Leave it uncached: English renders now
30 // and the next request retries.
31 load = getTranslationsSnapshot(locale).then((loaded) => {
32 pendingLoads.delete(locale);
33 const translations = loaded[locale];
34 if (translations && Object.keys(translations).length > 0) {
35 loadedTranslations[locale] = translations;
36 }
37 });
38 pendingLoads.set(locale, load);
39 }
40 return load;
41}
42
43function activateLocale(locale: string): void {
44 if (requestedLocale !== locale) return;
45 if (snapshot.locale === locale && snapshot.translations[locale] === loadedTranslations[locale]) {
46 return;
47 }
48 snapshot = { locale, translations: { ...loadedTranslations } };
49 document.documentElement.lang = locale;
50 for (const listener of listeners) listener();
51}
52
53function applyLanguageSetting(): void {
54 // Not desktopBridge.getSystemLocale(): that is the regional/time locale, not the UI language.
55 requestedLocale = resolveLocale(
56 getClientSettings().language,
57 navigator.languages,
58 SUPPORTED_LOCALES,
59 DEFAULT_LOCALE,
60 );
61 void whenLocaleReady();
62}
63
64/** GT's own locale setters write the saved setting, which stays the only source of truth. */
65export function persistGTLocale(locale: string): void {
66 void updateClientSettings({ language: locale });
67}
68
69export function startI18n(): void {
70 initializeI18n(persistGTLocale);
71 subscribeClientSettings(applyLanguageSetting);
72 applyLanguageSetting();
73}
74
75/** Resolves once the requested locale's translations are ready; immediate for English. */
76export function whenLocaleReady(): Promise<void> {
77 const locale = requestedLocale;
78 return loadLocale(locale).then(() => activateLocale(locale));
79}
80
81export function subscribeI18n(listener: () => void): () => void {
82 listeners.add(listener);
83 return () => {
84 listeners.delete(listener);
85 };
86}
87
88export function getI18nSnapshot(): I18nSnapshot {
89 return snapshot;
90}