Intl-T

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

ExpressionLoads
await tpreload onall allowedLocales at once (later awaits are instant)
await tpreload offonly the current locale
await t.esonly es
reading an unloaded messagenothing 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-fetched

Once 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:

messages/es.ts
export default {
  homepage: {
    title: "Inicio",
    welcome: "¡Bienvenido, {user}!",
  },
} as const;
i18n/translation.ts
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 --watch

Failure 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 succeeds

Resolving 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.

On this page