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)
| File | Owner | Role |
|---|---|---|
config.json | CLI (patch --init / patching) | Locale list + metadata for the generated module. |
loaders.generated.ts | CLI (regenerated by patching) | Lazy JSON imports, registry, loadLocaleMessages, etc. |
loader.ts (optional) | You | Thin 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):
| Export | Purpose |
|---|---|
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:
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).
Option B — Optional src/i18n/loader.ts (recommended stable surface)
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.
/**
* 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:
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.)
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
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 --initcreates missingconfig.jsonandloaders.generated.tsunder<src>/i18n/. It does not createloader.ts.patch --init --forceoverwrites only those two CLI-owned files and can reset thepatchingblock ini18nprune.config.*. Your optionalloader.tsis untouched.--forcewithout--initis 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.