Skip to content

fix(plugins): reject locale ids Intl cannot parse - #12533

Open
smwbev wants to merge 2 commits into
stablyai:mainfrom
smwbev:fix/canonicalize-pack-locale
Open

smwbev wants to merge 2 commits into
stablyai:mainfrom
smwbev:fix/canonicalize-pack-locale

Conversation

@smwbev

@smwbev smwbev commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Repurposed from canonicalizing the locale at read time to rejecting it at install, per review.

pluginLocaleIdSchema checks the shape of a locale id, not its grammar, and the shape check is looser than BCP 47 in one direction: en-US-US (duplicate region) and en-aaa (a subtag that is neither a region nor a script) both match /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.

A pack declaring one installs cleanly, loads, and translates every string. Then Intl.DateTimeFormat throws inside getIntlLocale(), the catch at src/renderer/src/i18n/i18n.ts:106-113 swallows it, and dates and relative times fall back to English with nothing logged. The pack author sees a working pack with English dates and no way to tell why.

The fix is one .refine on the schema delegating the grammar to Intl.getCanonicalLocales, so the tag is rejected at install where the author can still act on it. No new module, no cache, no renderer change. The bundled pt-BR pack is unaffected, as is every other well-formed tag.

Screenshots

No visual change.

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test — src/shared/plugins green at 147 tests; the 8 unrelated files that fail here fail identically on a clean upstream/main checkout (Node 26 against the required Node 24: node-pty ABI and timing-sensitive watchers)
  • pnpm build
  • Added tests: two rejection cases (en-US-US, en-aaa) alongside the existing en_US case, and a positive case pinning ru, pt-BR, zh-Hans-CN, en-US so the refine cannot start rejecting real packs

AI Review Report

Reviewed for scope, blast radius, and cross-platform behavior.

  • Blast radius. pluginLocaleIdSchema has exactly one consumer, pluginLanguagePackContributionSchema; grep across src/ confirms no other import. Nothing outside manifest parsing changes.
  • Regression risk on existing packs. Every locale tag shipped or plausibly shipped was run through the new schema: pt-BR (bundled), ru, en-US, zh-Hans-CN, ja, ko, es. All pass. The added positive test pins four of them so a future tightening cannot silently break them.
  • Cross-platform. Intl.getCanonicalLocales is ECMA-402, present in every Node and Chromium build this project targets; no platform branch, no path handling, no shell, no shortcut or label. Verified the behaviour is identical under the repo's Node 24 target and the Node 26 used locally. Nothing Electron-specific is touched.
  • Behaviour delta. Brute-forced the tags the regex accepts through old and new logic. The only difference is that tags Intl cannot parse are now rejected at parse time instead of loading and failing silently later. No previously-valid tag becomes invalid.

Security Audit

  • Input handling. The change only narrows what the manifest accepts, so it cannot widen the attack surface. Locale ids become i18next bundle keys and filenames; the existing regex already excluded path syntax, and that regex is unchanged — .refine runs after it.
  • Error handling. Intl.getCanonicalLocales throws RangeError on malformed input; the helper catches and returns false rather than letting it escape into manifest parsing. It catches everything deliberately — any throw from this call means the tag is unusable.
  • No new surface. No command execution, no path resolution, no auth, no secrets, no IPC, no new dependency. Intl is a language built-in.
  • Follow-up. None required for this change.

Notes

One measurement worth flagging. I bucketed every tag the current schema accepts by what Intl does with it — all two-letter primary subtags against typical second subtags (regions, scripts, and shape-legal fakes of both):

count share example
Intl throws — caught by this PR 1352 22% en-US-US, en-aaa
canonical, but no plural rules 3698 61% xx, aa-US, zz-ZZ
fully working 1034 17% af, pt-BR
const RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/
for (const tag of candidates) {
  if (!RE.test(tag)) continue
  try { Intl.getCanonicalLocales(tag) } catch { throws.push(tag); continue }
  ;(Intl.PluralRules.supportedLocalesOf(tag).length ? works : silent).push(tag)
}

This PR closes the 22%. The 61% is well-formed-but-unknown tags: they pass getCanonicalLocales, the pack loads, and Intl.PluralRules.supportedLocalesOf returns empty — same silent English fallback, different cause. .refine structurally cannot catch those, because the grammar is valid.

That is the half #12505 addresses, which is why I don't think it rests on the same premise. Its core claim — the synthetic plugin<hex> language never gets plural rules regardless of how valid the declared locale is — holds after this change. It currently imports canonicalizePackLocale, which this PR removes, so I'll rebase it off this branch once the direction here is settled.

Separately and out of scope: the regex is also narrower than BCP 47 in places. en-US-u-ca-buddhist and en-a-bbb are legal but rejected, because a single-letter extension singleton doesn't fit {2,8}. Happy to open that separately if it's worth having.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Plugin locale schema validation now calls Intl.getCanonicalLocales to reject malformed locale identifiers that match the existing pattern. Tests cover invalid duplicate-region and unknown-subtag values. Tests also cover valid locales with language, region, and script subtags.


🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: rejecting locale identifiers that Intl cannot parse.
Description check ✅ Passed The description covers the required sections, explains the change, documents testing, cross-platform review, security risks, and out-of-scope behavior.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@brennanb2025

Copy link
Copy Markdown
Contributor

Thanks for this — the writeup, the benchmark table, and the security notes are unusually careful, and I dug in properly because of it. Heads up first: I have not pushed anything to your branch.

The short version is that the ru_RU case can't reach getIntlLocale(). pluginLocaleIdSchema in src/shared/plugins/plugin-content-pack-contributions.ts has always matched /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/, which forbids underscores, so a manifest declaring ru_RU fails to parse and the plugin never loads at all. main already pins this — src/shared/plugins/plugin-content-pack-contributions.test.ts:122 asserts a en_US language pack is rejected. I brute-forced every string that schema accepts (~24k tags) through the before/after logic and found zero behaviour differences, apart from three cosmetic ones on deprecated Chinese variant tags (zh-latn2-guoyu and friends) that no real manifest ships. That's the same reason Intl.getCanonicalLocales in front of supportedLocalesOf is a no-op: supportedLocalesOf runs that canonicalization itself.

You were circling something real, though. That regex is looser than BCP-47, and that's the direction that bites: shape-legal typos like en-US-US (duplicate region) or en-aaa (a subtag that is neither a region nor a script) pass the manifest, the pack loads, every string translates — and then dates and relative times silently fall back to English with nothing logged. That is exactly the failure you described, just with a different trigger.

The fix for it is small and sits one layer up: a single .refine on pluginLocaleIdSchema delegating the grammar to Intl.getCanonicalLocales, so the tag is rejected at install where the pack author can see it and act on it. Around 18 lines with tests, no new module, no cache, no renderer change. The bundled pt-BR pack is unaffected.

Would you like to repurpose this PR to that, or would you rather we carry it? Genuinely your call — you found the gap.

One more thing worth a look: #12505 is stacked on this branch, so it may rest on the same premise.

@smwbev
smwbev force-pushed the fix/canonicalize-pack-locale branch from f988ae5 to 2f064a0 Compare August 8, 2026 07:05
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@smwbev smwbev changed the title fix(i18n): canonicalize the locale a language pack declares fix(plugins): reject locale ids Intl cannot parse Aug 8, 2026
@smwbev

smwbev commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Repurposed as you suggested — taking it, since you offered. The PR body has the full writeup; short version below.

.refine(isWellFormedLocaleId) on pluginLocaleIdSchema, delegating the grammar to Intl.getCanonicalLocales. 28 lines with tests, no new module, no cache, no renderer change. pt-BR unaffected, src/shared/plugins green at 147 tests. pack-locale.ts is gone — you were right that it was solving a case that can't occur.

One measurement I'd flag before you review. I bucketed every tag the current schema accepts by what Intl does with it, and the split is lopsided:

count share example
Intl throws — caught here 1352 22% en-US-US, en-aaa
canonical, but no plural rules 3698 61% xx, aa-US, zz-ZZ
fully working 1034 17% af, pt-BR

This PR closes the 22%. The 61% is well-formed-but-unknown tags — they pass getCanonicalLocales, the pack loads, PluralRules.supportedLocalesOf comes back empty, and dates fall back to English just as silently. .refine structurally cannot catch those: the grammar is valid.

That's the half #12505 covers, so I don't think it rests on the same premise — its claim is that the synthetic plugin<hex> language never gets plural rules no matter how valid the declared locale is, and that survives this change. It currently imports canonicalizePackLocale, so I'll rebase it off this branch once you're happy with the direction here.

Also out of scope but noted in the body: the regex is narrower than BCP 47 in places too — en-US-u-ca-buddhist is legal but rejected, because a single-letter extension singleton doesn't fit {2,8}. Separate PR if you want it.

`pluginLocaleIdSchema` checks the shape of a locale id but not its grammar,
and the shape check is looser than BCP 47 in one direction: `en-US-US`
(duplicate region) and `en-aaa` (a subtag that is neither region nor script)
both match it. A pack declaring one installs, loads, and translates every
string — and then `Intl.DateTimeFormat` throws inside `getIntlLocale()`, which
swallows the error and falls back to English. Nothing is logged, so the pack
author sees a working pack with English dates and no way to tell why.

Delegate the grammar to `Intl.getCanonicalLocales` in a `.refine`, so the tag
is rejected at install where the author can still act on it. The bundled
`pt-BR` pack and every other well-formed tag are unaffected.
@smwbev
smwbev force-pushed the fix/canonicalize-pack-locale branch from 2f064a0 to f69dc01 Compare August 8, 2026 07:33
@smwbev

smwbev commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Gentle nudge — no rush intended, I know the queue is deep.

This has been sitting green since 8 Aug: repurposed to the .refine you outlined, CodeRabbit closed its review with no comments, mergeable, and the four checklist commands are run and recorded in the description. Nothing is waiting on me here.

Two things downstream of your call on it, whenever you get to it:

#12505 needs a rebase either way, since it imports canonicalizePackLocale and this PR removes it. I've held off because rebasing before the direction is settled risks doing it twice. Its core claim is unchanged — the synthetic plugin<hex> language gets no plural rules regardless of how valid the declared locale is — and the measurement in this PR's Notes is why I think it addresses a different half of the problem rather than the same premise.

A second narrow widening of the plugin-chrome exemption is written and tested locally — 23 paths, the install form and the log controls, following the same rule as #12455. I'm holding it rather than opening a third PR into the same area while these two are open. Happy to keep holding it, or to open it, whichever is less noise for you.

And if the direction here has changed or this dropped down the list, just say so — I'd rather know than keep it warm.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants