Last touched: 2026-06-16 by @Skords-01. Next review: 2026-09-14. Status: Active
Сергеант поки UA-only і не приймає англомовних beta-юзерів. Запускати повний i18next / lingui runtime до того, як з'явиться продукт-вимога — це expensive yak-shave: ~20–30 годин на migration, плюс recurring cost кожного нового рядка.
Натомість ми робимо lightweight foundation, що готує ґрунт для майбутнього runtime-i18n за один крок:
- Винести всі hardcoded UA-strings із production-коду в
apps/web/src/shared/i18n/uk.ts(constants-каталог). - У day-to-day коді посилатися на
messages.<group>.<key>замість inline-літералів. - Коли (й якщо) з'явиться product-вимога — заміна
messages.x.yнаt('x.y')буде однорядковою для кожного use-site.
Цей doc — checklist готовності й operational guide.
Roadmap-довідник: docs/90-work/audits/2026-05-03-web-deep-dive item #18 (score 0.67).
- ✅ Створено
apps/web/src/shared/i18n/uk.tsз 6 групами:messages.auth.*,messages.sync.*,messages.validation.*,messages.actions.*,messages.empty.*,messages.errors.generic.*,messages.toast.*(round 14: розширено до 6 груп, ~80 ключів). - ✅
translateAuthError(apps/web/src/core/auth/AuthContext.tsx) переведено наmessages.auth.*. Існуючі тести (AuthContext.test.tsx— 22 кейси) лишаються зеленими — string-rendering ідентичний. - ✅ Структура каталогу типізована (
MessageCatalog). - ✅ Round 14 — Phase 1 ↦ Phase 3 закрито в одному PR (item #18 повний обсяг):
- Sync error-toast (
useSyncErrorToast.ts) — 5 рядків мігровано наmessages.sync.*(4 нових ключі). - Zod-validation — 7 форм (AuthPage, ResetPasswordPage,
ChangePasswordSection, WaitlistForm, Body, AddBudgetForm,
TagsSection) переведено на
messages.validation.*(~22 рядки, 20 нових ключів). Тести пройдено без зміни assertions. - ESLint rule
sergeant-design/no-cyrillic-jsx-literalдодано в warn-режимі з allowlist на 239 файлів (apps/web/eslint.i18n-allowlist.json). Burndown — зменшувати allowlist у наступних PR-ах; коли[]— promote доerror. - Unit tests rule-у: 13 кейсів (file scoping, allowlist behaviour, JSX text vs JSX attribute, MemberExpression skip, template-literal skip).
- Sync error-toast (
messages.sync.* — джерело: apps/web/src/core/cloudSync/**.
Закрито у round 14 — useSyncErrorToast.userFacingSyncErrorMessage
повністю на messages.sync.error* + messages.sync.retryCta.
messages.validation.* — джерело: zod-схеми у
apps/web/src/core/**/*.ts(x)? та apps/web/src/modules/**/forms/**.
Закрито 7 форм у round 14. Якщо в наступному PR-і додаєш zod-схему
з UA-message — додай новий ключ у validation.* (іменування — за
призначенням, не за рядком).
Recipe для нового рядка:
rg -n "z\.string\(\)\.min\([0-9]+, *\"[А-Я]" apps/web/src --type=tsКожне zod .email("..."), .min(N, "..."), etc. — переносити рядок
у messages.validation.<key> і відфайлити з allowlist той файл,
якщо він уже в JSON.
messages.actions.*, messages.empty.*, messages.errors.generic.*,
messages.toast.* створено в round 14 з типовими ключами (save, cancel,
nothingYet, network-error, saved, etc). Подальші round-и мігрують
inline-літерали в JSX → ці групи; кожна міграція знімає файл з
allowlist-у.
Tooling — i18n-burndown codemod (round 15+):
scripts/codemods/i18n-burndown/
— AST-кодомод, який бере allowlist-файл, шукає JSX-text + JSX-attribute
UA-літерали, мапить їх до існуючих ключів каталогу і переписує лише ті
файли, де усі літерали зматчилися (інакше пропускає, щоб не
залишати half-migrated компонент). Mapping будується рантайм-парсингом
apps/web/src/shared/i18n/uk.ts — нічого hand-maintain. Idempotent;
безпечно re-run-ити після додавання нових ключів. Dry-run за
замовчанням, --write застосовує і вибиває fully-migrated шляхи з
JSON-у allowlist.
node scripts/codemods/i18n-burndown/script.mjs # dry run
node scripts/codemods/i18n-burndown/script.mjs --write # apply
node scripts/codemods/i18n-burndown/script.mjs --filter=foo # subsetІмплементація — packages/eslint-plugin-sergeant-design/index.js
(пошук noCyrillicJsxLiteral). Покриває:
- JSXText nodes з
/[\u0400-\u04FF]/. - JSXAttribute string-literal values (e.g.
title="Закрити").
Виключає: tests (*.test.tsx, __tests__/), stories (*.stories.tsx),
сам каталог (apps/web/src/shared/i18n/**), MemberExpression-references
(messages.x.y), template literals (next-round scope), та файли з
allowlist у apps/web/eslint.i18n-allowlist.json.
Round-14 baseline: 239 файлів у allowlist. Кожен наступний PR
скорочує цей файл (одне-два видалення на PR). Після [] — promote
до "error" у eslint.config.js.
Якщо/коли з'явиться англомовний MVP:
- Додати
i18next+react-i18nextяк залежності@sergeant/web. - Створити
apps/web/src/shared/i18n/en.ts(mirror структуриuk.ts, всі value-strings перекласти). - Замінити
messages.x.yнаt('x.y')через codemod (jscodeshift або перетворення у IDE). - Локально вибрати локаль через
i18n.changeLanguage(...).
Час: ~4–6 годин для swap, бо foundation вже готовий.
Round 14 (item #18 повний обсяг) — Phase 1+2+3 закриті, далі — burndown allowlist-у через follow-up PR-и. Перевірити фактичну кількість файлів, які ще тримають inline-кирилицю в JSX:
jq 'length' apps/web/eslint.i18n-allowlist.json
# → 199 (post round-16 codemod)Або через ESLint warning count (eslint-rule безпосередньо):
cd apps/web && npx eslint . -f json 2>/dev/null \
| jq '[.[] | .messages[] | select(.ruleId == "sergeant-design/no-cyrillic-jsx-literal")] | length'
# Поточна сесія: 0 (всі inline-сайти allow-листі)Burndown plan (один файл за PR ↦ кілька десятків PR; з round-15 — пачки через codemod):
| Round | Allowlist size | Comment |
|---|---|---|
| 10 | n/a | Foundation only (catalog created, auth migrated) |
| 14 | 239 | Phase 1+2+3 закрито; rule в warn-mode + allowlist |
| 15 | 233 | i18n-burndown codemod landed; 6 файлів мігровано (7 replacements) |
| 16 | 199 | High-frequency burndown: +9 catalog-груп, codemod multi-line-import bug fixed; 34 файли мігровано (50 replacements) |
| 17–25 | ~100 | Settings panels + showcase sections мігровані |
| 17 | 236 | Fizruk pages burndown: Progress, Programs, Measurements, Body + Body/Journal{Section,EntryCard} мігровано у messages.fizruk.{progress,programs,measurements,body,journal} (87 JSX-літералів, 6 файлів знято з allowlist) |
| 25–40 | 0 | Promote rule до "error" |
Сирий мір по проекту (всі UA-strings, не тільки JSX-літерали; для референсу — НЕ closure-метрика):
rg -n --glob='apps/web/src/**' --glob='!apps/web/src/shared/i18n/**' \
--glob='!apps/web/src/**/*.test.{ts,tsx}' \
--glob='!apps/web/src/**/__tests__/**' \
'[\u0400-\u04FF]' | wc -l(Тести не рахуємо — вони залишаються із hardcoded UA-asserts назавжди.)
- ❌ Не додавати
i18nextruntime раніше Phase 4. Foundation-каталог достатній. - ❌ Не перекладати
uk.tsна англійську "про запас" — це створить divergence. - ❌ Не міняти UA-asserts у тестах.
Коли мігруєш string у uk.ts:
- Зберігай точну UA-копію (включно з пробілами, крапкою, тонкими апострофами
ʼ). - Існуючі тести мають продовжити проходити без змін у assertion-strings — це гарантує, що міграція — це rename, не behavior-change.
- Якщо рядок має параметри (template-literal) — додай як function
messages.x.y = (n) => "...${n}..."замість const-string.
Власник цього файлу й каталогу — @Skords-01. Ревью обов'язкове на будь-яку нову Phase.