Skip to content

Clean up editor i18n translations and add CI validation #1097

@makhnatkin

Description

@makhnatkin
  1. The editor's translation files (packages/editor/src/i18n/) have accumulated a number of inconsistencies over time. The same UI labels are translated differently across modules, some EN/RU pairs carry different meanings, there are English copy mistakes, two keysets are exact duplicates, and there is no enforced key naming convention.

  2. Also nothing prevents new issues from being introduced. We want to do a focused cleanup pass and add a lightweight lint step to CI so the baseline stays clean going forward.

Scope:

  • Fix translation inconsistencies where the same concept has different translations in different modules (e.g. Cancel, Preview, Try again)
  • Fix semantic mismatches where EN and RU do not mean the same thing (e.g. yfm-note warning label, forms image name field)
  • Fix English copy issues: typo in forms, plural in menubar, awkward phrasing in gpt/errors, locale-specific punctuation in bundle
  • Consolidate two identical keysets (hints/ and math-hint/)
  • Fix one concrete naming inconsistency in menubar (single vs double underscore separator); document the convention in a comment or CONTRIBUTING note (repo-wide rename is out of scope)
  • Add scripts/lint-i18n.mjs wired into the existing lint pipeline; checks: key parity between en/ru per module, no empty values, no obviously untranslated strings

Implementation Steps

Step 1 — Fix translation inconsistencies (same concept, different translation)

Location Key Problem Fix
forms/ru.json, gpt/extension/ru.json common_action_cancel, confirm-cancel "Отменить" (verb) — should match common which uses "Отмена" (noun) Change to "Отмена"
yfm-layout/ru.json cell.preview "Превью" — everywhere else Preview → "Предпросмотр" Change to "Предпросмотр"
gpt/dialog/en.json refetch and try-again Both have value "Try again" in EN, but different RU translations ("Попробовать ещё" vs "Иначе"). Before fixing: read GptDialog.tsx and useGpt.tsx to understand what each button actually does. If refetch is dead code, remove the key. If buttons have different behavior, fix the RU to match EN semantics; do not change try-again EN value based solely on the differing RU translation. Investigate first, then either remove dead key or align RU

Step 2 — Fix semantic mismatches (EN and RU mean different things)

Location Key EN RU Fix
yfm-note/ warning "Warning" "Важно" (Important) ⚠️ Needs product decision — "Warning" and "Important" carry different meaning; do not change without alignment
forms/ image_name "Title" "Подпись к рисунку" (Caption) ⚠️ Needs product decision — "Title" and "Caption" are different concepts; do not change without alignment
yfm-layout/ action.align.left/center/right/stretch "Left alignment" (brief) vs "Вся секция по левому краю" (descriptive); "Stretch to the full width" vs "Растягивать секцию на всю ширину" Expand EN: "Align entire section to the left / center / right / full width"
placeholder/ table_cell "Cell content" "Текст" Align both — "Text" / "Текст" to match layout_cell

Step 3 — Fix English copy issues

Location Key Problem Fix
forms/en.json image_link_href_help Typo: "Adress" → "Address"
menubar/en.json more_action "More action" (singular) → "More actions"
gpt/errors/en.json start-again-button "To the beginning" (word-for-word from RU) → "Start over"
bundle/en.json settings_hint Russian guillemets «+» in an English string "+" button

Step 4 — Consolidate duplicate keysets

hints/ and math-hint/ have identical en.json and ru.json.

packages/editor/package.json has a ./_/* wildcard export mapping to build/esm/* and build/cjs/*, which means both paths (_/i18n/hints and _/i18n/math-hint) are technically externally reachable. To avoid a breaking change:

  1. Use hints/ as the canonical keyset — it has more internal consumers (items.tsx, wysiwyg.ts) vs math-hint/ which is used only in hint.tsx, so this minimises churn
  2. Migrate all internal consumers to the canonical keyset
  3. Migrate hint.tsx (sole consumer of math-hint) to import from hints/ instead; change math-hint/index.ts to a thin re-export — this preserves _/i18n/math-hint module import compatibility
  4. Add a @deprecated JSDoc comment on the re-export file

The en.json and ru.json files in the deprecated module are not deleted — only index.ts is changed to a re-export. Raw JSON subpath imports (_/i18n/hints/en.json) are therefore preserved physically as a side-effect.

Step 5 — Fix key naming inconsistency inside menubar + document convention

list_action_disabled uses a single underscore while neighboring keys use double underscore as a namespace separator (list__action_lift, list__action_sink). Rename to list__action_disabled and update all consumers.

Document the chosen convention (snake_case with __ as namespace separator) in docs/guidelines-contributions.md under a new "i18n keys" section. Repo-wide rename of other modules is out of scope for this issue.

Step 6 — Write and wire scripts/lint-i18n.mjs

Checks (exit non-zero on failure):

  1. For every module, all keys in en.json exist in ru.json and vice versa
  2. No empty or whitespace-only values
  3. No value in ru.json identical to its en.json counterpart, except an allowlist of legitimate shared terms (product names, markup language names, etc.)

Advisory-only (print warnings, do not fail):
4. Duplicate values within the same locale file

Structure — two files, one purpose:

  • packages/editor/scripts/lint-i18n.js — linter logic as a CommonJS .js module (returns structured errors); lives in scripts/ alongside check-circular-deps.js, outside runtime source tree and ./_/* wildcard; .js extension is importable by Jest without adding mjs to moduleFileExtensions
  • scripts/lint-i18n.js — thin root-level CommonJS runner (require() + process.exit(1)); .js not .mjs so require() is available natively without createRequire

Unit tests (packages/editor/src/i18n/__tests__/lint.test.ts): import the .js module, pass in-memory fixture objects — covers all lint rules as pure functions.

Integration smoke test (same file or separate lint.integration.test.ts): create a small temporary fixture directory on disk with 2–3 modules (one valid, one with a missing key, one with an empty value), run the full file-walk logic against it, assert exit behaviour — catches bugs in file discovery and module traversal that unit tests miss.

Add to root package.json scripts group (root lint already runs all lint:* via run-p, so no separate CI step needed):

"lint:i18n": "node scripts/lint-i18n.mjs"

Verification

Steps 1–3 (copy fixes):

  • pnpm lint:i18n exits 0
  • pnpm test passes

Step 1 (GPT try-again/refetch investigation):

  • Confirm the target button's behavior by reading GptDialog.tsx and useGpt.tsx; add an assertion or snapshot covering the GPT dialog copy if none exists

Steps 4–5 (keyset consolidation + key rename):

  • pnpm lint:i18n exits 0
  • Grep for all consumers of the removed/renamed keys before and after to confirm no dangling references
  • Add targeted unit tests for MathHint consumer and list__action_disabled usage; key renames can break runtime without failing broad suite

Step 6 (lint script):

  • Jest tests in packages/editor/src/i18n/__tests__/lint.test.ts pass
  • Introduce a deliberate key mismatch in a fixture object — test must fail
  • Confirm pnpm lint (root) picks up lint:i18n via run-p

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions