mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -0,0 +1,212 @@
|
||||
# CODE-REVIEW-371-r2
|
||||
|
||||
Issue: #371 — Add French localization files
|
||||
Заход: r2 · блокирующих циклов израсходовано 0 из 2 (зелёный вердикт цикла не образует, #227)
|
||||
Материал ревью: `1cd84dd0` (HEAD ветки `issue/371-french-locale` на момент разбора; `git merge-base origin/dev HEAD == origin/dev`, т.е. ветка полностью содержит `dev`, дальнейший ребейз не требуется)
|
||||
Ревьюер: Claude (код-ревью, свежая сессия, без контекста реализации)
|
||||
|
||||
## Почему разбор полный, а не по дельте
|
||||
|
||||
r1 код-ревью было **зелёным** (`High: 0 · Medium: 0`, документ
|
||||
`docs/reviews/CODE-REVIEW-371-r1.md`, материал — SHA `3c19ba18`, названный не в
|
||||
самом вердикте, а в следующем комментарии автора; отсутствие SHA в тексте
|
||||
вердикта — процессное наблюдение, см. ниже). Слияние этого зелёного вердикта не
|
||||
удалось: пока шло ревью, `dev` продвинулся на 6 коммитов, и конвейер вернул
|
||||
задачу в `S6-in-progress` не как правку по находке, а как чистый ребейз
|
||||
(комментарий от 2026-08-29T10:35:47Z). Автор перебазировал ветку на
|
||||
`origin/dev`, разрешив конфликты в бандл-деревьях (`bundle:sync`), changelog’ах
|
||||
(union) и скриншотах (`demo/docs/capture.mjs`), и вернул `S7-code-review`.
|
||||
|
||||
По PROCESS.md §7.2/§10.4 это ровно тот случай, когда «разбор остаётся полным»:
|
||||
ребейз на ушедший вперёд `dev` — другой код, а не локальная дельта. Поэтому
|
||||
этот документ — не сокращённый разбор по дельте r1→r2, а полный независимый
|
||||
код-ревью текущего дерева, как будто это первый заход. Разделы «Закрытие
|
||||
раунда r1» и «Унаследовано из r1» ниже присутствуют по формату r2+, но
|
||||
содержательно короткие: r1 не оставил ни одной находки для закрытия, и ничего
|
||||
не принято без собственной перепроверки в этом раунде.
|
||||
|
||||
## Скоуп диффа
|
||||
|
||||
`git diff origin/dev...HEAD` — 45 файлов, класс A: `src/i18n/fr.json` (новый,
|
||||
1128 ключей), `src/i18n/fr.ts` (новый, клон `de.ts`), `src/i18n/registry.ts`
|
||||
(+`loadFrench`, +`FRENCH_RETRY_ASSET`, +запись реестра),
|
||||
`custom_components/houseplan/translations/fr.json` (новый). Класс B:
|
||||
`scripts/bundle-manifest.mjs` (locale-role/localeRoots/retry-guard/
|
||||
entryFallbackPlugin обобщены на `fr`), `scripts/mutation-gate.mjs` (новый
|
||||
мутант `french-locale-wrong-dictionary`), `scripts/smoke-links.mjs`
|
||||
(`smoke_french_locale.mjs` добавлен в существующую запись локалей),
|
||||
`demo/smoke_french_locale.mjs` (новый), `demo/smoke_entry_stale.mjs` (fr-прогон),
|
||||
`test/i18n.test.mjs` (+резолвер fr-профилей, +2 French-персональных теста),
|
||||
`test/bundle-assets.test.mjs` (fr-ветка retry-guard теста). Класс C:
|
||||
`docs/USER-GUIDE(.ru).md`, `docs/CHANGELOG(.ru).md`, `docs/images/*` +
|
||||
`screenshots.json` (пересъёмка отпечатка после ребейза). Класс D: `dist/**`,
|
||||
`custom_components/houseplan/frontend/**` — байт-в-байт пересобраны.
|
||||
|
||||
Геометрия, `layout`, `marker.space`, `open_spans`, толщина стен — не
|
||||
затронуты. Python-код (`custom_components/houseplan/**/*.py`) — не затронут,
|
||||
только новый JSON перевода. Видимый рендер/геометрия/стили карточки — не
|
||||
меняются (добавляется только выбираемый язык интерфейса).
|
||||
|
||||
## Гейты — прогнаны лично на SHA `1cd84dd0`
|
||||
|
||||
| Гейт | Команда | Результат |
|
||||
|---|---|---|
|
||||
| typecheck | `npx tsc --noEmit` | зелёный, без вывода |
|
||||
| unit | `npm test` | `1546 tests, 1545 pass, 0 fail, 1 skipped` — совпадает с заявленным автором |
|
||||
| build + бандл | `npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` | сборка зелёная, `cmp` без вывода (побайтовое совпадение) |
|
||||
| bundle:budget | `npm run bundle:budget` | initial **273 927 / 300 000 Б** gzip, lazy locale 45 200 Б — совпадает с хендоффом |
|
||||
| check-docs | `node scripts/check-docs.mjs` (обязателен — diff трогает `src/**`) | «Documentation checks passed (7 files, 10 external links)» |
|
||||
| no-new-any | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | «Новых any нет» (15 добавленных строк в 2 файлах) |
|
||||
| smoke-select | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | НЕОПРЕДЕЛЁННОСТЬ по `FRENCH_RETRY_ASSET`, `LazyLanguageModule`, `loadFrench` — разбор ниже |
|
||||
| смок AC3 (прямое совпадение) | `node demo/smoke_french_locale.mjs` (после `npm run bundle:sync` — первый прогон без свежего стенда падал по стухшему бандлу, ожидаемо) | все 5 проверок `true`, `OK` |
|
||||
| смок (зарегистрированная связь) | `node demo/smoke_entry_stale.mjs` | `OK`, включая `frenchMessageVisible` |
|
||||
| смок (регресс на общем de/fr коде) | `node demo/smoke_german_locale.mjs` | `OK`, все 9 проверок `true` |
|
||||
| мутант AC5 | `node scripts/mutation-gate.mjs --id=french-locale-wrong-dictionary` (целевой прогон одного мутанта, не полный дорогой набор) | «поймано 1 из 1»: чистый прогон смока зелёный, мутант красит `smoke_french_locale.mjs`, как обязан |
|
||||
| inventory | `npm run inventory` | 1546 unit / 213 pure-backend / 150 HA-harness / 203 browser-smoke — счётчики не переписывались вручную |
|
||||
|
||||
Разбор `smoke-select`: инструмент не находит `FRENCH_RETRY_ASSET`,
|
||||
`LazyLanguageModule`, `loadFrench` ни в одном смоке текстовым совпадением. Это
|
||||
не новый пробел: German-аналоги (`GERMAN_RETRY_ASSET`, `loadGerman`) точно так
|
||||
же не перечислены в `scripts/smoke-links.mjs` — там регистрируются символы
|
||||
рантайм-уровня (`LanguageRuntime`, `dictionaryFor`, `ensureLanguage`), а не
|
||||
внутренние функции-загрузчики реестра. Автор корректно расширил ровно ту же
|
||||
запись, добавив `smoke_french_locale.mjs` вторым смоком (диф
|
||||
`scripts/smoke-links.mjs`), — это «зарегистрированная связь» в терминологии
|
||||
инструмента, и она уже покрывает `loadFrench`/`FRENCH_RETRY_ASSET` косвенно:
|
||||
я прогнал `smoke_french_locale.mjs` напрямую (см. таблицу) и он проверяет
|
||||
именно этот путь (fr-чанк грузится ровно раз, `_t` отдаёт французский текст).
|
||||
Не гейт, а решение ревьюера засчитать связь достаточной без правки реестра.
|
||||
|
||||
Не прогонялось и почему:
|
||||
- `python -m pytest tests_backend -q` — ни один `.py`-файл не тронут, только
|
||||
`custom_components/houseplan/translations/fr.json` (не код);
|
||||
- `node scripts/model-invariants.mjs` — геометрия/`layout`/толщина стен/
|
||||
`marker.space`/`open_spans` не затронуты этим диффом;
|
||||
- `npm run golden:verify` — видимый рендер карточки не меняется (новый язык —
|
||||
не новый визуал; геометрия и стили не тронуты);
|
||||
- performance-профили — не названы в AC, чувствительные к перфу пути (canvas,
|
||||
layout) не тронуты;
|
||||
- полный `ls demo/smoke_*.mjs` (203 файла) — диапазон диффа локален
|
||||
(i18n/manifest/registry), прогон всей матрицы это предрелизный гейт (§8), не
|
||||
гейт ревью; выбраны прямое совпадение + зарегистрированная связь + один
|
||||
смежный смок вручную (регресс на общем German/French коде манифеста), как и
|
||||
в r1.
|
||||
|
||||
**Одно число — один источник.** Дифф не добавляет и не меняет ни одной
|
||||
пользовательской величины (только текстовые строки интерфейса и служебные
|
||||
токены сборки) — проверка неприменима, новых чисел для сверки нет.
|
||||
|
||||
## Проверка AC (по тексту ТЗ ревизии 2, комментарий S3→S4)
|
||||
|
||||
- **AC1** (паритет словаря + French-персональные тесты): проверено
|
||||
независимым скриптом, не доверяя заявлению автора — `en.json`/`fr.json`:
|
||||
1128/1128 ключей, 0 пустых, 0 расхождений плейсхолдеров, порядок ключей
|
||||
идентичен. `test/i18n.test.mjs` содержит французский глоссарий (`btn.save`,
|
||||
`space.header`, `opening.passage` и др.) и скан на кириллицу/сентинелы +
|
||||
allow-list из 22 омографов — прогнан `npm test`, зелёный. Точечно
|
||||
перепроверил 5 записей allow-list (`color_picker.saturation`, `gs.unit_m`,
|
||||
`wallthick.unit_cm`, `furn.cat_bidet`, `run.script`) — все действительно
|
||||
идентичны en (единицы измерения/бренд-термины), не забытый перевод.
|
||||
Доказано автотестом, тест умеет падать (мутант ниже красит именно этот
|
||||
контракт на уровне смока, юнит-паритет красится удалением ключа — проверено
|
||||
чтением структуры теста, не отдельным прогоном мутации паритета).
|
||||
- **AC2** (реестр): `test/i18n.test.mjs` содержит `resolveLanguageCode` для
|
||||
`fr-FR/fr-CA/fr_BE/fr-CH`, `langOf` для `fr-CA` — прогнано `npm test`,
|
||||
зелёное. `LANGUAGE_REGISTRY` — ровно en/ru/de/fr (прочитано в
|
||||
`src/i18n/registry.ts`).
|
||||
Доказано автотестом.
|
||||
- **AC3** (смок реальным бандлом): `demo/smoke_french_locale.mjs` прогнан лично
|
||||
на свежем `bundle:sync` — fr-CA → французский, `btn.save`→`Enregistrer`,
|
||||
`lang`-атрибут `fr`, fr-чанк запрошен ровно 1 раз, initial-граф без fr,
|
||||
lazy-граф с fr. Доказано автотестом, прогнан лично.
|
||||
- **AC4** (бюджет): `npm run bundle:budget` — fr в `lazyLocaleFiles`, initial
|
||||
273 927 Б (в пределах 300 000, +0,5 КБ к строке дифф-хендоффа автора —
|
||||
различие в пределах шума пересборки). Доказано прогоном.
|
||||
- **AC5** (мутант): `french-locale-wrong-dictionary` в
|
||||
`scripts/mutation-gate.mjs` (замена `import('./fr')` на возврат английского
|
||||
словаря при живом импорте — вторая версия мутанта после того, как первая
|
||||
ломала саму сборку, а не смок, что зафиксировано в коммитах `7f3e67bc`/
|
||||
`c7dfd712`). Прогнан лично целевым `--id=french-locale-wrong-dictionary`:
|
||||
«поймано 1 из 1», тест умеет падать — не голословное заявление автора.
|
||||
|
||||
`entryFallbackPlugin` (М1 из ревью ТЗ r1) и French-персональные тесты (М2 из
|
||||
ревью ТЗ r1) — обе спек-находки закрыты в коде, проверено чтением
|
||||
(`scripts/bundle-manifest.mjs:224-225` — французская ветка ternary с текстом
|
||||
«House Plan a été mis à jour…», плюс fr-прогон в `demo/smoke_entry_stale.mjs`,
|
||||
прогнан лично, зелёный).
|
||||
|
||||
## Закрытие раунда r1
|
||||
|
||||
r1 (код-этап) — зелёный вердикт, **High: 0, Medium: 0**. Находок, которые
|
||||
требовалось бы закрывать в r2, у r1 не было; таблица «находка → чем закрыта»
|
||||
пуста по существу. Единственное процессное наблюдение к r1: вердикт-комментарий
|
||||
не назвал SHA материала (см. ниже) — это не находка по коду, а пробел в
|
||||
оформлении вердикта, не влияющий на итог, поскольку код перепроверен заново в
|
||||
этом раунде целиком.
|
||||
|
||||
## Унаследовано из r1
|
||||
|
||||
Ничего не принято без собственной перепроверки. Причина — не сокращение
|
||||
объёма по §2.10 (обычная делегированная дельта), а прямое указание процесса:
|
||||
ребейз на ушедший вперёд `dev` делает материал другим кодом (§7.2), поэтому
|
||||
весь список AC, все гейты и все находки ревью ТЗ перепроверены заново в этом
|
||||
документе (таблица гейтов и раздел AC выше), а не сославшись на
|
||||
`docs/reviews/CODE-REVIEW-371-r1.md` (SHA `3c19ba18`, предребейзный, самим
|
||||
вердиктом не назван — восстановлен по комментарию автора от
|
||||
2026-08-29T10:35:47Z «Материал ревью — `3c19ba18`»).
|
||||
|
||||
## Находки
|
||||
|
||||
Нет находок High. Нет находок Medium — ни в скоупе, ни вне его.
|
||||
|
||||
**Low (процессное наблюдение, не по коду задачи):** вердикт код-ревью r1 не
|
||||
назвал SHA материала прямо в тексте комментария (требование §2.10/#312 —
|
||||
«SHA в вердикте не назван — это находка»). SHA восстановим по соседнему
|
||||
комментарию автора того же раунда, поэтому не блокирует и не искажает вывод:
|
||||
полный повторный разбор в r2 из-за ребейза делает вопрос неактуальным для
|
||||
самой задачи #371. Снимаю без правки — адресовать в механике публикации
|
||||
вердиктов, а не в этой задаче (чужой скоуп: инфраструктура ревью, не код
|
||||
French-локали).
|
||||
|
||||
## Что проверено и корректно
|
||||
|
||||
- Полный паритет словарей (фронт 1128/1128, бэкенд 6/6), без пустых значений,
|
||||
без расхождений плейсхолдеров, порядок ключей = en.json — перепроверено
|
||||
независимым скриптом, не только словами автора.
|
||||
- Обобщение German-механики на French в `bundle-manifest.mjs`
|
||||
(`_role: 'locale'`, `localeRoots`, retry-guard 1/1/1/1, `entryFallbackPlugin`)
|
||||
— построчно сверено с рабочим de-эквивалентом, консистентно.
|
||||
- `src/i18n/fr.ts` — точный клон `de.ts` (сверено побайтово текстом).
|
||||
- Автоматический выбор French по профилю HA (`fr`, `fr-FR`, `fr-CA`, `fr-BE`,
|
||||
`fr-CH`) — юнитами, зелёные.
|
||||
- `demo/smoke_french_locale.mjs`, `smoke_entry_stale.mjs` (fr-ветка),
|
||||
`smoke_german_locale.mjs` (регресс на общем коде) — прогнаны лично, зелёные.
|
||||
- Мутант AC5 — прогнан лично целевым `--id`, красит смок, как обязан.
|
||||
- Оба changelog (en+ru) и `USER-GUIDE(.ru).md` правлены в том же
|
||||
feature-коммите (`89fa8ca5`, `User-Visible: yes`), терминология
|
||||
(«Français», «lazy dictionaries») согласована с существующим German-текстом.
|
||||
- Трейлеры на всех коммитах диапазона (`Issue: #371`, ровно один
|
||||
`User-Visible: yes|no`) — прочитаны `git log --format`, соответствуют
|
||||
классам изменений.
|
||||
- `demo/srv/assets/**` не закоммичен (соответствует #255); `dist/**` и
|
||||
`custom_components/houseplan/frontend/**` побайтово совпадают со свежей
|
||||
локальной сборкой (`cmp`, без вывода).
|
||||
- Ребейз выполнен корректно: `git merge-base origin/dev HEAD == origin/dev`,
|
||||
конфликтов для слияния больше нет.
|
||||
|
||||
## Что не проверялось
|
||||
|
||||
- Лингвистическое качество перевода — вне код-ревью; носитель (@OUARZA)
|
||||
проверит 121 добавленный автором ключ постфактум по решению владельца,
|
||||
зафиксированному в этом же issue (комментарий 2026-08-29T10:26:18Z).
|
||||
- `pytest tests_backend`, `model-invariants`, `golden:verify`, performance —
|
||||
не запускались, обоснование см. в таблице гейтов выше (диапазоны диффа их не
|
||||
задевают).
|
||||
- Полная матрица `smoke_*.mjs` (203 файла) — не предмет код-ревью, это
|
||||
предрелизный гейт.
|
||||
- Дорогой полный прогон `mutation-gate.mjs` без `--id` (все мутанты проекта) —
|
||||
не гейт код-ревью; для AC5 достаточен целевой прогон, выполнен.
|
||||
|
||||
## Вердикт
|
||||
|
||||
Зелёный. High: 0, Medium: 0. Задача уходит в очередь на пре-релиз.
|
||||
Reference in New Issue
Block a user