-
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.
-
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:
- 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
- Migrate all internal consumers to the canonical keyset
- 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
- 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):
- For every module, all keys in
en.json exist in ru.json and vice versa
- No empty or whitespace-only values
- 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
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.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:
yfm-notewarning label,formsimage name field)forms, plural inmenubar, awkward phrasing ingpt/errors, locale-specific punctuation inbundlehints/andmath-hint/)menubar(single vs double underscore separator); document the convention in a comment or CONTRIBUTING note (repo-wide rename is out of scope)scripts/lint-i18n.mjswired into the existing lint pipeline; checks: key parity between en/ru per module, no empty values, no obviously untranslated stringsImplementation Steps
Step 1 — Fix translation inconsistencies (same concept, different translation)
forms/ru.json,gpt/extension/ru.jsoncommon_action_cancel,confirm-cancelcommonwhich uses "Отмена" (noun)yfm-layout/ru.jsoncell.previewgpt/dialog/en.jsonrefetchandtry-againGptDialog.tsxanduseGpt.tsxto understand what each button actually does. Ifrefetchis dead code, remove the key. If buttons have different behavior, fix the RU to match EN semantics; do not changetry-againEN value based solely on the differing RU translation.Step 2 — Fix semantic mismatches (EN and RU mean different things)
yfm-note/warningforms/image_nameyfm-layout/action.align.left/center/right/stretchplaceholder/table_celllayout_cellStep 3 — Fix English copy issues
forms/en.jsonimage_link_href_helpmenubar/en.jsonmore_actiongpt/errors/en.jsonstart-again-buttonbundle/en.jsonsettings_hint"+" buttonStep 4 — Consolidate duplicate keysets
hints/andmath-hint/have identicalen.jsonandru.json.packages/editor/package.jsonhas a./_/*wildcard export mapping tobuild/esm/*andbuild/cjs/*, which means both paths (_/i18n/hintsand_/i18n/math-hint) are technically externally reachable. To avoid a breaking change:hints/as the canonical keyset — it has more internal consumers (items.tsx,wysiwyg.ts) vsmath-hint/which is used only inhint.tsx, so this minimises churnhint.tsx(sole consumer ofmath-hint) to import fromhints/instead; changemath-hint/index.tsto a thin re-export — this preserves_/i18n/math-hintmodule import compatibility@deprecatedJSDoc comment on the re-export fileThe
en.jsonandru.jsonfiles in the deprecated module are not deleted — onlyindex.tsis 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 conventionlist_action_disableduses a single underscore while neighboring keys use double underscore as a namespace separator (list__action_lift,list__action_sink). Rename tolist__action_disabledand update all consumers.Document the chosen convention (snake_case with
__as namespace separator) indocs/guidelines-contributions.mdunder 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.mjsChecks (exit non-zero on failure):
en.jsonexist inru.jsonand vice versaru.jsonidentical to itsen.jsoncounterpart, 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.jsmodule (returns structured errors); lives inscripts/alongsidecheck-circular-deps.js, outside runtime source tree and./_/*wildcard;.jsextension is importable by Jest without addingmjstomoduleFileExtensionsscripts/lint-i18n.js— thin root-level CommonJS runner (require()+process.exit(1));.jsnot.mjssorequire()is available natively withoutcreateRequireUnit tests (
packages/editor/src/i18n/__tests__/lint.test.ts): import the.jsmodule, 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.jsonscripts group (rootlintalready runs alllint:*viarun-p, so no separate CI step needed):Verification
Steps 1–3 (copy fixes):
pnpm lint:i18nexits 0pnpm testpassesStep 1 (GPT try-again/refetch investigation):
GptDialog.tsxanduseGpt.tsx; add an assertion or snapshot covering the GPT dialog copy if none existsSteps 4–5 (keyset consolidation + key rename):
pnpm lint:i18nexits 0MathHintconsumer andlist__action_disabledusage; key renames can break runtime without failing broad suiteStep 6 (lint script):
packages/editor/src/i18n/__tests__/lint.test.tspasspnpm lint(root) picks uplint:i18nviarun-p