21 KiB
SPEC-REVIEW-371-r1
Issue: #371 — Add French localization files
Трек: 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.
Как проверялось
- Прочитан
docs/SCOPE.md,AGENTS.md,PROCESS.mdцеликом — критерии лёгкого трека (§5), обязательные разделы ТЗ (§7.1) и упрощённый шаблон дляsmall(проблема · контракт · AC · откат). - Прочитано тело issue #371 и оба комментария владельца (принятие вклада, S2→S3 с обоснованием трека).
- Сверены технические утверждения ТЗ с текущим кодом на
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). - Арифметически перепроверена «оценка вклада»:
en.jsonнаdev— 1126 ключей; 1026 (в присланном словаре) + 121 (недостающих) − 21 (устаревших) = 1126 — сходится, догадкой не является. - Проверено предыдущее полноформатное ТЗ немецкой локализации (#348, issue +
docs/specs/348-german-localization.md) как прецедент трека — German шёл полным треком (первое использование инфраструктуры #62 для реального языка), French заявленsmall, ссылаясь на то, что German эту инфраструктуру уже обкатал. Обоснование трека признано состоятельным (см. ниже). - Прочитан
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, regexde-). Это ТЗ покрывает: явно расписано, что для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 листовых ключей.
- 121 (новые ключи) − 21 (устаревшие) = 1126 = точное число ключей в
- К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— по этой роли или regexde-(: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.