Files
houseplan-card/docs/specs/062-i18n-registry.md
T
2026-08-28 02:44:26 +03:00

303 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Issue #62 — единый реестр языков и масштабируемые проверки i18n
- **Issue:** https://github.com/Matysh/houseplan-card/issues/62
- **Статус документа:** актуализировано 2026-08-28, готово к ревью ТЗ
- **Приоритет:** P3
- **Тип:** tech-debt, полный трек: задача меняет class A i18n и затрагивает
несколько поверхностей; критерий `small` «одна поверхность» не выполняется
- **Пользовательское изменение:** нет для существующих English/Russian
установок
## 1. Сценарий
Основной потребитель результата — contributor/maintainer House Plan. На этапе
добавления будущего языка он добавляет словарь и одну запись в реестр, а сборка
сама подключает язык к определению локали, visual editor и проверкам паритета.
Для всех трёх пользовательских персон из `docs/SCOPE.md` поверхность остаётся
той же: карточка и visual editor продолжают синхронно работать на English или
Russian согласно явной настройке либо языку профиля Home Assistant.
## 2. Что человек увидит до и после
Пользователь English/Russian не увидит изменений; будущий переводчик вместо
нескольких несвязанных правок получает один описанный и автоматически
проверяемый путь добавления языка.
## 3. Проблема
Список языков сейчас продублирован в нескольких местах:
- `src/i18n.ts` вручную задаёт `Lang`, imports, `DICTS` и русский fallback;
- `src/editor.ts` вручную перечисляет English/Russian в selector;
- `test/i18n.test.mjs` вручную проверяет только пару `en`/`ru`;
- frontend и backend translation directories никак не сверяются.
Из-за этого новый JSON может не попасть в runtime, selector или gate. Докстрока
`src/i18n.ts` при этом ошибочно обещает добавление языка без правок TypeScript.
Словари уже заметны по размеру, но при двух текущих языках асинхронная загрузка
создала бы новый loading/error lifecycle без пользовательской пользы. Поэтому
lazy loading исключён из #62 и должен стать отдельной задачей одновременно с
добавлением третьей локали или при подтверждённой проблеме размера bundle.
## 4. Цели и scope
1. Ввести один frontend registry как источник кодов, словарей, native labels и
стабильного порядка языков.
2. Вывести из registry тип `Lang`, language resolution и options visual editor.
3. Расширить i18n gates на все зарегистрированные frontend dictionaries и набор
backend translations.
4. Описать contribution flow и ограничения текущей подстановки placeholders.
5. Сохранить синхронный первый render и существующее поведение English/Russian.
6. Исправить устаревший комментарий `CardConfig.language` в `src/types.ts`.
## 5. Не входит в задачу
- добавление немецкого или любого другого языка;
- выбор следующей локали по географии пользователей;
- Weblate, Crowdin или другой внешний сервис;
- code splitting, динамический import и сетевой fetch словаря;
- RTL и зеркалирование геометрического UI;
- новая политика локализации пользовательской документации;
- изменение видимых английских или русских формулировок;
- plural rules/ICU MessageFormat;
- связывание Python runtime с TypeScript registry.
Упоминание lazy dictionaries удаляется из заголовка issue: #62 не должно
выглядеть как обещание асинхронной загрузки, которой в acceptance criteria нет.
## 6. Контракт поведения
### 6.1. Registry
В `src/i18n/registry.ts` вводится статически анализируемый реестр. Каждая запись
задаёт:
```ts
interface LanguageEntry {
code: string;
nativeLabel: string;
dictionary: Record<string, string>;
}
```
`en` — обязательная fallback-запись. `Lang` выводится из литеральных кодов
registry, а не поддерживается отдельным union. Registry экспортирует:
- упорядоченный readonly список записей;
- lookup зарегистрированной записи по нормализованному коду;
- тип `Lang`;
- английскую fallback-запись.
При двух локалях используются обычные static JSON imports. Добавление локали
требует её frontend JSON, backend JSON и одной логической записи registry
(включая её static import в том же модуле); `src/editor.ts`, `langOf()` и список
локалей в тестах больше не редактируются.
Существующие `editor.lang_en` и `editor.lang_ru` не используются selector после
рефакторинга, но в рамках #62 не удаляются: задача не смешивает инфраструктуру с
чисткой словарей.
### 6.2. Разрешение языка
`langOf(hass, configLanguage)` работает детерминированно:
1. зарегистрированное явное `configLanguage` побеждает;
2. для HA locale проверяется нормализованный exact tag;
3. затем primary subtag до первого `-` или `_`;
4. иначе возвращается `en`.
Сравнение регистронезависимое, `_` нормализуется в `-`. Например, `ru-RU` и
`ru_RU` разрешаются в `ru`, если отдельная региональная запись отсутствует.
Неизвестное явное legacy-значение не падает: оно трактуется как Auto и проходит
HA locale → English fallback.
`t()` и `hasTranslation()` сохраняют цепочку `selected dictionary → en → key`.
`subst()` и plural semantics не меняются.
### 6.3. Visual editor
Selector строит options в стабильном порядке:
1. локализованный `Auto`;
2. все registry entries в порядке registry.
Подпись языка — compile-time native label (`English`, `Русский`), а не ключ
`editor.lang_<code>` во всех словарях.
Существующие `language: en|ru|''` читаются без миграции. Если в сохранённом
config встретился неизвестный код, selector показывает отдельную временную
option с этим кодом и не меняет config при редактировании другого поля. После
явного выбора Auto/зарегистрированного языка временная option исчезает. Runtime
до такого выбора применяет обычный HA locale → English fallback.
### 6.4. Parity gate
`test/i18n.test.mjs` получает данные из скомпилированного pure registry export,
а не парсит TypeScript регулярным выражением. Для этого `src/i18n.ts` и
`src/i18n/registry.ts` включаются в `tsconfig.test.json`.
Gate проверяет:
- коды являются BCP 47 tags и уникальны после case-insensitive нормализации
(`_` считается эквивалентом `-`); canonical spelling registry сохраняется в
selector и точных именах frontend/backend файлов (`pt-BR`, а не принудительно
`pt-br`);
- `en` существует и является fallback;
- у каждой записи есть непустые `nativeLabel` и dictionary;
- каждый `src/i18n/<code>.json` зарегистрирован и у каждой записи есть ровно
один такой JSON;
- key set каждой локали равен English;
- значения непусты, placeholders совпадают с English;
- текущий help body/aria contract выполняется для всех словарей;
- набор `<code>.json` в `custom_components/houseplan/translations/` совпадает с
кодами registry, без незарегистрированных и отсутствующих файлов.
Тестовая fixture с третьей локалью не добавляется в production registry: вместо
этого unit-тесты проверяют generic resolution на искусственном registry/helper,
либо доказывают отсутствие ветвления по `ru` в resolver. Production file-set
gate остаётся только на реально поставляемых локалях.
## 7. UX
Внешний вид selector для Auto/English/Russian и выбранные значения остаются
прежними. Порядок остаётся `Auto`, `English`, `Русский`.
Единственный новый защитный UX — неизвестный сохранённый код виден как временная
option вместо пустого/ложного выбора и не затирается несвязанным изменением.
Новых loading indicators, flashes, requests или error messages нет.
## 8. Модель данных и миграция
Schema и persisted config не меняются. Миграции нет.
- `language: en`, `language: ru`, пустая строка и отсутствие поля сохраняют
прежний смысл;
- неизвестная строка сохраняется до явного изменения пользователем;
- runtime безопасно использует Auto fallback;
- `CardConfig.language` остаётся `string`, потому что обязан читать будущие и
неизвестные сохранённые значения; комментарий больше не перечисляет только
`en|ru`.
## 9. i18n и документация
Новых видимых строк нет. Native labels доверенные compile-time literals.
`CONTRIBUTING.md` получает раздел «Translations»:
- frontend JSON + backend JSON + registry entry;
- placeholders нельзя удалять, добавлять или переводить;
- `subst()` не поддерживает plural rules, поэтому строка должна избегать
грамматической формы, зависящей от `{n}`;
- добавление языка интерфейса само по себе не создаёт новый комплект
пользовательской документации; текущие English/Russian документы продолжают
сопровождаться по действующим правилам проекта;
- RTL — отдельная продуктовая задача.
Существующая строка в `Ground rules` про «one JSON file + registering it in
`src/i18n.ts`» не остаётся рядом вторым контрактом: она заменяется ссылкой на
новый раздел и формулировкой про frontend/backend JSON и
`src/i18n/registry.ts`.
Докстрока `src/i18n.ts` приводится в соответствие реальному flow.
## 10. Acceptance criteria и доказательства
- **AC1.** `Lang`, `langOf()` и options visual editor получают языки из одного
registry. **Доказательство:** typecheck + unit на registry/options.
- **AC2.** Exact tag, primary subtag, `_`/case normalization, explicit config,
invalid explicit config и English fallback покрыты матрицей unit-тестов.
**Доказательство:** unit-тест pure resolver.
- **AC3.** Frontend locale file set, backend locale file set и registry взаимно
однозначны. **Доказательство:** i18n file-set unit test.
- **AC4.** Каждый зарегистрированный словарь автоматически проходит key,
non-empty, placeholder и help-key parity. **Доказательство:** registry-driven
`test/i18n.test.mjs`.
- **AC5.** Selector имеет порядок Auto/English/Русский и строит registered
options без ручного списка `en`/`ru`. **Доказательство:** unit на pure options
helper и source diff `src/editor.ts` без ручного списка локалей.
- **AC6.** Неизвестный сохранённый язык не ломает card/editor, использует Auto
fallback и не затирается изменением другого editor field. **Доказательство:**
unit для resolver/options/form normalization.
- **AC7.** English/Russian dictionary values и видимый selector не меняются.
**Доказательство:** git diff словарей + существующие unit/build gates.
- **AC8.** В production bundle нет locale fetch, dynamic import, Promise-based
translation path или нового первого пустого кадра. **Доказательство:** static
registry source diff + успешный production build.
- **AC9.** CONTRIBUTING и комментарии описывают фактический contribution flow и
plural limitation. **Доказательство:** documentation diff.
## 11. План автотестов
### Unit
- registry uniqueness после нормализации, BCP 47 code format, English
presence/fallback;
- exact/primary/fallback locale matrix, case and `_` normalization;
- invalid explicit config;
- registry-driven dictionary/file/backend parity;
- placeholders и help keys для всех entries;
- selector order/native labels/unknown temporary option;
- сохранение неизвестного значения при несвязанном form change.
### Обязательные локальные gates
```text
npm run typecheck
npm test
npm run build
npm run inventory
```
Browser smoke, golden, backend HA harness и performance capture не требуются:
пиксели, backend runtime, bundle topology и пользовательские сценарии не
меняются. Production build обязателен для подтверждения static bundling.
## 12. Риски
| Риск | Мера |
| --- | --- |
| Locale зарегистрирован не во всех местах | взаимная file-set проверка |
| Resolver меняет старый fallback | полная матрица language resolution |
| Visual editor стирает неизвестное значение | временная option + unit round-trip |
| Рефакторинг добавляет async flash | static imports и запрет async path в AC8 |
| Backend/frontend расходятся | CI parity по обоим каталогам |
| Типы registry превращаются в общий `string` | compile-time `Lang` из `as const` codes |
## 13. Откат
Откат возвращает ручные imports/`DICTS`/selector options. Config schema,
словари и сохранённые значения не меняются, поэтому data rollback не нужен.
## 14. Release-артефакты
- **Changelog:** не нужен (`User-Visible: no`), поскольку English/Russian UX не
меняется; защитное отображение неизвестного кода — compatibility guard.
- **User guide:** не меняется.
- **Developer docs:** `CONTRIBUTING.md`, комментарий `src/i18n.ts` и комментарий
`CardConfig.language` обязательны.
- **Generated bundle:** пересобрать и синхронизировать штатно перед code review.
- **Golden/screenshots:** не требуются.
## 15. План реализации
1. Выделить registry и pure helpers resolution/options.
2. Перевести `src/i18n.ts` и `src/editor.ts` на registry.
3. Добавить compatibility handling неизвестного editor value.
4. Перевести i18n tests на compiled registry и включить locale file-set checks.
5. Обновить CONTRIBUTING/comments.
6. Выполнить обязательные gates, собрать и синхронизировать bundle.
## 16. Технические предположения — можно менять на ревью
- `src/i18n/registry.ts` содержит static imports и readonly entries; отдельный
generated manifest не нужен при двух языках.
- Pure helper для selector может жить рядом с registry, чтобы тесты не зависели
от Lit/`ha-form`.
- Backend runtime не импортирует и не генерирует registry; связь обеспечивается
только test gate.
- Code splitting заводится отдельным issue не раньше третьей локали или
измеримого bundle-size bottleneck.