ts-intl
Chapters:Overview & Installation

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

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 const or use JSON, dynamically imported dictionaries via await import retain full namespace autocomplete, literal key checking, and parameter type extraction ({param: type}).