From f69dc01a09aa31bcd4249092ac3c018cc690f28c Mon Sep 17 00:00:00 2001 From: Evgenii Date: Sat, 8 Aug 2026 14:05:22 +0700 Subject: [PATCH] fix(plugins): reject locale ids Intl cannot parse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- .../plugin-content-pack-contributions.test.ts | 12 ++++++++++++ .../plugin-content-pack-contributions.ts | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/shared/plugins/plugin-content-pack-contributions.test.ts b/src/shared/plugins/plugin-content-pack-contributions.test.ts index b3d1708ee025..e7755a2dec4e 100644 --- a/src/shared/plugins/plugin-content-pack-contributions.test.ts +++ b/src/shared/plugins/plugin-content-pack-contributions.test.ts @@ -120,6 +120,10 @@ describe('content-pack manifest contributions', () => { it.each([ ['language pack', { languagePacks: [{ locale: 'en_US', path: 'locale.json' }] }], + // Shape-legal but not well-formed: the old regex let these through, and the + // pack then loaded with dates silently falling back to English. + ['duplicate region', { languagePacks: [{ locale: 'en-US-US', path: 'locale.json' }] }], + ['unknown subtag', { languagePacks: [{ locale: 'en-aaa', path: 'locale.json' }] }], ['language pack path', { languagePacks: [{ locale: 'pt-BR', path: '../outside.json' }] }], ['VM recipe', { vmRecipes: [{ path: '\\\\server\\recipe.json' }] }], ['agent profile', { agents: [{ path: 'agents/../profile.json' }] }] @@ -127,6 +131,14 @@ describe('content-pack manifest contributions', () => { expect(parsePluginManifest(manifest(contributes)).ok).toBe(false) }) + it.each(['ru', 'pt-BR', 'zh-Hans-CN', 'en-US'])('accepts the well-formed locale %s', (locale) => { + expect( + parsePluginManifest(manifest({ languagePacks: [{ locale, path: 'locale.json' }] })) + ).toMatchObject({ + ok: true + }) + }) + it('rejects duplicate ids, locales, paths, and bindings', () => { const parsed = pluginManifestSchema.safeParse( manifest({ diff --git a/src/shared/plugins/plugin-content-pack-contributions.ts b/src/shared/plugins/plugin-content-pack-contributions.ts index bff64e26af41..c50c21ee1e57 100644 --- a/src/shared/plugins/plugin-content-pack-contributions.ts +++ b/src/shared/plugins/plugin-content-pack-contributions.ts @@ -7,13 +7,30 @@ export const PLUGIN_KEYBINDING_LIMIT = 256 export const PLUGIN_VM_RECIPE_LIMIT = 64 export const PLUGIN_AGENT_PROFILE_LIMIT = 64 +/** True when Intl can parse the tag's grammar, not just its shape. */ +function isWellFormedLocaleId(value: string): boolean { + try { + Intl.getCanonicalLocales(value) + return true + } catch { + return false + } +} + // Why: locale ids become i18next bundle keys and filenames. This bounded BCP // 47 subset covers current community packs without accepting path syntax. +// The shape check alone is looser than BCP 47 in one direction: tags like +// `en-US-US` or `en-aaa` match it but are not well-formed, so Intl throws on +// them. That throw surfaces nowhere — the pack installs, every string +// translates, and only dates and relative times quietly fall back to English. +// Delegating the grammar to Intl rejects the tag at install instead, where the +// pack author can still fix it. export const pluginLocaleIdSchema = z .string() .min(2) .max(35) .regex(/^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/, 'must be a portable locale identifier') + .refine(isWellFormedLocaleId, 'must be a well-formed locale identifier') export const pluginLanguagePackContributionSchema = z .object({