npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

inline-i18n-multi

v0.6.0

Published

Inline i18n - write translations inline, support multiple languages

Readme

inline-i18n-multi

npm version License: MIT

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:

  1. Search for "Hello" in JSON files
  2. Find the key greeting.hello
  3. Search for that key in your code
  4. 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-TWzhen)
  • 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-multi

Quick 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 values
  • selectordinal: ordinal plural categories (one, two, few, other)
  • number: decimal, percent, integer, currency, compact, compactLong
  • date: short, medium, long, full
  • time: 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 namespace

Debug 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')  // → true

React 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 dictionaries

Language 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_de
  • en_ja, en_zh, en_es, en_fr, en_de
  • ja_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 trace

Inline 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-react

View React package →

Next.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-next

View Next.js package →


Developer 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/cli

View CLI package →


Requirements

  • 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