17 KiB
CODE-REVIEW-371-r1
Issue: #371 — Add French localization files
Этап: code (PROCESS.md §2.7)
Заход: r1 · блокирующих циклов израсходовано 0 из 2 (лёгкий трек, бюджет §4 = 2)
SHA на момент ревью: 3c19ba1881a3fc3bb7f0a504d9ef332c3ccce192
База: origin/dev
Коммиты в диапазоне:
fd673b32feat: French localization — community contribution by @OUARZA (#371) —User-Visible: yesa29d9bc6test: pin the fr entry to the fr dictionary via a live-bundle mutant (#371) —User-Visible: no3c19ba18test: keep the fr chunk emitted in the #371 mutant (wrong dictionary via en) —User-Visible: no
Это первый заход код-ревью для #371 (спек-ревью уже прошло два захода r1/r2 и
закрылось зелёным на этапе spec); правило «объём по дельте» (§2.9) на код-этап
не переносится — здесь начинается собственный счётчик циклов и полный разбор.
Скоуп
Класс A (src/i18n/fr.json, src/i18n/fr.ts, src/i18n/registry.ts,
custom_components/houseplan/translations/fr.json) + класс B (scripts/bundle-manifest.mjs,
scripts/mutation-gate.mjs, scripts/smoke-links.mjs, demo/smoke_french_locale.mjs,
demo/smoke_entry_stale.mjs, test/i18n.test.mjs, test/bundle-assets.test.mjs) +
класс C (docs/CHANGELOG.md, docs/CHANGELOG.ru.md, docs/USER-GUIDE.md,
docs/USER-GUIDE.ru.md) + класс D (три копии бандла, docs/images/*,
docs/images/screenshots.json — регенерированы check-docs/capture.mjs
из-за роста sourceFingerprint над src/**, ожидаемо).
Задача — French как четвёртая (третья ленивая) локаль по контракту реестра
#62, буквальный клон механики German (#348). Соответствует core user job
J1/J4 (docs/SCOPE.md): расширяет доступность интерфейса без нового UX-контракта.
Как проверялось
Прогнано лично, гейты дешёвые, результаты — команда и вывод:
| Гейт | Команда | Результат |
|---|---|---|
| Типы | npx tsc --noEmit |
чисто, без ошибок |
| Юниты | npm test |
# tests 1544 / pass 1543 / fail 0 / skipped 1 (пропуск не связан с задачей — существовал до неё) |
| Сборка | npm run build |
успех, dist создан за 14.5s |
| Синхронизация бандла | git status --short после npm run build |
пусто — закоммиченный dist побайтово совпадает с пересобранным; отдельно сверены dist/houseplan-assets, custom_components/houseplan/frontend/houseplan-assets, dist/houseplan-assets.json, dist/houseplan-card.js через diff -rq/diff — расхождений нет |
| Бюджет | npm run bundle:budget |
initial View: 273826 B gzip (budget 300000, headroom 26174), lazy locale: 45215 B gzip — совпадает с числом автора (273 826) |
| Доки-отпечаток | node scripts/check-docs.mjs |
Documentation checks passed (7 files, 10 external links) |
| Выбор смоков | 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, т.к. стенд был устаревшим) |
OK, все 5 полей true |
| Смок AC (fallback) | node demo/smoke_entry_stale.mjs |
OK |
| Смок регресс | node demo/smoke_german_locale.mjs (немецкая ветка делит код с новой — проверка отсутствия регресса) |
OK, все 9 полей true |
| Мутант AC5 (дёшево) | node scripts/mutation-gate.mjs --check --id=french-locale-wrong-dictionary |
ok french-locale-wrong-dictionary (патч ложится на текущий код) |
| Паритет словаря (независимый скрипт, не доверяю заявлению автора на слово) | inline Python: сравнение ключей/порядка/плейсхолдеров en.json vs fr.json |
en keys 1128, fr keys 1128, missing/extra: [], order matches: True, empty fr values: 0, placeholder mismatches: 0 |
| Артефакты не-перевода (независимо) | inline Python: кириллица + сентинелы в fr.json |
cyrillic hits: [], sentinel hits: [] |
| Allow-list "равно en" (независимо) | inline Python: множество ключей с en[k]==fr[k] |
ровно те же 22 ключа, что в test/i18n.test.mjs allow-list — совпадение полное |
| Паритет бэкенда | inline Python: flatten custom_components/houseplan/translations/{en,fr}.json |
en keys 6, fr keys 6, missing/extra: [] |
| Манифест | inline Python по dist/houseplan-assets.json |
fr-BSq3jD_I.js в lazyLocaleFiles, отсутствует в initialViewFiles |
| Трейлеры | git log по трём коммитам |
Issue: #371 и User-Visible на всех трёх; User-Visible: yes (fd673b32) правит оба changelog в том же коммите (подтверждено диффом) |
Не проверялось и почему:
python -m pytest tests_backend -q— не запускался: diff не трогаетcustom_components/houseplan/**/*.py, только статический JSON перевода.npm run invariants(модельные инварианты, #254) — не запускался: diff не касается геометрии (нет правок рёбер комнат,layout,marker.space,open_spans, толщины стен) — ни один из грепов по этим токенам не дал совпадений в diff.npm run golden:verify— не запускался: видимый рендер плана (геометрия, стили, слои) не меняется; единственное видимое изменение — новый набор строк словаря за новым языковым тегом, который golden не покрывает.- Полный
node scripts/mutation-gate.mjs(без--check) — не запускался: документация самого скрипта прямо называет полный прогон дорогим (пересборка бандла в отдельном worktree) и относит его место к пред-релизному гейту (.github/workflows/mutation-gate.yml), не к ревью каждой беты; проверено дешёвой частью (--check, применимость патча) плюс отчёт автора «поймано 1/1» с описанием, как чинился инфраструктурный ложный провал мутанта (a29d9bc6→3c19ba18— тришейкингfr.tsвалил сборку раньше, чем гард успевал сработать; переработка обоснованная). - Полный набор
demo/smoke_*.mjs— не запускался:smoke-select.mjsподтверждает, что диапазон изменений локален (3 символа проекта, 2 файлаsrc/**), широкий прогон не оправдан задачей уровняsmall. Выбраны: прямое совпадение (smoke_french_locale.mjs, назван в AC3), зарегистрированная связь (smoke_entry_stale.mjs— фолбэк-плашка теперь четырёхветочная), плюсsmoke_german_locale.mjsвручную — кодde/frделит одну и ту же функцию сборки роли/локейл-рутов и ретрай-плагин, регрессия по соседству была бы правдоподобна. - «НЕОПРЕДЕЛЁННОСТЬ» инструмента (
loadFrench,FRENCH_RETRY_ASSET,LazyLanguageModule) — решение ревьюера: эти символы — прямые аналогиloadGerman/GERMAN_RETRY_ASSET, которые тоже нигде не зарегистрированы вscripts/smoke-links.mjs(там только общеконтрактныеLanguageRuntime,dictionaryFor,ensureLanguage— уже расширены наsmoke_french_locale.mjsавтором).smoke_french_locale.mjsпрямым исполнением проверяет именно то, что реализуют эти три символа (один ленивый fr-чанк на профиль fr-CA, отсутствие в initial-графе) — прогнан выше и зелёный. Дополнительной записи вsmoke-links.mjsне требуется по той же причине, по которой её нет для German-аналогов: связь уже находится прямым совпадением имени смока с темой. - Лингвистическое качество перевода — вне ревью кода по решению владельца в этом же issue (носитель, @OUARZA, проверит после публикации беты).
demo/smoke_opening_measure.mjsи прочие несвязанные смоки — задача не трогает геометрию/измерения, тема не пересекается.
Находки
Нет. High: 0, Medium: 0.
Отдельно проверены места, где легко было бы найти скрытую деградацию:
- Порядок ветвей в
entryFallbackPlugin(scripts/bundle-manifest.mjs:216-225): тернарникru → de → fr → en— французская ветка вставлена перед финальным английским умолчанием, а не после (что было бы недостижимым кодом). Смокsmoke_entry_stale.mjsподтверждает исполнение этой ветки. - Роль чанка (
_role: 'locale') (scripts/bundle-manifest.mjs:33): дизъюнкцияde.ts || fr.tsне меняет порядок остальных проверок (onboarding/editor) — де-факто проверено юнитомtest/bundle-assets.test.mjsи совпадением манифеста в бандле. - Мутант AC5 пришлось дважды перепроектировать в процессе (см. коммиты
a29d9bc6,3c19ba18): первая версия валила сборку раньше, чем успевал сработать смок-гард — это не найдено ревью как отдельная находка, потому что автор исправил это сам до подачи на ревью, а итоговый мутант (--checkподтверждён) действительно проверяет то, что заявлено вbecause: «профиль читает не тот словарь» ловится только браузерным смоком, не паритет-юнитами. - Единственное число, видимое дважды: не применимо — задача не добавляет
новую пользовательскую величину (только текстовые строки нового языка);
test/single-source-numbers.test.mjsне тронут и остаётся зелёным в общем прогоне юнитов.
Что проверено и корректно
- К1 (файлы).
src/i18n/fr.json: 1128/1128 ключей, порядок идентиченen.json, пустых значений и расхождений плейсхолдеров нет (независимая проверка скриптом, не только заявление автора).src/i18n/fr.ts— побуквенный клонde.tsс фингерпринт-хендшейком.custom_components/houseplan/translations/fr.json— 6/6 ключей, полный паритет сen.jsonбэкенда (независимая проверка). - К2 (реестр и сборка).
loadFrench/FRENCH_RETRY_ASSET— точная копия German-пути;localeRootsобобщён на(de|fr)-;_roleучитывает оба файла; retry-плагин требует ровно 1/1/1/1 замену (было 1/1/1) и падает иначе — юнитtest/bundle-assets.test.mjsэто фиксирует.entryFallbackPluginполучил французскую ветку (закрывает M1 спек-ревью r1) — подтверждено и текстом кода, и зелёнымsmoke_entry_stale.mjs. - К3 (авто-выбор).
fr,fr-FR,fr-CA,fr-BE,fr-CH→frпроверено юнитомtest/i18n.test.mjs(resolveLanguageCode,langOf) и смоком (fr-CAв реальном бандле). - К4 (доки).
USER-GUIDE.md/.ru.mdназывают французский в абзаце авто-языка с указанием тегов; обаCHANGELOGполучили запись с благодарностью автору вклада, в том же user-visible коммите. - AC1 — паритет доказан скриптом (см. таблицу), плюс французский
глоссарий продуктовых терминов и скан кириллицы/сентинелов
(
test/i18n.test.mjs:377-421) — оба зелёные, независимо перепроверены значения (btn.save→Enregistrerи т.д.) и allow-list из 22 «легитимных омографов» — множество совпадает бит-в-бит. - AC2 —
languageEntry('fr-CA')→frпокрыт в цикле по всем кодам реестра (test/i18n.test.mjs:56черезlanguageEntry(entry.code.toUpperCase()), выполняется и дляfr); реестр — ровно 4 записи (en/ru/de/fr), явных дубликатов кодов юнит проверяет черезSet(i18n: registry codes and English fallback are valid). - AC3 —
smoke_french_locale.mjsпрогнан лично: fr-CA-профиль коммитит французский, один запрос fr-чанка на страницу, initial-граф без fr; сценарий отказа не дублируется — доказан generic-веткой вsmoke_entry_stale.mjsс французским текстом. - AC4 —
npm run bundle:budget: initial 273 826 Б (совпадает с числом автора и headroom не тронут), lazy-locale выросла на fr-чанк (45 215 Б суммарно de+fr). - AC5 — мутант
french-locale-wrong-dictionaryвscripts/mutation-gate.mjs:683-693применяется к текущему коду (--check); по заявлению автора и описанию гарда ловится толькоsmoke_french_locale.mjs, что согласуется с архитектурой (паритет-юниты видят только статический импорт, не рантайм-подмену словаря).
Гейты класса A/B зелёные без исключений; регрессии в German-пути (общий код) не найдено; трейлеры и правило двух changelog'ов соблюдены.
Вердикт
Все AC (К1–К4, AC1–AC5) доказаны — либо автотестом, для которого проверена
способность падать (мутант AC5, юниты паритета/глоссария), либо независимой
перепроверкой скриптом поверх заявлений автора (парность словарей, манифест,
allow-list). Находок нет. Задача не расширяет и не сужает продуктовый скоуп
за пределы контракта реестра #62, соответствует docs/SCOPE.md (J1/J4) и не
деградирует German-путь.