Overview & Installation
ts-intl is a zero-dependency, framework-agnostic, strictly type-safe internationalization (i18n) library.
It provides an API design similar to next-intl, without depending on Next.js or a specific UI framework, suitable for modern TypeScript development.
Design Principles
- Zero Dependencies: Zero runtime dependencies and no code generation build steps.
- Framework Agnostic: Supports Astro, React, Vue, Svelte, Node.js, Bun, and browser environments.
- Strict Type Safety: Translation keys, dynamic interpolation parameters, namespaces, and multi-language structure alignment all feature static type checking.
Installation
Install @aaakul/ts-intl using a package manager:
# pnpm
pnpm add @aaakul/ts-intl
# npm
npm install @aaakul/ts-intl
# yarn
yarn add @aaakul/ts-intl
# bun
bun add @aaakul/ts-intl
Quick Start
1. Define Message Dictionaries
Assert TypeScript objects using as const so that the type system can infer key names and parameter types:
// messages/en-US.ts
export default {
common: {
title: "System Dashboard",
greeting: "Hello, {name: string}!",
items: {
one: "1 item in total",
other: "{count} items in total",
},
},
} as const;
// messages/zh-Hans.ts
export default {
common: {
title: "系统仪表盘",
greeting: "你好,{name: string}!",
items: {
one: "共 1 项数据",
other: "共 {count} 项数据",
},
},
} as const;
2. Initialize i18n Instance
// i18n.ts
import { createI18n } from "@aaakul/ts-intl";
import enUS from "./messages/en-US";
import zhHans from "./messages/zh-Hans";
export const {
getTranslations,
getFormatter,
isSupportedLanguage,
languages,
defaultLanguage,
} = createI18n({
defaultLanguage: "en-US",
messages: {
"en-US": enUS,
"zh-Hans": zhHans,
},
});
export type Language = (typeof languages)[number];
3. Obtain and Call Translator Functions
import { getTranslations } from "./i18n";
const t = getTranslations("en-US", "common");
// 1. Static key
const title = t("title");
// Inferred type: (key: "title") => string
// Output: "System Dashboard"
// 2. Interpolation with parameters
const greeting = t("greeting", { name: "Developer" });
// Inferred type: (key: "greeting", params: { name: string }) => string
// Output: "Hello, Developer!"
// 3. Plural rule handling
const items = t("items", { count: 5 });
// Inferred type: (key: "items", params: { count: number }) => string
// Output: "5 items in total"
Dictionary Formats
TypeScript Static Definitions (Recommended)
Define dictionaries using as const to support compile-time key validation and parameter type extraction:
export default {
auth: {
login: "Sign In",
welcome: "Welcome back, {username: string}!",
},
} as const;
JSON Dictionaries
Note: JSON format also supports key auto-completion and spell checking; because JSON does not support as const, template variable parameters will be inferred as loose types.
import enUS from "./messages/en-US.json";
import zhHans from "./messages/zh-Hans.json";
export const { getTranslations } = createI18n({
defaultLanguage: "en-US",
messages: { "en-US": enUS, "zh-Hans": zhHans },
});
Dynamic Imports with await import
In modern environments supporting Top-Level Await (such as Astro, Vite, Node 22+, Bun, etc.), dictionary files can be imported dynamically via await import(...):
import { createI18n } from "@aaakul/ts-intl";
const enUS = (await import("./messages/en-US.ts")).default;
const zhHans = (await import("./messages/zh-Hans.ts")).default;
export const { getTranslations } = createI18n({
defaultLanguage: "en-US",
messages: { "en-US": enUS, "zh-Hans": zhHans },
});
Locale Code Splitting
In applications with multiple locales, defining a loader map allows bundlers to split each language dictionary into its own chunk, loading only the active locale on demand:
import { createI18n } from "@aaakul/ts-intl";
const loaders = {
"en-US": () => import("./messages/en-US.ts"),
"zh-Hans": () => import("./messages/zh-Hans.ts"),
} as const;
export type SupportedLanguage = keyof typeof loaders;
export async function loadI18n<L extends SupportedLanguage>(lang: L) {
const messages = (await loaders[lang]()).default;
return createI18n({
defaultLanguage: lang,
messages: { [lang]: messages } as Record<L, typeof messages>,
});
}
Type Safety: As long as dictionary files export
as constor use JSON, dynamically imported dictionaries viaawait importretain full namespace autocomplete, literal key checking, and parameter type extraction ({param: type}).