mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-05 22:29:05 +00:00
@@ -0,0 +1,222 @@
|
||||
# SPEC-REVIEW-371-r1
|
||||
|
||||
Issue: [#371 — Add French localization files](https://github.com/Matysh/houseplan-card/issues/371)
|
||||
Трек: `small` (лёгкий), заход r1, лимит циклов лёгкого трека — 2, израсходовано 0/2 (жёлтый вердикт первого захода расходует цикл; см. §4 PROCESS.md).
|
||||
Ревьюер: Claude (роль «ревьюер ТЗ»), автор ТЗ: Codex (issue-комментарий от 2026-08-29).
|
||||
Материал: тело issue #371 на момент разбора (комментарий "S2→S3" от Matysh, 2026-08-29T09:59:53Z), без файла в `docs/specs/` — соответствует метке `small`.
|
||||
|
||||
## Скоуп
|
||||
|
||||
Добавление французского как четвёртого полноценного UI-языка (после en/ru/de),
|
||||
на инфраструктуре реестра локалей #62 и лениво-загружаемого немецкого
|
||||
прецедента #348. Контракт (К1–К4 в теле issue): файлы словарей
|
||||
(`src/i18n/fr.json`, `src/i18n/fr.ts`, `custom_components/houseplan/translations/fr.json`),
|
||||
запись в `registry.ts` + правки `bundle-manifest.mjs` (retry-токен, `_role`,
|
||||
regex), авто-выбор по HA-локали, правки USER-GUIDE и changelog. AC1–AC5 с
|
||||
указанием доказательства (unit/smoke/gate). User-Visible: yes.
|
||||
|
||||
## Как проверялось
|
||||
|
||||
1. Прочитан `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` целиком — критерии
|
||||
лёгкого трека (§5), обязательные разделы ТЗ (§7.1) и упрощённый шаблон для
|
||||
`small` (проблема · контракт · AC · откат).
|
||||
2. Прочитано тело issue #371 и оба комментария владельца (принятие вклада,
|
||||
S2→S3 с обоснованием трека).
|
||||
3. Сверены технические утверждения ТЗ с текущим кодом на `dev` (`914bb4ed`):
|
||||
`src/i18n/registry.ts`, `src/i18n/de.ts`, `scripts/bundle-manifest.mjs`,
|
||||
`test/i18n.test.mjs`, `docs/USER-GUIDE.md` / `.ru.md`, `docs/CHANGELOG.ru.md`,
|
||||
история коммитов `12cd8a1a` (German locale), `78c60207`/`4a241cef` (#353,
|
||||
entry-fallback).
|
||||
4. Арифметически перепроверена «оценка вклада»: `en.json` на `dev` —
|
||||
1126 ключей; 1026 (в присланном словаре) + 121 (недостающих) − 21
|
||||
(устаревших) = 1126 — сходится, догадкой не является.
|
||||
5. Проверено предыдущее полноформатное ТЗ немецкой локализации (#348, issue +
|
||||
`docs/specs/348-german-localization.md`) как прецедент трека — German шёл
|
||||
**полным** треком (первое использование инфраструктуры #62 для реального
|
||||
языка), French заявлен `small`, ссылаясь на то, что German эту
|
||||
инфраструктуру уже обкатал. Обоснование трека признано состоятельным (см.
|
||||
ниже).
|
||||
6. Прочитан `demo/smoke_german_locale.mjs` — файл существует, ссылка АС3 не
|
||||
висит в воздухе (`ls demo/smoke_*.mjs` → 202 файла).
|
||||
|
||||
Гейты (typecheck/test/build/scripts) на этом этапе не запускались — этап
|
||||
`spec`, продуктового кода ещё нет, запускать нечего.
|
||||
|
||||
## Находки
|
||||
|
||||
### M1 (Medium, в скоупе) — К2 не покрывает второй файл в bundle-manifest.mjs, который тоже хардкодит список локалей
|
||||
|
||||
`scripts/bundle-manifest.mjs` содержит **два** независимых места, где список
|
||||
UI-языков зашит по именам, а не выводится из `LANGUAGE_REGISTRY`:
|
||||
|
||||
- `bundleManifestPlugin`/`editorRuntimeRetryUrlPlugin` — то, что перечисляет
|
||||
К2 (роль `'locale'` по `/src/i18n/de.ts`, `DE_RETRY_ASSET_TOKEN`, regex
|
||||
`de-`). Это ТЗ покрывает: явно расписано, что для `fr` заводится параллельный
|
||||
токен, парная роль и обобщённый regex `(de|fr)-`.
|
||||
- `entryFallbackPlugin` (строки 192–224, конкретно тернарник строк 208–212):
|
||||
локализованный текст «страница устарела, перезагрузите» выбирается по
|
||||
`navigator.language`, но веткой обёрнуты только `ru` и `de` — любой другой
|
||||
язык, включая ещё не существующий на момент написания этого кода, молча
|
||||
получает английский текст. Функция добавлена **после** German (#348) —
|
||||
коммитом `78c60207`/`4a241cef` (issue #353, "lazy delivery survives flaky
|
||||
networks and stale caches"), и её ветки `ru`/`de` — прямое следствие того,
|
||||
что на тот момент это были все три UI-языка. Иными словами: у проекта уже
|
||||
есть прецедент "третий язык получает свою ветку в этом файле", и French
|
||||
станет четвёртым языком без такой ветки, если К2 останется как есть.
|
||||
|
||||
ТЗ формулирует К2 как исчерпывающий список правок именно в
|
||||
`bundle-manifest.mjs`: "FR-токен в retry-плагине..., `_role: 'locale'`...,
|
||||
regex localeRoots обобщается...". Это выглядит как полный список изменений
|
||||
файла, но пропускает второй хардкод в том же файле. Ни один AC (1–5) не
|
||||
покрывает entry-fallback текст — при код-ревью эта ветка просто не будет
|
||||
исполнена ни одним тестом, и дыра проедет в релиз незамеченной, ровно как
|
||||
исчезновение записи толщины или несинхронного числа: не сбой теста, а тихое
|
||||
несоответствие тому, что #353 уже установил как контракт «полноценный язык —
|
||||
значит, локализован transaction-fallback тоже».
|
||||
|
||||
**Что нужно решить, не владельцу.** Это техническое, не продуктовое: добавить
|
||||
`l.startsWith("fr") ? "House Plan a été mis à jour — veuillez recharger la
|
||||
page (Ctrl+F5)."` веткой рядом с `ru`/`de`, обновить троекратную проверку
|
||||
`editorReplacements`/`onboardingReplacements`/`germanReplacements` на четвёртый
|
||||
счётчик — либо явно записать в ТЗ решение «оставляем английский текст для
|
||||
French в этой ветке» с причиной (например: сама ветка emergency-only и не
|
||||
входит в обещание «полноценный язык»). Молчание — не то и не другое.
|
||||
|
||||
### M2 (Medium, в скоупе) — AC1 не решает судьбу двух German-специфичных тестов качества, добавленных как часть оригинального AC #348
|
||||
|
||||
`test/i18n.test.mjs` содержит **шесть** паритетных тестов, зациклённых по
|
||||
`LANGUAGE_REGISTRY` (строки 26–133) — они действительно "расширяются на fr"
|
||||
простым фактом попадания `fr` в реестр, как обещает AC1. Но там же есть **два**
|
||||
теста, написанных именно под немецкий и не читающих реестр вообще:
|
||||
|
||||
- `i18n: German catalog keeps the product glossary and has no translation
|
||||
sentinels` (строки 327–338) — точечные assert'ы конкретных немецких строк
|
||||
(`de['btn.save'] === 'Speichern'` и т.п.) плюс скан на маркеры машинного
|
||||
перевода (`ZXQPH`, `QXZ`, `⟦HP`) и на кириллицу;
|
||||
- `i18n: German values equal to English are explicitly reviewed` (строки
|
||||
340–370) — сверяет множество ключей, где `de[key] === en[key]`, с
|
||||
жёстко прописанным allow-list; появление нового совпадения красит тест —
|
||||
это ловит именно тот класс дефекта, который «0 расхождений плейсхолдеров, 0
|
||||
подозрительного контента» из оценки вклада не проверяет: пропущенный,
|
||||
скопированный из английского ключ, который синтаксически валиден (не пуст,
|
||||
плейсхолдеры совпадают), но семантически не переведён.
|
||||
|
||||
`git log -S` подтверждает: оба теста появились **в одном коммите** с самой
|
||||
немецкой локализацией (`12cd8a1a`), то есть были частью изначального AC
|
||||
German-задачи, а не более поздней надстройкой поверх неё. Раз ТЗ AC1 заявляет
|
||||
«существующий тестовый механизм расширяется на fr» как единый факт, а по
|
||||
факту половина механизма (родовые тесты) расширяется бесплатно, а другая
|
||||
половина (эти два теста) не расширяется вообще — это ровно тот случай
|
||||
«утверждение о поведении, которого нет ни в одном документе, выдано за
|
||||
решение» из инструкции ревью, только с обратным знаком: тут не придумано
|
||||
поведение, а не поставлен вопрос о нужном.
|
||||
|
||||
**Что нужно решить.** Либо АС1 явно включает french-аналоги этих двух тестов
|
||||
(глоссарий ключевых терминов + скан на артефакты машинного перевода/кириллицу
|
||||
+ allow-list явных совпадений с английским для `fr`), либо ТЗ прямо
|
||||
проговаривает, что для French эта дополнительная проверка не пишется сейчас и
|
||||
почему (например: контрибьютор уже прогнал скрипт с эквивалентной
|
||||
эвристикой, а нативная проверка обещана постфактум — согласно комментарию
|
||||
владельца в issue). Оба варианта закрывают находку; отсутствие любого из них —
|
||||
нет.
|
||||
|
||||
### L1 (Low, снимается ревьюером с записью) — формулировка AC5 избыточна и путает мутанта
|
||||
|
||||
"мутант «fr-словарь исключён из паритета»... формулировка проще: мутант
|
||||
«запись fr удалена из LANGUAGE_REGISTRY»" — два предложения описывают, по
|
||||
всей видимости, один и тот же мутант разными словами, оставляя
|
||||
неопределённость, сколько мутантов реально проверяется. Смысл читаем (мутация
|
||||
реестра должна покраснить AC2 и AC3), содержательной проблемы нет — это
|
||||
стилистическая шероховатость, не блокирует. Снимаю без правки: при
|
||||
реализации это разрешится тем, что действительно будет один тест на
|
||||
исчезновение записи `fr`.
|
||||
|
||||
### L2 (Low, снимается ревьюером с записью) — DoR-пункты touch/perf не проговорены явным предложением
|
||||
|
||||
DoR (§2.5 PROCESS.md) требует явно назвать влияние на perf и touch (или явное
|
||||
«нет»). ТЗ покрывает perf через AC4 (`bundle:budget`), но нигде не пишет
|
||||
фразой «touch не затронут». Для чисто словарной правки без нового UI это
|
||||
самоочевидно и уже подтверждается критериями лёгкого трека («нет влияния на
|
||||
touch-контракт» — один из пяти одновременных критериев `small`, названных в
|
||||
самом ТЗ). Не блокирует, добавить одну строку не помешает, но не обязательно.
|
||||
|
||||
## Что проверено и корректно
|
||||
|
||||
- **Классификация трека `small`.** Все пять критериев §5 проверены по
|
||||
отдельности: сложность/риск (единственная точка расширения — реестр,
|
||||
спроектированная под это самим автором инфраструктуры, судя по комментарию
|
||||
в `registry.ts`: «Adding a locale means adding its frontend/backend JSON
|
||||
files and one static entry»); одна поверхность (locale pipeline); миграции
|
||||
конфига нет (язык — уже существующее строковое поле, `languageOptions`
|
||||
сохраняет неизвестные сырые значения); нового UX-контракта нет (селектор
|
||||
языка уже существует, просто получает четвёртый пункт); влияния на
|
||||
perf/touch не заявлено и не просматривается в диффе, который описывает ТЗ.
|
||||
Прецедент German (#348) шёл полным треком именно потому, что обкатывал
|
||||
инфраструктуру впервые — довод, что второй проход по уже проверенному
|
||||
пути дешевле, содержателен, а не удобная отговорка.
|
||||
- **К1 (файлы).** Арифметика вклада проверяется: 1026 (снимок контрибьютора)
|
||||
+ 121 (новые ключи) − 21 (устаревшие) = 1126 = точное число ключей в
|
||||
`src/i18n/en.json` на `dev` сейчас. Это не бездоказательная цифра.
|
||||
Backend-паритет «6/6» тоже сходится: `custom_components/houseplan/translations/en.json`
|
||||
действительно содержит 6 листовых ключей.
|
||||
- **К2 (registry.ts).** Описанный `loadFrench`/токен/запись в
|
||||
`LANGUAGE_REGISTRY` — точная копия существующего `loadGerman`
|
||||
(`src/i18n/registry.ts:20-25,33-37`) и `src/i18n/de.ts` (клон тривиален:
|
||||
импорт JSON + экспорт fingerprint-токена). Технически выполнимо без
|
||||
дополнительных решений.
|
||||
- **К2 (bundle-manifest.mjs, заявленная часть).** Проверено построчно:
|
||||
`_role: 'locale'` определяется по `endsWith('/src/i18n/de.ts')`
|
||||
(`scripts/bundle-manifest.mjs:32`), `localeRoots` — по этой роли или
|
||||
regex `de-` (`:61-62`), `DE_RETRY_ASSET_TOKEN` и проверка счётчика замен
|
||||
1/1/1 в `editorRuntimeRetryUrlPlugin` (`:9,141-172`). Всё, что ТЗ обещает
|
||||
здесь исправить, действительно требует исправления и действительно
|
||||
ограничивается перечисленными тремя точками (не считая M1).
|
||||
- **К3 (авто-выбор).** `resolveLanguageCode` (`src/i18n/registry.ts:119-136`)
|
||||
уже обобщён — точное совпадение, затем primary-код (`locale.split('-')[0]`).
|
||||
Существующий юнит-тест `i18n.test.mjs:135-152` доказывает это для
|
||||
`de-DE`/`de-AT`/`de-CH` без отдельных записей на каждый диалект в реестре —
|
||||
то же самое автоматически сработает для `fr-FR`/`fr-CA`/`fr-BE`/`fr-CH` без
|
||||
дополнительного кода. Утверждение ТЗ «существующая нормализация... юнит
|
||||
фиксирует» не голословно.
|
||||
- **К4 (доки).** Абзац auto-языка в `docs/USER-GUIDE.ru.md:153-157` и
|
||||
зеркальный в `docs/USER-GUIDE.md` действительно построен по шаблону
|
||||
«`auto` учитывает... `de`, `de-DE`, `de-AT` и `de-CH`» — French по той же
|
||||
форме добавляется без изобретения новой терминологии. Прецедента
|
||||
благодарности контрибьютору по нику в changelog нет, но и противоречия
|
||||
стилю нет — это первый внешний вклад такого рода, и тон уже задан
|
||||
комментарием владельца в issue.
|
||||
- **АС3 (смок).** `demo/smoke_german_locale.mjs` существует — ссылка ТЗ не
|
||||
на пустое место.
|
||||
- **Продуктовая рамка.** Локализация интерфейса не входит буквальной строкой
|
||||
в J1–J7 `docs/SCOPE.md`, но прецедент #62/#348 уже прошёл этот вопрос: #62
|
||||
специально спроектирован как обобщённая инфраструктура под будущие языки, а
|
||||
German — первый язык, принятый на нём с P1. Открывать этот вопрос заново на
|
||||
четвёртом языке для того же самого пайплайна избыточно.
|
||||
- **Догадки, выданные за факт.** Не найдено ни одного утверждения о
|
||||
поведении, которого нет в коде/документах и которое не помечено как
|
||||
предположение. Оба технических пробела (M1, M2) — это не придуманное
|
||||
поведение, а нерешённые точки контракта; это ровно та категория, которую
|
||||
инструкция просит решать самому ревьюеру, а не выносить владельцу.
|
||||
|
||||
## Чего не проверял
|
||||
|
||||
- Не открывал приложенный контрибьютором zip-архив (`houseplan-fr-translations-only.zip`)
|
||||
— на этапе ТЗ файлов French в репозитории ещё нет (`ls src/i18n/` подтверждает:
|
||||
только `de.json/de.ts/en.json/ru.json/registry.ts/language-runtime.ts`), проверка
|
||||
реального содержимого словаря — предмет код-ревью, не ТЗ.
|
||||
- Не запускал `npm run typecheck`/`test`/`build` — класс A ещё не тронут,
|
||||
фиксировать зелёный прогон не на чем. Это будет обязательным гейтом
|
||||
код-ревью (#371, следующий этап), не этого.
|
||||
- Не проверял `npm run bundle:budget` вживую (нет собранного бандла с
|
||||
French) — числовой бюджет AC4 по своей природе проверяется только после
|
||||
реализации.
|
||||
- Не оценивал лингвистическое качество французского перевода — вне
|
||||
компетенции ревью ТЗ; process явно оставляет финальную проверку носителем
|
||||
языка на постфактум-ревью после беты (см. комментарий владельца в issue).
|
||||
|
||||
## Вердикт
|
||||
|
||||
Жёлтый: 0 High, 2 Medium в скоупе (M1, M2), оба чинятся правкой текста ТЗ в
|
||||
теле issue — не отдельным issue, без блокировки владельцем. Автор либо
|
||||
дополняет К2/AC1 решением по двум названным пробелам, либо явно фиксирует,
|
||||
почему они не нужны сейчас, и повторно выставляет `S4-spec-review`.
|
||||
Reference in New Issue
Block a user