Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/shared/plugins/plugin-content-pack-contributions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,25 @@ 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' }] }]
])('rejects unsafe or malformed %s contributions', (_label, contributes) => {
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({
Expand Down
17 changes: 17 additions & 0 deletions src/shared/plugins/plugin-content-pack-contributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down