Session 13: The Slate, Africa geo-restriction, OAuth providers, PropRow + GameCard (1311 tests)

This commit is contained in:
Kev
2026-06-11 03:48:07 -04:00
parent d957dee17b
commit 10159209fa
18 changed files with 1452 additions and 64 deletions
+40 -10
View File
@@ -1,34 +1,51 @@
'use client';
import { createContext, useContext, useMemo, ReactNode } from 'react';
import { Locale, DEFAULT_LOCALE, isLocale, LOCALE_META } from '@/lib/locales';
import { Locale, DEFAULT_LOCALE, isLocale, LOCALE_META, isAfricanCountry } from '@/lib/locales';
import { getTranslations, TFunction } from '@/lib/i18n';
/**
* Client-side locale context (Session 12).
* 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 from the
* request header and passes it as a prop to `<LocaleProvider>`. From
* there every client component can `useT()` without prop-drilling.
* 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 is stable per render of the provider,
* so consumers don't re-render on every parent render.
* 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, children }: { locale: string; children: ReactNode }) {
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);
return { locale: resolved, dir: LOCALE_META[resolved].dir, t: bundle.t };
}, [locale]);
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>;
}
@@ -47,3 +64,16 @@ export function useLocale(): { locale: Locale; dir: 'ltr' | 'rtl' } {
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 };
}