Internationalization (i18n) patterns for server and client components using getTranslations and useTranslations...
Translations are written inline using the t() function. A Babel plugin transforms these calls at compile time into hash-based lookups against generated JSON bundles. The same t() API works in both server and client components โ the Babel plugin selects the correct runtime automatically.
Source: t({ en: "Hello", zh: "ไฝ ๅฅฝ" })
โ Babel plugin
Server: __i18n_lookup("a8cfb50c") โ reads from server JSON bundle
Client: i18nLookup(_translations, "a8cfb50c") โ reads the translations that
const _translations = useI18nTranslations() reads from React context
at the top of the enclosing component or hook
en โ Englishzh โ ChineseDefined as SupportedLocale in apps/web/src/i18n/types.ts. Pages live under apps/web/src/app/[locale]/. en is the default and has no URL prefix (/design-system); zh has one (/zh/design-system).
t() FunctionImport from #src/i18n.ts. Pass an object with en and zh string literal properties:
import { t } from "#src/i18n.ts";
// Plain text
t({ en: "Hello", zh: "ไฝ ๅฅฝ" });
// With HTML markup โ returns ReactNode instead of string
t(
{ en: "<strong>Bold text</strong>", zh: "<strong>็ฒไฝๆๅญ</strong>" },
{ parse: true },
);
Both en and zh values must be string literals โ no variables, template literals, or expressions. The codegen and Babel plugin rely on statically extracting these at build time.
Supported HTML tags in { parse: true } mode: <strong>, <em>, <b>, <i>, <p>.
Use t() directly. No special setup needed โ the Babel plugin handles everything:
import { t } from "#src/i18n.ts";
export default function Page() {
return <h1>{t({ en: "Welcome", zh: "ๆฌข่ฟ" })}</h1>;
}
For page files (page.tsx under [locale]), the Babel plugin auto-injects setLocale() at the top of the default export function. You do not need to manually call setLocale or accept params โ the plugin makes the function async, reads params.locale, and calls setLocale(validateLocale(params.locale)) for you. It does the same for an exported generateMetadata in page and layout files.
Layout files that need the locale for other purposes (e.g. generateMetadata, routing logic) should still read params manually since they have non-i18n reasons to do so.
Use t() identically โ the Babel plugin detects "use client" and swaps in client-specific lookup functions that read from React context:
"use client";
import { t } from "#src/i18n.ts";
export function SearchButton() {
return <button>{t({ en: "Search", zh: "ๆ็ดข" })}</button>;
}
Client translations are provided automatically. The codegen generates a manifest mapping page/layout files to their client translation bundles. The Babel plugin reads this manifest and auto-wraps the default export's return value with <ClientTranslationsProvider>, so client components receive translations without any manual wiring.
Use the useLocale() hook when you need the locale value itself (not for translations):
"use client";
import { useLocale } from "#src/i18n/use-locale.ts";
export function LocaleAwareComponent() {
const locale = useLocale();
const formatter = new Intl.NumberFormat(locale);
return <span>{formatter.format(1234)}</span>;
}
import { getLocalePath } from "#src/i18n/get-locale-path.ts";
getLocalePath("/design-system", locale); // โ "/design-system" (en) or "/zh/design-system" (zh)
pnpm --filter web codegen:i18n)The codegen script in packages/i18n-codegen/ does:
t() calls from source files via AST parsingapps/web/src/_generated/i18n/translations.{en,zh}.jsonapps/web/src/_generated/i18n/client/{name}.{en,zh}.jsonapps/web/src/_generated/i18n/client-loaders/{name}.tsapps/web/src/_generated/i18n/manifest.jsonpnpm dev in apps/web watches and regenerates, and build and test run it first. Outside those, run codegen after adding/changing any t() call, or the Babel plugin won't find the translation key at runtime.
packages/babel-plugins/src/i18n/)Runs at compile time (both dev and build). Transforms:
t({en, zh}) โ __i18n_lookup(key) (server) or i18nLookup(_translations, key) (client)t({en, zh}, { parse: true }) โ __i18n_lookupParse(key) or i18nLookupParse(_translations, key)const _translations = useI18nTranslations() at the top of each component or hook that calls t(). A local helper that calls t() gets the translations as a new first parameter, which each caller passes. The rewrite runs in the plugin's pre(), before React Compiler, so the compiler caches each lookup by the translations.setLocale for page files with t() calls<ClientTranslationsProvider> for manifest entries// apps/web/src/app/[locale]/my-page/page.tsx
import { t } from "#src/i18n.ts";
export default function Page() {
return (
<div>
<h1>{t({ en: "My Page", zh: "ๆ็้กต้ข" })}</h1>
<p>{t({ en: "Some content", zh: "ไธไบๅ
ๅฎน" })}</p>
</div>
);
}
Then run pnpm --filter web codegen:i18n to regenerate bundles.
// apps/web/src/<area>/my-component.tsx
"use client";
import { t } from "#src/i18n.ts";
export function MyComponent() {
return <span>{t({ en: "Click me", zh: "็นๅปๆ" })}</span>;
}
The parent page/layout must be in the manifest for client translations to work. Run pnpm --filter web codegen:i18n โ the codegen traces imports and auto-generates the client bundle.
{
t(
{
en: "Read our <strong>terms of service</strong>",
zh: "้
่ฏปๆไปฌ็<strong>ๆๅกๆกๆฌพ</strong>",
},
{ parse: true },
);
}
t() โ values must be string literalspnpm --filter web codegen:i18n after adding new t() calls outside pnpm devsetLocale to page files โ the Babel plugin does this automaticallytranslations.json files โ translations live inline in the componentt() outside render scope โ t() must be called directly in a React component body, custom hook, or generateMetadata(). It cannot be used in useEffect, event handlers, callbacks (.map(), .then()), setTimeout, module scope, or exported non-component functions. The ESLint rule @tuja/no-t-outside-render enforces this. Non-exported helper functions are allowed only if every call site is in render scope.