import { createContext, useCallback, useContext, useEffect, useMemo, type ReactNode } from 'react'; import { resolveLocale, type Locale } from '@wellformed/shared'; import { useSession } from '../state/session'; import { messages, type MessageArgs, type MessageKey } from './messages'; /** * Which language the page speaks, and the `t` that speaks it. * * The choice lives on the workspace, beside its currency — see * shared/locale.ts. Inside a workspace everyone reads the same page in the * same language; changing it is a workspace setting, saved in the workspace * dialog like a rename. Outside a workspace — the login page, the marketing * page, the picker — there is nothing to ask, and the browser decides. */ export type TranslateFn = (key: K, ...args: MessageArgs) => string; interface I18nApi { /** The language everything renders in right now. */ locale: Locale; t: TranslateFn; } const I18nContext = createContext(null); export function I18nProvider({ children, locale: forced, }: { children: ReactNode; /** * A locale the URL or the server chose — /nl and /en are real addresses so * search engines have a stable page per language to index, and the SSR * entry renders outside a browser where there is no navigator to ask. */ locale?: Locale; }): ReactNode { const session = useSession(); const locale = forced ?? session.workspace?.locale ?? resolveLocale(typeof navigator === 'undefined' ? [] : (navigator.languages ?? [navigator.language])); // Screen readers and translation tools read the document, not our context. useEffect(() => { document.documentElement.lang = locale; }, [locale]); const t = useCallback( (key, ...args) => { const text = messages[key][locale] as string | ((...a: unknown[]) => string); return typeof text === 'function' ? text(...args) : text; }, [locale], ); const value = useMemo(() => ({ locale, t }), [locale, t]); return {children}; } export function useI18n(): I18nApi { const ctx = useContext(I18nContext); if (!ctx) throw new Error('useI18n must be used inside an I18nProvider.'); return ctx; } /** * Just the locale, for the many places that only format a number or a date. * Pass it to `formatCell` and `formatCount` from @wellformed/shared. */ export function useLocale(): Locale { return useI18n().locale; }