npm package: @kapsula-chat/i18n
Description: Lightweight chainable i18n for TypeScript with reactive plural forms and a Svelte component
Latest version: 1.0.0

# i18n

A lightweight, chainable i18n library for TypeScript. Translations live right next to the strings they describe — no external config files, no key lookups.

## Installation

```bash
bun install
```

## Usage

### Basic translation

Call `.tr(lang, translation)` on any string. Chain as many languages as you need. The result behaves exactly like a regular string.

```ts
import './i18n'
import { setLanguage } from './i18n'

const label = 'Save'.tr('ru', 'Сохранить').tr('de', 'Speichern')

setLanguage('en')
`${label}` // 'Save'

setLanguage('ru')
`${label}` // 'Сохранить'
```

### Plural forms

Pass an object of [CLDR plural categories](https://cldr.unicode.org/index/cldr-spec/plural-rules) (`one`, `few`, `many`, `other`, etc.) and a count as the third argument. Plural selection is handled by the built-in `Intl.PluralRules`.

```ts
const msg = 'item'.tr('ru', { one: 'элемент', few: 'элемента', many: 'элементов' }, 3)
// → 'элемента'
```

### Reactive plural forms

Pass a function as the count to make plurals react to changing values:

```ts
let count = 1
const msg = 'item'.tr('ru', { one: 'элемент', few: 'элемента', many: 'элементов' }, () => count)

`${msg}` // 'элемент'
count = 5
`${msg}` // 'элементов'
```

### Reacting to language changes

Use `onChange` to subscribe to language switches:

```ts
const label = 'Save'.tr('ru', 'Сохранить')

const off = label.onChange(value => {
    console.log('New value:', value)
})

setLanguage('ru') // logs: 'New value: Сохранить'

off() // unsubscribe
```

## Svelte component

For large text blocks or rich HTML, use the `<Tr>` component. It renders its content only when the current language matches `lang`. Reactively updates on language change.

```svelte
<script>
    import Tr from 'i18n/Tr.svelte'
    import { setLanguage } from 'i18n'
</script>

<Tr lang="en">
    <p>This is a <strong>large</strong> block of English text.</p>
</Tr>

<Tr lang="ru">
    <p>Это <strong>большой</strong> блок русского текста.</p>
</Tr>
```

When no `lang` is given, the block is always visible (useful as a fallback):

```svelte
<Tr lang="ru">Привет!</Tr>
<Tr>Hello!</Tr>  <!-- shown for all other languages -->
```

## API

### `setLanguage(lang: string)`
Sets the active language. Accepts BCP 47 tags (`'en'`, `'ru'`, `'zh-CN'`); normalised to the base language code.

### `getLanguage(): string`
Returns the current language code.

### `onLanguageChange(fn): () => void`
Subscribes to language changes. Returns an unsubscribe function.

### `normalizeLanguage(lang): string`
Normalises a language tag to a two-letter code, falling back to `'en'`.

### `String.prototype.tr(lang, value, n?)`
Extends any string with a translation. Returns a `Translated` instance that proxies all standard string methods.

| Parameter | Type | Description |
|-----------|------|-------------|
| `lang` | `string` | Language code |
| `value` | `string \| PluralForms` | Translation string or plural forms object |
| `n` | `number \| (() => number)` | Count for plural selection (optional) |

### `Translated`

| Member | Description |
|--------|-------------|
| `.tr(lang, value, n?)` | Adds another language translation (immutable) |
| `.onChange(callback)` | Subscribes to value changes on language switch; returns unsubscribe function |
| `.toString()` | Returns the translation for the current language |