Files
houseplan-card/docs/specs/348-german-localization.md
Matysh 12cd8a1af3 feat: add complete German localization
Add Deutsch across all card surfaces and backend flows, backed by the language registry introduced in #62. German loads as a fingerprint-checked page-shared locale chunk so EN/RU remain synchronous and the initial View budget stays intact. Root render gates prevent mixed-language flashes, retry once, and fail open to English. Extend parity, runtime, bundle, browser and visual coverage, plus contributor and user documentation.

Issue: #348
User-Visible: yes
2026-08-28 11:58:46 +03:00

24 KiB
Raw Permalink Blame History

Issue #348 — полная немецкая локализация House Plan

  • Issue: https://github.com/Matysh/houseplan-card/issues/348
  • Зависимость: #62 — единый реестр языков и автоматические i18n-gates
  • Статус документа: реализовано, передано на код-ревью
  • Приоритет / тип: P1 / feature
  • Трек: полный: задача меняет публичный языковой контракт, несколько frontend/backend-поверхностей, первый render и performance budget
  • Пользовательское изменение: да

1. Сценарий и пользовательская ценность

Немецкоязычный пользователь выбирает Deutsch в профиле Home Assistant либо явно в настройках карточки. После этого View, kiosk, встроенные редакторы, диалоги, подсказки, ошибки и config/options flow интеграции House Plan работают на немецком без английских или русских фрагментов.

Задача закрывает языковой барьер для крупной Home Assistant-аудитории и впервые проверяет инфраструктуру #62 на реальном третьем языке. После неё следующий перевод должен добавляться тем же проверяемым путём, без нового ручного списка локалей.

2. Что человек увидит до и после

До: de, de-DE, de-AT и de-CH не зарегистрированы и синхронно откатываются на English. В selector языка нет Deutsch.

После:

  • в selector языка после Русский появляется Deutsch;
  • Auto выбирает Deutsch для любого HA locale с primary subtag de;
  • явное language: de принудительно включает немецкий независимо от профиля;
  • все собственные строки House Plan отображаются на немецком;
  • при первой загрузке немецкого словаря пользователь не видит промежуточный English UI: на холодном mount допустим только короткий нейтральный busy-state, а уже отрисованный кадр при смене языка остаётся на месте до атомарной смены;
  • English и Русский продолжают работать и загружаться синхронно, как до задачи.

3. Границы перевода

3.1. Переводится

  • весь key set src/i18n/en.json: View, kiosk, Plan/Devices/Background editors, onboarding, диалоги, tooltips/help, aria-тексты, подтверждения, toasts, ошибки, backup/Optimize/vacuum и GUI editors обеих карточек;
  • custom_components/houseplan/translations/de.json: config flow, options flow и repair issue;
  • native label языка — Deutsch.

3.2. Не переводится карточкой

  • названия пространств, комнат, устройств, сущностей, HA areas и интеграций;
  • состояния, единицы, даты и значения, которые уже форматирует Home Assistant;
  • entity IDs, model names, filenames, URLs, stable config/API tokens;
  • бренд House Plan, Home Assistant, HA, HACS, Zigbee, LQI, Glow и общеупотребимые технические имена форматов;
  • полный немецкий комплект README/User Guide. Каноническая пользовательская документация по-прежнему ведётся на English и Русском по правилу #62.

4. Языковой и терминологический контракт

English dictionary — смысловой канон; Русский используется только для снятия неоднозначности. Перевод не должен буквально переносить ошибки или неудачную грамматику исходной строки: смысл, действие и уровень опасности сохраняются.

Базовый glossary:

Канонический термин Немецкий UI
View Ansicht
Plan editor Planeditor
Device editor Geräteeditor
Background editor Hintergrundeditor
Space Bereich
Room Raum
Wall / physical wall Wand / physische Wand
Zero-thickness wall Wand ohne Dicke
Partition Trennwand
Column Säule
Opening / passage Öffnung / offener Durchgang
Door / window / gate Tür / Fenster / Tor
Device / entity Gerät / Entität
Fill Füllung
Glow Glow
Save / Cancel / Delete Speichern / Abbrechen / Löschen

Правила качества:

  1. Используется вежливый нейтральный UI без du/Sie, где это возможно; команды формулируются коротким imperative/infinitive style.
  2. Существительные и заголовки следуют немецкому написанию с заглавной буквы; предложения — sentence case, не English Title Case.
  3. Используются настоящие ä, ö, ü, ß, а не ae/oe/ue/ss, кроме стабильных идентификаторов.
  4. Placeholders {name}, {n}, {reason} и остальные не переводятся, не добавляются и не удаляются; знаки вокруг них согласуются по-немецки.
  5. Строки с числом сохраняют нейтральную грамматику: текущий subst() не имеет ICU/plural rules.
  6. Кнопки и короткие labels не заканчиваются точкой; ошибки и полные подсказки могут заканчиваться точкой согласованно с English.
  7. В одном workflow нельзя смешивать синонимы из glossary без смысловой причины.

5. Locale resolution и persisted config

Реестр #62 получает canonical entry:

{ code: 'de', nativeLabel: 'Deutsch', ... }

Существующая exact → primary → English политика не меняется:

Explicit config HA locale Результат
de, DE любой de
пусто/нет de, de-DE, de_AT, de-CH de
неизвестное legacy-значение de-* de
пусто/нет неизвестная локаль en

language остаётся строковым полем без изменения schema и миграции. Уже сохранённое неизвестное значение selector по-прежнему показывает losslessly, пока пользователь явно не выберет Auto или зарегистрированный язык.

6. Runtime загрузки немецкого словаря

6.1. Почему словарь ленивый

На базе origin/dev перед задачей initial View graph занимает 255 910 B gzip из бюджета 256 000 B. Frontend English/Russian dictionaries имеют около 66/70 KiB raw каждый. Третий static import гарантированно нарушит принятый в #337 budget. Повышение бюджета либо перенос случайного View-кода в editor chunk не являются допустимым решением этой задачи.

English и Русский сохраняются eager/synchronous ради compatibility-контракта #62. Только de поставляется отдельным content-hashed dynamic locale chunk.

6.2. Registry и cache

Typed registry остаётся единственным источником code/native label/stable order, но поддерживает две формы записи:

  • eager dictionary (en, ru);
  • lazy loader (de).

Общий locale runtime:

  • дедуплицирует одновременные запросы de от нескольких экземпляров карточек;
  • хранит успешно загруженный dictionary в page-lifetime cache;
  • предоставляет синхронным t()/hasTranslation() только уже готовый словарь;
  • уведомляет подписанные Lit hosts об успешном завершении;
  • не делает fetch/import для en и ru;
  • очищает rejected promise и выполняет один bounded retry; после повторного отказа разблокирует UI с English fallback, пишет один console.warn и не создаёт бесконечного цикла запросов/рендеров.

Dynamic module экспортирует собственный build fingerprint. Loader принимает словарь только при совпадении с fingerprint initial chunk; stale/mixed bundle не может тихо отрисовать перевод от другой версии.

6.3. Render gate

Один shared host controller используется четырьмя самостоятельными корневыми поверхностями:

  1. houseplan-card;
  2. houseplan-space-card;
  3. GUI editor houseplan-card;
  4. GUI editor houseplan-space-card.

До готовности выбранного lazy dictionary:

  • холодный mount рисует только нейтральный aria-busy="true" progress surface без English/Russian текста и без интерактивных элементов;
  • если host уже имеет полный кадр и язык меняют на de, старый кадр остаётся неизменным до готовности и затем заменяется немецким атомарно;
  • редактор/диалог не может принять click по невидимой старой локали;
  • late resolve после смены обратно на en/ru или disconnect не возвращает host на de и не вызывает исключения;
  • ошибка после bounded retry переходит в рабочий English fallback, а не оставляет вечный spinner.

Вложенные локализованные компоненты не создают независимых запросов: они появляются только после gate корневого host либо подписываются на тот же cache.

7. Bundle manifest, доставка и budget

Locale chunk является полноценным HACS asset:

  • попадает в houseplan-assets.json, bundle sync, backend allowlist, release ZIP и tree/hash verification из #337;
  • входит в lazyFiles, но не в lazyEditorFiles и lazyOnboardingFiles; manifest получает явную lazyLocaleFiles группу;
  • не запрашивается при English/Russian View;
  • при немецком View загружается ровно один раз на страницу до появления первого локализованного кадра;
  • initial View graph после loader/registry изменений остаётся ≤ 256 000 B gzip.

Разрешено удалить только доказанно неиспользуемые legacy i18n keys (включая оставшиеся после #62 ручные labels editor.lang_en/editor.lang_ru), если это нужно для сохранения budget. Удаление подтверждается source-usage test и проходит одинаково во всех словарях; смысл живого UI не меняется. Повышать budget нельзя.

8. Layout, accessibility и touch

Немецкие строки обычно длиннее English. Перевод считается рабочим только если:

  • заголовки диалогов переносятся и не обрезаются;
  • footer actions не создают горизонтальный scroll и остаются доступны;
  • toolbars/context trays используют штатный wrap/scroll, не уменьшая рабочую область иначе, чем English/Russian;
  • View и kiosk при 390 px остаются полностью работоспособны по docs/TOUCH-SUPPORT.md;
  • tooltip/help не создаёт лишний dialog scroll;
  • aria-label, role=status/alert и busy-state имеют корректный язык либо нейтральны до загрузки;
  • lang="de" или эквивалентный локальный marker доступен корневой поверхности, чтобы browser/assistive technology применяли немецкие правила произношения.

Редакторы остаются desktop-first; их данные обязаны быть безопасны на touch, но новая точность touch-editing в задачу не входит.

9. Translation completeness и автоматические gates

Проверки #62 расширяются на eager и lazy entries и обязаны загружать production словари через registry API, а не обходить lazy path прямым чтением только ради зелёного теста.

Gate проверяет:

  • registry ↔ frontend files ↔ backend files one-to-one для en, ru, de;
  • exact key set относительно English, непустые значения, placeholders и полный help body/aria contract;
  • отсутствие кириллицы в de кроме явно allowlisted user/example literals;
  • отсутствие необоснованного English fallback: значения de не равны English, кроме allowlist брендов, единиц, стабильных терминов и односимвольных glyph;
  • glossary для ключевых публичных controls;
  • locale matrix de/de-*/explicit/fallback и selector order Auto, English, Русский, Deutsch;
  • lazy state machine: dedupe, success, race, retry/failure fallback, disconnect;
  • production bundle не содержит German dictionary в initial static graph.

Backend translation test проверяет German config/options/repair strings через HA translation API либо, если harness не предоставляет стабильный language API, точную file/key/placeholder parity плюс чтение de.json.

10. Acceptance criteria

  • AC1. Полный German key set. de зарегистрирован как Deutsch; frontend и backend dictionaries имеют полный набор ключей, непустые значения и точные placeholders/help pairs. Доказательство: registry-driven unit + backend translation test.
  • AC2. Locale resolution. Explicit de и Auto для de, de-DE, de_AT, de-AT, de-CH выбирают German; неизвестная locale использует English. Доказательство: unit matrix.
  • AC3. Нет смешанного UI. Репрезентативные View, device dialog, Plan editor, Devices editor, Background editor, onboarding и оба GUI editors содержат немецкие House Plan strings и не содержат English/Russian UI fragments. Доказательство: targeted Playwright smoke с semantic assertions.
  • AC4. Без языкового flash. На искусственно задержанном de import холодный mount показывает нейтральный busy-state; warm language switch сохраняет старый complete frame и атомарно публикует German; controls inert до commit. Доказательство: unit state machine + Playwright delayed-load smoke.
  • AC5. Без deadlock при ошибке. Два отказа lazy load дают один warning и рабочий English fallback; смена языка/disconnect во время pending не публикует устаревший кадр. Доказательство: unit с controllable loader.
  • AC6. Performance. German dictionary отсутствует в initial static graph; English/Russian не запрашивают locale chunk; German загружает его один раз; initial View ≤ 256 000 B gzip. Доказательство: bundle-manifest/budget unit, production build manifest и Playwright network trace.
  • AC7. Доставка. Locale chunk присутствует в manifest, HACS tree, backend allowlist и ZIP-verification path; classified как lazyLocaleFiles, а не editor/onboarding. Доказательство: bundle tree/backend tests.
  • AC8. Layout/accessibility. Немецкие View/kiosk и репрезентативные dialogs не имеют clipping/горизонтального scroll при desktop и 390 px; busy-state нейтрален и aria-busy; German surface сообщает locale assistive technology. Доказательство: smoke metrics + reviewed golden desktop/mobile.
  • AC9. English/Russian compatibility. Existing eager first render, locale selector values, persisted config и видимые EN/RU strings не меняются, кроме появления option Deutsch и удаления доказанно мёртвых registry-label keys. Доказательство: unit regression + bundle network smoke + diff review.
  • AC10. Документация и release artifacts. User guides перечисляют de, contribution docs описывают eager/lazy third-locale flow, а оба changelog сообщают о German UI и ссылке #348. Доказательство: docs diff/check-docs.

11. План тестов и обязательные gates

Unit / backend

  • registry/file/key/placeholder/help/glossary parity для трёх языков;
  • locale resolution и selector order;
  • pure locale loader/controller state machine с deferred promises;
  • bundle manifest role classification и initial budget;
  • locale asset allowlist/tree validation;
  • German backend translation structure/lookup.

Browser / visual

  • новый demo/smoke_german_locale.mjs: semantic coverage AC3, locale switch, delayed loader, failure fallback и network count;
  • German desktop golden: основной View + открытый device/settings dialog;
  • German 390 px golden: View/kiosk либо самый длинный гарантированный View dialog; отсутствие overflow измеряется smoke, а не только глазами;
  • существующие EN/RU docs/golden не перезаписываются без визуальной причины.

Локально перед code review

npm run typecheck
npm test
npm run bundle:sync
npm run bundle:budget
npm run inventory
node scripts/smoke-select.mjs --base origin/dev --head HEAD
node demo/smoke_german_locale.mjs
node scripts/check-docs.mjs
git diff --check

Backend HA harness и golden capture запускаются по селектору/CI на Linux; на Windows локальный Chromium smoke обязателен, если зависимости доступны.

12. Затронутые файлы и модули

Ожидаемый scope (точные имена helper-файлов может уточнить реализация):

  • src/i18n/de.json, lazy module/controller под src/i18n/;
  • src/i18n/registry.ts, src/i18n.ts;
  • src/houseplan-card.ts, src/space-card.ts, src/editor.ts, src/space-editor.ts — shared readiness gate;
  • custom_components/houseplan/translations/de.json;
  • scripts/bundle-manifest.mjs, bundle/tree/backend route tests при необходимости;
  • test/i18n.test.mjs, locale loader и bundle tests;
  • targeted smoke/golden matrix;
  • CONTRIBUTING.md, docs/USER-GUIDE.md, docs/USER-GUIDE.ru.md, docs/TESTING.md, оба changelog и этот документ;
  • generated bundle tree и принятые Linux golden baselines.

Новых config keys и backend API нет.

13. Риски и меры

Риск Мера
Машинный/буквальный перевод меняет смысл опасного действия glossary, ручной аудит confirmations/errors/security strings, semantic smoke
German entry формально есть, часть UI падает в English exact key parity + no-English allowlist + representative browser coverage
Длинные строки ломают диалоги и kiosk desktop/mobile metrics и golden
Lazy locale даёт English flash root render gate и delayed-load smoke
Lazy locale зависает после 404/mixed deploy fingerprint, bounded retry, fail-open English
Несколько карточек скачивают chunk многократно page cache и dedupe unit/network trace
Locale chunk ошибочно считается editor-only явная manifest role и AC7
Loader превышает initial budget даже без словаря измерение build; только доказанная очистка мёртвых i18n keys, budget не повышается
EN/RU стали async или изменили первый кадр eager entries остаются; network/render regression

14. Откат

Откат удаляет de entry/files и lazy locale runtime, возвращает registry/tests к eager en/ru. Persisted language: de безопасно становится неизвестным legacy value: selector показывает его временной option, runtime применяет HA locale → English fallback по контракту #62. Data migration и rollback backend config не нужны.

15. Release-артефакты

  • docs/CHANGELOG.md и docs/CHANGELOG.ru.md: значимый bullet со ссылкой #348;
  • User Guide EN/RU: language = auto|en|ru|de, Auto locale matrix и короткое описание German lazy first load/fallback;
  • CONTRIBUTING: третий язык может быть lazy, registry entry определяет loader;
  • docs/TESTING.md: German semantic/layout/network checks;
  • generated multi-file bundle и release ZIP validation;
  • Linux-reviewed German desktop/mobile golden baselines;
  • security изменений нет; performance artifact — manifest с initial/lazy locale gzip и network trace.

16. План реализации

  1. Расширить registry pure contract eager/lazy entries и написать loader tests.
  2. Добавить shared Lit readiness gate и race/failure tests четырёх root hosts.
  3. Добавить de.json, перевести/вычитать frontend и backend strings, включить glossary/no-fallback gates.
  4. Развести locale/editor/onboarding roles в bundle manifest и проверить package/backend/ZIP delivery.
  5. Добавить semantic/layout/network smoke и German golden scenarios.
  6. Обновить docs/changelogs, собрать и синхронизировать bundle.
  7. Прогнать gates, передать точный SHA на независимое code review.