Dynamic Import
Load locales on demand instead of bundling them all
Give locales loaders instead of objects and Intl-T fetches messages when they are needed — the server can preload everything,
while the client downloads only the language it is actually showing.
Loaders
import { createTranslation } from "intl-t";
export const t = createTranslation({
locales: {
en: () => import("./messages/en.json"),
es: () => import("./messages/es.json"),
},
});Types are inferred from each loader's return, and the module record is unwrapped for you — returning import("./en.json") directly
is fine, { default } never becomes part of your tree. values defaults declared inside a loaded tree apply when it arrives, with
anything you already set kept on top.
import { createTranslation } from "intl-t";
export const t = createTranslation({
locales: locale => import(`./messages/${locale}.json`) as Promise<typeof import("./messages/en.json")>,
allowedLocales: ["en", "es"] as const, // required — keys can't be inferred from a function
});The as Promise<typeof import("./messages/en.json")> assertion gives every locale the shape of your main one. With a template-string
import the bundler creates a chunk per matching file automatically.
Keep each loader a literal import("...") expression. Bundlers include a JSON in the bundle only when they can see the import — which is
also what makes loaders work on Edge.
The await contract
| Expression | Loads |
|---|---|
await t — preload on | all allowedLocales at once (later awaits are instant) |
await t — preload off | only the current locale |
await t.es | only es |
| reading an unloaded message | nothing yet — it renders undefined and the load starts in the background |
t.setLocale("es") | es, on demand |
const t = createTranslation({
locales: { en: () => import("./en.json"), es: () => import("./es.json") },
});
t.es.hello; // undefined — not loaded yet
await t.es; // loads only `es`
t.es.hello; // "¡Hola Mundo!"
await t.es; // instant — already resolved, never re-fetchedOnce loaded, a locale stays cached — awaiting it again is instant and never re-fetches.
Lazy by default
Nothing loads until something is accessed or awaited — in every environment, with either loader form. preload only decides what
await t means: the current locale (default) or all of them.
export const { t } = await createTranslation({
locales: {
en: () => import("./messages/en.json"),
es: () => import("./messages/es.json"),
},
preload: true, // `await t` now resolves with every allowed locale loaded
});Turn it on for servers — one await at boot, then any request renders any language with zero per-request loads. Leave it off for
clients — the visitor downloads only the language on screen, and switching fetches the next one on demand. Enabling preload
never blocks rendering by itself: the all-locales fetch happens on the first await t, and if you never await, the tree simply
stays lazy.
Perfect types: .ts locale files
JSON imports widen strings ("Welcome, {user}!" becomes string), which costs the variable inference. Two ways to keep the full
literal types with loaders:
export default {
homepage: {
title: "Inicio",
welcome: "¡Bienvenido, {user}!",
},
} as const;import { createTranslation } from "intl-t";
import en from "./messages/en";
// static: instant first paint, and the source of types
export const t = createTranslation({
locales: {
en,
es: () => import("./messages/es"), // lazy — typeof import resolves the `as const` statically
},
mainLocale: "en",
});mainLocale is the source of the tree's type — keep it static for a zero-fetch primary language, or make it a loader too: the
types stay perfect either way, since typeof import never costs runtime bytes.
Keep your JSON and generate literal declarations next to it — autocomplete and variable inference come from the generated
.d.json.ts files. See TypeScript → Declarations.
npx @intl-t/declarations ./messages --watchFailure and retry
A loader that rejects makes the await reject — it does not hang or resolve with missing messages. The failed locale is dropped
from the cache, and the next access retries the loader, so a transient network failure heals itself:
await t.en; // rejects — the import failed
await t.en; // retries the loader; resolves once it succeedsResolving up front
getLocales runs every loader immediately and returns a plain messages record — useful when you want the resolved objects
themselves rather than lazy behavior:
import { createTranslation, getLocales } from "intl-t";
const locales = await getLocales(locale => import(`./messages/${locale}.json`), ["en", "es"] as const);
export const { t } = createTranslation({ locales });Next.js and Suspense
With the Next.js integration, loading is handled for you: await getTranslation() resolves the request locale and
its messages, and the provider sends the loaded messages to the client for hydration — the client never
re-downloads what the server already rendered.
Static rendering: preload at the page
With static rendering, a page can render before its layout. Add await getTranslation() at the top of the
page so the locale's messages are loaded before anything reads them — it keeps the page static, and after that first await the rest of
your components don't need to await anything.