From 05165031c361bca86b5b3c093da872c12ce59a5d Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sat, 12 Sep 2026 16:16:40 +0500 Subject: [PATCH] docs: add i18n update plan --- docs/plan-preview-i18n.md | 291 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 docs/plan-preview-i18n.md diff --git a/docs/plan-preview-i18n.md b/docs/plan-preview-i18n.md new file mode 100644 index 0000000..93cbd2b --- /dev/null +++ b/docs/plan-preview-i18n.md @@ -0,0 +1,291 @@ +# План: 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/i18n` → `lib/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 + чисто» (строки 83–89) и «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-строк нового дизайна (только те, что остаются + после чистки mono-лейблов, см. решение B); + - новая глобальная секция `fields: Record` — переводы подписей + полей по ключу из схемы (решение C); + - новая глобальная секция `groups: Record` — переводы + заголовков групп полей (решение A). + +### Фаза 1: Словари — наполнение (только новая ветка `lib/i18n`, v1 не трогаем) + +- **1.1** Заполнить `en.ts` и `ru.ts` секциями `preview`, `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: Компоненты — основной блок (по одному коммиту на компонент) + +Каждый коммит: удаление декоративных mono-лейблов (решение B) + замена +оставшихся строк на `t`/хелперы + lint/check. Список mono-лейблов, которые +**удаляем**: eyebrow'ы (`PNG PROCESSING / SINGLE TOOL`, `KIT SHOWCASE`, +`EASY-PNG-TOOLS / CATALOG`), статусы (`LIVE PREVIEW`, `rendering ok`), панельные +подписи (`TOOL SETTINGS`), дублирующие капшены (`SOURCE / RESULT`, +`GENERATOR / RESULT`). Оставляем и локализуем только информативные: категории +каталога, счётчики инструментов, подписи данных (`FORMAT`, `parts`, `px`, «ZIP +(PNG)»), alt-тексты, а также содержательные заголовки («Configure output»). + +- `SchemaToolView.svelte`: убрать eyebrow/`LIVE PREVIEW`/`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»/«parts» и alt. +- `SchemaPreview.svelte`: убрать панельные + `SOURCE / RESULT`/`GENERATOR / RESULT`, перевести meta-капшены (`FORMAT` и + т.п.). +- `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`: убрать eyebrow, перевести 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.svelte` — `groupLabel(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_LABELS` → `t("categories.X")` (ключи + уже есть в dict). + +### Фаза 6: Тесты + +- **6.1** `new-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). Последний шаг, аккуратный отдельный коммит. + +## 5. Решения + +- **A. Layout groups**: заголовки групп храним i18n-ключами прямо в схеме + (`title: "groups.canvas"`), перевод — в глобальной секции `groups` словаря + (`Dict.groups: Record`). В UI — `groupLabel(title)` = + `t(title)` с фолбэком на сырой ключ. +- **B. Mono-лейблы**: декоративные лейблы без полезной информации **удаляем** + (eyebrow'ы, статусы, панельные капшены, дублирующие подписи секций) — они + занимают место. Оставляем только там, где реально нужно, и локализуем: + счётчики инструментов, категории каталога, подписи данных (FORMAT, parts, px, + «ZIP (PNG)»), alt-тексты, содержательные headings. +- **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` следует за локалью.