// Generates the translation keys no plugin language pack may replace, from markers in source. // Usage: node config/scripts/protected-copy-keys.mjs [--fix] import fs from 'node:fs/promises' import { existsSync } from 'node:fs' import path from 'node:path' import process from 'node:process' import { pathToFileURL } from 'node:url' import { collectSourceFiles, LOCALIZATION_SOURCE_ROOTS } from './verify-localization-catalog.mjs' import { collectKeyLiterals } from '../source-extraction/verify-localization-extraction.mjs' export const PROTECTED_COPY_KEYS_PATH = path.join( 'src', 'shared', 'plugins', 'languages', 'protected-copy-keys.json' ) const EN_CATALOG_PATH = path.join('src', 'renderer', 'src', 'i18n', 'locales', 'en.json') const SYNC_COMMAND = 'pnpm run sync:localization-protected-copy' const SECURITY_SURFACE_RE = /surface=["']security["']|surface:\s*["']security["']| base + extension).find( (candidate) => existsSync(candidate) && !candidate.endsWith('.json') ) if (resolved && path.dirname(resolved) === path.dirname(filePath)) { imports.push(resolved) } } return imports } /** Keys and dynamic key prefixes referenced by marked files and their sibling imports. */ export function collectProtectedCopyKeys(sources, englishPaths) { const marked = [...sources.keys()].filter((file) => protectedCopyMarker(sources.get(file))) const files = new Set(marked) for (const file of marked) { if (PROTECTED_COPY_FILE_SCOPE_RE.test(sources.get(file))) { continue } for (const imported of siblingImports(file, sources.get(file))) { if (sources.has(imported)) { files.add(imported) } } } const evidence = { literals: new Set(), dynamicPrefixes: new Set() } for (const file of files) { collectKeyLiterals(sources.get(file), evidence) } const keys = new Set() for (const literal of evidence.literals) { const key = literal.replace(PLURAL_SUFFIX_RE, '') if (englishPaths.has(key) || englishPaths.has(`${key}_other`)) { keys.add(key) } } // Why two segments: a bare `auto.` head would protect the whole catalog. const prefixes = [...evidence.dynamicPrefixes].filter((prefix) => { const segments = prefix.split('.').filter(Boolean) return segments.length >= 2 && [...englishPaths].some((key) => key.startsWith(prefix)) }) return { keys: [...keys].sort(), prefixes: [...new Set(prefixes)].sort() } } async function readSources(root) { const sources = new Map() for (const sourceRoot of LOCALIZATION_SOURCE_ROOTS) { for (const file of await collectSourceFiles(root, path.join(root, sourceRoot))) { sources.set(file, await fs.readFile(file, 'utf8')) } } return sources } export async function main(root = process.cwd(), args = process.argv.slice(2)) { const english = JSON.parse(await fs.readFile(path.join(root, EN_CATALOG_PATH), 'utf8')) const generated = collectProtectedCopyKeys(await readSources(root), flattenCatalog(english)) const expected = `${JSON.stringify(generated, null, 2)}\n` const outputPath = path.join(root, PROTECTED_COPY_KEYS_PATH) if (args.includes('--fix')) { await fs.writeFile(outputPath, expected) console.log(`Wrote ${generated.keys.length} protected copy keys to ${PROTECTED_COPY_KEYS_PATH}`) return 0 } const current = await fs.readFile(outputPath, 'utf8').catch(() => '') if (current !== expected) { console.error( `${PROTECTED_COPY_KEYS_PATH} is stale: security dialogs, destructive confirmations, or @protected-copy files changed their translation keys. Run \`${SYNC_COMMAND}\`.` ) return 1 } return 0 } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { process.exit(await main()) }