f-ui

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)

LayerRoleTypical API
Your app / this siteRouting, docs UI, marketing copy, auth stringsParaglide m.*(), setLocale, Fumadocs i18n
f-ui registry componentsLabels 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 (en or zh-CN) via normalizeFuiLocale in bcp47.ts. Unknown tags fall back to en.
  • 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:

HookReturnImport 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 FuiI18nProvider above the subtree (or rely on optional locale on 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). For useDatePickerI18n, drives racLocaleTag for Chinese vs inherit for English.
  • t?Only useDataTableI18n and useMultiSelectI18n. If set, this translator wins completely for that hook instance: the provider’s host t is not consulted (useful for tests or an isolated override).

Resolution order (useDataTableI18n / useMultiSelectI18n)

  1. opts.t — full takeover for that call site.
  2. Host t from the nearest <FuiI18nProvider>, with per-key fallback when it returns missing / empty / key unchanged.
  3. 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.

PropsEffect
NeitherUses outer provider only.
locale onlyNested provider with that locale and no host t for that subtree (full takeover of host translations for f-ui under it).
t onlyNested provider keeps outer locale, installs an adapter built from your t.
BothNested provider sets both.

Bundles and keys (in this repository)

Under each component, locales/ holds:

  • keys.ts — Key union, TranslateFn, shared formatI18nVars.
  • en.ts / zh-CN.ts — Exhaustive bundles (TypeScript enforces parity).
  • get-builtin-translator.ts — Maps FuiLocale to 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).

  • InstallationInstallation for CLI and registry URLs.
  • Language-SelectorLanguage-Selector documents the control; wiring it to Paraglide / notifyAppLocaleChanged is app-specific.
  • Data-TableData-Table for component usage; strings come from the resolver + bundles described here.

On this page