import * as React from 'react'; import { useParam } from '../router'; import { DEFAULT_LANG, isLang, type Lang } from './config'; import { az } from './messages/az'; import { en, type Messages } from './messages/en'; import { ru } from './messages/ru'; /** * The language, held where every other global toggle is held: the hash. * * `useParam` is the showcase's one mechanism for a global setting (`theme`, * `figma`, the icon and button filters), and `Link` carries every param across * navigation — so `#/tokens?lang=ru` is a shareable link and no nav code had to * learn about languages. The context on top exists only because a language is a * whole catalogue rather than a single value, and threading that through props * would touch every component on the way down. * * All three catalogues are imported statically. They are a few kB each; a * dynamic import would buy nothing and cost a Suspense boundary. */ const CATALOGUES: Record = { az, ru, en }; interface I18nValue { lang: Lang; messages: Messages; setLang: (lang: Lang) => void; } const I18nContext = React.createContext(null); export function I18nProvider({ children }: { children: React.ReactNode }) { const [param, setParam] = useParam('lang', DEFAULT_LANG); /* The hash is user-editable, so `?lang=zz` has to land on the default rather than index the catalogue with `undefined`. */ const lang = isLang(param) ? param : DEFAULT_LANG; /* Same mechanism as the theme class in `app.tsx`: an attribute on the root element, set from an effect rather than written into `index.html`, which only ever knows the default. Screen readers and `lang`-scoped CSS read it. */ React.useEffect(() => { document.documentElement.lang = lang; }, [lang]); const value = React.useMemo( () => ({ lang, messages: CATALOGUES[lang], setLang: setParam }), [lang, setParam] ); return {children}; } function useI18n(): I18nValue { const value = React.useContext(I18nContext); if (!value) throw new Error('useI18n must be used inside '); return value; } /** The catalogue for the current language. Read keys off it directly. */ export function useMessages(): Messages { return useI18n().messages; } /** The current language and the setter, shaped like `useParam`/`useState`. */ export function useLang(): [Lang, (lang: Lang) => void] { const { lang, setLang } = useI18n(); return [lang, setLang]; } /** * `Entry.group` is a plain `string` in the registry — the registry is data the * pages read, not copy — so the nine values are turned into words here. The cast * is what lets an unknown group fall through to itself rather than fail to * compile: adding a tenth group should show up untranslated, not break the build. */ export function useGroupLabel(): (group: string) => string { const { messages } = useI18n(); return React.useCallback( (group: string) => (messages.groups as Record)[group] ?? group, [messages] ); }