From 993f983ec3ce747da730aa73e6ecea25f226ca7f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 27 Oct 2026 01:43:02 +0000 Subject: [PATCH 01/34] i18n: string-extraction codemod, literal ratchet and en-XA pseudo-locale 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 --- .gitignore | 1 + CONTRIBUTING.md | 16 + apps/editor/app/client-bootstrap.tsx | 2 + apps/editor/app/layout.tsx | 2 + bun.lock | 5 +- i18n/README.md | 85 ++- i18n/literal-allowlist.json | 43 ++ i18n/literal-baseline.json | 198 ++++++ i18n/pseudo-surfaces.ts | 47 ++ package.json | 5 +- packages/editor/package.json | 1 + .../editor/src/i18n/locales/en-XA/messages.ts | 1 + packages/editor/src/i18n/pseudo-locale.ts | 17 + scripts/i18n-codemod.ts | 462 ++++++++++++++ scripts/i18n-literals.test.ts | 168 +++++ scripts/i18n-literals.ts | 595 ++++++++++++++++++ scripts/i18n-pseudo-audit.ts | 144 +++++ scripts/i18n.ts | 274 +++++--- 18 files changed, 1980 insertions(+), 86 deletions(-) create mode 100644 i18n/literal-allowlist.json create mode 100644 i18n/literal-baseline.json create mode 100644 i18n/pseudo-surfaces.ts create mode 100644 packages/editor/src/i18n/locales/en-XA/messages.ts create mode 100644 packages/editor/src/i18n/pseudo-locale.ts create mode 100644 scripts/i18n-codemod.ts create mode 100644 scripts/i18n-literals.test.ts create mode 100644 scripts/i18n-literals.ts create mode 100644 scripts/i18n-pseudo-audit.ts diff --git a/.gitignore b/.gitignore index e41ddbb868..8d9dbf9930 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ og-test .claude/launch.json .claude/settings.local.json .vscode/launch.json +.i18n-audit/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d5f928dbb3..b438dba567 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,6 +60,22 @@ and reports inflated counts. `bun run test` goes through Turborepo, which builds workspace dependencies first (several packages import theirs from `dist/`) and runs each package's own scoped test script. +### User-visible text + +Every string a person reads in the editor (labels, tooltips, `aria-label`, +`title`, placeholders, menus, dialogs, errors) goes through the i18n catalog: + +```tsx +const i18n = usePascalI18n() +
@@ -115,7 +119,9 @@ export function EditorNavbar({ sceneId, name, onRename }: EditorNavbarProps) { type="button" > - Share + + {i18n._({ id: 'app.editorNavbar.share', message: 'Share' })} + {shareOpen && ( diff --git a/apps/editor/components/share-panel.tsx b/apps/editor/components/share-panel.tsx index 3ca45b2070..d6521c3948 100644 --- a/apps/editor/components/share-panel.tsx +++ b/apps/editor/components/share-panel.tsx @@ -1,5 +1,6 @@ 'use client' +import { usePascalI18n } from '@pascal-app/core' import { Check, Copy, ExternalLink } from 'lucide-react' import { type KeyboardEvent, type RefObject, useEffect, useRef, useState } from 'react' import { cn } from '@/lib/utils' @@ -38,6 +39,7 @@ async function writeShared(sceneId: string, shared: boolean): Promise { } export function SharePanel({ sceneId, anchorRef, onClose }: SharePanelProps) { + const i18n = usePascalI18n() // null while the current state is loading. const [shared, setShared] = useState(null) const [saving, setSaving] = useState(false) @@ -53,7 +55,12 @@ export function SharePanel({ sceneId, anchorRef, onClose }: SharePanelProps) { .catch(() => { if (cancelled) return setShared(false) - setError('Could not read the sharing state.') + setError( + i18n._({ + id: 'app.sharePanel.couldNotReadTheSharingState', + message: 'Could not read the sharing state.', + }), + ) }) return () => { cancelled = true @@ -87,7 +94,17 @@ export function SharePanel({ sceneId, anchorRef, onClose }: SharePanelProps) { setShared(await writeShared(sceneId, next)) } catch { setShared(previous) - setError(next ? 'Could not turn on the public link.' : 'Could not turn off the public link.') + setError( + next + ? i18n._({ + id: 'app.sharePanel.couldNotTurnOnThePublic', + message: 'Could not turn on the public link.', + }) + : i18n._({ + id: 'app.sharePanel.couldNotTurnOffThePublic', + message: 'Could not turn off the public link.', + }), + ) } finally { setSaving(false) } @@ -99,7 +116,7 @@ export function SharePanel({ sceneId, anchorRef, onClose }: SharePanelProps) { return ( // Keystrokes in here (Delete, Cmd+A, tool keys) must not reach the editor's shortcuts.
{ if (event.key !== 'Escape') event.stopPropagation() @@ -108,7 +125,7 @@ export function SharePanel({ sceneId, anchorRef, onClose }: SharePanelProps) { >
- Public link + {i18n._({ id: 'app.sharePanel.publicLink', message: 'Public link' })}

- Anyone with the link can view this project. They cannot edit it. + {i18n._({ + id: 'app.sharePanel.anyoneWithTheLinkCanView', + message: 'Anyone with the link can view this project. They cannot edit it.', + })}

{error &&

{error}

} @@ -127,7 +147,7 @@ export function SharePanel({ sceneId, anchorRef, onClose }: SharePanelProps) {
- Hide controls + {i18n._({ id: 'app.sharePanel.hideControls', message: 'Hide controls' })}

- Only the 3D view, no header or toolbar. + {i18n._({ + id: 'app.sharePanel.onlyThe3dViewNoHeader', + message: 'Only the 3D view, no header or toolbar.', + })}

- Link + + {i18n._({ id: 'app.sharePanel.link', message: 'Link' })} +
event.currentTarget.select()} readOnly @@ -158,16 +183,18 @@ export function SharePanel({ sceneId, anchorRef, onClose }: SharePanelProps) { target="_blank" > - Open + {i18n._({ id: 'app.sharePanel.open', message: 'Open' })}
- Embed code + + {i18n._({ id: 'app.sharePanel.embedCode', message: 'Embed code' })} +