18 KiB
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
- Ввести один frontend registry как источник кодов, словарей, native labels и стабильного порядка языков.
- Вывести из registry тип
Lang, language resolution и options visual editor. - Расширить i18n gates на все зарегистрированные frontend dictionaries и набор backend translations.
- Описать contribution flow и ограничения текущей подстановки placeholders.
- Сохранить синхронный первый render и существующее поведение English/Russian.
- Исправить устаревший комментарий
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 вводится статически анализируемый реестр. Каждая запись
задаёт:
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) работает детерминированно:
- зарегистрированное явное
configLanguageпобеждает; - для HA locale проверяется нормализованный exact tag;
- затем primary subtag до первого
-или_; - иначе возвращается
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 в стабильном порядке:
- локализованный
Auto; - все 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 diffsrc/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
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. План реализации
- Выделить registry и pure helpers resolution/options.
- Перевести
src/i18n.tsиsrc/editor.tsна registry. - Добавить compatibility handling неизвестного editor value.
- Перевести i18n tests на compiled registry и включить locale file-set checks.
- Обновить CONTRIBUTING/comments.
- Выполнить обязательные 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.