117 lines
3.5 KiB
TypeScript
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;
|
|
}
|