| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- /**
- * Script to find missing translations in locale files
- *
- * Usage:
- * node scripts/find-missing-translations.js [options]
- *
- * Options:
- * --locale=<locale> Only check a specific locale (e.g. --locale=fr)
- * --file=<file> Only check a specific file (e.g. --file=chat.json)
- * --area=<area> Only check a specific area (core, webview, package-nls, or all)
- * --help Show this help message
- */
- const path = require("path")
- const { promises: fs } = require("fs")
- const readFile = fs.readFile
- const readdir = fs.readdir
- const stat = fs.stat
- // Process command line arguments
- const args = process.argv.slice(2).reduce(
- (acc, arg) => {
- if (arg === "--help") {
- acc.help = true
- } else if (arg.startsWith("--locale=")) {
- acc.locale = arg.split("=")[1]
- } else if (arg.startsWith("--file=")) {
- acc.file = arg.split("=")[1]
- } else if (arg.startsWith("--area=")) {
- acc.area = arg.split("=")[1]
- // Validate area value
- if (!["core", "webview", "package-nls", "all"].includes(acc.area)) {
- console.error(`Error: Invalid area '${acc.area}'. Must be 'core', 'webview', 'package-nls', or 'all'.`)
- process.exit(1)
- }
- }
- return acc
- },
- { area: "all" },
- ) // Default to checking all areas
- // Show help if requested
- if (args.help) {
- console.log(`
- Find Missing Translations
- A utility script to identify missing translations across locale files.
- Compares non-English locale files to the English ones to find any missing keys.
- Usage:
- node scripts/find-missing-translations.js [options]
- Options:
- --locale=<locale> Only check a specific locale (e.g. --locale=fr)
- --file=<file> Only check a specific file (e.g. --file=chat.json)
- --area=<area> Only check a specific area (core, webview, package-nls, or all)
- 'core' = Backend (src/i18n/locales)
- 'webview' = Frontend UI (webview-ui/src/i18n/locales)
- 'package-nls' = VSCode package.nls.json files
- 'all' = Check all areas (default)
- --help Show this help message
- Output:
- - Generates a report of missing translations for each area
- `)
- process.exit(0)
- }
- // Paths to the locales directories
- const LOCALES_DIRS = {
- core: path.join(__dirname, "../src/i18n/locales"),
- webview: path.join(__dirname, "../webview-ui/src/i18n/locales"),
- }
- // Determine which areas to check based on args
- const areasToCheck = args.area === "all" ? ["core", "webview", "package-nls"] : [args.area]
- // Recursively find all keys in an object
- function findKeys(obj, parentKey = "") {
- let keys = []
- for (const [key, value] of Object.entries(obj)) {
- const currentKey = parentKey ? `${parentKey}.${key}` : key
- if (typeof value === "object" && value !== null) {
- // If value is an object, recurse
- keys = [...keys, ...findKeys(value, currentKey)]
- } else {
- // If value is a primitive, add the key
- keys.push(currentKey)
- }
- }
- return keys
- }
- // Get value at a dotted path in an object
- function getValueAtPath(obj, path) {
- const parts = path.split(".")
- let current = obj
- for (const part of parts) {
- if (current === undefined || current === null) {
- return undefined
- }
- current = current[part]
- }
- return current
- }
- // Shared utility to safely parse JSON files with error handling
- async function parseJsonFile(filePath) {
- try {
- const content = await readFile(filePath, "utf8")
- return JSON.parse(content)
- } catch (error) {
- if (error.code === "ENOENT") {
- return null // File doesn't exist
- }
- throw new Error(`Error parsing JSON file '${filePath}': ${error.message}`)
- }
- }
- // Validate that a JSON object has a flat structure (no nested objects)
- function validateFlatStructure(obj, filePath) {
- for (const [key, value] of Object.entries(obj)) {
- if (typeof value === "object" && value !== null) {
- console.error(`Error: ${filePath} should be a flat JSON structure. Found nested object at key '${key}'`)
- process.exit(1)
- }
- }
- }
- // Function to check translations for a specific area
- async function checkAreaTranslations(area) {
- const LOCALES_DIR = LOCALES_DIRS[area]
- // Get all locale directories (or filter to the specified locale)
- const dirContents = await readdir(LOCALES_DIR)
- const allLocales = await Promise.all(
- dirContents.map(async (item) => {
- const stats = await stat(path.join(LOCALES_DIR, item))
- return stats.isDirectory() && item !== "en" ? item : null
- }),
- )
- const filteredLocales = allLocales.filter(Boolean)
- // Filter to the specified locale if provided
- const locales = args.locale ? filteredLocales.filter((locale) => locale === args.locale) : filteredLocales
- if (args.locale && locales.length === 0) {
- console.error(`Error: Locale '${args.locale}' not found in ${LOCALES_DIR}`)
- process.exit(1)
- }
- console.log(
- `\n${area === "core" ? "BACKEND" : "FRONTEND"} - Checking ${locales.length} non-English locale(s): ${locales.join(", ")}`,
- )
- // Get all English JSON files
- const englishDir = path.join(LOCALES_DIR, "en")
- const englishDirContents = await readdir(englishDir)
- let englishFiles = englishDirContents.filter((file) => file.endsWith(".json") && !file.startsWith("."))
- // Filter to the specified file if provided
- if (args.file) {
- if (!englishFiles.includes(args.file)) {
- console.error(`Error: File '${args.file}' not found in ${englishDir}`)
- process.exit(1)
- }
- englishFiles = englishFiles.filter((file) => file === args.file)
- }
- // Load file contents in parallel
- const englishFileContents = await Promise.all(
- englishFiles.map(async (file) => {
- const filePath = path.join(englishDir, file)
- const content = await parseJsonFile(filePath)
- if (!content) {
- console.error(`Error: Could not read file '${filePath}'`)
- process.exit(1)
- }
- return { name: file, content }
- }),
- )
- console.log(
- `Checking ${englishFileContents.length} translation file(s): ${englishFileContents.map((f) => f.name).join(", ")}`,
- )
- // Precompute English keys per file
- const englishFileKeys = new Map(englishFileContents.map((f) => [f.name, findKeys(f.content)]))
- // Results object to store missing translations
- const missingTranslations = {}
- // Process all locales in parallel
- await Promise.all(
- locales.map(async (locale) => {
- missingTranslations[locale] = {}
- // Process all files for this locale in parallel
- await Promise.all(
- englishFileContents.map(async ({ name, content: englishContent }) => {
- const localeFilePath = path.join(LOCALES_DIR, locale, name)
- // Check if the file exists in the locale
- const localeContent = await parseJsonFile(localeFilePath)
- if (!localeContent) {
- missingTranslations[locale][name] = { file: "File is missing entirely" }
- return
- }
- // Find all keys in the English file
- const englishKeys = englishFileKeys.get(name) || []
- // Check for missing keys in the locale file
- const missingKeys = []
- for (const key of englishKeys) {
- const englishValue = getValueAtPath(englishContent, key)
- const localeValue = getValueAtPath(localeContent, key)
- if (localeValue === undefined) {
- missingKeys.push({
- key,
- englishValue,
- })
- }
- }
- if (missingKeys.length > 0) {
- missingTranslations[locale][name] = missingKeys
- }
- }),
- )
- }),
- )
- return { missingTranslations, hasMissingTranslations: outputResults(missingTranslations, area) }
- }
- // Function to output results for an area
- function outputResults(missingTranslations, area) {
- let hasMissingTranslations = false
- console.log(`\n${area === "core" ? "BACKEND" : "FRONTEND"} Missing Translations Report:\n`)
- for (const [locale, files] of Object.entries(missingTranslations)) {
- if (Object.keys(files).length === 0) {
- console.log(`✅ ${locale}: No missing translations`)
- continue
- }
- hasMissingTranslations = true
- console.log(`📝 ${locale}:`)
- for (const [fileName, missingItems] of Object.entries(files)) {
- if (missingItems.file) {
- console.log(` - ${fileName}: ${missingItems.file}`)
- continue
- }
- console.log(` - ${fileName}: ${missingItems.length} missing translations`)
- for (const { key, englishValue } of missingItems) {
- console.log(` ${key}: "${englishValue}"`)
- }
- }
- console.log("")
- }
- return hasMissingTranslations
- }
- // Function to check package.nls.json translations
- async function checkPackageNlsTranslations() {
- const SRC_DIR = path.join(__dirname, "../src")
- // Read the base package.nls.json file
- const baseFilePath = path.join(SRC_DIR, "package.nls.json")
- const baseContent = await parseJsonFile(baseFilePath)
- if (!baseContent) {
- console.warn(`Warning: Base package.nls.json not found at ${baseFilePath} - skipping package.nls checks`)
- return { missingTranslations: {}, hasMissingTranslations: false }
- }
- // Validate that the base file has a flat structure
- validateFlatStructure(baseContent, baseFilePath)
- // Get all package.nls.*.json files
- const srcDirContents = await readdir(SRC_DIR)
- const nlsFiles = srcDirContents
- .filter((file) => file.startsWith("package.nls.") && file.endsWith(".json"))
- .filter((file) => file !== "package.nls.json") // Exclude the base file
- // Filter to the specified locale if provided
- const filesToCheck = args.locale
- ? nlsFiles.filter((file) => {
- const locale = file.replace("package.nls.", "").replace(".json", "")
- return locale === args.locale
- })
- : nlsFiles
- if (args.locale && filesToCheck.length === 0) {
- console.error(`Error: Locale '${args.locale}' not found in package.nls files`)
- process.exit(1)
- }
- console.log(
- `\nPACKAGE.NLS - Checking ${filesToCheck.length} locale file(s): ${filesToCheck.map((f) => f.replace("package.nls.", "").replace(".json", "")).join(", ")}`,
- )
- console.log(`Checking against base package.nls.json with ${Object.keys(baseContent).length} keys`)
- // Results object to store missing translations
- const missingTranslations = {}
- // Get all keys from the base file (package.nls files are flat, not nested)
- const baseKeys = Object.keys(baseContent)
- // Process all locale files in parallel
- await Promise.all(
- filesToCheck.map(async (file) => {
- const locale = file.replace("package.nls.", "").replace(".json", "")
- const localeFilePath = path.join(SRC_DIR, file)
- const localeContent = await parseJsonFile(localeFilePath)
- if (!localeContent) {
- console.error(`Error: Could not read file '${localeFilePath}'`)
- process.exit(1)
- }
- // Validate that the locale file has a flat structure
- validateFlatStructure(localeContent, localeFilePath)
- // Check for missing keys
- const missingKeys = []
- for (const key of baseKeys) {
- const baseValue = baseContent[key]
- const localeValue = localeContent[key]
- if (localeValue === undefined) {
- missingKeys.push({
- key,
- englishValue: baseValue,
- })
- }
- }
- if (missingKeys.length > 0) {
- missingTranslations[locale] = {
- "package.nls.json": missingKeys,
- }
- }
- }),
- )
- return { missingTranslations, hasMissingTranslations: outputPackageNlsResults(missingTranslations) }
- }
- // Function to output package.nls results
- function outputPackageNlsResults(missingTranslations) {
- let hasMissingTranslations = false
- console.log(`\nPACKAGE.NLS Missing Translations Report:\n`)
- for (const [locale, files] of Object.entries(missingTranslations)) {
- if (Object.keys(files).length === 0) {
- console.log(`✅ ${locale}: No missing translations`)
- continue
- }
- hasMissingTranslations = true
- console.log(`📝 ${locale}:`)
- for (const [fileName, missingItems] of Object.entries(files)) {
- console.log(` - ${fileName}: ${missingItems.length} missing translations`)
- for (const { key, englishValue } of missingItems) {
- console.log(` ${key}: "${englishValue}"`)
- }
- }
- console.log("")
- }
- return hasMissingTranslations
- }
- // Main function to find missing translations
- async function findMissingTranslations() {
- try {
- console.log("Starting translation check...")
- let anyAreaMissingTranslations = false
- // Check each requested area
- for (const area of areasToCheck) {
- if (area === "package-nls") {
- const { hasMissingTranslations } = await checkPackageNlsTranslations()
- anyAreaMissingTranslations = anyAreaMissingTranslations || hasMissingTranslations
- } else {
- const { hasMissingTranslations } = await checkAreaTranslations(area)
- anyAreaMissingTranslations = anyAreaMissingTranslations || hasMissingTranslations
- }
- }
- // Summary
- if (!anyAreaMissingTranslations) {
- console.log("\n✅ All translations are complete across all checked areas!")
- } else {
- console.log("\n✏️ To add missing translations:")
- console.log("1. Add the missing keys to the corresponding locale files")
- console.log("2. Translate the English values to the appropriate language")
- console.log("3. Run this script again to verify all translations are complete")
- // Exit with error code to fail CI checks
- process.exit(1)
- }
- } catch (error) {
- console.error("Error:", error.message)
- console.error(error.stack)
- process.exit(1)
- }
- }
- // Run the main function
- findMissingTranslations()
|