import i18n from '../i18n'; import { isNativePlatform } from './capacitor'; // Mirrors the Java-side LANG_KEY + SupportedLang clamp in PushStrings.java // and the normalizeLang() in sw.ts. Keep the three implementations in sync: // supported set = {en, ru}, anything else collapses to en (i18n.ts sets // fallbackLng: 'en'). Normalising on the WRITE side means readers never see // an unsupported tag — for a user whose device locale is e.g. 'fr', the // resource-qualifier picker would otherwise land on values/ (English) while // i18next on the main thread rendered some other default; we make both // surfaces agree on English as the lingua franca fallback. const PREFS_LANG_KEY = 'vojo.appLanguage'; type SupportedLang = 'en' | 'ru'; const normalize = (raw: string | undefined | null): SupportedLang => { if (!raw) return 'en'; const lower = raw.toLowerCase(); if (lower.startsWith('ru')) return 'ru'; return 'en'; }; const writeNativePref = async (lang: SupportedLang): Promise => { if (!isNativePlatform()) return; try { const { Preferences } = await import('@capacitor/preferences'); await Preferences.set({ key: PREFS_LANG_KEY, value: lang }); } catch { /* non-fatal — PushStrings falls back to device locale, normalised to {en, ru} with en as the default for anything else */ } }; // Post to the active Service Worker so its push-fallback loader picks // the same language the main app is rendering. The SW side persists in // IndexedDB for cold wake; see sw.ts setLanguage handler. // // First-load race: on the very first page visit after SW install, // `navigator.serviceWorker.controller` is null — the SW has just // registered and hasn't taken over yet. A direct postMessage would // silently drop. We keep the latest language in `pendingSwLang` and // re-attempt when (a) the registration becomes ready and (b) on every // `controllerchange` event. Both paths no-op once the controller is // set and the post has already landed, so duplicates are cheap. let pendingSwLang: SupportedLang | undefined; let swControllerWatchInstalled = false; const trySendSwLang = (): void => { if (pendingSwLang === undefined) return; if (typeof navigator === 'undefined') return; const sw = navigator.serviceWorker; if (!sw?.controller) return; try { sw.controller.postMessage({ type: 'setLanguage', lang: pendingSwLang }); } catch { /* ignore — next languageChanged / controllerchange re-posts */ } }; const installSwControllerWatch = (): void => { if (swControllerWatchInstalled) return; if (typeof navigator === 'undefined') return; const sw = navigator.serviceWorker; if (!sw) return; swControllerWatchInstalled = true; // `ready` resolves when there is an active registration — usually // around install/activate. Our SW calls `self.clients.claim()` in // its activate handler, so shortly after `ready` the page gains a // controller and `controllerchange` fires; we listen to both so the // first-install race window is covered regardless of which fires // first. sw.ready.then(trySendSwLang).catch(() => undefined); sw.addEventListener('controllerchange', trySendSwLang); }; const postToServiceWorker = (lang: SupportedLang): void => { pendingSwLang = lang; trySendSwLang(); }; const broadcast = (raw: string | undefined | null): void => { const lang = normalize(raw); writeNativePref(lang).catch(() => undefined); postToServiceWorker(lang); }; // One-way JS → {Android FCM, web SW} bridge for the in-app i18next // language. Idempotent in practice: the i18next listener API dedupes // equal-valued changes. Intended to be invoked exactly once from App // bootstrap; we never detach the listener for the process lifetime. export const installPushLanguageBridge = (): void => { installSwControllerWatch(); // `resolvedLanguage` is what i18next actually serves — it already // applies `fallbackLng`/`supportedLngs` so a detector result of `fr` // arrives here as `en`. Falling back to `language` covers the narrow // window before the first `languageChanged` where `resolvedLanguage` // may still be undefined (i18next API guidance). const initial = i18n.resolvedLanguage ?? i18n.language; if (initial) broadcast(initial); i18n.on('languageChanged', (lng) => { broadcast(lng); }); };