Skip to content

[Maintainer] i18n: string-extraction codemod + CI literal guard - #1167

Merged
Aymericr merged 25 commits into
mainfrom
cursor/i18n-codemod-guard-6d7c
Oct 27, 2026
Merged

Aymericr merged 25 commits into
mainfrom
cursor/i18n-codemod-guard-6d7c

Conversation

@Aymericr

@Aymericr Aymericr commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

What does this PR do?

Adds the tooling for the full localization migration (#621, after #1148 and #1162). It also migrates the viewer toolbar, so the new literal baseline starts lower than the scan rather than higher.

  • Codemod. bun run i18n:literals -- convert [--apply] <path…> (scripts/i18n-codemod.ts) rewrites hardcoded JSX text, aria-*/title/placeholder/alt/label/tooltip… props, conditional and template-literal copy, and toast/alert/confirm/setError payloads into the existing runtime form, i18n._({ id, message, values }). It adds usePascalI18n() to the owning component (one-line bodies stay valid) and the import. Adjacent text and simple interpolations become one ICU message.
    • --keys=… --vars=… turns module data into lazy /* i18n */ descriptors (MessageDescriptor, now re-exported by @pascal-app/core).
    • Copy is ICU-encoded so the compiled message renders exactly the original text.
      • Adjacent static pieces are encoded together.
      • No-break spaces are kept.
      • Text before a placeholder is encoded knowing a { follows.
      • Attribute entities are decoded.
      • Emitted strings escape CR and line separators.
    • Listed as MANUAL and never edited:
      • app modules and editor package entry exports without 'use client';
      • named .map(function …) callbacks (also parenthesized) and async functions;
      • complex interpolations, concatenations and string constants.
    • A second run, including a repeated --vars pass, changes nothing.
  • CI literal guard (inside i18n:check, already in bun run ci). i18n/literal-baseline.json lists each file's legacy copy by text: 2,884 candidates today. The baseline format is version 3, since fingerprints are now rendered text.
    • Per file: adding or swapping a string fails, and a new file must have none.

    • The baseline may only shrink, measured across the repository: each text it adds must consume an equal text removed elsewhere, duplicates counted. Copy that moves with a git mv, split or extraction passes after update.

    • The allowlist may not gain entries relative to the base either. One-off exemptions use // i18n-ignore: <reason>; a bare i18n-ignore exempts nothing.

    • Text is recorded as it renders: JSX whitespace rules, full HTML entity decoding (entities, attributes included) and {' '} joins, so equivalent spellings match. A no-break space is content. The allowlist applies to the whole joined text.

    • These count as copy:

      • hyphenated labels;
      • const strings rendered in JSX or inside a sentence (resolved through lexical scope);
      • copy built with +, constants included.

      Toast descriptions are counted once.

  • Review policy. Drafts ship as English until reviewed; regressions never ship.
    • CI accepts AI drafts only when they are flagged fuzzy + Review: required. They are never compiled into packs, so users see English.
    • bun scripts/i18n.ts release-check compares the committed catalogs with a base revision, never the working tree.
      • In CI the base is the PR base (part of i18n:check, depth-2 checkout). On push to main it is github.event.before, which covers every commit of the push.
      • In release.yml the base is the latest @pascal-app/editor@<version> tag, since the editor package ships the catalogs.
      • When that tag predates the catalogs (true today: @pascal-app/editor@1.0.6), it falls back to origin/main when that is another commit with catalogs, then to the commit that introduced the catalogs, and otherwise fails loudly.
      • An explicit --base that cannot be resolved or read fails the job.
    • It fails when an entry reviewed at the base, with unchanged English, became a draft.
    • Renames are declared with a renamed-from: <old id> descriptor comment.
      • i18n:extract carries the reviewed translation over when the English is unchanged. CI fails only if a declared rename arrives without it.
      • An undeclared new ID that shares English with a removed reviewed ID is reported as a possible rename (console and job summary), never an error. It may be an unrelated addition, and a missed carry-over just shows English.
      • No one has to mark Review: human themselves.
    • It also inspects the compiled packs themselves and fails if any draft msgstr is shipped.
    • Both jobs write the per-locale draft count, the base used and any rename warnings to the job summary. check --reviewed remains a manual sign-off command; it is not in any workflow.
    • extract never approves a newly filled translation by itself.
  • Pseudo-locale. en-XA (accented, ~30 % longer, bracketed) is compiled from English and checked for drift. apps/editor/lib/dev-locales.ts registers it only outside production, inside a NODE_ENV branch that the bundler drops. A local next build has no en-XA strings in .next/static or .next/server.
  • Screenshot audit. bun run i18n:pseudo-audit (playwright-core 1.64.1, matching the tree, with the system Chrome; a dev tool, not a CI job) opens each surface in i18n/pseudo-surfaces.ts and saves full-viewport screenshots.
    • It fails on visible text or accessible names with plain ASCII words, and on visible controls without an accessible name.
    • Allowlisted data is removed as whole words only, so the unit in no longer hides Find or Pin.
  • Viewer toolbar. Stack/Exploded/Solo, wall mode, the Display menu, Walkthrough, Preview and Enter/Exit VR are migrated. Scene theme names moved to a shared helper used by both toolbars. The Display menu is w-max min-w-60 max-w-80, so longer labels no longer wrap or truncate.
  • Docs. i18n/README.md covers the guard, codemod, review states, how to declare a rename, release behaviour (including which tag the release compares with) and the audit. It includes "Releases ship drafts as English until they are reviewed." CONTRIBUTING.md gets a short "User-visible text" section.

How to test

  1. bun run i18n:check: passes. It reports 2,884 legacy candidates and 64 drafts per locale awaiting review, and writes that table to $GITHUB_STEP_SUMMARY in CI.
  2. In packages/editor/src/i18n/locales/fr/messages.po, mark editor.buildPanel.advanced as #, fuzzy + Review: required and rerun: it fails with fr: "editor.buildPanel.advanced" is reviewed on the base branch with the same English and must stay reviewed. I ran this locally.
  3. Rename the ID editor.buildPanel.advanced in build-panel.tsx to editor.buildPanel.advancedSection, with comment: 'renamed-from: editor.buildPanel.advanced', then run bun run i18n:extract. The French entry arrives as Avancé with Review: human, and i18n:check passes. I ran this locally and reverted.
  4. Add <p>Hello there</p> to a component, or change an existing literal's text: the check fails and names the file, line and text. Add {/* i18n-ignore: demo */} above it and the check passes; a bare {/* i18n-ignore */} does not. <p>Pascal{' '}</p> passes like <p>Pascal</p>.
  5. bun scripts/i18n.ts release-check prints the base it compares with and passes; --base nonexistent-ref exits 1.
    • Three tests run the default lookup, without --base-dir, in a temporary git repository. It uses an @pascal-app/editor@* tag that has catalogs, ignoring v* tags. It falls back to the catalogs' first commit when the tag predates them. It fails loudly when no base exists. The first two exit 1 on a reviewed-to-draft change.
    • The job tests on temporary catalog copies exit 1 when French Avancé becomes a draft with the same English, and when a draft is injected into the compiled French pack.
  6. bun test ./scripts/i18n.test.ts ./scripts/i18n-literals.test.ts ./packages/editor/src/i18n: 55 pass. The tests use fixtures, temp copies and temp repositories, never the real catalogs. They include:
    • the unrelated-addition (warning only) and declared-rename cases;
    • the git mv/split baseline test and the allowlist ratchet;
    • the release tag lookup;
    • codemod round trips for braces, no-break spaces, quoted placeholders, attribute entities and CR.
  7. bun dev, then bun run i18n:pseudo-audit -- --out .i18n-audit: screenshots land in .i18n-audit/.

Follow-ups (not in this PR)

  • Make pseudo-locale availability an explicit deployment flag (NEXT_PUBLIC_PASCAL_PSEUDO_LOCALE), so production-built Preview deployments can enable it.
  • Store error codes/descriptors instead of translated strings in component state, so cached text follows a later locale change.
  • Resolve the supported locale before setting <html lang> (inherited from [Maintainer] i18n: locale registry, formatting and first locales #1148).
  • Resolve React import aliases (memo as reactMemo) in the hook-owner check; aliased wrappers are currently refused conservatively.

Screenshots / screen recording

The editor in en-XA from the audit. The migrated top bar and toolbar are pseudo-localized; the tool tiles, tool dock and sidebar are later surface PRs:

Editor in 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
Changes CI and release gates for translations plus a large baseline policy surface; the toolbar migration is localized UI but mistakes could block merges or releases.

Reviewed by Cursor Bugbot for commit 53abaae. 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
@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-27T01:48:34.885299Z 993f983 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.

@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: 993f983ec3

ℹ️ 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 scripts/i18n-literals.ts Outdated
if (!/\p{L}{2}/u.test(trimmed)) return false
if (allow.has(trimmed)) return false
// Identifiers, icon names, CSS classes, paths and URLs.
if (/^[a-z0-9]+(?:[-_:./#][a-z0-9]+)+$/i.test(trimmed) && !/\s/.test(trimmed)) return false

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 Scan hyphenated user-visible labels

Do not classify every separator-delimited token as a non-translatable identifier. Because this predicate is shared by JSX text and known UI attributes/properties, literals such as <button>Sign-in</button>, aria-label="zoom-in", or label: 'Read-only' match this regex and produce no candidate. New hardcoded copy in those forms therefore passes i18n:check, defeating the literal guard for a common class of labels.

Useful? React with 👍 / 👎.

Comment thread scripts/i18n-literals.ts
Comment on lines +389 to +393
ts.isPropertyAssignment(property) &&
ts.isIdentifier(property.name) &&
property.name.text === 'description'
) {
literalSink(property.initializer, 'notify')

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 Deduplicate notification description candidates

Avoid emitting a second candidate for a notification option's description. For a TSX call such as toast.error('Failed', { description: 'Try again' }), this loop reports the description as notify, then normal AST traversal reports the same literal again through the generic UI_PROPERTIES branch. convert --apply consequently applies two edits to the identical source range and corrupts the generated file, while the check path also counts one literal twice.

Useful? React with 👍 / 👎.

Comment thread apps/editor/app/client-bootstrap.tsx Outdated
import '../lib/bootstrap'
import { createI18n, defaultI18n, PascalI18nProvider } from '@pascal-app/core'
import { registerBundledLocales } from '@pascal-app/editor/locales'
import { registerPseudoLocale } from '@pascal-app/editor/locales/pseudo'

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 Exclude the pseudo catalog from production imports

Load the pseudo-locale module only through a production-excluded path rather than a static root-client import. The conditional suppresses registerPseudoLocale() but does not conditionally execute the ES module import; pseudo-locale.ts eagerly imports the generated en-XA/messages.ts, whose JSON.parse initializer is therefore part of the production client dependency graph. As the English catalog grows, every production editor load carries and evaluates a duplicate development-only catalog.

Useful? React with 👍 / 👎.

@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 scripts/i18n-codemod.ts Outdated
Comment thread scripts/i18n-pseudo-audit.ts

Copy link
Copy Markdown
Member Author

Tooling cross-review at b4cc700: CHANGES REQUESTED

CI is green on this SHA (quality, cli-smoke, Changed paths, ci), and Bugbot is neutral. The codemod isn't applied anywhere, so no product copy changes. Locally, i18n:check passes (about 23s, of which the literal scan is about 1.2s).

The guard catches new JSX text, title, ternary copy, toast() calls and new files. It ignores classNames, ids, data-testid and test files.

P1: the review relaxation is a real weakening.

  • check --reviewed doesn't run anywhere: not in ci.yml and not in release.yml. The sign-off gate is now manual-only.
  • A PR can turn a Review: human translation into fuzzy + Review: required, and default CI passes. The entry silently drops out of the compiled pack, so users fall back to English. Your own test scripts/i18n-literals.test.ts:134 does exactly this to the reviewed Avancé.
  • Unreviewed text can't reach users, which is good.
  • Fix: make default CI fail when an entry that is human on the merge-base, with an unchanged Source-Hash, becomes unreviewed. Then either run check --reviewed in release.yml before publish, or record in the README that releases ship drafts as English.

P2:

  • Baseline raises: i18n:literals -- update can raise counts and CI never notices. This PR raises viewer-toolbar.tsx from 43 to 53. Fail on increases against the base branch unless they're deliberate.
  • Count-only ratchet: in a file that already has literals, replacing Share with Delete everything now passes. Fixed copy in baselined files can be swapped for new copy.
  • Guard gaps: // i18n-ignore with no reason is accepted. Hyphenated labels (Codex) and module constants rendered in JSX also slip through.
  • CI cost: the AI-draft test in scripts/i18n-literals.test.ts adds about 65s to bun run test and rewrites real catalogs in place. Use a temp copy instead.
  • Open bot findings: Codex flags that the pseudo catalog is statically imported into the production client. Bugbot (Medium) flags that the codemod's hook insert breaks single-line bodies, and (Low) that allowlist substring stripping hides words in the audit.
  • Nits: playwright-core is pinned to 1.58.0, below the 1.64.1 already in the tree. The allowlisted key names (Space, Enter, Tab, Del) are translatable in some locales.

Security isn't needed: no auth, payments, secrets, infra, moderation, user data or schema files are touched.

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

@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 scripts/i18n.ts
cursor Bot pushed a commit that referenced this pull request Oct 27, 2026
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.
cursor Bot pushed a commit that referenced this pull request Oct 27, 2026

@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 scripts/i18n-literals.ts
They can render inside server components, where an inserted hook crashes
the request.
cursor Bot pushed a commit that referenced this pull request Oct 27, 2026
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.
cursor Bot pushed a commit that referenced this pull request Oct 27, 2026
…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.
cursor Bot pushed a commit that referenced this pull request Oct 27, 2026
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.
cursor Bot pushed a commit that referenced this pull request Oct 27, 2026

@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 scripts/i18n.ts
…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.
cursor Bot pushed a commit that referenced this pull request Oct 27, 2026

Copy link
Copy Markdown
Member Author

Tooling cross-review at 285ce3c: CHANGES REQUESTED

I checked this locally at /tmp, at the head commit. CI is green: quality, ci, cli-smoke, Changed paths and Bugbot all pass. Most of my earlier findings are fixed. One P1 is left in the release half of the gate.

P1: the release regression check never compares against a real release, so it can't fail

  • release-check gets its base from lastReleaseTag(), which runs git describe --match 'v[0-9]*'. But release.yml now only creates @pascal-app/<pkg>@<version> tags. The newest v* tag is v1.0.3, from 2026-09-24, and no PO catalogs exist at that tag. So the base is empty, reviewRegressions checks nothing, and the base will never move forward.
  • I reproduced it. I took fr core.level.basement, which is Review: human on main, and made it fuzzy / Review: required with the same English. Then I ran bun run i18n:compile, which is what a real downgrade does.
    • bun scripts/i18n.ts release-check exits 0 with "No reviewed translation regressed against ebe69be…" (v1.0.3).
    • release-check --base origin/main fails correctly.
    • i18n:check (the CI path) fails correctly.
  • The release-job tests all pass --base-dir, so the tag lookup is never tested.
  • Fix: resolve the base from the newest release tag that actually exists, for example --match '@pascal-app/editor@*'. Add a test that runs the default base lookup against a fixture repo with a release tag. The README line that says "last v* release tag" also needs updating.
  • The no-tag case is fine. It falls back to baseRevision(), and on main that throws "release-check needs a base".

Verified fixed

  • CI regression check: runs inside i18n:check. PRs compare against HEAD^1 with fetch-depth 2, and it throws if that commit is missing. Pushes compare against event.before, which is fetched first. It reads the base content with git show <rev>:path, not the working tree. The rename-with-same-English case is covered too.
  • Leak check: packLeaks parses the JSON inside the compiled messages.ts packs. The pack test injects a draft and the job fails.
  • Job summary: per-locale draft counts are written to $GITHUB_STEP_SUMMARY. I got zh-CN, fr, es and ja at 64 each. The README now says "Releases ship drafts as English until they are reviewed."
  • Literal guard:
    • Changing a string and running update fails against the base.
    • Moving a file passes.
    • i18n-ignore with no reason, a rendered constant, a template that uses a constant, and aria-label="Read-only" are all flagged.
  • The viewer-toolbar baseline is 0.
  • The codemod now produces valid code for arrow and one-line function bodies.
  • The audit allowlist matches whole words only, and it can't grow against the base.
  • playwright-core 1.64.1 is the only copy in the tree.
  • Bugbot fix: annotatedState never approves a new fill, and there is a test for it.
  • The AI-draft test now takes under 1ms and leaves the catalogs unchanged.
  • Pseudo-locale: I ran next build. No en-XA catalog text is in .next/static or the server chunks. "en-XA" appears only in a source map.
  • I ran bun test scripts/i18n.test.ts scripts/i18n-literals.test.ts: 42 pass, and the tree stays clean. i18n:check passes at this head.

P2 (doesn't block)

  • Two older tests in i18n.test.ts are still slow, about 48s and 23s: "source drift…" and "catalog checking does not rewrite…".
  • I didn't run i18n:pseudo-audit because it needs a browser and a dev server.

Security isn't needed: there are no changes to auth, secrets or user data.

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.
cursor Bot pushed a commit that referenced this pull request Oct 27, 2026

@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 1032c0f. Configure here.

Comment thread scripts/i18n-literals.ts Outdated
- 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.
cursor Bot pushed a commit that referenced this pull request Oct 27, 2026
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.
cursor Bot pushed a commit that referenced this pull request Oct 27, 2026

Copy link
Copy Markdown
Member Author

Tooling cross-review at 53abaae: APPROVE

I checked this locally at /tmp, at the head commit. CI is green: quality, ci, cli-smoke, Changed paths and Bugbot all pass. The P1 from 285ce3c is fixed.

Release base (the earlier P1): fixed

  • lastReleaseTag('editor') now matches @pascal-app/editor@*. If that tag has no catalogs, the base falls back to origin/main when that is a different commit, and then to the commit that first added the catalogs. If none of those works, it exits 1 and asks for --base. It never compares against an empty base.
  • I repeated my reproduction: fr core.level.basement made a draft with the same English, then i18n:compile.
    • In this checkout the default release-check used origin/main (@pascal-app/editor@1.0.6 has no catalogs) and exited 1.
    • I also set up a copy that looks like the release job: full history and tags, with origin/main equal to HEAD. It used 374b781 ([Maintainer] i18n: locale registry, formatting and first locales #1148, 85 reviewed entries per locale). It exited 1 with the regression committed, and exited 0 when clean.
    • The real job uses fetch-depth: 0, so the tags and the commit that added the catalogs are both available.
  • New tests run the default tag lookup without --base-dir, in a temporary repo. They cover the editor tag with a v* decoy, the fallback when the tag has no catalogs, and the loud failure when no base exists.
  • The README is corrected.

Renamed IDs: weaker, but not silent

  • An undeclared rename with the same English is no longer an error. I tried one: core.level.basement renamed to core.level.basementRenamed, then extract.
    • CI still fails on the empty msgstr.
    • Once that msgstr is filled, it becomes a draft and shows as English. Neither the regression check nor the leak check catches it, but [i18n warning] possible rename … appears on the console and in the job summary.
    • A declared renamed-from: carries the translation over only when the old entry was reviewed with the same English, and it fails if the carry-over is missing.
  • Accepted as a warning: same English alone doesn't prove a rename, and the worst case is English text, not a wrong translation.

Spot checks

  • A joined Pascal{' '}Editor is allowlisted as a whole. Save&nbsp;now keeps its no-break space. It's {n} o'clock encodes correctly.
  • A const in a concatenation is flagged, and the codemod marks it MANUAL.
  • .map callbacks with parentheses convert correctly.
  • A trailing i18n-ignore no longer exempts the next line.
  • Baseline v3 is fine: update and the repo-wide growth check are unchanged.
  • I ran bun test scripts/i18n.test.ts scripts/i18n-literals.test.ts: 53 pass and the tree stays clean. i18n:check passes, with 64 drafts per locale in the summary.

P2 (doesn't block)

Security isn't needed: there are no changes to auth, secrets or user data.

@Aymericr
Aymericr merged commit aedd05d into main Oct 27, 2026
5 checks passed
cursor Bot pushed a commit that referenced this pull request Oct 27, 2026
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
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