Skip to content
Closed
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5ba37df
test: add comprehensive translation linting system
May 9, 2025
850507c
test: improve translation linting test
May 9, 2025
00cbedc
test: improve translation linting test robustness
May 10, 2025
6c02002
test: fix translation validation and output format
May 10, 2025
400e4cc
feat: add translation file management script
May 10, 2025
df19876
fix: improve display of missing translation files
May 10, 2025
6a7829d
fix: unify file existence checking in translation linting
May 10, 2025
09683af
test: remove unused exitCode from lint-translations.test.ts
May 10, 2025
020b2f9
lang: add PRIVACY.md translations for all locales
May 10, 2025
e82bb97
feat: support escaped dots in translation paths
May 10, 2025
dcad172
fix: detect empty object keys in translation linting
May 10, 2025
c8cdaf7
feat: change translation key escaping to use double dots
May 10, 2025
9e775d2
feat: improve translation management guidance
May 10, 2025
37d19f1
lang: remove extra translations
May 10, 2025
d4a89cf
test: migrate find-missing-i18n-key.js to test suite
May 10, 2025
41d3fb7
test: remove old i18n scripts
May 10, 2025
921f8ed
refactor: move shared test utilities to dedicated module
May 10, 2025
d897a2b
feat: scan en locale for missing i18n keys
May 10, 2025
d80c896
fix: restore critical functionality in i18n utils
May 10, 2025
d21e9bc
feat: migrate translation validation to Jest tests
May 10, 2025
9a70cdc
lang: add missing modes.noMatchFound translations
May 10, 2025
a117bcd
lang: improve Hindi grammar in privacy policy
May 10, 2025
8cc7d4a
feat: improve lint-translations with usage function and locale override
May 11, 2025
7955ca1
fix: prevent false positives in translation linting results
May 11, 2025
45a0847
fix: improve translation linting output format
May 11, 2025
ce202f4
fix: show ALL KEYS/ALL CONTENT for missing translation files
May 11, 2025
eb20b5a
fix: improve i18n key detection in source code
May 11, 2025
6ab5faa
test: add file:line to i18n key error output
May 13, 2025
4f21bb5
fix: reduce i18n key false positives by handling dynamic keys
May 13, 2025
155263e
fix: improve display of missing translations
May 14, 2025
5326a77
refactor(i18n): Improve translation linting key handling and display
May 20, 2025
679254e
locales: Refactor translation linting for improved configuration and …
May 20, 2025
e0b27a1
refactor: Isolate file/size checks in lint-translations
May 21, 2025
9d90884
fix: ignore dotfiles in translation linting
May 21, 2025
9be8c06
fix(i18n): Refine missing key reporting for lint-translations
May 21, 2025
1f2c77e
fix: hide English translations for missing keys in lint output
Jun 5, 2025
f35577d
refactor: create dedicated functions for rendering translation issues
Jun 5, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/code-qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
- name: Install dependencies
run: npm run install:all
- name: Verify all translations are complete
run: node scripts/find-missing-translations.js
run: npx jest --verbose locales/__tests__/lint-translations.test.ts locales/__tests__/find-missing-i18n-keys.test.ts

knip:
runs-on: ubuntu-latest
Expand Down
82 changes: 79 additions & 3 deletions .roo/rules-translate/001-general-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,87 @@
- Watch for placeholders and preserve them in translations
- Be mindful of text length in UI elements when translating to languages that might require more characters
- Use context-aware translations when the same string has different meanings
- Always validate your translation work by running the missing translations script:
- Always validate your translation work by running the translation tests:
```
node scripts/find-missing-translations.js
npx jest --verbose locales/__tests__/lint-translations.test.ts locales/__tests__/find-missing-i18n-keys.test.ts
```
- Address any missing translations identified by the script to ensure complete coverage across all locales
- Address any missing translations identified by the tests by using `node scripts/manage-translations.js` to ensure complete coverage across all locales:

```sh
./scripts/manage-translations.js
Usage:
Command Line Mode:
Add/update translations:
node scripts/manage-translations.js [-v] TRANSLATION_FILE KEY_PATH VALUE [KEY_PATH VALUE...]
Delete translations:
node scripts/manage-translations.js [-v] -d TRANSLATION_FILE1 [TRANSLATION_FILE2 ...] [ -- KEY1 ...]

Key Path Format:
- Use single dot (.) for nested paths: 'command.newTask.title'
- Use double dots (..) to include a literal dot in key names (like SMTP byte stuffing):
'settings..path' -> { 'settings.path': 'value' }

Examples:
'command.newTask.title' -> { command: { newTask: { title: 'value' } } }
'settings..path' -> { 'settings.path': 'value' }
'nested.key..with..dots' -> { nested: { 'key.with.dots': 'value' } }

Line-by-Line JSON Mode (--stdin):
Each line must be a complete, single JSON object/array
Multi-line or combined JSON is not supported

Add/update translations:
node scripts/manage-translations.js [-v] --stdin TRANSLATION_FILE
Format: One object per line with exactly one key-value pair:
{"command.newTask.title": "New Task"}
{"settings..path": "Custom Path"}
{"nested.key..with..dots": "Value with dots in key"}

Delete translations:
node scripts/manage-translations.js [-v] -d --stdin TRANSLATION_FILE
Format: One array per line with exactly one key:
["command.newTask.title"]
["settings..path"]
["nested.key..with..dots"]

Options:
-v Enable verbose output (shows operations)
-d Delete mode - remove keys instead of setting them
--stdin Read line-by-line JSON from stdin

Examples:
# Add via command line, it is recommended to execute multiple translations simultaneously.
# The script expects a single file at a time with multiple key-value pairs, not multiple files.
node scripts/manage-translations.js package.nls.json command.newTask.title "New Task" [ key2 translation2 ... ] && \
node scripts/manage-translations.js package.nls.json settings..path "Custom Path" && \
node scripts/manage-translations.js package.nls.json nested.key..with..dots "Value with dots"

# Add multiple translations (one JSON object per line):
translations.txt:
{"command.newTask.title": "New Task"}
{"settings..path": "Custom Path"}
node scripts/manage-translations.js --stdin package.nls.json < translations.txt

# Delete multiple keys (one JSON array per line):
delete_keys.txt:
["command.newTask.title"]
["settings..path"]
["nested.key..with..dots"]
node scripts/manage-translations.js -d --stdin package.nls.json < delete_keys.txt

# Using here document for batching:
node scripts/manage-translations.js --stdin package.nls.json << EOF
{"command.newTask.title": "New Task"}
{"settings..path": "Custom Path"}
EOF

# Delete using here document:
node scripts/manage-translations.js -d --stdin package.nls.json << EOF
["command.newTask.title"]
["settings..path"]
["nested.key..with..dots"]
EOF
```

# 9. TRANSLATOR'S CHECKLIST

Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ module.exports = {
transformIgnorePatterns: [
"node_modules/(?!(@modelcontextprotocol|delay|p-wait-for|serialize-error|strip-ansi|default-shell|os-name|strip-bom)/)",
],
roots: ["<rootDir>/src", "<rootDir>/webview-ui/src"],
roots: ["<rootDir>/src", "<rootDir>/webview-ui/src", "<rootDir>/locales", "<rootDir>/scripts"],
modulePathIgnorePatterns: [".vscode-test"],
reporters: [["jest-simple-dot-reporter", {}]],
setupFiles: ["<rootDir>/src/__mocks__/jest.setup.ts"],
Expand Down
221 changes: 221 additions & 0 deletions locales/__tests__/find-missing-i18n-keys.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
const fs = require("fs")
const path = require("path")
import {
languages,
bufferLog,
printLogs,
clearLogs,
fileExists,
loadFileContent,
parseJsonContent,
getValueAtPath,
} from "./utils"

// findMissingI18nKeys: Directories to traverse and their corresponding locales
const SCAN_SOURCE_DIRS = {
components: {
path: "webview-ui/src/components",
localesDir: "webview-ui/src/i18n/locales",
},
src: {
path: "src",
localesDir: "src/i18n/locales",
},
}

// i18n key patterns for findMissingI18nKeys
const i18nScanPatterns = [
/{t\("([^"]+)"\)}/g,
/i18nKey="([^"]+)"/g,
/t\("([a-zA-Z][a-zA-Z0-9_]*[:.][a-zA-Z0-9_.]+)"\)/g,
]

// Check if the key exists in all official language files, return a list of missing language files
function checkKeyInLocales(key: string, localesDir: string): Array<[string, boolean]> {
const [file, ...pathParts] = key.split(":")
const jsonPath = pathParts.join(".")
const missingLocales = new Map<string, boolean>() // true = file missing, false = key missing

// Check all official languages including English
languages.forEach((locale) => {
const filePath = path.join(localesDir, locale, `${file}.json`)
const localePath = `${locale}/${file}.json`

// If file doesn't exist or can't be loaded, mark entire file as missing
if (!fileExists(filePath)) {
missingLocales.set(localePath, true)
return
}

const content = loadFileContent(filePath)
if (!content) {
missingLocales.set(localePath, true)
return
}

const json = parseJsonContent(content, filePath)
if (!json) {
missingLocales.set(localePath, true)
return
}

// Only check for missing key if file exists and is valid
if (getValueAtPath(json, jsonPath) === undefined) {
missingLocales.set(localePath, false)
}
})

return Array.from(missingLocales.entries())
}

// Recursively traverse the directory
export function findMissingI18nKeys(): { output: string } {
clearLogs() // Clear buffer at start
let results: Array<{ key: string; file: string; missingLocales: Array<{ path: string; isFileMissing: boolean }> }> =
[]

function walk(dir: string, baseDir: string, localesDir: string) {
const files = fs.readdirSync(dir)

for (const file of files) {
const filePath = path.join(dir, file)
const stat = fs.statSync(filePath)

// Exclude test files and __mocks__ directory
if (filePath.includes(".test.") || filePath.includes("__mocks__")) continue

if (stat.isDirectory()) {
walk(filePath, baseDir, localesDir) // Recursively traverse subdirectories
} else if (stat.isFile() && [".ts", ".tsx", ".js", ".jsx"].includes(path.extname(filePath))) {
const relPath = path.relative(process.cwd(), filePath)
const content = fs.readFileSync(filePath, "utf8")

// Match all i18n keys
const matches = new Set<string>()
for (const pattern of i18nScanPatterns) {
let match
while ((match = pattern.exec(content)) !== null) {
matches.add(match[1])
}
}

// Check each unique key against all official languages
matches.forEach((key) => {
const missingLocales = checkKeyInLocales(key, localesDir)
if (missingLocales.length > 0) {
results.push({
key,
missingLocales: missingLocales.map(([locale, isFileMissing]) => ({
path: path.join(path.relative(process.cwd(), localesDir), locale),
isFileMissing,
})),
file: relPath,
})
}
})
}
}
}

// Walk through all directories and check against official languages
Object.entries(SCAN_SOURCE_DIRS).forEach(([_name, config]) => {
// Create locales directory if it doesn't exist
if (!fs.existsSync(config.localesDir)) {
bufferLog(`Warning: Creating missing locales directory: ${config.localesDir}`)
fs.mkdirSync(config.localesDir, { recursive: true })
}
walk(config.path, config.path, config.localesDir)
})

// Process results
bufferLog("=== i18n Key Check ===")

if (!results || results.length === 0) {
bufferLog("\n✅ All i18n keys are present!")
} else {
bufferLog("\n❌ Missing i18n keys:")

// Group by file status
const missingFiles = new Set<string>()
const missingKeys = new Map<string, Set<string>>()

results.forEach(({ key, missingLocales }) => {
missingLocales.forEach(({ path: locale, isFileMissing }) => {
if (isFileMissing) {
missingFiles.add(locale)
} else {
if (!missingKeys.has(locale)) {
missingKeys.set(locale, new Set())
}
missingKeys.get(locale)?.add(key)
}
})
})

// Show missing files first
if (missingFiles.size > 0) {
bufferLog("\nMissing translation files:")
Array.from(missingFiles)
.sort()
.forEach((file) => {
bufferLog(` - ${file}`)
})
}

// Then show files with missing keys
if (missingKeys.size > 0) {
bufferLog("\nFiles with missing keys:")

// Group by file path to collect all keys per file
const fileKeys = new Map<string, Map<string, Set<string>>>()
results.forEach(({ key, file, missingLocales }) => {
missingLocales.forEach(({ path: locale, isFileMissing }) => {
if (!isFileMissing) {
const [_localeDir, _localeFile] = locale.split("/")
const filePath = locale
if (!fileKeys.has(filePath)) {
fileKeys.set(filePath, new Map())
}
if (!fileKeys.get(filePath)?.has(file)) {
fileKeys.get(filePath)?.set(file, new Set())
}
fileKeys.get(filePath)?.get(file)?.add(key)
}
})
})

// Show missing keys grouped by file
Array.from(fileKeys.entries())
.sort()
.forEach(([file, sourceFiles]) => {
bufferLog(` - ${file}:`)
Array.from(sourceFiles.entries())
.sort()
.forEach(([_sourceFile, keys]) => {
Array.from(keys)
.sort()
.forEach((key) => {
bufferLog(` ${key}`)
})
})
})
}

// Add simple command line example
if (missingKeys.size > 0) {
bufferLog("\nTo add missing translations:")
bufferLog(
" node scripts/manage-translations.js <locale_file> 'key' 'translation' [ 'key2' 'translation2' ... ]",
)
}
}

return { output: printLogs() }
}

describe("Find Missing i18n Keys", () => {
test("findMissingI18nKeys scans for missing translations", () => {
const result = findMissingI18nKeys()
expect(result.output).toContain("✅ All i18n keys are present!")
})
})
Loading