Files
easy-png-tools2/docs/plan-preview-i18n.md
T
2026-09-13 09:29:28 +05:00

24 KiB
Raw Blame History

План: i18n в preview (новый UI)

Статус: готов к реализации (вопросы §5 решены). Соответствует беклог-задаче «добавить i18n в preview» (docs/backlog.md). Реализация не начата.

1. Что получается

Новый UI (web/src/routes/** кроме v1, web/src/lib/components/** кроме v1) переходит на существующий модуль lib/i18n: переключатель RU/EN в шапке работает по-настоящему, все строки локализованы, ошибки уже локализуются. Инфраструктура i18n уже есть и проверена (v1-ветка) — задача в том, чтобы подключить к ней preview и закрыть пробелы словарей.

До начала локализации v1 изолируется: у v1 появляется полная замороженная копия lib/i18nlib/v1/i18n, у нового UI — своя независимая ветка lib/i18n, которую дальше меняем без оглядки на v1 (Фаза 0). Переключённая локаль (localStorage) остаётся общей для обеих версий — язык переживает переход между страницами.

2. Текущее состояние (исследование)

Уже есть

  • lib/i18n/locale.svelte.ts — состояние локали (runes), персист в localStorage, обновление document.documentElement.lang.
  • lib/i18n/t.ts — перевод по точечному ключу с фолбэком на базовую локаль и интерполяцией {var}.
  • lib/i18n/dict.ts — тип Dict, BASE_LOCALE = "en" (база — английский, ru — перевод), LOCALES = ["ru", "en"].
  • lib/i18n/ru.ts (~1351 строка) и en.ts (188 строк) — полные переводы инструментов v1 по id (title/description/params/options/results).
  • lib/i18n/tool-strings.ts — хелперы для v1 (toolTitle/toolDescription/ paramLabel/optionLabel/searchDoc).
  • lib/i18n/matching.ts — кросс-языковой поиск (ё→е, диакритика, скоринг).
  • Тесты: i18n.test.ts, tools-i18n.test.ts, matching.test.ts, smoke-i18n.test.ts, search-coverage.test.ts.
  • +layout.svelte уже вызывает initLocale() на клиенте.

Пробелы в новом UI

  • LangToggle.svelte — заглушка с локальным let lang = $state("RU"), не связана с locale.svelte.ts.
  • Новый UI почти целиком на хардкод-английском:
    • ~150+ строк по ~30 компонентам и маршрутам (заголовки, кнопки, аria, плейсхолдеры, подписи секций);
    • 122 инструмента с английскими title/description в lib/registry/*.ts;
    • ~60+ подписей опций select;
    • ~30 заголовков layout-групп (title: "Canvas", "Background", …);
    • вердикты анализаторов — raw English строки ("Yes — valid PNG signature."), при том что v1 возвращает ключи словаря ("verifyYes") и рендерит их через tools[id].results;
    • labelOf(id) в SchemaFields.svelte генерирует подписи полей из field ID (keepAspect → "Keep Aspect");
    • категории в catalog.ts (GROUP_LABELS) — English uppercase, хотя в dict уже есть ключи categories.X.
  • Из всех новых компонентов t() вызывает только SchemaToolView.svelte, и только для ошибок (ToolError.key).

Совпадение регистров

  • Новый registry и старый v1 registry используют одинаковые id инструментов (resize-png, flip-png, …) — переводы title/description из v1 ru.ts служат исходником для новой ветки lib/i18n/ru.ts (копирование данных при наполнении Фазы 2, не общий модуль).
  • Расхождение: param ids в ru.ts (width/height/keepAspect) не равны field ids новоно schema (size dimension, anchor). Нужны новые переводы полей.
  • ru.ts tools[id].results для анализаторов уже содержит ключи (verifyYes, grayscaleNo, orientationPortrait и т.д.).

Архитектурные ограничения

  • Изоляция old↔new (isolation/no-mixed-imports): сейчас lib/i18n/ — shared. По решению задачи (Фаза 0) i18n форкается: v1 получает полную копию lib/v1/i18n, а lib/i18n переходит в ветку «new» (правило изоляции запретит старому коду импортить новый i18n). До форка в общем модуле два скрытых v1-импорта: dict.ts тянет CategoryId из ../v1/categories, tool-strings.ts$lib/v1/registry; после форка оба живут только в копии. Новому UI нужен параллельный schema-tool-strings.ts поверх $lib/registry.
  • Tone detection в SchemaTextResult.svelte: /^(yes|true)/i по value — сломается при русских вердиктах («Да», «Нет»).
  • tools-i18n.test.ts покрывает только v1 TOOLS; нужен аналог для нового registry.
  • known-issues.spec.ts: test.fixme("error message is localized, not a raw i18n key").

3. Как делаем

Принципы:

  • Мелкие атомарные коммиты (правило AGENTS.md); самому не коммитить — коммитит разработчик после ревью.
  • Изменения «снизу вверх»: сначала общий слой i18n, потом словари, потом UI.
  • Каждый этап заканчивается зелёными pnpm --dir web lint + pnpm --dir web check + тесты.
  • Решения по §5 зафиксированы и учитываются с первых правок словарей.

4. Фазы

Фаза 0: Форк i18n для v1 + подготовка общего слоя

Выполняется строго до остальных фаз, без изменения поведения UI. Цель — два независимых i18n-слоя: v1 получает замороженную копию, новый UI — свою ветку, которую дальше меняем свободно. На выходе фазы — зелёные lint, check, test, test:rules.

  • 0.1 Форк: скопировать web/src/lib/i18n/web/src/lib/v1/i18n/ (полный снимок: dict.ts, en.ts, ru.ts, t.ts, locale.svelte.ts, matching.ts, tool-strings.ts + все 5 тестов). v1 работает дальше только с копией — поздние правки нового i18n её не затрагивают.
  • 0.2 Привести копию к v1-канону (относительные пути от нового места):
    • копия dict.ts: импорт CategoryId ../v1/categories../categories (= lib/v1/categories);
    • копии тестов (tools-i18n.test.ts, search-coverage.test.ts, smoke-i18n.test.ts): ../v1/registry../registry (= lib/v1/registry);
    • импорт $lib/v1/registry в копии tool-strings.ts остаётся валидным;
    • перепроверить остальные относительные и $lib-пути в копии.
  • 0.3 Переключить импорты старого UI: все $lib/i18n/* в routes/v1/** и lib/v1/components/** (39 импортов в 23 файлах) → $lib/v1/i18n/*.
  • 0.4 Очистить новый lib/i18n/ от v1-специфики, иначе isolation поймает new→old:
    • удалить tool-strings.ts (завязан на v1-типы ParamDef/ToolEntry; для нового UI вместо него schema-tool-strings.ts, см. 0.8);
    • удалить v1-центричные тесты tools-i18n.test.ts, search-coverage.test.ts, smoke-i18n.test.ts (они импортируют ../v1/registry; живут только в копии);
    • остаются: dict.ts, en.ts, ru.ts, t.ts, locale.svelte.ts, matching.ts, i18n.test.ts, matching.test.ts.
  • 0.5 Обновить правило изоляции и конфиги:
    • isolation/no-mixed-imports.js: добавить lib/i18n/** в DEFAULT_NEW (копия по-прежнему old через lib/v1/**);
    • eslint-plugins/__tests__/no-mixed-imports.test.ts: кейсы «old → i18n чисто» (строки 8389) и «lib/i18n — shared» (строки 99–105) обновить под новую классификацию: old→новый i18n = нарушение; добавить фикстуру __fixtures__/src/lib/v1/i18n/t.ts для кейса old-копия;
    • eslint.config.js: в allowDefaultProject добавить фикстуру eslint-plugins/__fixtures__/src/lib/v1/i18n/t.ts; поправить комментарий изоляции «Общее (core/, i18n/, theme)» → «Общее (core/, theme)»;
    • eslint-plugins/README.md: убрать i18n из списка shared (§ Isolation).
  • 0.6 Новый lib/i18n/dict.ts: CategoryId переключить с ../v1/categories на ../categories (новый структурно-идентичный тип) — скрытых v1-зависимостей в новом i18n не остаётся.
  • 0.7 Проверка форка: pnpm --dir web lint, check, test (v1-копия тестов подхватывается vitest include: src/**/*.test.ts), test:rules; смоук: v1-страницы и оба переключателя RU/EN работают как раньше, языки обеих версий согласованы.
  • 0.8 Добавить lib/i18n/schema-tool-strings.ts поверх $lib/registry (новый тип ToolEntry):
    • toolTitle(tool), toolDescription(tool) с фолбэком на registry English;
    • fieldLabel(field, id) — перевод по ключу из fields-секции словаря (см. решение C), фолбэк на labelOf(id);
    • groupLabel(title) — перевод по ключу из groups-секции словаря (см. решение A);
    • verdictText(toolId, key) — рендер вердикта по ключу;
    • verdictTone(key)success|danger|info по имени ключа (*Yes/*No/*Portrait/*Landscape) — не по строке.
  • 0.9 Расширить тип Dict (только новая ветка lib/i18n):
    • секция preview не заводится: строки нового UI живут в собственных категориях — расширяем существующие (catalog, header, dropZone, sourceCard, resultCard, paramsCard, textResult, ui) и заводим две маленькие новые: actions (Generate / Open image / Download result…) и textSource (placeholder, Try sample…);
    • новая глобальная секция fields: Record<string, string> — переводы подписей полей по ключу из схемы (решение C);
    • новая глобальная секция groups: Record<string, string> — переводы заголовков групп полей (решение A).

Фаза 1: Словари — наполнение (только новая ветка lib/i18n, v1 не трогаем)

  • 1.1 Заполнить en.ts и ru.ts секциями actions, textSource, fields, groups и ключами недостающих строк в существующих секциях.
  • 1.2 Проинвентаризировать разницу между 122 инструментами нового registry и ключами ru.tools: для инструментов без перевода (нет в v1) добавить title/description в ru.ts новой ветки (по категориям — несколько коммитов).
  • 1.3 Добавить в ru.ts ключи fields/groups для используемых в схемах ключей полей и заголовков групп (по ходу проставления ключей в schema, Фаза 5).
  • 1.4 Добавить tools[id].options[fieldId][value] для select-полей, чьи опции нужно локализовать.
  • 1.5 Тест полноты (задел на Фазу 6): для каждого ключа label в schema есть перевод в обоих словарях.

Фаза 2: LangToggle → реальный locale

  • 2.1 LangToggle.svelte: убрать локальный lang, подключить getLocale(), setLocale(), LOCALES. Модель — language switch из routes/v1/+layout.svelte.
  • 2.2 Проверить e2e navigation.spec.ts «language toggle marks the active button» — должен проходить без изменений.

Фаза 3: Routes + layout

  • 3.1 +layout.svelte: breadcrumbs через t(), динамический lang.
  • 3.2 +page.svelte, list-tools/+page.svelte: footer-строка → t("header.footerNote").
  • 3.3 tools/[id]/+page.svelte: «Tool not found», fallback title → t().

Фаза 4: Компоненты — основной блок (коммиты кучками по смыслу)

Все текстовые подписи в компонентах — информативные, сохраняем и переводим на t/хелперы. Каждый коммит: замена строк + lint/check.

  • SchemaToolView.svelte: title/description → toolTitle()/toolDescription(), перевести TOOL SETTINGS, «Configure output» и «This tool has no schema yet.».
  • SchemaSourceTile.svelte: перевести figcaption SOURCE, «choose an image»/«no source…» и alt.
  • SchemaResultTile.svelte: перевести figcaption RESULT, «no result yet» и alt; суффикс «parts» — в PreviewTile.
  • SchemaPreview.svelte: перевести панельный лейбл PREVIEW PANEL и meta-капшены SOURCE / RESULT / FORMAT; значения «parts»/«text» → t().
  • SchemaActions.svelte: Generating…, Generate, Open image, Download result.
  • SchemaFields.svelte: Reset, «updates automatically», labelOf(field.id)fieldLabel(field).
  • SchemaTextSource.svelte: placeholder, Try sample, Render text.
  • SchemaTextResult.svelte: перевести лейбл RESULT у вердикта, Copy / Download .txt; tone detection → verdictTone().
  • CatalogToolbar.svelte: фильтры → t("categories.all") + t("categories.X"), поисковый placeholder.
  • CatalogHeader.svelte: перевести heading/lead и счётчик «TOOLS AVAILABLE».
  • CatalogGroup.svelte: перевести суффикс счётчика «TOOLS».
  • Dropzone.svelte: текст зоны → t("dropZone.pickDefault").
  • Schema field controls (по одному или группой): Width/Height, From/To, Backing plate, Opacity, Font/Size/Color/Bold, Sans/Serif/Mono, Angle, «+ Add color», «Remove color».
  • TopNav.svelte, ThemeToggle.svelte, CodeBlock.svelte, StepCard.svelte, Footer.svelte.

Фаза 5: Вердикты + ключи в schema

  • 5.1 Анализаторы в registry/analyze.ts: возвращают ключи ("verifyYes" вместо raw English).
  • 5.2 SchemaPreview.svelte: рендер вердикта через verdictText(tool.id, key).
  • 5.3 Layout groups в registry/*.ts (решение A): title → ключ (title: "groups.canvas"); в SchemaFields.sveltegroupLabel(title). В Dict — глобальная секция groups.
  • 5.4 Лейблы полей в registry/*.ts (решение C): на поле в схеме проставляется label-ключ (field.number({ label: "fields.thickness", … })), вывод в UI — fieldLabel(field) = t(label). В Dict — глобальная секция fields.
  • 5.5 Категории в catalog.ts: GROUP_LABELSt("categories.X") (ключи уже есть в dict).

Фаза 6: Тесты

  • 6.1 tools-i18n.test.ts — полнота словарей для текущего registry: title/description ru для всех инструментов, каждый label-ключ схемы есть в fields, каждый groups-ключ есть в groups, без лишних ключей.
  • 6.2 Фикс known-issues.spec.ts test.fixme — ошибки локализуются.
  • 6.3 В новой ветке lib/i18n появится собственный smoke-i18n.test.ts (аналог v1: переключение локали, html lang, контент без смеси языков) — v1-копия теста не трогается.

Фаза 7: Кросс-языковой поиск

Переключить поиск в новом каталоге с toLowerCase() на scoreDoc()/normalizeForSearch() из matching.ts (входит в объём, см. решение D). Последний шаг, аккуратный отдельный коммит.

Фаза 8: Линтер словарей (i18n key linter)

Замена ручной проверки ключей на автоматическую. Инфраструктура уже есть: eslint-plugins/ умеет читать соседние файлы (isolation/no-mixed-imports) и гоняться фикстурами (test:rules).

  • 8.1 Новое правило i18n/dict-consistency в web/eslint-plugins/. Применяется к файлам-словарям (конвенция: экспортируют Dict, лежат в lib/i18n/); новые локали подхватываются автоматически — правка правила не нужна.
  • 8.2 Проверки — только warn, никогда error (пропущенный перевод не валит сборку):
    • Паритет дерева ключей: каждый ключ (dot-path a.b.c) присутствует во всех остальных локалях, не только против BASE. Нет — missing key "x.y" in en.
    • Пустые значения: "" и строка из одних пробелов — empty value "x.y" in en.
    • Паритет плейсхолдеров: у одинакового ключа набор {name} одинаковый во всех локалях — поймать потерянную при переводе переменную.
  • 8.3 Цель — zero-warn. Сейчас en.ts неполный (в tools только результаты-помощники, остальной английский — из реестра), поэтому после включения будет много warn'ов: доводим en.ts до полноты ru.ts (секции + tools полностью, source — registry-English, без дрейфа). До достижения цели допускается allowlist-механизм в ядре правила для осознанных отклонений.
  • 8.4 Тесты правила — фикстуры в web/eslint-plugins/__fixtures__/, док в web/eslint-plugins/README.md; включение в eslint.config.js как "warn".

Фаза 9: Множественные числа (plural-формы)

  • 9.1 Тип значения словаря расширяем: string | PluralForms, где PluralForms = { other: string } & Partial<{ one, few, many }> по CLDR-правилам локали (ru: обязательны one/few/many; other — всегда, как фолбэк). Новая функция tPlural(path, n) в lib/i18n/: выбирает категорию через Intl.PluralRules(locale).select(n) и подставляет её с интерполяцией {n}.
  • 9.2 Первые потребители: «часть/части/частей» (split-into-parts-png), счётчики инструментов (catalog.toolsCount), «N параметров», «N файлов».
  • 9.3 Паритет в линтере (Фаза 8): если ключ — plural-объект в одной локали, он обязан быть plural-объектом во всех остальных (тип-паритет; string в одной из локалей — warn). Набор форм между локалями не сравнивается дословно — он проверяется по правилам конкретной локали (other обязателен; en: one/other, ru: one/few/many).
  • 9.4 Открытый вопрос на этапе реализации: неоднозначность «объект-секция» (Record<string, string>) vs plural-объект в дереве словаря. Варианты: отдельная секция plurals в словаре, конвенция имён ключей, вывод типа из контекста. Фаза начинается с проработки этого вопроса, не с кода.

5. Решения

  • A. Layout groups: заголовки групп храним i18n-ключами прямо в схеме (title: "groups.canvas"), перевод — в глобальной секции groups словаря (Dict.groups: Record<string, string>). В UI — groupLabel(title) = t(title) с фолбэком на сырой ключ.
  • C. Лейблы полей: в схеме задаём i18n-ключ (field.number({ label: "fields.thickness", … })), в UI выводится перевод — fieldLabel(field) = t(label). Ключи — глобальная секция fields словаря, фолбэк на текущий labelOf(id) для несопоставленных.
  • D. Объём: переводим всё сразу — и каркас, и контент реестра, и вердикты, и кросс-языковой поиск (Фаза 7). Крупные изменения кода разбиваем на небольшие атомарные шаги/коммиты.

6. Проверки

  • pnpm --dir web lint
  • pnpm --dir web exec svelte-check --tsconfig ./tsconfig.json
  • pnpm --dir web test
  • pnpm --dir web test:rules (если менялся eslint/isolation-конфиг)
  • Смоук: переключатель RU/EN на новом UI; страницы каталога/инструмента, ошибка, вердикт — без смеси языков; html lang следует за локалью.