Files
casadoc-app/src/i18n/index.tsx
T
2026-08-26 23:01:07 +03:00

117 lines
3.5 KiB
TypeScript

import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { I18n } from 'i18n-js';
import { getLocales } from 'expo-localization';
import * as SecureStore from 'expo-secure-store';
import en from './en';
import it from './it';
// English and Italian. Follows the phone's locale unless overridden in Profile.
export type AppLanguage = 'en' | 'it';
/** 'system' means follow the phone. */
export type LanguagePreference = AppLanguage | 'system';
export const SUPPORTED_LANGUAGES: AppLanguage[] = ['en', 'it'];
const PREF_KEY = 'casadoc.language';
const i18n = new I18n({ en, it });
i18n.defaultLocale = 'en';
i18n.enableFallback = true;
/** Resolve the phone's locale to a language we actually ship. */
export function deviceLanguage(): AppLanguage {
const tags = getLocales();
for (const l of tags) {
const code = (l.languageCode ?? '').toLowerCase();
if (code === 'it') return 'it';
if (code === 'en') return 'en';
}
return 'en'; // neither Italian nor English → English
}
function resolve(pref: LanguagePreference): AppLanguage {
return pref === 'system' ? deviceLanguage() : pref;
}
// Applied immediately, so translation calls before the provider mounts work.
i18n.locale = deviceLanguage();
/** For non-React code; inside components use `useT()` so it re-renders. */
export function t(key: string, options?: Record<string, unknown>): string {
return i18n.t(key, options as any);
}
/** Current BCP-47-ish locale, for Intl date/number formatting. */
export function currentLocale(): string {
return i18n.locale === 'it' ? 'it-IT' : 'en-GB';
}
interface I18nState {
language: AppLanguage;
preference: LanguagePreference;
setPreference: (pref: LanguagePreference) => void;
t: (key: string, options?: Record<string, unknown>) => string;
}
const I18nContext = createContext<I18nState | undefined>(undefined);
export function I18nProvider({ children }: { children: React.ReactNode }) {
const [preference, setPreferenceState] = useState<LanguagePreference>('system');
const [ready, setReady] = useState(false);
useEffect(() => {
(async () => {
try {
const saved = await SecureStore.getItemAsync(PREF_KEY);
if (saved === 'en' || saved === 'it' || saved === 'system') {
setPreferenceState(saved);
i18n.locale = resolve(saved);
}
} catch {
// fall back to the system language
} finally {
setReady(true);
}
})();
}, []);
const setPreference = useCallback((pref: LanguagePreference) => {
setPreferenceState(pref);
i18n.locale = resolve(pref);
SecureStore.setItemAsync(PREF_KEY, pref).catch(() => {
// a lost preference only means we fall back to the phone's language
});
}, []);
const language = resolve(preference);
const value = useMemo<I18nState>(
() => ({
language,
preference,
setPreference,
t: (key: string, options?: Record<string, unknown>) => {
i18n.locale = language;
return i18n.t(key, options as any);
},
}),
[language, preference, setPreference],
);
// Avoids a flash of the wrong language before the preference loads.
if (!ready) return null;
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
}
export function useI18n(): I18nState {
const ctx = useContext(I18nContext);
if (!ctx) throw new Error('useI18n must be used within <I18nProvider>');
return ctx;
}
export function useT() {
return useI18n().t;
}