Internationalization
How f-ui separates app i18n from component strings—I18nProvider, resolver hooks, bundles, and host translators.
f-ui components ship built-in English and Chinese (Simplified) message bundles. They do not import Paraglide, next-intl, or any app runtime. Instead, a small shared layer (I18nProvider + useXxxI18n hooks) gives you one tree-scoped locale and an optional bridge to your i18n system.
Two layers (do not confuse them)
| Layer | Role | Typical API |
|---|---|---|
| Your app / this site | Routing, docs UI, marketing copy, auth strings | Paraglide m.*(), setLocale, Fumadocs i18n |
| f-ui registry components | Labels inside Data List, Multi-Select, Date Picker chrome | <I18nProvider>, useDataListI18n, … |
Changing the app language must re-render whatever passes locale into <I18nProvider>. If you only call getLocale() once in a parent that never subscribes to locale changes, f-ui strings can stay stuck while the rest of the app updates.
Install the i18n primitives
The registry item fui-i18n adds the shared provider and helpers (paths under components/f-ui/i18n/). Other items (e.g. QueryList / Table, Multi-Select, Date Picker) declare it as a registry dependency when you install via the shadcn CLI.
pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/fui-i18n.json<I18nProvider>
Mount once near the root of the tree that contains f-ui components.
Props
locale— Any BCP-47–style tag (e.g.en,en-US,zh,zh-CN,zh-Hans). It is normalized to a discrete bundle id (enorzh-CN) vianormalizeFuiLocaleinbcp47.ts. Unknown tags fall back toen.t(optional) — Host translator: a function your app supplies so f-ui can use your message catalogs for component keys, with automatic fallback to built-in bundles when you return “missing” (see below).
Children — Everything under the provider reads the same context until a nested provider overrides it.
Reactive locale (required for in-app language switching)
If users can change language without a full page reload, the component that renders <I18nProvider> must re-render when the runtime locale changes.
In this repository, Paraglide uses setLocale(..., { reload: false }) and notifyAppLocaleChanged(); the root route uses useAppLocale() (a useSyncExternalStore wrapper) so locale={appLocale} always tracks the cookie. The same Paraglide + notify pattern works on Next.js App Router and TanStack hosts — Paraglide is not tied to TanStack. See Integrating Into Your App — Host app-locale.
Your app can use any equivalent pattern (Paraglide, next-intl, Redux, Zustand, router locale segment, etc.)—the rule is React must see a new locale prop, not a one-off getLocale() read from a non-rendering parent.
Host translator (t)
Optional. Shape (simplified):
type FuiHostTranslator = (
component: "data-list" | "date-picker" | "multi-select",
key: string,
vars?: Record<string, string | number>,
) => string | undefined;Discriminators are kebab-case and match component folders / registry names.
Fallback rule: For each key, if the host returns undefined, "", or the same string as key, the per-component resolver uses the built-in bundle for the current normalized locale. That way new f-ui keys keep working before you add host messages.
Per-component resolvers: useXxxI18n()
Each localized component family exposes one hook:
| Hook | Return | Import path (after install) |
|---|---|---|
useDataListI18n | { t, locale } | @/components/f-ui/data-list-chrome/hooks/use-data-list-i18n |
useMultiSelectI18n | { t, locale } | @/components/f-ui/multi-select/hooks/use-multi-select-i18n |
useDatePickerI18n | { locale, racLocaleTag } | @/components/f-ui/date-picker/hooks/use-date-picker-i18n |
useDatePickerI18n has no t — date-picker strings come from React Aria / the calendar, not a f-ui message map.
When you call them
- Default: you do not call these hooks.
<QueryList>,<Table>,<MultiSelect>,<DatePicker>, and their built-in parts already use them internally. - Do call them when you build custom UI that should match the same locale and overrides as the rest of that f-ui family—for example a custom filter summary, a wrapper around the list toolbar, or headless multi-select chrome that needs the same “Clear all” / placeholder copy as the stock component.
Requirements:
- Hooks are
"use client"modules: only use them in client components (or in client children under a server boundary). - Mount
I18nProviderabove the subtree (or rely on optionallocaleon the hook—see below).
Optional arguments
All three accept an optional options object:
locale?: string— BCP-47 tag; normalized the same way as the provider. Chooses the built-in bundle when resolving fallbacks (useDataListI18n/useMultiSelectI18n). ForuseDatePickerI18n, drivesracLocaleTagfor Chinese vs inherit for English.t?— OnlyuseDataListI18nanduseMultiSelectI18n. If set, this translator wins completely for that hook instance: the provider’s hosttis not consulted (useful for tests or an isolated override).
Resolution order (useDataListI18n / useMultiSelectI18n)
opts.t— full takeover for that call site.- Host
tfrom the nearest<I18nProvider>, with per-key fallback when it returns missing / empty / key unchanged. - Built-in bundle for
normalizeFuiLocale(opts.locale ?? contextLocale).
Examples
Data List — Custom Label Next to the List
Same t as built-in parts.
"use client";
import { useDataListI18n } from "@/components/f-ui/data-list-chrome/hooks/use-data-list-i18n";
export function MyFilterHint() {
const { t, locale } = useDataListI18n();
return (
<p className="text-muted-foreground text-xs">
{t("filterPanel.apply")} · {locale}
</p>
);
}Place <MyFilterHint /> inside the same <I18nProvider> (or list recipe) tree as the table. Keys are the DataListI18nKey union in data-list-chrome/locales/keys.ts; use t(key, vars) when the key uses {var} placeholders.
Data List — English Built-Ins for Fallbacks
const { t } = useDataListI18n({ locale: "en" });locale picks the English built-in bundle when a key falls back. If the provider has a host t that returns a real string for a key, that host value still wins; only missing / empty / unchanged keys use the bundle.
Data List — Fully Custom Translator
import type { DataListTranslateFn } from "@/components/f-ui/data-list-chrome/locales/keys";
const myT: DataListTranslateFn = (key, vars) =>
key === "filterPanel.apply" ? "OK" : key;
const { t } = useDataListI18n({ t: myT });Multi-Select — Same Pattern
"use client";
import { useMultiSelectI18n } from "@/components/f-ui/multi-select/hooks/use-multi-select-i18n";
export function MyMultiSelectLegend() {
const { t } = useMultiSelectI18n();
return <span className="text-xs">{t("multiSelect.placeholder")}</span>;
}Date Picker — Locale for a Custom RAC Subtree
"use client";
import { useDatePickerI18n } from "@/components/f-ui/date-picker/hooks/use-date-picker-i18n";
import { I18nProvider } from "react-aria-components";
export function MyDateFieldChrome() {
const { racLocaleTag } = useDatePickerI18n();
return (
<I18nProvider locale={racLocaleTag ?? "en-US"}>
{/* segments / calendar */}
</I18nProvider>
);
}Usually the stock DatePicker already wraps RAC correctly; this is for advanced composition.
What uses the hooks inside f-ui
Built-in parts call these hooks. List recipes (QueryList, Table, and data-list chrome) resolve strings through useDataListI18n — they do not expose a separate t / locale field on the list handle for consumers to read.
Recipe props: locale and t on list / select recipes
Some recipes accept locale and t as sugar: they wrap their subtree in a nested <I18nProvider> so one surface can differ from the rest of the page.
| Props | Effect |
|---|---|
| Neither | Uses outer provider only. |
locale only | Nested provider with that locale and no host t for that subtree (full takeover of host translations for f-ui under it). |
t only | Nested provider keeps outer locale, installs an adapter built from your t. |
| Both | Nested provider sets both. |
Bundles and keys (in this repository)
Under each component, locales/ holds:
keys.ts— Key union,TranslateFn, sharedformatI18nVars.en.ts/zh-CN.ts— Exhaustive bundles (TypeScript enforces parity).get-builtin-translator.ts— MapsFuiLocaleto the right bundle.
Adding a locale means updating FuiLocale in i18n/types.ts and normalizeFuiLocale in i18n/bcp47.ts in the same change.
React Aria and Date Picker
Date machinery uses React Aria’s <I18nProvider>. In this app, the RAC provider wraps outside <I18nProvider> so segment order and placeholders follow the same BCP-47 tag (via toReactAriaLocaleTag where Paraglide uses short codes like zh). f-ui’s provider only scopes f-ui message bundles and host t; date-picker still resolves racLocaleTag from the same normalized locale.
Portability: no app-only imports inside f-ui
Registry components must not import from your app’s lib/ (e.g. tooltip delays, feature flags). Shared constants for a component family live under that family’s lib/ (example: filter-panel tooltip delay in data-list-chrome’s lib/).
Related
- Installation — Installation for CLI and registry URLs.
- Language-Selector — Language-Selector documents the control; wiring it to Paraglide /
notifyAppLocaleChangedis app-specific. - Table — Table for the data-list Table view; chrome strings use
useDataListI18n.