Skip to content

[Maintainer] i18n: migrate tools and toolbar - #1171

Merged
Aymericr merged 48 commits into
mainfrom
cursor/i18n-tools-6d7c
Oct 27, 2026
Merged

Aymericr merged 48 commits into
mainfrom
cursor/i18n-tools-6d7c

Conversation

@Aymericr

@Aymericr Aymericr commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

What does this PR do?

Localizes the editor's tools and toolbar surfaces. Part of #621.

  • Node definition copy at display time. Definitions stay English, because MCP, agents and exports read them. The editor translates them where they are shown.
    • packages/editor/src/i18n/node-messages.ts looks up node.<kind>.label / node.<kind>.description, and editor.toolHint.* for hint labels, gesture/key names and chip labels. The English is the fallback, so a plugin kind without messages shows its own English; a plugin can ship the same IDs with registerMessages.
    • Chip labels that spell a number ("Cuts: 3, even") share one message with {value}.
    • i18n:extract generates packages/nodes/src/i18n/definition-messages.ts (/* i18n */ descriptors for every built-in kind's label, description, tool and affordance hints, keys and chips) so these IDs are extracted. i18n:check fails when that file drifts.
    • The generator loads the definitions from source in a child Bun process, so it works before the build.
  • Build tab (apps/editor): tool tiles (base tools and registry kinds), sections, search (matches the translated names), roof types and features, fence features, MEP, the kitchen browser and its palette commands. The XR wand panel model is translated too. The external XR plugin's collect* bindings still receive English, because that plugin renders plain labels itself.
  • Bottom tool dock:
    • Select and Delete;
    • the measurement and construction-dimension menus;
    • camera orbit and top view;
    • point-and-ask and Follow Pascal;
    • the references popover. Scan and guide counts are now ICU plurals instead of English s suffixes, and the reference level name uses the localized level name.
  • Floating action menu run and register counts (ICU plurals), and the cursor bubble icons' names.
  • Tool HUD:
    • HUD titles, including Move / Edit / Split {kind} with the translated kind name;
    • hint rows from both the editor and the definitions;
    • the snapping, grid-step and continuation chips, with grid steps formatted for the locale;
    • the Shift "guided constraints bypassed" hint;
    • key and mouse-button names in ShortcutToken;
    • the camera controls hint (Pan, Rotate, Zoom).
  • Cut opening HUD: the mezzanine drag hint is swapped by descriptor ID, so the HUD shows "Draw the opening". The old comparison with the English string stopped matching once hints became descriptors.
  • Accessibility: icon-only ActionButtons now carry their label as the accessible name. Follow Pascal had none.
  • Public API notes:
    • furnishTools labels and HudTitle.label are now MessageDescriptors.
    • getSnappingModeLabel keeps returning English, because the external XR plugin reads it.
  • Catalogs: 614 new messages. zh-CN/fr/es/ja drafts are fuzzy + Review: required (AI, AI-Provider: cursor-agent), shown in English until a fluent reviewer approves. The literal baseline drops from 2,843 to 2,592.
  • Audit: surfaces build-tab, mep-tools, tool-dock, camera-hint, wall-tool-hud, door-tool-hud, measure-menu, references-menu. They use data-surface markers on the build tab and the dock.

How to test

  1. bun run i18n:check: passes and reports 721 entries per locale awaiting review (107 already on main plus 614 here).
  2. bun dev, open a project with ?lang=en-XA: the build tab, tool dock, HUD (arm Rectangle or Door) and camera hint are bracketed and accented. Arm the wall split or a window to see chip labels.
  3. bun run i18n:pseudo-audit -- build-tab mep-tools tool-dock camera-hint wall-tool-hud door-tool-hud measure-menu references-menu: all eight PASS, with no untranslated strings and no unnamed controls. I ran this; screenshots below.
  4. Tests I ran:
    • bun --cwd packages/editor run test: 2,669 pass, including the updated HUD, contextual-help and wall-variant tests (before the opening fix).
    • The helper suite with the new Cut opening HUD test: 8 pass. The new test fails on the old comparison.
    • bun --cwd apps/editor run test: 74 pass.
    • bun test ./scripts/i18n.test.ts ./scripts/i18n-literals.test.ts: 53 pass.
    • Type checks for editor, app and nodes are clean.

Still English after this PR

  • The XR plugin's own panel text.
  • Plugin node kinds that ship no messages. The MEP grid shows Road from @pascal-app/plugin-streetscape.
  • Kitchen item names, which come from the cabinet presets in nodes and belong to the furnishing PR.
  • Inspector fields, the left rail and the level selector, which belong to the next surface PRs.

Screenshots / screen recording

en-XA (accented, ~30 % longer, bracketed), captured by i18n:pseudo-audit at 1440×900.

Build tab:
Build tab en-XA

MEP tools under Advanced (Road is the streetscape plugin's own kind):
MEP tools en-XA

Wall (Rectangle room) HUD with snapping and grid chips:
Wall HUD en-XA

Door HUD with type/style chips from the definition:
Door HUD en-XA

Measurement menu in the tool dock:
Measure menu en-XA

References popover:
References en-XA

Tool dock and camera hint:
Tool dock en-XA

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

Note

Medium Risk
Broad UI string migration with new MessageDescriptor types on public configs; behavior should be unchanged in English but search/HUD/plural formatting paths are touched across many interaction surfaces.

Reviewed by Cursor Bugbot for commit 104b9c7. Bugbot is set up for automated code reviews on this repo. Configure here.

…ale audit

- scripts/i18n-literals.ts: shared scanner for user-visible literals, a
  per-file ratchet (i18n/literal-baseline.json) wired into i18n:check, and
  report/update commands; exemptions via i18n/literal-allowlist.json or an
  i18n-ignore comment
- scripts/i18n-codemod.ts: rewrites JSX text, UI props, conditional and
  template copy and notification payloads into i18n._ descriptors with the
  usePascalI18n hook; module-level and server copy is listed for a person
- en-XA pseudo-locale compiled from English, registered by the app outside
  production, plus a Playwright audit that screenshots surfaces and lists
  copy that bypassed the catalog
- i18n:check accepts AI drafts flagged fuzzy + Review: required (they are
  never compiled into packs); check --reviewed keeps the sign-off gate
…rface helpers; codemod accepts explicit files
Localizes the project navbar, share panel, app header, the viewport view
bar (3D/2D tabs, split, ground mode, more-views menu) and the viewer
toolbar with its Display menu. Module option lists become lazy
descriptors; scene theme names share one helper with the viewer controls.
zh-CN/fr/es/ja drafts are fuzzy and await human review.
…view

A translation that is Review: human on the base branch with the same
English must stay reviewed. CI checks out the merge commit's first parent
for the comparison, and release.yml runs check --reviewed before publish.
The review rules are now a pure function so tests use fixtures.
The baseline lists each file's legacy copy by text, so new or swapped copy
fails, and CI fails when the baseline gains entries relative to the base.
i18n-ignore needs a reason; hyphenated labels and string constants rendered
in JSX are copy; toast descriptions count once. The codemod keeps one-line
bodies valid and converts explicitly named .ts modules. Enter, Space, Tab
and Del leave the allowlist.
Scene theme names move to a shared helper used by both toolbars. 64 new
messages; zh-CN/fr/es/ja drafts are fuzzy and await human review.
A translation that appears without a Source-Hash becomes a fuzzy draft
unless a person already marked it Review: human, so pasted or tool-filled
copy cannot reach the compiled packs without review.
Server pages render AppHeader, so the usePascalI18n hook there returned a
500 on the home page (cli-smoke).
They can render inside server components, where an inserted hook crashes
the request.
A const is flagged only when the nearest declaration of the rendered name
is that const string, so shadowing parameters, locals and same-named
constants elsewhere in the file no longer swap the copy.
…eaks

release.yml runs release-check instead of check --reviewed: it compares the
committed catalogs with the last v* tag and fails when a reviewed entry
with unchanged English became a draft, or when a compiled pack contains a
draft. i18n:check runs the same checks against the PR base in CI. Both
write per-locale draft counts to the job summary. Job-level tests run the
command against temporary catalog copies.
Moved, split or extracted copy no longer fails: each text the baseline adds
must consume an equal text removed elsewhere, duplicates counted. JSX text is
recorded as it renders (whitespace, full HTML entities via entities,
{' '} joins), constants inside sentences become part of the sentence, and
concatenated copy is a candidate. The allowlist may not gain entries
relative to the base. Named callbacks and async functions are not hook
owners.
…dempotent --vars

Copy is encoded so the compiled message renders the original text. Hooks
are never placed in named callbacks or async functions, and editor package
entry modules without 'use client' are left for a person. Descriptor mode
stops at existing descriptors and i18n._ calls.
…ent.before

A reviewed ID replaced by a new ID with the same English needs its
translation carried over or reviewed again. On push, CI fetches and compares
with the commit before the push.
Below sm their text is hidden, so both buttons carry the translated
aria-label; the navbar-mobile audit surface checks names at 390px.
…rror

extract marks a translation whose English changed fuzzy + Review: required
and keeps the old Source-Hash; that entry is never shipped, so ordinary CI
counts it as awaiting review. A reviewed entry with a stale hash, and every
stale entry under --reviewed, still fail.
Releases tag packages @pascal-app/<pkg>@<version>, so the v* lookup found
v1.0.3, which has no catalogs, and the check could never fail. The base is
now the latest @pascal-app/editor@* tag (the package that ships the
catalogs); when that tag predates the catalogs it falls back to origin/main
(another commit with catalogs), then to the commit that introduced them,
and otherwise fails loudly. Tests run the default lookup in a temporary
repository and fail on a reviewed-to-draft change.
- Same-English matches between a removed reviewed ID and a new ID are a
  warning (console and job summary), since they may be unrelated additions.
  A rename declared with a renamed-from: <old id> descriptor comment is
  carried over by i18n:extract and fails CI only if it arrives without the
  reviewed translation.
- release-check fails on a --base that cannot be resolved or read.
- The allowlist applies to whole joined sentences; constants inside
  concatenations count as copy; parenthesized named callbacks are not hook
  owners.
- The codemod encodes adjacent static pieces together, keeps no-break
  spaces, encodes text before a placeholder knowing a { follows, decodes
  attribute entities, and escapes CR and line separators in emitted strings.
- The baseline format is version 3, since fingerprints now use rendered
  text.
A previous-line ignore applies only when that line is a standalone comment,
so an exemption written after one literal no longer hides a new literal on
the following line.
Node definitions stay English for MCP and agents. The editor now looks
their presentation labels and descriptions up as node.<kind>.label and
node.<kind>.description, and tool hints, gesture keys and chip labels as
editor.toolHint.*, with the English as fallback. i18n:extract generates
packages/nodes/src/i18n/definition-messages.ts from the built-in
definitions so those IDs are extracted, and i18n:check fails when it
drifts. Plugin kinds can register the same IDs.
Tool tiles, sections, search, roof types and features, fence features,
MEP and kitchen browser. Registry tools use the node label lookup; the XR
plugin still receives English, since it renders plain labels itself.
Select/Delete, measurement and construction-dimension menus, camera
actions, point-and-ask, follow Pascal, references (scan and guide counts
are ICU plurals), floating action menu counts, and cursor icons. Action
buttons carry their label as the accessible name.
HUD titles (including Move/Edit/Split {kind}), hint rows, snapping, grid
step and continuation chips, the Shift bypass hint, key and mouse-button
names, and the camera controls hint. Grid steps use locale length
formatting.
614 messages; zh-CN/fr/es/ja drafts are fuzzy and await human review. The
pseudo-locale audit covers the build tab, MEP tools, tool dock, camera
hint, wall and door HUDs, and the measurement and reference menus.
@Aymericr Aymericr added the bot:maintainer label Oct 27, 2026 — with Cursor
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-27T05:28:24.437660Z 14bba89 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

i18n:check runs before the build in CI, where @pascal-app/* resolve to
missing dist/ files. The generator now loads the built-in definitions in a
child Bun process with a tsconfig whose paths map every workspace package
export to its source.

Copy link
Copy Markdown
Member Author

Tooling cross-review at 4176867: APPROVE

I reviewed only this PR's own diff on top of #1169 (70f7702, which already contains #1167), checked out locally at /tmp. CI is green at the head: quality, ci, cli-smoke, Changed paths and Bugbot all pass.

  • Lingui and extraction
    • The build tab, kitchen browser, build palette, bottom tool dock, floating action menu, tool HUD, helpers and shortcut tokens all go through i18n._ or /* i18n */ descriptors.
    • Node definitions stay English. node-messages.ts translates them when they're displayed.
    • i18n:extract generates packages/nodes/src/i18n/definition-messages.ts (285 descriptors, with no id that has two messages). The generator throws on conflicting ids, and i18n:check fails if the file drifts.
    • en gains 108 node.* ids, and every new id is a draft in all four locales.
  • [Maintainer] i18n: string-extraction codemod + CI literal guard #1167's gates
    • Against 70f7702, i18n:check passes, and so does release-check --base 70f77021, which runs the regression and leak checks.
    • Drafts rise to 721 per locale.
    • The literal baseline has only deletions (2843 to 2592 candidates). literal-allowlist.json is unchanged.
    • biome.jsonc only excludes the generated file, the same way the compiled messages.ts is excluded.
  • Tests
    • bun test in packages/editor: 2669 pass.
    • The changed tests still check the English text. They now read it through the descriptor's .message, and the grid chip expects a no-break space from locale formatting.
  • Audit: I ran i18n:pseudo-audit against a local dev server. build-tab, tool-dock, camera-hint, wall-tool-hud, door-tool-hud, measure-menu, references-menu, mep-tools and navbar-mobile all pass, and the wall-tool-hud screenshot shows the pseudo-text.
  • Behaviour: the changes are display-only. The XR plugin bindings still receive English, as the PR body says, and icon-only ActionButtons gain their accessible name. Security isn't needed.

P2 (doesn't block)

  • Build search matches only the translated label, so in a non-English UI an English query like "wall" no longer finds the tool. Consider matching the English source too.
  • At runtime, hint ids are a slug of the first six words. A plugin hint whose first six words match a built-in hint, but whose text differs, would show the built-in's catalog text, even in English. The generator only guards against collisions between built-in hints.
  • Grid: 0.50 m now renders with a no-break space. It's intended, but anything that matches the old string exactly will break.

Stacking: this PR contains #1169 and #1167. After they merge, it needs main merged in or a rebase, and a re-check.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/editor/src/components/ui/helpers/helper-manager.tsx Outdated
The hint label is a message descriptor now, so comparing it with the
English 'Draw the mezzanine' never matched and the opening HUD showed the
mezzanine copy.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 104b9c7. Configure here.

onClick={() => cyclePaintScope()}
shortcut="Shift"
tooltip={`${verb} scope — click or press Shift to cycle`}
tooltip={i18n._({ id: 'editor.helpers.contextualHelperPanel.verbScopeClickOrPressShift', message: '{verb} scope — click or press Shift to cycle', values: { verb } })}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Paint scope verb stays English

Low Severity

The paint-scope chip builds verb as hardcoded English (Pick / Erase / Paint) and interpolates it into the new localized tooltip, plus the chip label and aria-label. Non-English locales get a mixed-language sentence that translators cannot fix, because {verb} is a value rather than a translatable message.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 104b9c7. Configure here.

/>
<span className="min-w-0 flex-1 truncate">{levelName}</span>
<span className="text-[10px] text-muted-foreground">{index + 1} below</span>
<span className="text-[10px] text-muted-foreground">{index + 1}{' '}{i18n._({ id: 'editor.actionMenu.viewToggles.below', message: 'below' })}</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Below count ignores word order

Low Severity

The reference-floor list renders the offset as a concatenated number plus a standalone below message. Translators cannot change word order, plural form, or classifier, so locales that need the number inside the phrase (or a different order) show an ungrammatical label.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 104b9c7. Configure here.

Copy link
Copy Markdown
Member Author

Tooling cross-review at 104b9c7: APPROVE

I re-confirmed my approval at 4176867 against this head, checked out locally at /tmp.

  • Only the Bugbot fix changed
  • The fix is correct
    • At 4176867, openingHints compared a MessageDescriptor label to the string 'Draw the mezzanine'. That never matched, so the Cut opening HUD showed the mezzanine wording.
    • It now compares label.id with the shared DRAW_MEZZANINE descriptor and swaps in DRAW_OPENING. The id doesn't change with the locale, and the label is translated only when it's rendered, so the fix works in every locale.
    • drawTheOpening is in en and in all four target catalogs.
  • The test fails without the fix. With the old helper-manager.tsx restored, the new test fails: it received […, "Draw the mezzanine", …]. With the fix, packages/editor passes all 2670 tests.
  • Merged tree
    • i18n:check passes: 2592 literal candidates and 721 drafts per locale.
    • release-check passes against origin/main (@pascal-app/editor@1.0.6 has no catalogs).
  • CI: CI (quality), cli-smoke and Changed paths all pass. mcp-ci doesn't run now, which is expected: against main, this PR no longer touches core, MCP, scene-API files or bun.lock. Bugbot finished as neutral with 2 new Low findings, listed below.

P2 (doesn't block)

  • Bugbot (contextual-helper-panel.tsx, around line 375): the paint-scope verb (Pick / Erase / Paint) is hardcoded English and is inserted into translated text, so other locales get mixed-language chips.
  • Bugbot (view-toggles.tsx, around line 939): the {n} below reference-floor label joins a number to a separate below message. It should be one ICU message so translators can control word order and plurals.
  • My earlier P2s from 4176867 still stand.

Security isn't needed.

@Aymericr
Aymericr merged commit ee8d567 into main Oct 27, 2026
4 checks passed
@Aymericr Aymericr mentioned this pull request Oct 27, 2026
3 of 4 tasks
@Aymericr
Aymericr deleted the cursor/i18n-tools-6d7c branch October 27, 2026 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants