# 6. Internationalisation (i18n)

> How translation works, why the core bundle stays English-only, and how to load
> one of the shipped locales or write your own.

## The model: English source strings are the keys

i18n is **gettext-style**: the English UI string _is_ the lookup key. A locale is
just a map that overrides some of those strings. Two consequences:

- **English needs no dictionary**: an unknown key resolves to itself, so the
  core works with an empty locale.
- **Plugins are free**: a plugin's literal label (e.g. `'Stickers'`) is
  automatically a translatable key, with no key scheme to learn.

The translator is a pure function (`src/core/i18n/i18n.ts`):

```ts
import { createTranslator } from '@jodit/image-editor';

const t = createTranslator({ Save: 'Сохранить' });
t('Save'); // 'Сохранить'
t('Undo'); // 'Undo'  (falls back to the key)
t('{w} × {h} px', { w: 800, h: 600 }); // '800 × 600 px'  (placeholder interpolation)
```

The active locale is **part of state**, so switching language is the same
`update` as everything else and triggers a normal re-render:

```ts
editor.update({ locale: 'ru' });
```

## The core bundle ships English only

No dictionary is bundled into `dist/jodit-image-editor.js`. The editor holds a
`LocaleRegistry` that starts empty; an unregistered (or `'en'`) locale yields the
identity translator. Locales are **separate files**, loaded only if you import
them, so paying for Russian or Chinese is opt-in.

```
dist/
├── jodit-image-editor.js          ← core, English-only
└── locales/
    ├── ru.js  ru.d.ts
    ├── es.js  es.d.ts
    ├── fr.js  fr.d.ts
    ├── de.js  de.d.ts
    └── zh.js  zh.d.ts
```

## Using a shipped locale

Five popular languages ship as standalone modules: **Russian (`ru`), Spanish
(`es`), French (`fr`), German (`de`), Chinese Simplified (`zh`)**. Import the
ones you need and register them:

```ts
import { ImageEditor } from '@jodit/image-editor';
import ru from '@jodit/image-editor/locales/ru';
import es from '@jodit/image-editor/locales/es';

const editor = new ImageEditor({
  container: '#editor',
  locales: [ru, es], // register
  locale: 'ru', // start in Russian
});

// later, switch at runtime:
editor.update({ locale: 'es' });
editor.update({ locale: 'en' }); // built-in English (no import needed)
```

Each module's default export is a `Locale`:

```ts
interface Locale {
  id: string; // 'ru'
  name: string; // 'Русский', shown in a language picker
  messages: Record<string, string>;
}
```

## Writing a custom locale

A locale is plain data. Define one and pass it the same way (or register it from
a plugin):

```ts
import type { Locale } from '@jodit/image-editor';

const it: Locale = {
  id: 'it',
  name: 'Italiano',
  messages: {
    Save: 'Salva',
    Crop: 'Ritaglia',
    Rotate: 'Ruota',
    // …only the strings you want to override; the rest fall back to English
  },
};

new ImageEditor({ container: '#editor', locales: [it], locale: 'it' });
```

From a plugin, use the extension API; `register*` calls return a disposer:

```ts
const plugin = {
  name: 'it-locale',
  setup: (api) => api.registerLocale(it),
};
```

Registering a locale id that already exists **merges** into it, so a plugin can
top up missing strings without clobbering the base.

## How a string becomes translatable

The render layer receives the translator as an injected dependency; it never
reaches for a global, so `view = f(state)` stays intact. Components call `t(...)`:

- The shell (`renderApp`) gets `ctx.t` (the rail labels, top-bar titles).
- Each tool panel gets `t` in its `ToolContext`: `renderPanel({ state, update, t })`.
- Tool and filter **labels** are translated by passing them through `t` at render
  time (`t(tool.label)`, `t(filter.label)`), so they localise without changing
  the tool/filter definitions.

That means a custom tool localises for free:

```ts
api.registerTool({
  id: 'stickers',
  label: 'Stickers', // becomes a translation key
  icon: ICONS.annotate,
  renderPanel: ({ t }) => h('div', { class: 'jie-toolrow' }, [/* … */ t('Add sticker')]),
});
```

## Building a language picker

`Locale.name` is meant for UI. A minimal picker:

```ts
import ru from '@jodit/image-editor/locales/ru';
import es from '@jodit/image-editor/locales/es';
const locales = [ru, es];

const editor = new ImageEditor({ container: '#editor', locales });
// <select> whose options are [{ id:'en', name:'English' }, ...locales]
select.onchange = () => editor.update({ locale: select.value });
```

## Keeping translations honest

A test (`src/locales/locales.test.ts`) asserts every shipped locale covers
**exactly the same key set**, so adding a new UI string fails CI until every
language is updated, and no locale drifts behind.

← Back to the [docs index](./README.md).
