# Локализация и ввод — `$.i18n`, `$.tr` и дополнения `$.input`

Подсистема `i18n.js` даёт словари, перевод строк с подстановкой параметров,
плюрализацию и автоподстановку текста в узлы. Вторая часть документа —
новые методы `input.js`: мёртвая зона осей и сохранение привязок.

```js
$.ready(() => {
    $.i18n.add('ru', { 'menu.play': 'Играть' });
    $.i18n.add('en', { 'menu.play': 'Play' });
    $.i18n.lang('ru');
    $.i18n.auto(true);

    $('<ui.label>', { tr: 'menu.play' }).at(400, 60).appendTo($.ui);
    $.tr('menu.play');                 // → 'Играть'
});
```

---

## 1. Словари и языки — `$.i18n`

| Метод | Назначение |
|---|---|
| `$.i18n.add(lang, dict)` | добавить/дополнить словарь языка; повторный `add` сливает ключи |
| `$.i18n.load(lang, urlOrPath)` | загрузить словарь из JSON-файла |
| `$.i18n.load(path)` | то же, код языка берётся из имени файла (`i18n/en.json` → `en`) |
| `$.i18n.lang([code])` | без аргумента — текущий язык, с аргументом — переключить |
| `$.i18n.fallback([code])` | запасной язык (по умолчанию `ru`) |
| `$.i18n.langs()` | коды всех загруженных языков, по алфавиту |
| `$.i18n.has(key)` | есть ли перевод ключа (текущий язык → запасной → любой загруженный) |

Словарь — объект `{ 'ключ': 'текст' }`. Значение может быть массивом форм для
плюрализации (см. §3). `$.i18n.load` читает файл через `$.fs.readJSON`;
вместо пути можно передать готовый объект словаря (удобно в тестах).

```js
$.i18n.load('en', 'i18n/en.json');
$.i18n.load('i18n/ru.json');           // язык угадан по имени файла
$.i18n.lang('en');
$.i18n.langs();                        // → ['en', 'ru']
```

## 2. Перевод строк — `$.tr`

`$.tr(key, params?, fallback?)` возвращает перевод, подставляя `{name}` из
`params`. Если ключа нет — возвращает `fallback`, а без него сам `key`, и
**один раз** пишет предупреждение в лог (повторные вызовы не спамят).

```js
$.i18n.add('ru', { 'hud.score': 'Очки: {score}', 'hud.time': 'Время: {t} с' });
$.tr('hud.score', { score: 120 });     // → 'Очки: 120'
$.tr('нет.такого');                    // → 'нет.такого' + предупреждение
$.tr('нет.такого', {}, '—');           // → '—'
```

Неизвестный параметр в шаблоне остаётся как есть (`'{name}'`), чтобы опечатка
была видна, а не превращалась в `undefined`.

## 3. Плюрализация — `$.i18n.plural`

Значение ключа-массива трактуется как формы. `$.i18n.plural(key, count)`
выбирает форму по числу и подставляет `{n}`.

```js
$.i18n.add('ru', {
    'item': ['{n} штука', '{n} штуки', '{n} штук'],
});
$.i18n.plural('item', 1);    // → '1 штука'
$.i18n.plural('item', 3);    // → '3 штуки'
$.i18n.plural('item', 11);   // → '11 штук'
$.i18n.plural('item', 21);   // → '21 штука'
```

Правила упрощены, но крайние случаи учтены:

| Язык | Формы |
|---|---|
| `ru` (и `uk`, `be`) | 1, 21, 101 → форма 1; 2–4, 22–24 → форма 2; 0, 5–20, 11–14 → форма 3 |
| `en` | 1 → форма 1; всё остальное → форма 2 |
| прочие | 1 → форма 1; иначе форма 2 |

Если форм меньше, чем вернул индекс, берётся последняя. `.plural()` можно
спросить и как `$.tr.plural(key, count)`.

## 3.1. Клипы — варианты одного текста

Одну и ту же фразу в озвучке и в субтитрах нужно уложить в разное время, а
короткую подпись на кнопке взять иначе, чем длинную в диалоге. Поэтому
значением ключа может быть **список вариантов**:

```js
$.i18n.add('ru', {
    'npc.greet': ['Привет!', 'Здорово!', 'Ага.'],
});

$.i18n.clip('npc.greet', 0);        // { text: 'Привет!', index: 0, total: 3 }
$.i18n.clip('npc.greet', 7);        // вариант по ЗЕРНУ 7 — всегда один и тот же
$.i18n.clipCount('npc.greet');      // 3
$.tr.clip('npc.greet', 2);          // 'Ага.' — только текст
$.tr.clipInfo('npc.greet', 2);      // { text, index, total }
```

| Метод | Назначение |
|---|---|
| `$.i18n.clip(key, selector?, params?)` | `{ text, index, total }` |
| `$.i18n.clipCount(key)` | сколько вариантов (0 — ключа нет или он не список) |
| `$.tr.clip(key, selector?)` | только текст варианта |
| `$.tr.clipInfo(key, selector?)` | текст и НОМЕР варианта |

**Выбор.** `selector` — это **номер** варианта (0, 1, 2 …) либо **зерно**:
`clipIndex(seed, n)` перемешивает зерно, поэтому одно и то же зерно всегда даёт
тот же вариант, а соседние зёрна — разные. Так реплика NPC не «дрожит» между
кадрами, но у разных NPC звучит по-разному. Если вариантов нет, `clip` вернёт
первую форму или сам ключ, а `clipInfo` — `{ index: -1, total: 0 }`.

**Клипы и плюрализация вместе.** Ключ может быть объектом с двумя списками:

```js
'both': { plural: ['{n} вещь', '{n} вещи', '{n} вещей'], clip: ['коротко', 'длинно'] }
$.i18n.plural('both', 3);       // '3 вещи'
$.tr.clip('both', 1);           // 'длинно'
```

## 4. Автоподстановка в узлы — `$.i18n.auto`

`$.i18n.auto(true)` включает перевод узлов с атрибутом `tr`. Текст
обновляется при появлении узла и при каждой смене языка или словаря —
этим занимается `tickI18n()`, который `api.js` вызывает каждый кадр.

```js
$.i18n.auto(true);
$('<ui.label>', { tr: 'menu.play' }).appendTo($.ui);   // текст станет переводом

// Число рядом с ключом даёт плюральную форму:
$('<ui.label>', { tr: { key: 'item', n: 3 } });
```

Если на узле с `tr` вызвать `.text('…')` вручную, при следующей смене языка
автоподстановка перезапишет текст — убирайте `tr` там, где нужен свой текст.

## 5. Сохранение выбранного языка

Язык хранится в `$.store` под ключом `i18n.lang` и восстанавливается при
старте (`installI18n` читает store раньше первого кадра).

```js
$.i18n.lang('en');                 // $.store.set('i18n.lang', 'en')
$.store.save();                    // запись на диск — когда удобно игре
```

`$.i18n.lang()` сам `save()` не вызывает: моментом записи распоряжается игра
(или `$.store.autoSave`).

## 6. Чистые функции (для тестов без движка)

| Функция | Что делает |
|---|---|
| `format(text, params)` | подстановка `{name}`, неизвестные скобки без изменений |
| `pluralIndex(count, lang)` | индекс формы: 0/1/2 |
| `lookup(dicts, key)` | значение ключа из словаря или массива словарей |
| `clipIndex(selector, count)` | индекс варианта по номеру или зерну |
| `clipsOf(value)` | список вариантов из значения словаря (или `null`) |

```js
import { format, pluralIndex, lookup } from '../../src/highlevel/i18n.js';
format('{a}+{b}', { a: 1, b: 2 });   // '1+2'
pluralIndex(11, 'ru');               // 2
lookup([{}, { a: 2 }], 'a');         // 2
```

---

# Дополнения `$.input`

## 7. Мёртвая зона осей — `$.input.deadzone`

`$.input.deadzone(value)` задаёт мёртвую зону (0..1, по умолчанию `0.2`),
`$.input.deadzone()` — читает её. Значение применяется в `axis()` и к
аналоговому стику в `vec()`, чтобы стик не «дрожал», а клавиатурные оси
работали как раньше.

```js
$.input.deadzone(0.3);
$.input.deadzone();        // → 0.3
$.input.vec('wasd');       // вклад стика меньше 0.3 считается нулевым
```

Некорректное значение (не число, отрицательное) не применяется — в лог
уходит предупреждение. Аргумент больше 1 ограничивается единицей.

## 8. Привязки: сохранение, загрузка, перенастройка

| Метод | Назначение |
|---|---|
| `$.input.saveBindings()` | записать `bindings()` в `$.store` под ключом `input.bindings` |
| `$.input.loadBindings()` | восстановить привязки из `$.store` |
| `$.input.rebind(action, keys)` | `bind()` с проверками: пустой список/нестроковый ключ — предупреждение |
| `$.input.actions()` | имена всех объявленных действий |
| `$.input.describe(action)` | `{ action, keys, down, pressed }` — для отладки и меню |

`loadBindings()` восстанавливает только непустые массивы строк: испорченная
запись (`'space'` вместо `['space']`, числа, пустой массив) пропускается с
предупреждением, соседние привязки загружаются нормально.

```js
$.input.bind('jump', ['space']);
$.input.saveBindings();
$.store.save();

// позже, в новой сессии:
$.input.loadBindings();
$.input.describe('jump');  // → { action: 'jump', keys: ['space'], down: false, pressed: false }
```

## 9. Ограничения

* `$.i18n.load` читает файлы через `$.fs` (`engine.fs`), сети нет — только
  локальные пути;
* автоматический выбор языка по системе не делается: по умолчанию `ru`,
  затем язык из `$.store`;
* плюральные правила — упрощённые (две формы для `en`, три для `ru`); для
  экзотических языков задайте формы под нужное число вручную;
* **клипы — это список вариантов, а не плюрализация**: если ключ-массив передан
  в `plural()`, он трактуется как формы, если в `clip()` — как варианты. Один
  массив не может быть и тем, и другим одновременно — для этого есть объектная
  форма `{ plural: [...], clip: [...] }`;
* **зерно перемешивается, а не берётся по модулю**: `clip(key, 3)` при трёх
  вариантах — это НОМЕР 3 (выйдет за список → последний), а не «четвёртый по
  кругу»; для зерна берите числа больше числа вариантов;
* `$.input.rebind` для необъявленного действия создаёт его, но пишет
  предупреждение: чаще всего это опечатка.
