Skip to content

[Maintainer] i18n: migrate navbar and top bar - #1169

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

Aymericr merged 39 commits into
mainfrom
cursor/i18n-navbar-6d7c

Conversation

@Aymericr

@Aymericr Aymericr commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

What does this PR do?

Localizes the editor's navbar and top bar. Part of #621. Depends on #1167 (codemod, literal guard, en-XA, and the viewer toolbar, which moved there): this branch merges it, so the diff includes #1167 until that merges.

  • Project navbar (editor-navbar.tsx): the all-projects link, project-name field and rename tooltip, Preview, Share.
    • Below sm the Preview and Share text is hidden, so both buttons now carry the translated aria-label.
    • The navbar-mobile audit surface (390 px) checks that every control is named. With the labels removed it fails on both buttons.
  • Share panel: labels, descriptions, copy/copied, and its three error messages.
  • App header: the home link label. AppHeader is rendered by server pages, so the translated link is a small client leaf (app-home-link.tsx) and the shell stays server-safe.
  • Viewport view bar (view-bar.tsx): the 3D/2D tabs, close/pin/unpin, more-views menu, Split/Swap, and the ground-mode group.
    • Built-in view names are translated by view ID; host and plugin views keep the label they register, because EditorViewDescriptor is host API.
    • The ground status used to be assembled from template fragments; it is now one ICU select message per state.
  • Catalogs: 43 messages beyond [Maintainer] i18n: string-extraction codemod + CI literal guard #1167. zh-CN/fr/es/ja drafts are fuzzy + Review: required (AI, AI-Provider: cursor-agent); the UI shows English until a fluent reviewer approves. The literal baseline shrinks; it never grows.
  • Audit manifest: navbar, navbar-mobile, share-panel, display-menu and more-views surfaces added to i18n/pseudo-surfaces.ts.

How to test

  1. bun run i18n:check: passes and reports 107 entries per locale awaiting review (64 from [Maintainer] i18n: string-extraction codemod + CI literal guard #1167 plus 43 here).
  2. bun dev, open / and a project with ?lang=en-XA. The home header, navbar, share panel, view bar and viewer toolbar show bracketed, accented text. curl '/?lang=en-XA' returns 200 with aria-label="[Þášçáļ ĥöɱé ~~~]" server-rendered.
  3. bun run i18n:pseudo-audit -- navbar navbar-mobile share-panel display-menu more-views: all five PASS, with no untranslated strings and no unnamed controls. I ran this; screenshots below.

Follow-ups (not in this PR)

Screenshots / screen recording

en-XA (accented, ~30 % longer, bracketed), captured by i18n:pseudo-audit. The left build panel, bottom tool dock and camera hints are later surface PRs.

Navbar and view bar:
Navbar en-XA

Navbar at 390 px (Preview and Share are named although their text is hidden):
Navbar mobile en-XA

Share panel:
Share panel en-XA

Display menu (one line per row after the width fix):
Display menu en-XA

More views menu:
More views 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

Low Risk
Presentation-layer string wiring and locale catalogs only; English defaults unchanged and behavior is the same aside from translated copy and accessibility labels.

Reviewed by Cursor Bugbot for commit 96ca6c7. 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.
@Aymericr Aymericr added the bot:maintainer label Oct 27, 2026 — with Cursor
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

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 ✅ Completed 2026-10-27T02:06:26.009782Z 562f425 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.

@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 1 potential issue.

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 562f425. Configure here.

Comment thread apps/editor/components/app-shell.tsx Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 562f42581e

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/editor/components/app-shell.tsx Outdated
}

export function AppHeader({ children }: { children?: ReactNode }) {
const i18n = usePascalI18n()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Mark the header as a client component before using the hook

AppHeader is rendered from the server-component home and settings pages, but this module has no 'use client' boundary and now calls usePascalI18n, which is exported from a client-only module and uses React client hooks. Rendering either route will therefore attempt to invoke a client export from the server (or render outside the client provider), causing a server-render failure rather than producing the localized label. Move the localized header into a client component or add an appropriate client boundary.

Useful? React with 👍 / 👎.

Comment thread scripts/i18n-literals.ts Outdated
Comment on lines +514 to +517
for (const [path, candidates] of scan) {
total += candidates.length
const allowed = baseline.files[path] ?? 0
if (candidates.length > allowed) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Track baseline candidates instead of only their count

The guard compares only the number of candidates in each file, so a change can delete one legacy literal and introduce a different untranslated literal while keeping the count at or below allowed; i18n:check then passes despite the stated guarantee that a file may never gain hardcoded copy. This is especially easy in files with large baseline allowances. Persist candidate identities (for example, stable text/location fingerprints) and require current candidates to be a subset of the baseline rather than treating the count as a reusable budget.

Useful? React with 👍 / 👎.

…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.

Copy link
Copy Markdown
Member Author

Tooling cross-review at 70f7702: APPROVE

I reviewed only this PR's own diff on top of #1167 (53abaae), 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 navbar, the share panel, the app-home link (moved to a client leaf so AppHeader stays server-safe) and the view bar all go through i18n._({ id, message }), and module data uses /* i18n */ descriptors.
    • The mobile Preview and Share buttons get aria-labels from the same ids as their visible sm:inline text.
    • Every new id is in en and in all four target catalogs as fuzzy / Review: required.
    • The compiled target packs are unchanged, because drafts are left out.
  • [Maintainer] i18n: string-extraction codemod + CI literal guard #1167's gates
    • Against 53abaae, i18n:check passes, and so does release-check --base 53abaae, which runs the regression and leak checks.
    • Drafts rise from 64 to 107 per locale.
    • The literal baseline only loses entries (2884 to 2843 candidates, and nothing added). literal-allowlist.json is unchanged.
  • The 390px audit is real
    • I ran i18n:pseudo-audit against a local dev server with navbar-mobile, navbar and share-panel, and all three pass. The navbar-mobile screenshot shows the icon-only buttons at 390px.
    • With the two new aria-labels removed, navbar-mobile fails with "control has no accessible name" twice. So the check catches exactly this bug.
  • Behaviour: these are string-only changes. The share-panel logic and writeShared are unchanged, and the ground label's text is the same through an ICU select. Security isn't needed.

P2 (doesn't block)

  • viewLabel() looks up the 3D, 2D and Plugins names by view id and ignores view.label for them. Those labels are defined in the editor's own editor-views.ts and use-editor-views.ts, so there's no change today. A low-risk note.

Stacking: this PR contains #1167's head. After #1167 merges, it needs main merged in or a rebase, and a re-check, before it merges.

@Aymericr
Aymericr merged commit 1d35ad4 into main Oct 27, 2026
4 checks passed
cursor Bot pushed a commit that referenced this pull request Oct 27, 2026
@Aymericr Aymericr mentioned this pull request Oct 27, 2026
3 of 4 tasks
@Aymericr
Aymericr deleted the cursor/i18n-navbar-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