stacks/ts-i18n
publicClone
Push over the same URL. A password will not work: create a token under access tokens and use it in place of one.
- .github
- .vscode
- docs
- examples
- packages
- scripts
- .editorconfig 147 B
- .gitattributes 12 B
- .gitignore 110 B
- buddy-bot.config.ts 862 B
- bun.lock 45.8 KB
- bunfig.toml 107 B
- CHANGELOG.md 9.0 KB
- CLAUDE.md 1.5 KB
- deps.yaml 32 B
- i18n 56 B
- LICENSE.md 1.1 KB
- package.json 2.3 KB
- pickier.config.ts 1.5 KB
- README.md 9.9 KB
- tsconfig.json 1.0 KB

ts-i18n
Fast, Bun‑native TypeScript i18n loader with YAML/TS support, runtime translation, and type generation. Framework‑agnostic: use it in any template engine or with React/Vue.
Table of Contents
- Overview
- Features
- Installation
- Quick Start
- Directory Structure
- Configuration
- Authoring Translations
- Runtime API
- CLI
- Type Generation
- Per-locale JSON Output
- Framework Integration
- Security
- Performance
- Testing
- License
Overview
ts-i18n focuses on a simple and fast developer experience:
- Keep translations in easy‑to‑read YAML files or in TypeScript with optional dynamic values.
- Load at runtime, generate per‑locale JSON for bundlers if needed, and optionally generate type definitions for key safety.
Features
- YAML (.yml/.yaml) and TS/JS translations
- Folder structure:
locales/en.ymlorlocales/en/*.ymlorlocales/en/*.ts - Runtime translator with fallback locales
- Dynamic values via functions in TS files
- Per‑locale JSON output (optional)
- Type generation for translation keys
- Type‑safe keys and parameter inference from TS base locale (optional)
- TS‑first by default; enable YAML as needed
Installation
# bun
bun add ts-i18n
# npm
npm i ts-i18n
# pnpm
pnpm add ts-i18n
# yarn
yarn add ts-i18nQuick Start
import type { TranslatorFor } from 'ts-i18n'
import { createTranslator, loadTranslations } from 'ts-i18n'
import base from './locales/en/index'
type Base = typeof base
const trees = await loadTranslations({
translationsDir: 'locales',
defaultLocale: 'en',
fallbackLocale: 'pt',
sources: ['ts'], // TS-first
})
const trans: TranslatorFor<Base> = createTranslator<Base>(trees, { defaultLocale: 'en', fallbackLocale: 'pt' })
trans('home.title') // "Home"
trans('dynamic.hello', { name: 'Ada' }) // "Hello, Ada"Directory Structure
Recommended (Option A):
locales/
en.yml
pt.yml
en/
auth.yml
dynamic.ts- YAML files should be strictly nested objects with primitive leaves (string/number/boolean/null).
- TS/JS files should export a default object. Values can be functions for dynamic messages.
Configuration
ts-i18n reads .config/i18n.config.ts via bunfig when using the CLI. You can also pass the same options programmatically.
export interface I18nConfig {
translationsDir: string // e.g. 'locales'
defaultLocale: string // e.g. 'en'
fallbackLocale?: string | string[] // e.g. 'pt' or ['pt', 'es']
include?: string[] // optional globs relative to translationsDir. If set, overrides sources
verbose?: boolean
outDir?: string // where to write per-locale JSON (optional)
typesOutFile?: string // where to write generated d.ts (optional)
sources?: ('ts' | 'yaml')[] // default: ['ts']
}Sample config:
// .config/i18n.config.ts
export default {
translationsDir: 'locales',
defaultLocale: 'en',
fallbackLocale: 'pt',
sources: ['ts', 'yaml'],
outDir: 'dist/i18n',
typesOutFile: 'dist/i18n/keys.d.ts',
}Scaffold a sample config:
# generates .config/i18n.config.ts from defaults
npx ts-i18n initAuthoring Translations
YAML files
# locales/en.yml
home:
title: Home
user:
profile:
name: NameTypeScript files (satisfies Dictionary)
Use satisfies Dictionary for editor hints and static checks.
// locales/en/app.ts
import type { Dictionary } from 'ts-i18n'
export default {
home: {
title: 'Home',
},
dynamic: {
welcome: ({ name }: { name: string }) => `Welcome, ${name}!`,
},
} satisfies DictionaryRuntime API
import { createTranslator, generateTypes, loadTranslations, writeOutputs } from 'ts-i18n'
const cfg = { translationsDir: 'locales', defaultLocale: 'en', fallbackLocale: 'pt', sources: ['ts'] }
const trees = await loadTranslations(cfg)
const trans = createTranslator(trees, { defaultLocale: cfg.defaultLocale, fallbackLocale: cfg.fallbackLocale })
// translate
trans('home.title')
trans('dynamic.welcome', { name: 'Ada' })
// optional outputs
await writeOutputs(trees, 'dist/i18n')
await generateTypes(trees, 'dist/i18n/keys.d.ts')Notes:
- If a key is missing in the active locale,
transfalls back tofallbackLocale(or returns the key if not found). - Interpolation is handled by your template engine; for dynamic TS values, pass
paramstotrans.
CLI
# Build per-locale JSON (when outDir is set) and generate types
# TS-only / YAML-only / explicit sources
ts-i18n build --ts-only
ts-i18n build --yaml-only
ts-i18n build --sources ts,yaml
# Generate types from a TS base module (zero parsing required)
ts-i18n build --types-from ./locales/en/index.ts
# List discovered locales and their top-level namespaces
ts-i18n list --sources ts,yaml
# Check missing keys vs base locale
ts-i18n check --sources ts,yaml
# Create a sample config file
ts-i18n init --out .config/i18n.config.tsType Generation
Generates a union type of keys for DX.
import { generateTypes } from 'ts-i18n'
await generateTypes(trees, 'dist/i18n/keys.d.ts')
// -> export type TranslationKey = 'home.title' | 'user.profile.name' | ...If your base locale is authored in TypeScript, you can generate fully typed keys and parameter types inferred from the TS file (including function params for dynamic messages):
import { generateTypesFromModule } from 'ts-i18n'
// path is resolved by TypeScript at type time; keep it the same path you import from in your app
await generateTypesFromModule('./locales/en/index.ts', 'dist/i18n/keys.d.ts')This emits a declaration file that exports the following types:
// dist/i18n/keys.d.ts
export type TranslationKey // dot‑path keys inferred from your base TS object
export type ParamsFor<K extends TranslationKey> // parameter type for dynamic function leaves
export type TypedTranslator // a translator type constrained to the inferred keys/paramsUsage in your app:
import type { ParamsFor, TranslationKey, TypedTranslator } from './dist/i18n/keys'
import { createTranslator, loadTranslations } from 'ts-i18n'
const trees = await loadTranslations({ translationsDir: 'locales', defaultLocale: 'en' })
// Option A: annotate the returned translator
const trans: TypedTranslator = createTranslator(trees, { defaultLocale: 'en' })
// Option B: use the generic to bind the base type yourself
// import type Base from './locales/en/index'
// const trans = createTranslator<Base>(trees, { defaultLocale: 'en' })
// Auto‑complete on keys, and correct param types per key
trans('home.title')
trans('dynamic.welcome', { name: 'Ada' })
// ^? ParamsFor<'dynamic.welcome'> → { name: string }Per-locale JSON Output
Write serializable JSON per locale (function values are stripped).
import { writeOutputs } from 'ts-i18n'
await writeOutputs(trees, 'dist/i18n')
// -> dist/i18n/en.json, dist/i18n/pt.json, ...Framework Integration
Template engines
Use the returned function directly: . For dynamic messages: .
React
import React, { createContext, useContext } from 'react'
import { createTranslator, loadTranslations } from 'ts-i18n'
const I18nContext = createContext<(k: string, p?: any) => string>(() => '')
export function I18nProvider({ children }: { children: React.ReactNode }) {
const [transFn, setTransFn] = React.useState(() => (k: string) => k)
React.useEffect(() => {
(async () => {
const trees = await loadTranslations({ translationsDir: 'locales', defaultLocale: 'en', fallbackLocale: 'pt' })
setTransFn(() => createTranslator(trees, { defaultLocale: 'en', fallbackLocale: 'pt' }))
})()
}, [])
return <I18nContext.Provider value={transFn}>{children}</I18nContext.Provider>
}
export function useTrans() {
return useContext(I18nContext)
}Vue
import { createTranslator, loadTranslations } from 'ts-i18n'
import { createApp, inject } from 'vue'
const key = Symbol('i18n')
export async function installI18n(app) {
const trees = await loadTranslations({ translationsDir: 'locales', defaultLocale: 'en', fallbackLocale: 'pt' })
const trans = createTranslator(trees, { defaultLocale: 'en', fallbackLocale: 'pt' })
app.provide(key, trans)
}
export function useTrans() {
return inject(key)
}Security
- TS/JS translation modules and
.config/i18n.config.tsexecute at build-time. Treat them as trusted code. - Prefer YAML-only builds for untrusted sources.
Performance
- Parallel file parsing and pre-flattened lookup maps for O(1) translations.
- JSON outputs strip function values.
Testing
bun testHigh coverage tests validate loader behavior, translator fallback, outputs, type generation, and edge cases.
License
MIT — see LICENSE.md.