export const PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES = 20_000 export const PLUGIN_LANGUAGE_CATALOG_MAX_DEPTH = 16 const DANGEROUS_CATALOG_KEYS = new Set(['__proto__', 'prototype', 'constructor']) const PROTECTED_TRANSLATION_PREFIXES = [ 'auto.components.settings.PluginConsentDialog', 'auto.components.settings.PluginInstallDialog', 'auto.components.settings.PluginKeybindingConsentPreview', 'auto.components.settings.PluginMarketplaceBrowser', 'auto.components.settings.PluginMarketplaceListingRow', 'auto.components.settings.PluginMarketplacePreviewDialog', 'auto.components.settings.PluginMarketplaceSourceDialog', 'auto.components.settings.PluginRemoveDialog', 'auto.components.settings.PluginRollbackDialog', 'auto.components.settings.PluginSettingsRow', 'auto.components.settings.PluginVmRecipeConsentPreview', 'auto.components.settings.PluginsSettingsSection' ] export type PluginLanguagePackRegistration = { id: `plugin:${string}` resourceLanguage: `plugin${string}` pluginKey: string locale: string catalog: Record } export function pluginLanguageResourceId(id: `plugin:${string}`): `plugin${string}` { let encoded = '' for (let index = 0; index < id.length; index += 1) { encoded += id.charCodeAt(index).toString(16).padStart(4, '0') } // Why: i18next parses punctuation in language tags during resource lookup; // fixed-width UTF-16 hex preserves a collision-free qualified-ID mapping. return `plugin${encoded}` } export type PluginLanguagePackParseResult = | { ok: true; catalog: Record; entries: number } | { ok: false; error: string } type CatalogFrame = { source: Record target: Record path: string depth: number } function isCatalogObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } function protectedTranslation(path: string): boolean { return PROTECTED_TRANSLATION_PREFIXES.some( (prefix) => path === prefix || path.startsWith(`${prefix}.`) ) } function hasUnsafeCatalogKeyCharacter(key: string): boolean { return key.includes('.') || Array.from(key).some((character) => character.charCodeAt(0) <= 31) } export function parsePluginLanguagePackArtifact(raw: string): PluginLanguagePackParseResult { let json: unknown try { json = JSON.parse(raw) } catch { return { ok: false, error: 'language pack must contain one JSON object' } } if (!isCatalogObject(json)) { return { ok: false, error: 'language pack root must be an object' } } const catalog: Record = {} const stack: CatalogFrame[] = [{ source: json, target: catalog, path: '', depth: 0 }] let entries = 0 while (stack.length > 0) { const frame = stack.pop()! if (frame.depth > PLUGIN_LANGUAGE_CATALOG_MAX_DEPTH) { return { ok: false, error: `catalog exceeds depth ${PLUGIN_LANGUAGE_CATALOG_MAX_DEPTH}` } } for (const [key, value] of Object.entries(frame.source)) { entries += 1 if (entries > PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES) { return { ok: false, error: `catalog exceeds ${PLUGIN_LANGUAGE_CATALOG_MAX_ENTRIES} entries` } } if ( key.length === 0 || key.length > 128 || DANGEROUS_CATALOG_KEYS.has(key) || hasUnsafeCatalogKeyCharacter(key) ) { return { ok: false, error: `catalog key ${key || '(empty)'} is not safe` } } const path = frame.path ? `${frame.path}.${key}` : key if (protectedTranslation(path)) { return { ok: false, error: `catalog cannot replace protected security copy at ${path}` } } if (typeof value === 'string') { if (value.length > 8192) { return { ok: false, error: `translation at ${path} exceeds 8192 characters` } } frame.target[key] = value continue } if (!isCatalogObject(value)) { return { ok: false, error: `translation at ${path} must be a string or object` } } const child: Record = {} frame.target[key] = child stack.push({ source: value, target: child, path, depth: frame.depth + 1 }) } } return { ok: true, catalog, entries } }