/** * `@nifrajs/web-react/i18n` - React bindings for `@nifrajs/i18n`. `` builds a * `Formatter` (memoized) and provides it; `useT()` reads it. Both `locale` + `messages` are * serializable, so a loader returns them, the page renders with the negotiated catalog on the server, * and the client rebuilds the same formatter from the same props (no mismatch). In an app that * registers its locale routing (`i18n` in `backend/framework.ts`), `locale` defaults to the page's and * `useLocale()` names it. No JSX (the package builds with plain `tsc`). */ import { createFormatter, type Formatter, type FormatterOptions, type MessageKey, type RegisteredMessages, type Translation, } from "@nifrajs/i18n" import { type RichChunks, type RichRenderer, renderRich } from "@nifrajs/i18n/rich" import { localeOfProps } from "@nifrajs/web/client" import { createContext, createElement, Fragment, type ReactNode, useContext, useMemo } from "react" import { RouterContext } from "./router.ts" const I18nContext = createContext(null) export interface I18nProviderProps extends FormatterOptions { /** The locale to format in. Default: the page's locale tag, in an app that registers its locale * routing. */ readonly locale?: string /** The catalog for `locale`, checked against the registered catalog type when one is declared. */ readonly messages: Translation readonly children?: ReactNode } /** Provide a {@link Formatter} (built from `locale` + `messages`, with the optional `fallback`, * `onMissing`, `timeZone` and `numberingSystem` of `createFormatter`) to the subtree. Memoized * on those props, so switching locale rebuilds it and re-renders consumers; formatters are cached by * catalog identity, so an inline `fallback={[en]}` still yields the same instance. */ export function I18nProvider(props: I18nProviderProps): ReactNode { const { messages, fallback, onMissing, timeZone, numberingSystem } = props const page = localeOfProps(useContext(RouterContext).renderProps) const locale = props.locale ?? page?.lang if (locale === undefined) { throw new Error( "[nifra/web-react] needs a `locale`, or an app that registers its locale routing (export `i18n` from backend/framework.ts)", ) } const formatter = useMemo( () => createFormatter(locale, messages, { fallback, onMissing, timeZone, numberingSystem }), [locale, messages, fallback, onMissing, timeZone, numberingSystem], ) return createElement(I18nContext.Provider, { value: formatter }, props.children) } /** Read the current {@link Formatter} (`{ locale, t, get, n, d }`). Throws if no `` is above. */ export function useT(): Formatter { const formatter = useContext(I18nContext) if (formatter === null) { throw new Error("[nifra/web-react] useT() must be used within an ") } return formatter } /** * The page's locale key (`ur`, the URL segment), in an app that registers its locale routing (`i18n` * in `backend/framework.ts`): from the request on the server, from the URL after each navigation. * Without one, the nearest ``'s locale; with neither, it throws. */ export function useLocale(): string { const page = localeOfProps(useContext(RouterContext).renderProps) const formatter = useContext(I18nContext) const locale = page?.key ?? formatter?.locale if (locale === undefined) { throw new Error( "[nifra/web-react] useLocale() needs an app that registers its locale routing (export `i18n` from backend/framework.ts), or an above it", ) } return locale } /** Tag handlers for {@link rich}, by tag name: each receives its tag's content as one node. */ export type RichTags = Readonly ReactNode>> // Children passed as arguments, not an array, so React asks for no keys. const join = (chunks: RichChunks): ReactNode => chunks.length === 0 ? null : chunks.length === 1 ? chunks[0] : createElement(Fragment, null, ...chunks) const REACT_RICH: RichRenderer = { join, lineBreak: () => createElement("br") } /** * The message at `key` with its tags rendered by `tags`, as React nodes - no HTML, no * `dangerouslySetInnerHTML`. `"Read the terms"` with * `{ link: (content) => {content} }` renders the link around "terms"; a tag with * no handler renders its content as text, `
` is a `
`, and interpolated values are always * text. See `@nifrajs/i18n/rich`. */ export function rich( formatter: Formatter, key: MessageKey, tags?: RichTags, vars?: Readonly>, ): ReactNode { return renderRich(REACT_RICH, formatter, key, tags, vars) }