103 lines
4.2 KiB
TypeScript
103 lines
4.2 KiB
TypeScript
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<void> => {
|
|
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);
|
|
});
|
|
};
|