inline-i18n-multi
v0.6.0
Published
Inline i18n - write translations inline, support multiple languages
Maintainers
Readme
inline-i18n-multi
Write translations inline. Find them instantly.
For complete documentation, examples, and best practices, please read the full documentation on GitHub.
The Problem
Traditional i18n libraries separate translations from code:
// Component.tsx
<p>{t('greeting.hello')}</p>
// en.json
{ "greeting": { "hello": "Hello" } }
// ko.json
{ "greeting": { "hello": "안녕하세요" } }When you see "Hello" in your app and want to find it in the code, you have to:
- Search for "Hello" in JSON files
- Find the key
greeting.hello - Search for that key in your code
- Finally find
t('greeting.hello')
This is slow and frustrating.
The Solution
With inline-i18n-multi, translations live in your code:
<p>{it('안녕하세요', 'Hello')}</p>See "Hello" in your app? Just search for "Hello" in your codebase. Done.
Features
- Inline translations - Write translations right where you use them
- Instant search - Find any text in your codebase immediately
- Type-safe - Full TypeScript support with variable type checking
- Multiple languages - Support for any number of locales
- i18n compatible - Support for traditional key-based translations with JSON dictionaries
- ICU Message Format - Plural, select, date, number, time, relative time, and list formatting
- Variable interpolation -
{name}syntax for dynamic values - Locale Fallback Chain - BCP 47 parent locale support (
zh-TW→zh→en) - Missing Translation Warning - Development-time diagnostics with customizable handlers
- Namespace Support - Organize translations for large apps (
t('common:greeting')) - Debug Mode - Visual indicators for missing/fallback translations
- Currency Formatting - Locale-aware currency display (
{price, currency, USD}) - Compact Number Formatting - Short number display (
{count, number, compact}) - Rich Text Interpolation - Embed React components in translations (
<link>text</link>) - Lazy Loading - Async dictionary loading on demand (
loadAsync()) - Custom Formatter Registry - Register custom ICU-style formatters (
registerFormatter('phone', fn)) - Interpolation Guards - Handle missing variables gracefully (
missingVarHandler) - Locale Detection - Auto-detect user locale from navigator, cookie, URL, or header (
detectLocale()) - Selectordinal - Ordinal plural formatting (
{rank, selectordinal, one {#st} two {#nd} ...})
Installation
# npm
npm install inline-i18n-multi
# yarn
yarn add inline-i18n-multi
# pnpm
pnpm add inline-i18n-multiQuick Start
import { it, setLocale } from 'inline-i18n-multi'
// Set current locale
setLocale('en')
// Shorthand syntax (Korean + English)
it('안녕하세요', 'Hello') // → "Hello"
// Object syntax (multiple languages)
it({ ko: '안녕하세요', en: 'Hello', ja: 'こんにちは' }) // → "Hello"
// With variables
it('안녕, {name}님', 'Hello, {name}', { name: 'John' }) // → "Hello, John"Key-Based Translations (i18n Compatible)
For projects that already use JSON translation files, or when you need traditional key-based translations:
import { t, loadDictionaries } from 'inline-i18n-multi'
// Load translation dictionaries
loadDictionaries({
en: {
greeting: { hello: 'Hello', goodbye: 'Goodbye' },
items: { count_one: '{count} item', count_other: '{count} items' },
welcome: 'Welcome, {name}!'
},
ko: {
greeting: { hello: '안녕하세요', goodbye: '안녕히 가세요' },
items: { count_other: '{count}개 항목' },
welcome: '환영합니다, {name}님!'
}
})
// Basic key-based translation
t('greeting.hello') // → "Hello" (when locale is 'en')
// With variables
t('welcome', { name: 'John' }) // → "Welcome, John!"
// Plural support (uses Intl.PluralRules)
t('items.count', { count: 1 }) // → "1 item"
t('items.count', { count: 5 }) // → "5 items"
// Override locale
t('greeting.hello', undefined, 'ko') // → "안녕하세요"Utility Functions
import { hasTranslation, getLoadedLocales, getDictionary } from 'inline-i18n-multi'
// Check if translation exists
hasTranslation('greeting.hello') // → true
hasTranslation('missing.key') // → false
// Get loaded locales
getLoadedLocales() // → ['en', 'ko']
// Get dictionary for a locale
getDictionary('en') // → { greeting: { hello: 'Hello', ... }, ... }ICU Message Format
For complex translations with plurals and conditional text:
import { it, setLocale } from 'inline-i18n-multi'
setLocale('en')
// Plural
it({
ko: '{count, plural, =0 {항목 없음} other {# 개}}',
en: '{count, plural, =0 {No items} one {# item} other {# items}}'
}, { count: 0 }) // → "No items"
it({
ko: '{count, plural, =0 {항목 없음} other {# 개}}',
en: '{count, plural, =0 {No items} one {# item} other {# items}}'
}, { count: 1 }) // → "1 item"
it({
ko: '{count, plural, =0 {항목 없음} other {# 개}}',
en: '{count, plural, =0 {No items} one {# item} other {# items}}'
}, { count: 5 }) // → "5 items"
// Select
it({
ko: '{gender, select, male {그} female {그녀} other {그들}}',
en: '{gender, select, male {He} female {She} other {They}}'
}, { gender: 'female' }) // → "She"
// Combined with text
it({
ko: '{name}님이 {count, plural, =0 {메시지가 없습니다} other {# 개의 메시지가 있습니다}}',
en: '{name} has {count, plural, =0 {no messages} one {# message} other {# messages}}'
}, { name: 'John', count: 3 }) // → "John has 3 messages"
// Date formatting
it({
en: 'Created: {date, date, long}',
ko: '생성일: {date, date, long}'
}, { date: new Date() }) // → "Created: January 15, 2024"
// Number formatting
it({
en: 'Price: {price, number}',
ko: '가격: {price, number}'
}, { price: 1234.56 }) // → "Price: 1,234.56"Supported ICU types:
plural:zero,one,two,few,many,other(and exact matches like=0,=1)select: match on string valuesselectordinal: ordinal plural categories (one,two,few,other)number:decimal,percent,integer,currency,compact,compactLongdate:short,medium,long,fulltime:short,medium,long,full
Relative Time Formatting
it({
en: 'Updated {time, relativeTime}',
ko: '{time, relativeTime} 업데이트됨'
}, { time: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000) })
// → "Updated 3 days ago"
// Styles: long (default), short, narrow
it({ en: '{time, relativeTime, short}' }, { time: pastDate })List Formatting
it({
en: 'Invited: {names, list}',
ko: '초대됨: {names, list}'
}, { names: ['Alice', 'Bob', 'Charlie'] })
// → "Invited: Alice, Bob, and Charlie"
// Types: conjunction (and), disjunction (or), unit
it({ en: '{options, list, disjunction}' }, { options: ['A', 'B'] })
// → "A or B"Currency Formatting
it({
en: 'Total: {price, currency, USD}',
ko: '합계: {price, currency, KRW}'
}, { price: 42000 })
// en → "Total: $42,000.00" / ko → "합계: ₩42,000"
// Defaults to USD when currency code omitted
it({ en: '{price, currency}' }, { price: 100 })
// → "$100.00"Compact Number Formatting
it({
en: '{count, number, compact} views',
ko: '{count, number, compact} 조회'
}, { count: 1500000 })
// en → "1.5M views" / ko → "150만 조회"
it({ en: '{count, number, compactLong}' }, { count: 1500000 })
// → "1.5 million"Namespace Support
Organize translations for large applications:
import { loadDictionaries, t, getLoadedNamespaces, clearDictionaries } from 'inline-i18n-multi'
// Load with namespace
loadDictionaries({
en: { hello: 'Hello' },
ko: { hello: '안녕하세요' }
}, 'common')
// Use with namespace prefix
t('common:hello') // → "Hello"
// Without namespace = 'default' (backward compatible)
loadDictionaries({ en: { greeting: 'Hi' } })
t('greeting') // → "Hi"
getLoadedNamespaces() // → ['common', 'default']
clearDictionaries('common') // Clear specific namespaceDebug Mode
Visual indicators for debugging:
import { configure, setLocale, it, t } from 'inline-i18n-multi'
configure({ debugMode: true })
setLocale('fr')
it({ en: 'Hello', ko: '안녕하세요' }) // → "[fr -> en] Hello"
t('missing.key') // → "[MISSING: fr] missing.key"Rich Text Interpolation
Embed React components within translations:
import { RichText, useRichText } from 'inline-i18n-multi-react'
// Component syntax
<RichText
translations={{
en: 'Read <link>terms</link> and <bold>agree</bold>',
ko: '<link>약관</link>을 읽고 <bold>동의</bold>해주세요'
}}
components={{
link: (text) => <a href="/terms">{text}</a>,
bold: (text) => <strong>{text}</strong>
}}
/>
// Hook syntax
const richT = useRichText({
link: (text) => <a href="/terms">{text}</a>,
bold: (text) => <strong>{text}</strong>
})
richT({ en: 'Click <link>here</link>', ko: '<link>여기</link> 클릭' })Lazy Loading
Load dictionaries asynchronously on demand:
import { configure, loadAsync, isLoaded, t } from 'inline-i18n-multi'
configure({
loader: (locale, namespace) => import(`./locales/${locale}/${namespace}.json`)
})
await loadAsync('ko', 'dashboard')
t('dashboard:title')
isLoaded('ko', 'dashboard') // → trueReact Hook
import { useLoadDictionaries } from 'inline-i18n-multi-react'
function Dashboard() {
const { isLoading, error } = useLoadDictionaries('ko', 'dashboard')
if (isLoading) return <Spinner />
if (error) return <Error message={error.message} />
return <Content />
}Custom Formatter Registry
Register custom ICU-style formatters for domain-specific formatting:
import { registerFormatter, clearFormatters, it, setLocale } from 'inline-i18n-multi'
setLocale('en')
// Register a phone number formatter
registerFormatter('phone', (value, locale, style?) => {
const s = String(value)
if (locale === 'ko') return `${s.slice(0, 3)}-${s.slice(3, 7)}-${s.slice(7)}`
return `(${s.slice(0, 3)}) ${s.slice(3, 6)}-${s.slice(6)}`
})
// Use in translations
it({
en: 'Call {num, phone}',
ko: '전화: {num, phone}'
}, { num: '2125551234' })
// → "Call (212) 555-1234"
// Register a formatter with style support
registerFormatter('mask', (value, locale, style?) => {
const s = String(value)
if (style === 'email') {
const [user, domain] = s.split('@')
return `${user[0]}***@${domain}`
}
return s.slice(0, 2) + '***' + s.slice(-2)
})
it({ en: 'Email: {email, mask, email}' }, { email: '[email protected]' })
// → "Email: j***@example.com"
// Clear all custom formatters
clearFormatters()Reserved names (plural, select, selectordinal, number, date, time, relativeTime, list, currency) cannot be used as custom formatter names and will throw an error.
Interpolation Guards
Handle missing interpolation variables gracefully with a custom handler:
import { configure, it, setLocale } from 'inline-i18n-multi'
setLocale('en')
// Configure a missing variable handler
configure({
missingVarHandler: (varName, locale) => {
console.warn(`Missing variable "${varName}" for locale "${locale}"`)
return `[${varName}]`
}
})
// When a variable is missing, the handler is called instead of leaving {varName}
it({ en: 'Hello, {name}!' })
// logs: Missing variable "name" for locale "en"
// → "Hello, [name]!"
// Works with ICU patterns too
it({ en: '{count, plural, one {# item} other {# items}}' })
// logs: Missing variable "count" for locale "en"
// → "{count}"
// Without a handler, missing variables are left as-is: {varName}Locale Detection
Auto-detect the user's locale from multiple sources:
import { detectLocale, setLocale } from 'inline-i18n-multi'
// Basic detection from browser navigator
const locale = detectLocale({
supportedLocales: ['en', 'ko', 'ja'],
defaultLocale: 'en',
})
setLocale(locale)
// Multiple sources in priority order
const detected = detectLocale({
supportedLocales: ['en', 'ko', 'ja'],
defaultLocale: 'en',
sources: ['cookie', 'url', 'navigator'],
cookieName: 'NEXT_LOCALE', // default
})
// Server-side detection from Accept-Language header
const ssrLocale = detectLocale({
supportedLocales: ['en', 'ko', 'ja'],
defaultLocale: 'en',
sources: ['header'],
headerValue: 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
})
// → "ko"
// BCP 47 parent matching (en-US matches en)
const matched = detectLocale({
supportedLocales: ['en', 'ko'],
defaultLocale: 'en',
sources: ['navigator'],
})
// Browser reports "en-US" → matches "en"Detection sources:
| Source | Description |
|--------|-------------|
| navigator | Browser navigator.languages / navigator.language |
| cookie | Reads locale from document.cookie (configurable name) |
| url | First path segment (e.g., /ko/about matches ko) |
| header | Parses Accept-Language header value (for SSR) |
Sources are tried in order; the first match wins. If no source matches, defaultLocale is returned.
Selectordinal
Ordinal plural formatting for ranking and ordering (e.g., 1st, 2nd, 3rd):
import { it, setLocale } from 'inline-i18n-multi'
setLocale('en')
// Ordinal suffixes
it({
en: '{rank, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}'
}, { rank: 1 }) // → "1st"
it({
en: '{rank, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}'
}, { rank: 2 }) // → "2nd"
it({
en: '{rank, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}'
}, { rank: 3 }) // → "3rd"
it({
en: '{rank, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}'
}, { rank: 4 }) // → "4th"
// Handles English irregulars correctly
it({
en: '{rank, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}'
}, { rank: 11 }) // → "11th" (not "11st")
it({
en: '{rank, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}'
}, { rank: 21 }) // → "21st"
// Combined with text
it({
en: 'You finished {rank, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} place!'
}, { rank: 3 }) // → "You finished 3rd place!"Uses Intl.PluralRules with { type: 'ordinal' } for locale-aware ordinal categories.
Configuration
Configure global settings for fallback behavior and warnings:
import { configure, getConfig, resetConfig } from 'inline-i18n-multi'
configure({
fallbackLocale: 'en', // Final fallback (default: 'en')
autoParentLocale: true, // BCP 47 parent (zh-TW → zh)
fallbackChain: { // Custom chains
'pt-BR': ['pt', 'es', 'en']
},
warnOnMissing: true, // Enable warnings
onMissingTranslation: (w) => { // Custom handler
console.warn(`Missing: ${w.requestedLocale}`)
}
})Locale Fallback Chain
Automatic locale fallback with BCP 47 support:
setLocale('zh-TW')
it({ en: 'Hello', zh: '你好' }) // → '你好' (falls back to zh)
t('greeting') // Also works with dictionariesLanguage Pair Helpers
For common language combinations, use the shorthand helpers:
import { it_ja, en_zh, ja_es } from 'inline-i18n-multi'
// Korean ↔ Japanese
it_ja('안녕하세요', 'こんにちは')
// English ↔ Chinese
en_zh('Hello', '你好')
// Japanese ↔ Spanish
ja_es('こんにちは', 'Hola')Available helpers:
it(ko↔en),it_ja,it_zh,it_es,it_fr,it_deen_ja,en_zh,en_es,en_fr,en_deja_zh,ja_es,zh_es
API Reference
Core Functions
| Function | Description |
|----------|-------------|
| it(ko, en, vars?) | Translate with Korean and English |
| it(translations, vars?) | Translate with object syntax |
| setLocale(locale) | Set current locale |
| getLocale() | Get current locale |
Key-Based Translation
| Function | Description |
|----------|-------------|
| t(key, vars?, locale?) | Key-based translation with optional locale override |
| loadDictionaries(dicts, namespace?) | Load translation dictionaries with optional namespace |
| loadDictionary(locale, dict, namespace?) | Load dictionary for a single locale with optional namespace |
| hasTranslation(key, locale?) | Check if translation key exists (supports namespace:key) |
| getLoadedLocales() | Get array of loaded locale codes |
| getLoadedNamespaces() | Get array of loaded namespace names |
| getDictionary(locale, namespace?) | Get dictionary for a specific locale and namespace |
| clearDictionaries(namespace?) | Clear dictionaries (all or specific namespace) |
Configuration
| Function | Description |
|----------|-------------|
| configure(options) | Configure global settings (fallback, warnings, debug, missingVarHandler) |
| getConfig() | Get current configuration |
| resetConfig() | Reset configuration to defaults |
| loadAsync(locale, namespace?) | Asynchronously load dictionary using configured loader |
| isLoaded(locale, namespace?) | Check if dictionary has been loaded |
| parseRichText(template, names) | Parse rich text template into segments |
Custom Formatters
| Function | Description |
|----------|-------------|
| registerFormatter(name, formatter) | Register a custom ICU-style formatter |
| clearFormatters() | Clear all custom formatters |
Locale Detection
| Function | Description |
|----------|-------------|
| detectLocale(options) | Auto-detect user's locale from multiple sources |
React Hooks & Components
| Export | Description |
|--------|-------------|
| RichText | Rich text translation component with embedded components |
| useRichText(components) | Hook returning function for rich text translations |
| useLoadDictionaries(locale, ns?) | Hook for lazy loading dictionaries with loading state |
Types
type Locale = string
type Translations = Record<Locale, string>
type TranslationVars = Record<string, string | number | Date | string[]>
interface Config {
defaultLocale: Locale
fallbackLocale?: Locale
autoParentLocale?: boolean
fallbackChain?: Record<Locale, Locale[]>
warnOnMissing?: boolean
onMissingTranslation?: WarningHandler
debugMode?: boolean | DebugModeOptions
loader?: (locale: Locale, namespace: string) => Promise<Record<string, unknown>>
missingVarHandler?: (varName: string, locale: string) => string
}
interface DebugModeOptions {
showMissingPrefix?: boolean
showFallbackPrefix?: boolean
missingPrefixFormat?: (locale: string, key?: string) => string
fallbackPrefixFormat?: (requestedLocale: string, usedLocale: string, key?: string) => string
}
interface TranslationWarning {
type: 'missing_translation'
key?: string
requestedLocale: string
availableLocales: string[]
fallbackUsed?: string
}
type WarningHandler = (warning: TranslationWarning) => void
type CustomFormatter = (value: unknown, locale: string, style?: string) => string
type DetectSource = 'navigator' | 'cookie' | 'url' | 'header'
interface DetectLocaleOptions {
/** Locales your app supports */
supportedLocales: Locale[]
/** Fallback when no source matches */
defaultLocale: Locale
/** Detection sources in priority order (default: ['navigator']) */
sources?: DetectSource[]
/** Cookie name to read (default: 'NEXT_LOCALE') */
cookieName?: string
/** Accept-Language header value (for SSR) */
headerValue?: string
}
interface RichTextSegment {
type: 'text' | 'component'
content: string
componentName?: string
}Why Inline Translations?
Traditional i18n
Code → Key → JSON file → Translation
↑
Hard to traceInline i18n
Code ← Translation (same place!)| Aspect | Traditional | Inline | |--------|-------------|--------| | Finding text in code | Hard (key lookup) | Easy (direct search) | | Adding translations | Create key, add to JSON | Write inline | | Refactoring | Update key references | Automatic | | Code review | Check JSON separately | All visible in diff | | Type safety | Limited | Full support |
Framework Integrations
React
React hooks and components for inline translations. Includes LocaleProvider for context management, useLocale() hook for locale state, and T component for JSX translations. Automatic cookie persistence when locale changes.
npm install inline-i18n-multi-reactNext.js
Full Next.js App Router integration with SSR/SSG support. Server Components use async it(), Client Components use React bindings. Includes SEO utilities: createMetadata() for dynamic metadata, getAlternates() for hreflang links, and createI18nMiddleware() for locale detection.
npm install inline-i18n-multi-nextDeveloper Tools
CLI
Command-line tools for translation management. Find translations with inline-i18n find "text", validate consistency with inline-i18n validate, and generate coverage reports with inline-i18n coverage.
npm install -D @inline-i18n-multi/cliRequirements
- Node.js 18+
- TypeScript 5.0+ (recommended)
Documentation
Please read the full documentation on GitHub for:
- Complete API reference
- Framework integrations (React, Next.js)
- CLI tools
- Best practices and examples
License
MIT
