Roo-Code/scripts/find-missing-translations.js
roomote[bot] 807cc999a5
feat: add package.nls.json checking to find-missing-translations script (#8255)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Roo Code <roomote@roocode.com>
Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
2025-09-23 11:10:23 -04:00

432 lines
13 KiB
JavaScript
Executable file
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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()