80 lines
2.7 KiB
TypeScript
80 lines
2.7 KiB
TypeScript
'use client';
|
|
|
|
import { createContext, useContext, useMemo, ReactNode } from 'react';
|
|
import { Locale, DEFAULT_LOCALE, isLocale, LOCALE_META, isAfricanCountry } from '@/lib/locales';
|
|
import { getTranslations, TFunction } from '@/lib/i18n';
|
|
|
|
/**
|
|
* Client-side locale + region context (Session 12; Session 13 added
|
|
* the `country` field from the CF-IPCountry header).
|
|
*
|
|
* The root layout (server component) resolves the locale + country
|
|
* from request headers and passes them as props to `<LocaleProvider>`.
|
|
* From there every client component can `useT()` / `useRegion()`
|
|
* without prop-drilling or repeating the resolution.
|
|
*
|
|
* Memoized: the `t` function and derived booleans are stable per
|
|
* render of the provider, so consumers don't re-render on every
|
|
* parent render.
|
|
*/
|
|
|
|
interface LocaleContextValue {
|
|
locale: Locale;
|
|
dir: 'ltr' | 'rtl';
|
|
t: TFunction;
|
|
// Session 13 region fields.
|
|
country: string; // 'NG', 'US', '' (unknown / non-Cloudflare path)
|
|
inAfrica: boolean; // true when country ∈ AFRICAN_COUNTRIES
|
|
}
|
|
|
|
const LocaleContext = createContext<LocaleContextValue | null>(null);
|
|
|
|
export function LocaleProvider({
|
|
locale,
|
|
country = '',
|
|
children,
|
|
}: { locale: string; country?: string; children: ReactNode }) {
|
|
const value = useMemo<LocaleContextValue>(() => {
|
|
const resolved: Locale = isLocale(locale) ? locale : DEFAULT_LOCALE;
|
|
const bundle = getTranslations(resolved);
|
|
const cc = String(country || '').toUpperCase();
|
|
return {
|
|
locale: resolved,
|
|
dir: LOCALE_META[resolved].dir,
|
|
t: bundle.t,
|
|
country: cc,
|
|
inAfrica: isAfricanCountry(cc),
|
|
};
|
|
}, [locale, country]);
|
|
return <LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>;
|
|
}
|
|
|
|
export function useT(): TFunction {
|
|
const ctx = useContext(LocaleContext);
|
|
if (!ctx) {
|
|
// Fall back to English silently — better than a crash if some
|
|
// component renders outside the provider (test envs, storybook).
|
|
return getTranslations('en').t;
|
|
}
|
|
return ctx.t;
|
|
}
|
|
|
|
export function useLocale(): { locale: Locale; dir: 'ltr' | 'rtl' } {
|
|
const ctx = useContext(LocaleContext);
|
|
if (!ctx) return { locale: DEFAULT_LOCALE, dir: 'ltr' };
|
|
return { locale: ctx.locale, dir: ctx.dir };
|
|
}
|
|
|
|
/**
|
|
* Session 13 — region hook for components that need to gate by
|
|
* geography (pricing, regulatory disclaimers, regional payment
|
|
* methods). Returns `inAfrica: false` when country is unknown
|
|
* (degrade-closed: don't surface region-specific UX on unverified
|
|
* traffic).
|
|
*/
|
|
export function useRegion(): { country: string; inAfrica: boolean } {
|
|
const ctx = useContext(LocaleContext);
|
|
if (!ctx) return { country: '', inAfrica: false };
|
|
return { country: ctx.country, inAfrica: ctx.inAfrica };
|
|
}
|