Skip to content

Loader contracts (loader_generated)

This page explains file ownership, the generated module API, and how to wire loaders.generated.ts into your app (with an optional loader.ts wrapper you write yourself).

Patching field names live in config.md. This page is the runtime integration guide.


Files under <src>/i18n/ (typical)

FileOwnerRole
config.jsonCLI (patch --init / patching)Locale list + metadata for the generated module.
loaders.generated.tsCLI (regenerated by patching)Lazy JSON imports, registry, loadLocaleMessages, etc.
loader.ts (optional)YouThin re-exports or glue to i18next / Lingui / custom code. Not created by patch --init.

In i18nprune.config.*, patching.loaderPath must be the path to loaders.generated.ts (same physical file the patcher rewrites). patching.configPath points at config.json.


Generated module API

loaders.generated.ts exposes (names are stable):

ExportPurpose
getDefaultLocaleCode()Promise<LocaleCode> — default from registry / config.
getLocales(codes?)Promise<GetLocalesResult> — all locales, or filtered; ok: false if an unknown code was requested.
getLocale(code)Promise<LocaleMeta | undefined>
loadLocaleMessages(code)Promise<LoadLocaleMessagesResult>lazy import() of the locale JSON; returns { ok, code, messages } or { ok: false, code, error }.

Types include LocaleCode, LocaleMeta, LocaleMessages (Record<string, unknown> for JSON leaves).

Lazy loading: Inside the generated block, each locale is loaded via a () => import("./…/xx.json")-style function. Calling loadLocaleMessages("fr") triggers that import; nothing else is fetched up front.


Option A — Import the generated module directly

If you are fine coupling app code to the generated path:

typescript
import {
  loadLocaleMessages,
  getDefaultLocaleCode,
  type LocaleMessages,
} from './i18n/loaders.generated.js';

export async function bootI18n(locale: string) {
  const res = await loadLocaleMessages(locale);
  if (!res.ok) throw new Error(res.error);
  const messages: LocaleMessages = res.messages;
  // feed `messages` into your i18n library or renderer
}

Adjust the import path to match your src layout and bundler (.js extension is common for ESM TypeScript emit).


Place a small file you own next to config.json and loaders.generated.ts. The CLI does not create or overwrite it; copy the pattern below when you want a single import path for the rest of your app.

typescript
/**
 * App-owned i18n surface. i18nprune only regenerates `./loaders.generated.ts`.
 * Re-export the generated API so app code never imports the generated file by name.
 */
export {
  getDefaultLocaleCode,
  getLocale,
  getLocales,
  loadLocaleMessages,
} from './loaders.generated.js';

export type {
  LocaleCode,
  LocaleMeta,
  LocaleMessages,
  GetLocalesResult,
  LoadLocaleMessagesResult,
} from './loaders.generated.js';

Then:

typescript
import { loadLocaleMessages, getDefaultLocaleCode } from './i18n/loader.js';

const code = await getDefaultLocaleCode();
const pack = await loadLocaleMessages(code);
if (pack.ok) console.log(Object.keys(pack.messages).length);

Example — i18next (i18next)

Conceptual pattern: load one locale’s JSON, then add it as a resource bundle. (Version-specific APIs may differ; check your i18next majors.)

typescript
import i18n from 'i18next';
import { loadLocaleMessages, getDefaultLocaleCode } from './i18n/loader.js';

export async function initI18next() {
  const lng = await getDefaultLocaleCode();
  const res = await loadLocaleMessages(lng);
  if (!res.ok) throw new Error(res.error);

  await i18n.init({
    lng,
    fallbackLng: lng,
    interpolation: { escapeValue: false },
  });
  i18n.addResourceBundle(lng, 'translation', res.messages, true, true);
  return i18n;
}

/** Switch locale: lazy-load JSON then add bundle. */
export async function setI18nextLanguage(code: string) {
  const res = await loadLocaleMessages(code);
  if (!res.ok) throw new Error(res.error);
  i18n.addResourceBundle(code, 'translation', res.messages, true, true);
  await i18n.changeLanguage(code);
}

Use your real namespace(s) instead of 'translation' if you namespace messages.


Example — custom / minimal

typescript
import { getLocales, loadLocaleMessages } from './i18n/loader.js';

export async function debugPrintLocales() {
  const all = await getLocales();
  if (!all.ok) return;
  for (const row of all.locales) {
    const r = await loadLocaleMessages(row.code);
    console.log(row.code, r.ok ? 'loaded' : r.error);
  }
}

Generated file boundaries

loaders.generated.ts uses markers:

  • // i18nprune:generated:start// i18nprune:generated:end — replaced by patching.
  • // i18nprune:user:start// i18nprune:user:end — preserved; safe for tiny helpers inside the generated file only.

Prefer loader.ts (or app modules) for substantial integration code so merges stay simple.


patch --init and --force

  • patch --init creates missing config.json and loaders.generated.ts under <src>/i18n/. It does not create loader.ts.
  • patch --init --force overwrites only those two CLI-owned files and can reset the patching block in i18nprune.config.*. Your optional loader.ts is untouched.
  • --force without --init is ignored (warning).

If either CLI-owned file looks corrupt, use patch --init --force to renew them.


Analyzer coverage

Patching analysis can report config path/read/parse/schema issues, catalog metadata mismatches, and config-vs-disk locale drift — see i18nprune patch and issues/patching.


See also