Skip to content

refactor(i18n): Sort i18n keys in translation files to match English ordering #4572

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions .github/workflows/code-qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
uses: ./.github/actions/setup-node-pnpm
- name: Verify all translations are complete
run: node scripts/find-missing-translations.js
- name: Verify all translations are ordered properly
run: node scripts/lint-locale-key-ordering.js

knip:
runs-on: ubuntu-latest
Expand Down
212 changes: 212 additions & 0 deletions scripts/lint-locale-key-ordering.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
#!/usr/bin/env node

/**
* Script to lint locale file key ordering consistency
*
* This script ensures that all locale files maintain the same key ordering
* as their corresponding English locale files. This helps maintain consistency
* across all committed translations.
*
* Usage:
* node scripts/lint-locale-key-ordering.js [options]
* tsx scripts/lint-locale-key-ordering.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, or both)
* --help Show this help message
*/

const fs = require("fs")
const path = require("path")

// 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", "both"].includes(acc.area)) {
console.error(`Error: Invalid area '${acc.area}'. Must be 'core', 'webview', or 'both'.`)
process.exit(1)
}
}
return acc
},
{ area: "both" },
)

if (args.help) {
console.log(`
Locale Key Ordering Linter - Ensures consistent key ordering across locale files

Usage: tsx scripts/lint-locale-key-ordering.js [options]

Options:
--locale=<locale> Check specific locale (e.g. --locale=fr)
--file=<file> Check specific file (e.g. --file=chat.json)
--area=<area> Check area: core, webview, or both (default)
--help Show this help

Exit: 0=consistent, 1=issues found
`)
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 === "both" ? ["core", "webview"] : [args.area]

// Extract keys from JSON object recursively in dot notation
function extractKeysInOrder(obj, prefix = "") {
const keys = []
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key
keys.push(fullKey)
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
keys.push(...extractKeysInOrder(value, fullKey))
}
}
return keys
}

// Compare key ordering and find differences
function compareKeyOrdering(englishKeys, localeKeys) {
const englishSet = new Set(englishKeys)
const localeSet = new Set(localeKeys)
const missing = englishKeys.filter((key) => !localeSet.has(key))
const extra = localeKeys.filter((key) => !englishSet.has(key))

const commonKeys = englishKeys.filter((key) => localeSet.has(key))
const localeCommonKeys = localeKeys.filter((key) => englishSet.has(key))
const outOfOrder = []

for (let i = 0; i < commonKeys.length; i++) {
if (commonKeys[i] !== localeCommonKeys[i]) {
outOfOrder.push({
expected: commonKeys[i],
actual: localeCommonKeys[i],
})
}
}

return { missing, extra, outOfOrder }
}

function checkAreaKeyOrdering(area) {
const LOCALES_DIR = LOCALES_DIRS[area]
const allLocales = fs.readdirSync(LOCALES_DIR).filter((item) => {
return fs.statSync(path.join(LOCALES_DIR, item)).isDirectory() && item !== "en"
})

const locales = args.locale ? allLocales.filter((locale) => locale === args.locale) : allLocales
if (args.locale && locales.length === 0) {
console.error(`Error: Locale '${args.locale}' not found in ${LOCALES_DIR}`)
process.exit(1)
}

console.log(`\n${area} - Checking key ordering for ${locales.length} locale(s): ${locales.join(", ")}`)

const englishDir = path.join(LOCALES_DIR, "en")
let englishFiles = fs.readdirSync(englishDir).filter((file) => file.endsWith(".json") && !file.startsWith("."))

if (args.file) {
if (!englishFiles.includes(args.file)) {
console.error(`Error: File '${args.file}' not found in ${englishDir}`)
process.exit(1)
}
englishFiles = [args.file]
}

console.log(`Checking ${englishFiles.length} file(s): ${englishFiles.join(", ")}`)
let hasOrderingIssues = false

for (const locale of locales) {
const localeIssues = []

for (const fileName of englishFiles) {
const englishFilePath = path.join(englishDir, fileName)
const localeFilePath = path.join(LOCALES_DIR, locale, fileName)

if (!fs.existsSync(localeFilePath)) {
localeIssues.push(` ⚠️ ${fileName}: File missing`)
continue
}

try {
const englishContent = JSON.parse(fs.readFileSync(englishFilePath, "utf8"))
const localeContent = JSON.parse(fs.readFileSync(localeFilePath, "utf8"))
const issues = compareKeyOrdering(extractKeysInOrder(englishContent), extractKeysInOrder(localeContent))

if (issues.missing.length + issues.extra.length + issues.outOfOrder.length > 0) {
localeIssues.push(` ❌ ${fileName}: Key ordering issues`)

if (issues.missing.length > 0) {
const preview = issues.missing.slice(0, 3).join(", ")
localeIssues.push(
` Missing: ${preview}${issues.missing.length > 3 ? ` (+${issues.missing.length - 3} more)` : ""}`,
)
}
if (issues.extra.length > 0) {
const preview = issues.extra.slice(0, 3).join(", ")
localeIssues.push(
` Extra: ${preview}${issues.extra.length > 3 ? ` (+${issues.extra.length - 3} more)` : ""}`,
)
}
if (issues.outOfOrder.length > 0) {
const preview = issues.outOfOrder
.slice(0, 2)
.map((issue) => `expected '${issue.expected}' but found '${issue.actual}'`)
.join(", ")
localeIssues.push(
` Order: ${preview}${issues.outOfOrder.length > 2 ? ` (+${issues.outOfOrder.length - 2} more)` : ""}`,
)
}
}
} catch (e) {
localeIssues.push(` ❌ ${fileName}: JSON error - ${e.message}`)
}
}

if (localeIssues.length > 0) {
console.log(`\n 📋 Checking locale: ${locale}`)
localeIssues.forEach((issue) => console.log(issue))
hasOrderingIssues = true
}
}

return hasOrderingIssues
}

function lintLocaleKeyOrdering() {
try {
console.log("🔍 Starting locale key ordering check...")
const anyAreaHasIssues = areasToCheck.some((area) => checkAreaKeyOrdering(area))

if (!anyAreaHasIssues) {
console.log("✅ All locale files have consistent key ordering!")
process.exit(0)
} else {
console.log("\n❌ Key ordering inconsistencies detected!")
console.log("\n💡 To fix: Use MCP sort_i18n_keys tool or manually reorder keys to match English files")
process.exit(1)
}
} catch (error) {
console.error("Error:", error.message)
process.exit(1)
}
}

lintLocaleKeyOrdering()
12 changes: 3 additions & 9 deletions src/i18n/locales/ca/common.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
{
"input": {
"task_prompt": "Què vols que faci Roo?",
"task_placeholder": "Escriu la teva tasca aquí"
},
"extension": {
"name": "Roo Code",
"description": "Tot un equip de desenvolupadors d'IA al teu editor."
Expand Down Expand Up @@ -90,10 +86,8 @@
"enter_absolute_path": "Introdueix una ruta completa (p. ex. D:\\RooCodeStorage o /home/user/storage)",
"enter_valid_path": "Introdueix una ruta vàlida"
},
"settings": {
"providers": {
"groqApiKey": "Clau API de Groq",
"getGroqApiKey": "Obté la clau API de Groq"
}
"input": {
"task_prompt": "Què vols que faci Roo?",
"task_placeholder": "Escriu la teva tasca aquí"
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These keys live in settings.json now, they must have be left here at some point!

}
6 changes: 0 additions & 6 deletions src/i18n/locales/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,5 @@
"input": {
"task_prompt": "Was soll Roo tun?",
"task_placeholder": "Gib deine Aufgabe hier ein"
},
"settings": {
"providers": {
"groqApiKey": "Groq API-Schlüssel",
"getGroqApiKey": "Groq API-Schlüssel erhalten"
}
}
}
6 changes: 0 additions & 6 deletions src/i18n/locales/es/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,5 @@
"input": {
"task_prompt": "¿Qué debe hacer Roo?",
"task_placeholder": "Escribe tu tarea aquí"
},
"settings": {
"providers": {
"groqApiKey": "Clave API de Groq",
"getGroqApiKey": "Obtener clave API de Groq"
}
}
}
6 changes: 0 additions & 6 deletions src/i18n/locales/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,5 @@
"input": {
"task_prompt": "Que doit faire Roo ?",
"task_placeholder": "Écris ta tâche ici"
},
"settings": {
"providers": {
"groqApiKey": "Clé API Groq",
"getGroqApiKey": "Obtenir la clé API Groq"
}
}
}
1 change: 0 additions & 1 deletion src/i18n/locales/fr/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"invalid_settings_format": "Format JSON des paramètres MCP invalide. Veuillez vous assurer que vos paramètres suivent le format JSON correct.",
"invalid_settings_syntax": "Format JSON des paramètres MCP invalide. Veuillez vérifier le syntaxe de votre fichier de paramètres.",
"invalid_settings_validation": "Format de paramètres MCP invalide : {{errorMessages}}",

"create_json": "Échec de la création ou de l'ouverture de .roo/mcp.json : {{error}}",
"failed_update_project": "Échec de la mise à jour des serveurs MCP du projet"
},
Expand Down
6 changes: 0 additions & 6 deletions src/i18n/locales/hi/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,5 @@
"input": {
"task_prompt": "Roo को क्या करना है?",
"task_placeholder": "अपना कार्य यहाँ लिखें"
},
"settings": {
"providers": {
"groqApiKey": "ग्रोक एपीआई कुंजी",
"getGroqApiKey": "ग्रोक एपीआई कुंजी प्राप्त करें"
}
}
}
1 change: 0 additions & 1 deletion src/i18n/locales/hi/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"invalid_settings_format": "अमान्य MCP सेटिंग्स JSON फॉर्मेट। कृपया सुनिश्चित करें कि आपकी सेटिंग्स सही JSON फॉर्मेट का पालन करती हैं।",
"invalid_settings_syntax": "अमान्य MCP सेटिंग्स JSON फॉर्मेट। कृपया अपनी सेटिंग्स फ़ाइल में सिंटैक्स त्रुटियों की जांच करें।",
"invalid_settings_validation": "अमान्य MCP सेटिंग्स फॉर्मेट: {{errorMessages}}",

"create_json": ".roo/mcp.json बनाने या खोलने में विफल: {{error}}",
"failed_update_project": "प्रोजेक्ट MCP सर्वर अपडेट करने में विफल"
},
Expand Down
6 changes: 0 additions & 6 deletions src/i18n/locales/it/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,5 @@
"input": {
"task_prompt": "Cosa deve fare Roo?",
"task_placeholder": "Scrivi il tuo compito qui"
},
"settings": {
"providers": {
"groqApiKey": "Chiave API Groq",
"getGroqApiKey": "Ottieni chiave API Groq"
}
}
}
1 change: 0 additions & 1 deletion src/i18n/locales/it/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"invalid_settings_format": "Formato JSON delle impostazioni MCP non valido. Assicurati che le tue impostazioni seguano il formato JSON corretto.",
"invalid_settings_syntax": "Formato JSON delle impostazioni MCP non valido. Verifica gli errori di sintassi nel tuo file delle impostazioni.",
"invalid_settings_validation": "Formato delle impostazioni MCP non valido: {{errorMessages}}",

"create_json": "Impossibile creare o aprire .roo/mcp.json: {{error}}",
"failed_update_project": "Errore durante l'aggiornamento dei server MCP del progetto"
},
Expand Down
6 changes: 0 additions & 6 deletions src/i18n/locales/ja/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,5 @@
"input": {
"task_prompt": "Rooにどんなことをさせますか?",
"task_placeholder": "タスクをここに入力してください"
},
"settings": {
"providers": {
"groqApiKey": "Groq APIキー",
"getGroqApiKey": "Groq APIキーを取得"
}
}
}
1 change: 0 additions & 1 deletion src/i18n/locales/ja/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"invalid_settings_format": "MCP設定のJSONフォーマットが無効です。設定が正しいJSONフォーマットに従っていることを確認してください。",
"invalid_settings_syntax": "MCP設定のJSONフォーマットが無効です。設定ファイルの構文エラーを確認してください。",
"invalid_settings_validation": "MCP設定フォーマットが無効です:{{errorMessages}}",

"create_json": ".roo/mcp.jsonの作成または開くことに失敗しました:{{error}}",
"failed_update_project": "プロジェクトMCPサーバーの更新に失敗しました"
},
Expand Down
6 changes: 0 additions & 6 deletions src/i18n/locales/ko/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,5 @@
"input": {
"task_prompt": "Roo에게 무엇을 시킬까요?",
"task_placeholder": "여기에 작업을 입력하세요"
},
"settings": {
"providers": {
"groqApiKey": "Groq API 키",
"getGroqApiKey": "Groq API 키 받기"
}
}
}
1 change: 0 additions & 1 deletion src/i18n/locales/ko/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"invalid_settings_format": "잘못된 MCP 설정 JSON 형식입니다. 설정이 올바른 JSON 형식을 따르는지 확인하세요.",
"invalid_settings_syntax": "잘못된 MCP 설정 JSON 형식입니다. 설정 파일의 구문 오류를 확인하세요.",
"invalid_settings_validation": "잘못된 MCP 설정 형식: {{errorMessages}}",

"create_json": ".roo/mcp.json 생성 또는 열기 실패: {{error}}",
"failed_update_project": "프로젝트 MCP 서버 업데이트에 실패했습니다"
},
Expand Down
1 change: 0 additions & 1 deletion src/i18n/locales/nl/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"invalid_settings_format": "Ongeldig MCP-instellingen JSON-formaat. Zorg ervoor dat je instellingen het juiste JSON-formaat volgen.",
"invalid_settings_syntax": "Ongeldig MCP-instellingen JSON-formaat. Controleer je instellingenbestand op syntaxfouten.",
"invalid_settings_validation": "Ongeldig MCP-instellingenformaat: {{errorMessages}}",

"create_json": "Aanmaken of openen van .roo/mcp.json mislukt: {{error}}",
"failed_update_project": "Bijwerken van project MCP-servers mislukt"
},
Expand Down
6 changes: 0 additions & 6 deletions src/i18n/locales/pl/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,5 @@
"input": {
"task_prompt": "Co ma zrobić Roo?",
"task_placeholder": "Wpisz swoje zadanie tutaj"
},
"settings": {
"providers": {
"groqApiKey": "Klucz API Groq",
"getGroqApiKey": "Uzyskaj klucz API Groq"
}
}
}
1 change: 0 additions & 1 deletion src/i18n/locales/pl/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"invalid_settings_format": "Nieprawidłowy format JSON ustawień MCP. Upewnij się, że Twoje ustawienia są zgodne z poprawnym formatem JSON.",
"invalid_settings_syntax": "Nieprawidłowy format JSON ustawień MCP. Sprawdź, czy w pliku ustawień nie ma błędów składniowych.",
"invalid_settings_validation": "Nieprawidłowy format ustawień MCP: {{errorMessages}}",

"create_json": "Nie udało się utworzyć lub otworzyć .roo/mcp.json: {{error}}",
"failed_update_project": "Nie udało się zaktualizować serwerów MCP projektu"
},
Expand Down
Loading