Internationalization
How f-ui separates app i18n from component strings—FuiI18nProvider, 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 (FuiI18nProvider + 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 Table, Multi-Select, Date Picker chrome | <FuiI18nProvider>, useDataTableI18n, … |
Changing the app language must re-render whatever passes locale into <FuiI18nProvider>. 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. Data 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<FuiI18nProvider>
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 <FuiI18nProvider> 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.
Your app can use any equivalent pattern (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-table" | "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) |
|---|---|---|
useDataTableI18n | { t, locale } | @/components/f-ui/data-table/hooks/use-data-table-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.
<DataTable>,<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 table 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
FuiI18nProviderabove 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 (useDataTableI18n/useMultiSelectI18n). ForuseDatePickerI18n, drivesracLocaleTagfor Chinese vs inherit for English.t?— OnlyuseDataTableI18nanduseMultiSelectI18n. 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 (useDataTableI18n / useMultiSelectI18n)
opts.t— full takeover for that call site.- Host
tfrom the nearest<FuiI18nProvider>, with per-key fallback when it returns missing / empty / key unchanged. - Built-in bundle for
normalizeFuiLocale(opts.locale ?? contextLocale).
Examples
Data Table — Custom Label Next to the Table
Same t as built-in parts.
"use client";
import { useDataTableI18n } from "@/components/f-ui/data-table/hooks/use-data-table-i18n";
export function MyFilterHint() {
const { t, locale } = useDataTableI18n();
return (
<p className="text-muted-foreground text-xs">
{t("filterPanel.apply")} · {locale}
</p>
);
}Place <MyFilterHint /> inside the same <FuiI18nProvider> (or <DataTable>) tree as the table. Keys are the DataTableI18nKey union in locales/keys.ts; use t(key, vars) when the key uses {var} placeholders.
Data Table — English Built-Ins for Fallbacks
const { t } = useDataTableI18n({ 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 Table — Fully Custom Translator
import type { DataTableTranslateFn } from "@/components/f-ui/data-table/locales/keys";
const myT: DataTableTranslateFn = (key, vars) =>
key === "filterPanel.apply" ? "OK" : key;
const { t } = useDataTableI18n({ 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; there is no t on DataTableContextValue. useDataTable does not expose locale / t; it calls useDataTableI18n() internally only to pass t into column builders that need translated headers/actions at definition time.
Recipe props: locale and t on <DataTable> (and similar)
Some recipes accept locale and t as sugar: they wrap their subtree in a nested <FuiI18nProvider> so one table 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 <FuiI18nProvider> 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-Table’s lib/filter-panel-tooltip-delay.ts).
Related
- Installation — Installation for CLI and registry URLs.
- Language-Selector — Language-Selector documents the control; wiring it to Paraglide /
notifyAppLocaleChangedis app-specific. - Data-Table — Data-Table for component usage; strings come from the resolver + bundles described here.