diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index 667d0b6bf8..da0f83f38c 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -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 diff --git a/.roo/rules-translate/001-general-rules.md b/.roo/rules-translate/001-general-rules.md index bdb18bea64..0cc5c48f91 100644 --- a/.roo/rules-translate/001-general-rules.md +++ b/.roo/rules-translate/001-general-rules.md @@ -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 diff --git a/jest.config.js b/jest.config.js index e05776918e..b79cfc75b1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -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: ["/src", "/webview-ui/src"], + roots: ["/src", "/webview-ui/src", "/locales", "/scripts"], modulePathIgnorePatterns: [".vscode-test"], reporters: [["jest-simple-dot-reporter", {}]], setupFiles: ["/src/__mocks__/jest.setup.ts"], diff --git a/locales/__tests__/find-missing-i18n-keys.test.ts b/locales/__tests__/find-missing-i18n-keys.test.ts new file mode 100644 index 0000000000..4c9c40f80d --- /dev/null +++ b/locales/__tests__/find-missing-i18n-keys.test.ts @@ -0,0 +1,495 @@ +const fs = require("fs") +const path = require("path") +import { bufferLog, printLogs, clearLogs, loadFileContent, parseJsonContent } from "./utils" + +// findMissingI18nKeys: Directories to traverse and their corresponding locales +const SCAN_SOURCE_DIRS = { + components: { + path: "webview-ui/src", + localesDir: "webview-ui/src/i18n/locales", + }, + src: { + path: "src", + localesDir: "src/i18n/locales", + }, +} + +// i18n key patterns for findMissingI18nKeys +const i18nScanPatterns = [ + /\bt\([\s\n]*"([^"]+)"/g, + /\bt\([\s\n]*'([^']+)'/g, + /\bt\([\s\n]*`([^`]+)`/g, + /i18nKey="([^"]+)"/g, +] + +// Track line numbers for source code keys +const lineMap = new Map() + +// Check if the key exists in all official language files, return a list of missing language files + +// Function 1: Accumulate source keys +function accumulateSourceKeys(): Set { + const sourceCodeKeys = new Set() + + 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))) { + // Read file content + const content = fs.readFileSync(filePath, "utf8") + + // Match all i18n keys + const matches = new Set() + for (const pattern of i18nScanPatterns) { + let match + while ((match = pattern.exec(content)) !== null) { + const key = match[1] + const lineNumber = content.slice(0, match.index).split("\n").length + matches.add(key) + sourceCodeKeys.add(key) + lineMap.set(key, { + file: path.relative(process.cwd(), filePath), + line: lineNumber, + }) + } + } + } + } + } + + // Walk through all directories to collect source code keys + Object.entries(SCAN_SOURCE_DIRS).forEach(([_name, config]) => { + // Create locales directory if it doesn't exist + if (!fs.existsSync(config.localesDir)) { + fs.mkdirSync(config.localesDir, { recursive: true }) + } + walk(config.path, config.path, config.localesDir) + }) + + return sourceCodeKeys +} + +// Function 2: Accumulate translation keys +function accumulateTranslationKeys(): Set { + const translationFileKeys = new Set() + + // Helper function to extract all keys from a JSON object with their full paths + function extractKeysFromJson(obj: any, prefix: string): string[] { + const keys: string[] = [] + + function traverse(o: any, p: string) { + if (o && typeof o === "object") { + Object.keys(o).forEach((key) => { + const newPath = p ? `${p}.${key}` : key + if (o[key] && typeof o[key] === "object") { + traverse(o[key], newPath) + } else { + keys.push(`${prefix}:${newPath}`) + } + }) + } + } + + traverse(obj, "") + return keys + } + + // Check all locale directories for translation keys + Object.entries(SCAN_SOURCE_DIRS).forEach(([_name, config]) => { + const enLocalesDir = path.join(config.localesDir, "en") + if (fs.existsSync(enLocalesDir)) { + const enFiles = fs.readdirSync(enLocalesDir) + + for (const file of enFiles) { + if (path.extname(file) === ".json") { + const filePath = path.join(enLocalesDir, file) + const content = loadFileContent(filePath) + const json = parseJsonContent(content, filePath) + + if (json) { + // Extract all keys from the JSON file + const fileKeys = extractKeysFromJson(json, file.replace(".json", "")) + + // Add all keys to the translation file keys set + fileKeys.forEach((key) => { + translationFileKeys.add(key) + }) + } + } + } + } + }) + + return translationFileKeys +} + +// Function 3: Return all keys in source that are not in translations +function getKeysInSourceNotInTranslation(sourceKeys: Set, translationKeys: Set): string[] { + return Array.from(sourceKeys) + .filter((key) => !translationKeys.has(key)) + .sort() +} + +// Function to convert a key into segments and mark dynamic parts as undefined +function keyToSegments(key: string): (string | undefined)[] { + return key.split(".").map((segment) => (segment.includes("${") ? undefined : segment)) +} + +// Function to check if a static key matches a dynamic key pattern +function matchesKeyPattern(staticKey: string, dynamicKey: string): boolean { + const staticSegments = staticKey.split(".") + const dynamicSegments = keyToSegments(dynamicKey) + + if (staticSegments.length !== dynamicSegments.length) { + return false + } + + return dynamicSegments.every((dynSeg, i) => dynSeg === undefined || dynSeg === staticSegments[i]) +} + +// Function 4: Return all keys in translations that are not in source +function getKeysInTranslationNotInSource( + sourceKeys: Set, + translationKeys: Set, + dynamicKeys: string[] = [], +): string[] { + return Array.from(translationKeys) + .filter((key) => { + // If key is directly used in source, it's not unused + if (sourceKeys.has(key)) { + return false + } + + // If key matches any dynamic key pattern, it's not unused + if (dynamicKeys.some((dynamicKey) => matchesKeyPattern(key, dynamicKey))) { + return false + } + + return true + }) + .sort() +} + +// Function to find dynamic i18n keys (containing ${...}) +function findDynamicKeys(sourceKeys: Set): string[] { + return Array.from(sourceKeys) + .filter((key) => key.includes("${")) + .sort() +} + +// Function to find non-namespaced t() calls +export function findNonNamespacedI18nKeys(sourceKeys: Set): string[] { + return Array.from(sourceKeys) + .filter((key) => !key.includes(":")) + .sort() +} + +export function findMissingI18nKeys(): { output: string; nonNamespacedKeys: string[] } { + clearLogs() // Clear buffer at start + + // Helper function to extract all keys from a JSON object with their full paths + function extractKeysFromJson(obj: any, prefix: string): string[] { + const keys: string[] = [] + + function traverse(o: any, p: string) { + if (o && typeof o === "object") { + Object.keys(o).forEach((key) => { + const newPath = p ? `${p}.${key}` : key + if (o[key] && typeof o[key] === "object") { + traverse(o[key], newPath) + } else { + keys.push(`${prefix}:${newPath}`) + } + }) + } + } + + traverse(obj, "") + return keys + } + + // Get source code keys and translation keys + const sourceCodeKeys = accumulateSourceKeys() + const translationFileKeys = accumulateTranslationKeys() + + // Find special keys + const dynamicKeys = findDynamicKeys(sourceCodeKeys) + const nonNamespacedKeys = findNonNamespacedI18nKeys(sourceCodeKeys) + + // Create sets for set operations + const dynamicSet = new Set(dynamicKeys) + const nonNamespacedSet = new Set(nonNamespacedKeys) + const remainingSourceKeys = new Set( + Array.from(sourceCodeKeys).filter((key) => !dynamicSet.has(key) && !nonNamespacedSet.has(key)), + ) + + // Find keys in source not in translations and vice versa + const missingTranslationKeys = getKeysInSourceNotInTranslation(remainingSourceKeys, translationFileKeys) + const unusedTranslationKeys = getKeysInTranslationNotInSource(remainingSourceKeys, translationFileKeys, dynamicKeys) + + // Track unused keys in English locale files + const unusedKeys: Array<{ key: string; file: string }> = [] + + // Populate unusedKeys for the original output format + Object.entries(SCAN_SOURCE_DIRS).forEach(([_name, config]) => { + const enLocalesDir = path.join(config.localesDir, "en") + if (fs.existsSync(enLocalesDir)) { + const enFiles = fs.readdirSync(enLocalesDir) + + for (const file of enFiles) { + if (path.extname(file) === ".json") { + const filePath = path.join(enLocalesDir, file) + const content = loadFileContent(filePath) + const json = parseJsonContent(content, filePath) + + if (json) { + // Extract all keys from the JSON file + const fileKeys = extractKeysFromJson(json, file.replace(".json", "")) + + // Check if each key is used in source code + fileKeys.forEach((key) => { + if (!sourceCodeKeys.has(key)) { + unusedKeys.push({ + key, + file: path.relative(process.cwd(), filePath), + }) + } + }) + } + } + } + } + }) + + // Accumulate all debug information into a single string + let summaryOutput = "\n=== i18n Keys Summary ===\n" + + // Add summary counts + summaryOutput += `\nTotal source code keys: ${sourceCodeKeys.size}\n` + summaryOutput += `Total translation file keys: ${translationFileKeys.size}\n` + + // Dynamic keys + summaryOutput += `\n1. Dynamic i18n keys (${dynamicKeys.length}):\n` + if (dynamicKeys.length === 0) { + summaryOutput += " None - all i18n keys are static\n" + } else { + dynamicKeys.forEach((key) => { + const loc = lineMap.get(key) + summaryOutput += ` - ${loc?.file}:${loc?.line}: ${key}\n` + }) + } + + // Non-namespaced keys + summaryOutput += `\n2. Non-namespaced t() calls (${nonNamespacedKeys.length}):\n` + if (nonNamespacedKeys.length === 0) { + summaryOutput += " None - all t() calls use namespaces\n" + } else { + nonNamespacedKeys.forEach((key) => { + const loc = lineMap.get(key) + summaryOutput += ` - ${loc?.file}:${loc?.line}: ${key}\n` + }) + } + + // Keys in source code but not in translation files + summaryOutput += `\n3. Keys in source code but not in translation files (${missingTranslationKeys.length}):\n` + if (missingTranslationKeys.length === 0) { + summaryOutput += " None - all source code keys have translations\n" + } else { + missingTranslationKeys.forEach((key) => { + summaryOutput += ` - ${key}\n` + }) + } + + // Keys in translation files but not in source code (excluding dynamic matches) + summaryOutput += `\n4. Unused translation keys (${unusedTranslationKeys.length}):\n` + if (unusedTranslationKeys.length === 0) { + summaryOutput += " None - all translation keys are either directly used or matched by dynamic patterns\n" + } else { + // Group keys by locale directory and file + const localeFileMap = new Map>() + + // Process each key to extract file path and actual key + unusedTranslationKeys.forEach((fullKey) => { + // Extract file name and key path + const parts = fullKey.split(":") + if (parts.length >= 2) { + const filePrefix = parts[0] + const keyPath = parts.slice(1).join(".") + + // Find the locale directory for this file prefix + let foundLocaleDir = "" + Object.entries(SCAN_SOURCE_DIRS).forEach(([_name, config]) => { + const enLocalesDir = path.join(config.localesDir, "en") + const jsonFilePath = path.join(enLocalesDir, `${filePrefix}.json`) + if (fs.existsSync(jsonFilePath)) { + foundLocaleDir = path.relative(process.cwd(), jsonFilePath) + } + }) + + // If we found the locale directory, add the key to the map + if (foundLocaleDir) { + if (!localeFileMap.has(foundLocaleDir)) { + localeFileMap.set(foundLocaleDir, new Map()) + } + + if (!localeFileMap.get(foundLocaleDir)?.has(filePrefix)) { + localeFileMap.get(foundLocaleDir)?.set(filePrefix, []) + } + + localeFileMap.get(foundLocaleDir)?.get(filePrefix)?.push(keyPath) + } else { + // Fallback to the old behavior if we can't find the locale directory + if (!localeFileMap.has(filePrefix)) { + localeFileMap.set(filePrefix, new Map()) + } + + if (!localeFileMap.get(filePrefix)?.has("unknown")) { + localeFileMap.get(filePrefix)?.set("unknown", []) + } + + localeFileMap.get(filePrefix)?.get("unknown")?.push(keyPath) + } + } + }) + + // Display keys grouped by file + Array.from(localeFileMap.entries()) + .sort() + .forEach(([filePath, prefixMap]) => { + summaryOutput += ` - ${filePath}:\n` + + Array.from(prefixMap.entries()) + .sort() + .forEach(([_prefix, keys]) => { + keys.sort().forEach((key) => { + summaryOutput += ` ${key}\n` + }) + }) + }) + } + + // Add to buffer as a single log entry + bufferLog(summaryOutput) + + return { + output: printLogs(), + nonNamespacedKeys, + } +} + +describe("Find Missing i18n Keys", () => { + // Cache the source and translation keys + let sourceKeys: Set + let translationKeys: Set + let keysInSourceNotInTranslation: string[] + let keysInTranslationNotInSource: string[] + + beforeAll(() => { + // Accumulate keys once for all tests + sourceKeys = accumulateSourceKeys() + translationKeys = accumulateTranslationKeys() + + // Find special keys + const dynamicKeys = findDynamicKeys(sourceKeys) + const nonNamespacedKeys = findNonNamespacedI18nKeys(sourceKeys) + + // Create sets for set operations + const dynamicSet = new Set(dynamicKeys) + const nonNamespacedSet = new Set(nonNamespacedKeys) + const remainingSourceKeys = new Set( + Array.from(sourceKeys).filter((key) => !dynamicSet.has(key) && !nonNamespacedSet.has(key)), + ) + + // Find differences + keysInSourceNotInTranslation = getKeysInSourceNotInTranslation(remainingSourceKeys, translationKeys) + keysInTranslationNotInSource = getKeysInTranslationNotInSource( + remainingSourceKeys, + translationKeys, + dynamicKeys, + ) + + // Clear logs at start + clearLogs() + + // Accumulate debug information into a buffer + bufferLog("\n=== DEBUG: i18n Keys Summary ===") + bufferLog(`\nTotal source code keys: ${sourceKeys.size}`) + bufferLog(`Total translation file keys: ${translationKeys.size}`) + + bufferLog(`\n1. Keys in source code but not in translation files (${keysInSourceNotInTranslation.length}):`) + if (keysInSourceNotInTranslation.length === 0) { + bufferLog(" None - all source code keys have translations") + } else { + keysInSourceNotInTranslation.forEach((key) => { + bufferLog(` - ${key}`) + }) + } + + bufferLog(`\n2. Keys in translation files but not in source code (${keysInTranslationNotInSource.length}):`) + if (keysInTranslationNotInSource.length === 0) { + bufferLog(" None - all translation keys are used in source code") + } else { + // Group keys by file prefix for better readability + const filePathMap = new Map() + + keysInTranslationNotInSource.forEach((fullKey) => { + const parts = fullKey.split(":") + if (parts.length >= 2) { + const filePrefix = parts[0] + + if (!filePathMap.has(filePrefix)) { + filePathMap.set(filePrefix, []) + } + + filePathMap.get(filePrefix)?.push(parts.slice(1).join(".")) + } + }) + + Array.from(filePathMap.entries()) + .sort() + .forEach(([filePrefix, keys]) => { + bufferLog(` - ${filePrefix}:`) + keys.sort().forEach((key) => { + bufferLog(` ${key}`) + }) + }) + } + + // Store the buffer output for tests + printLogs() + }) + + // Test 1: Fail if there are keys in source not in translations + test("Test 1: Fail if there are keys in source not in translations", () => { + // Run the original function to get the output + const result = findMissingI18nKeys() + + // Check for the expected message in the output + expect(result.output).toContain("None - all source code keys have translations") + + // This test should fail if there are any keys in source not in translations + expect(keysInSourceNotInTranslation.length).toBe(0) + }) + + // Test 2: Fail if there are keys in translations not in source + test("Test 2: Fail if there are keys in translations not in source", () => { + // Run the original function to get the output + const result = findMissingI18nKeys() + + // Check for the expected message in the output + expect(result.output).toContain("None - all translation keys are used in source code") + + // This test should fail if there are any keys in translations not in source + // We expect this test to fail in the current codebase + expect(keysInTranslationNotInSource.length).toBe(0) + }) +}) diff --git a/locales/__tests__/lint-translations.test.ts b/locales/__tests__/lint-translations.test.ts new file mode 100644 index 0000000000..e916206ee4 --- /dev/null +++ b/locales/__tests__/lint-translations.test.ts @@ -0,0 +1,1154 @@ +import * as fs from "fs" +const path = require("path") +import { + languages as originalLanguages, + type Language, + bufferLog, + printLogs, + clearLogs, + fileExists, + loadFileContent, + parseJsonContent, + getValueAtPath, + escapeDotsForDisplay, +} from "./utils" + +/** + * Renders the section for completely missing files for a specific locale + * @param locale The language locale + * @param files Array of target file paths that are completely missing + * @param area The translation area + * @param results Results object containing file results + * @param mapping The path mapping containing namespace information + * @returns Void - outputs directly to bufferLog + */ +function renderMissingFilesSection( + locale: string, + files: string[], + area: string, + results: Results, + mapping: PathMapping, +): void { + bufferLog(` ${locale}: missing ${files.length} files`) + files.sort().forEach((targetFilePath) => { + bufferLog(` ${targetFilePath}`) + + // Find the source file that this targetFilePath corresponds to + let sourceFileForMissing = "unknown_source.file" + const fileResultForMissing = results[area][locale][targetFilePath] + if ( + fileResultForMissing && + fileResultForMissing.missing.length === 1 && + fileResultForMissing.missing[0].key.length === 1 + ) { + sourceFileForMissing = fileResultForMissing.missing[0].key[0] + } else { + // Fallback if not easily found (should be available for completely missing files) + const allSourcesForMapping = enumerateSourceFiles(mapping.source) + for (const sf of allSourcesForMapping) { + if (resolveTargetPath(sf, mapping.targetTemplate, locale) === targetFilePath) { + sourceFileForMissing = sf + break + } + } + } + + if (mapping.reportFileLevelOnly === true) { + bufferLog(` Missing file: ALL CONTENT (entire file)`) + } else { + // JSON files + const sourceContent = parseJsonContent(loadFileContent(sourceFileForMissing), sourceFileForMissing) + if (sourceContent) { + let issues = checkMissingTranslations(sourceContent, {}) // Get all keys + // Filter out issues where the source value is an object + const primitiveIssues = issues.filter( + (issue) => typeof issue.sourceValue !== "object" || issue.sourceValue === null, + ) + bufferLog(` Missing keys: ALL KEYS (${primitiveIssues.length} total)`) + } else { + bufferLog(` Missing keys: Unable to load corresponding source file ${sourceFileForMissing}`) + } + } + }) +} + +/** + * Renders the section for files with missing keys for a specific locale + * @param locale The language locale + * @param fileMap Map of target files to their missing keys and source files + * @param mapping The path mapping containing namespace information + * @param options Lint options for verbose output + * @returns Void - outputs directly to bufferLog + */ +function renderMissingKeysSection( + locale: string, + fileMap: Map; sourceFile: string }>, + mapping: PathMapping, +): void { + const filesWithMissingKeys = Array.from(fileMap.keys()) + if (filesWithMissingKeys.length > 0) { + bufferLog(` ${locale}: ${filesWithMissingKeys.length} files with missing translations`) + filesWithMissingKeys.sort().forEach((targetFilePath) => { + bufferLog(` ${targetFilePath}`) + const fileData = fileMap.get(targetFilePath) + if (fileData && fileData.keys.size > 0) { + bufferLog(` Missing keys (${fileData.keys.size} total):`) + const originalSourceFileName = fileData.sourceFile + + Array.from(fileData.keys) + .map((spa) => spa.split("\u0000")) // pathArrayKey + .sort((a, b) => escapeDotsForDisplay(a).localeCompare(escapeDotsForDisplay(b))) + .forEach((pathArrayKey) => { + let displayKeyPrefix = "" + if (mapping.displayNamespace) { + displayKeyPrefix = mapping.displayNamespace + ":" + } else if (mapping.useFilenameAsNamespace && originalSourceFileName !== "unknown_source.file") { + displayKeyPrefix = path.basename(originalSourceFileName, ".json") + ":" + } + + if (mapping.reportFileLevelOnly === true) { + // Should not be reached if logic is correct, keys are not reported for fileLevelOnly + bufferLog(` - ${escapeDotsForDisplay(pathArrayKey)}`) // Fallback + } else { + bufferLog(` - ${displayKeyPrefix}${escapeDotsForDisplay(pathArrayKey)}`) + } + }) + } + }) + } +} + +/** + * Renders the section for extra translations for a specific locale + * @param locale The language locale + * @param fileMap Map of target files to their extra translation issues + * @param mapping The path mapping containing namespace information + * @returns Void - outputs directly to bufferLog + */ +function renderExtraTranslationsSection( + locale: string, + fileMap: Map, + mapping: PathMapping, +): void { + fileMap.forEach(({ issues: extras, sourceFile: derivedSourceFile }, targetFilePath) => { + if (!mapping) return + + // Attempt to derive sourceFile if it's "DERIVE_LATER" + let actualSourceFile = derivedSourceFile + if (actualSourceFile === "DERIVE_LATER") { + const allSourcesForMapping = enumerateSourceFiles(mapping.source) + for (const sf of allSourcesForMapping) { + if (resolveTargetPath(sf, mapping.targetTemplate, locale) === targetFilePath) { + actualSourceFile = sf + break + } + } + if (actualSourceFile === "DERIVE_LATER") actualSourceFile = "unknown_source.file" + } + + const extraFileMarkers = extras.filter((ex) => ex.key.length === 1 && ex.key[0] === "EXTRA_FILE_MARKER") + const extraJsonKeys = extras.filter((ex) => !(ex.key.length === 1 && ex.key[0] === "EXTRA_FILE_MARKER")) + + extraFileMarkers.forEach((issue) => { + const extraFilePathDisplay = issue.localeValue as string // Full path to extra file + bufferLog(` ${locale}: ${extraFilePathDisplay} (Extra File)`) + }) + + if (extraJsonKeys.length > 0) { + let displayKeyPrefix = "" + if (mapping.displayNamespace) { + displayKeyPrefix = mapping.displayNamespace + ":" + } else if (mapping.useFilenameAsNamespace && actualSourceFile !== "unknown_source.file") { + displayKeyPrefix = path.basename(actualSourceFile, ".json") + ":" + } + + bufferLog(` ${locale}: ${targetFilePath}: ${extraJsonKeys.length} extra translations`) + extraJsonKeys + .sort((a, b) => escapeDotsForDisplay(a.key).localeCompare(escapeDotsForDisplay(b.key))) + .forEach((issue) => { + bufferLog(` ${displayKeyPrefix}${escapeDotsForDisplay(issue.key)}`) + }) + } + }) +} + +// Create a mutable copy of the languages array that can be overridden +let languages = [...originalLanguages] + +// Track unique errors to avoid duplication +const seenErrors = new Set() + +interface PathMapping { + name: string + area: "docs" | "core" | "webview" | "package-nls" // Remains for grouping/filtering + source: string | string[] + targetTemplate: string + useFilenameAsNamespace?: boolean // For areas like 'core', 'webview' + displayNamespace?: string // Explicit namespace for display, e.g., "nls" for package-nls + reportFileLevelOnly?: boolean // True for 'docs', false for JSON-based areas +} + +interface LintOptions { + locale?: string[] + file?: string[] + area?: string[] + check?: ("missing" | "extra" | "all")[] + help?: boolean + verbose?: boolean + allowUnknownLocales?: boolean +} + +interface TranslationIssue { + key: string[] // Changed from string to string[] + sourceValue?: any + localeValue?: any +} + +interface FileResult { + missing: TranslationIssue[] + extra: TranslationIssue[] + error?: string + sizeWarning?: string // Added for file size issues +} + +interface Results { + [area: string]: { + [locale: string]: { + [file: string]: FileResult + } + } +} + +const PATH_MAPPINGS: PathMapping[] = [ + { + name: "Documentation", + area: "docs", + source: ["CODE_OF_CONDUCT.md", "CONTRIBUTING.md", "README.md", "PRIVACY.md"], + targetTemplate: "locales//", + reportFileLevelOnly: true, // Key change: Only report missing/extra files + // useFilenameAsNamespace and displayNamespace are omitted (or false/undefined) + }, + { + name: "Core UI Components", + area: "core", // Internal grouping + source: "src/i18n/locales/en", // Directory + targetTemplate: "src/i18n/locales//", + useFilenameAsNamespace: true, // e.g., "common", "tools" + reportFileLevelOnly: false, // Key change: report keys + // displayNamespace is omitted + }, + { + name: "Webview UI Components", + area: "webview", // Internal grouping + source: "webview-ui/src/i18n/locales/en", // Directory + targetTemplate: "webview-ui/src/i18n/locales//", + useFilenameAsNamespace: true, // e.g., "chat", "settings" + reportFileLevelOnly: false, // Key change: report keys + // displayNamespace is omitted + }, + { + name: "Package NLS", + area: "package-nls", // Internal grouping + source: "package.nls.json", // File + targetTemplate: "package.nls..json", + displayNamespace: "nls", // Key change: Explicit display namespace + reportFileLevelOnly: false, // Key change: report keys + // useFilenameAsNamespace is omitted (or false/undefined) + }, +] + +function enumerateSourceFiles(source: string | string[]): string[] { + if (Array.isArray(source)) { + return source.map((file) => (file.startsWith("/") ? file.slice(1) : file)) + } + + const files: string[] = [] + const sourcePath = path.join("./", source) + + if (!fs.existsSync(sourcePath)) { + bufferLog(`Source path does not exist: ${sourcePath}`) + return files + } + + const stats = fs.statSync(sourcePath) + if (stats.isFile()) { + files.push(source) + } else { + const entries = fs.readdirSync(sourcePath, { withFileTypes: true }) + for (const entry of entries) { + if (entry.isFile() && !entry.name.startsWith(".")) { + files.push(path.join(source, entry.name)) + } + } + } + + return files +} + +function resolveTargetPath(sourceFile: string, targetTemplate: string, locale: string): string { + const targetPath = targetTemplate.replace("", locale) + + if (targetTemplate === "/") { + return sourceFile.replace(".json", `.${locale}.json`) + } + + if (!targetTemplate.endsWith("/")) { + return targetPath + } + + const fileName = path.basename(sourceFile) + return path.join(targetPath, fileName) +} + +// Helper function to recursively compare objects and identify differences +function compareObjects( + sourceObj: any, + targetObj: any, + currentPath: string[] = [], +): { missing: TranslationIssue[]; extra: TranslationIssue[] } { + const missing: TranslationIssue[] = [] + const extra: TranslationIssue[] = [] + + // Missing Keys Loop (Iterate sourceObj keys) + for (const key in sourceObj) { + if (Object.prototype.hasOwnProperty.call(sourceObj, key)) { + const newPath = [...currentPath, key] + if (!Object.prototype.hasOwnProperty.call(targetObj, key)) { + missing.push({ key: newPath, sourceValue: sourceObj[key] }) + } else if ( + typeof sourceObj[key] === "object" && + sourceObj[key] !== null && + typeof targetObj[key] === "object" && + targetObj[key] !== null + ) { + const nestedResult = compareObjects(sourceObj[key], targetObj[key], newPath) + missing.push(...nestedResult.missing) + extra.push(...nestedResult.extra) + } + } + } + + // Extra Keys Loop (Iterate targetObj keys) + for (const key in targetObj) { + if (Object.prototype.hasOwnProperty.call(targetObj, key)) { + const newPath = [...currentPath, key] + if (!Object.prototype.hasOwnProperty.call(sourceObj, key)) { + // Crucially: If typeof targetObj[key] === "object" && targetObj[key] !== null + // (i.e., the value of the extra key is an object), then do not add it to the extra list. + if (!(typeof targetObj[key] === "object" && targetObj[key] !== null)) { + extra.push({ key: newPath, localeValue: targetObj[key] }) + } + } + } + } + return { missing, extra } +} + +function checkMissingTranslations(sourceContent: any, targetContent: any): TranslationIssue[] { + if ( + typeof sourceContent !== "object" || + sourceContent === null || + typeof targetContent !== "object" || + targetContent === null + ) { + return [] + } + const { missing } = compareObjects(sourceContent, targetContent) + return missing +} + +function checkExtraTranslations(sourceContent: any, targetContent: any): TranslationIssue[] { + if ( + typeof sourceContent !== "object" || + sourceContent === null || + typeof targetContent !== "object" || + targetContent === null + ) { + return [] + } + const { extra } = compareObjects(sourceContent, targetContent) + return extra +} + +function getFilteredLocales(localeArgs?: string[]): Language[] { + const baseLocales = languages.filter((locale) => locale !== "en") + + if (!localeArgs || localeArgs.includes("all")) { + return baseLocales + } + + return localeArgs as unknown as Language[] +} + +function filterMappingsByArea(mappings: PathMapping[], areaArgs?: string[]): PathMapping[] { + if (!areaArgs || areaArgs.includes("all")) { + return mappings + } + + return mappings.filter((mapping) => areaArgs.includes(mapping.area)) +} + +function filterSourceFiles(sourceFiles: string[], fileArgs?: string[]): string[] { + if (!fileArgs || fileArgs.includes("all")) { + return sourceFiles + } + + return sourceFiles.filter((file) => { + const basename = path.basename(file) + return fileArgs.includes(basename) + }) +} + +function checkIfFileMissing(targetFilePath: string): boolean { + return !fileExists(targetFilePath) +} + +function checkFileSizeDifference(sourceFilePath: string, targetFilePath: string): { warning?: string; error?: string } { + try { + const sourceStats = fs.statSync(sourceFilePath) + const targetStats = fs.statSync(targetFilePath) + const sourceSize = sourceStats.size + const targetSize = targetStats.size + + if (targetSize > sourceSize * 3) { + return { + warning: `Target file ${targetFilePath} is more than 3x larger than source ${sourceFilePath}. It may require retranslation to be within +/- 20% of the source file size.`, + } + } + return {} // No warning, no error from this function's core logic + } catch (e: any) { + return { error: `Error getting file stats for size comparison: ${e.message}` } + } +} + +function processFileLocale( + sourceFile: string, + sourceContent: any, + mapping: PathMapping, + locale: Language, + checksToRun: string[], + results: Results, +): void { + const targetFile = resolveTargetPath(sourceFile, mapping.targetTemplate, locale) + + results[mapping.area] = results[mapping.area] || {} + results[mapping.area][locale] = results[mapping.area][locale] || {} + results[mapping.area][locale][targetFile] = { + missing: [], + extra: [], + } + + const reportFileLevelOnly = mapping.reportFileLevelOnly === true + + if (reportFileLevelOnly) { + // Handle file-level checks (e.g., for "docs") + if (checkIfFileMissing(targetFile)) { + results[mapping.area][locale][targetFile].missing = [ + { + key: [sourceFile], // Source filename as the key + sourceValue: undefined, + }, + ] + return // No further processing for this file + } + + // Target file exists, perform size check + const sizeCheckResult = checkFileSizeDifference(sourceFile, targetFile) + if (sizeCheckResult.warning) { + results[mapping.area][locale][targetFile].sizeWarning = sizeCheckResult.warning + } + if (sizeCheckResult.error) { + results[mapping.area][locale][targetFile].error = sizeCheckResult.error + } + // Do NOT attempt to load/parse content as JSON or call key-based checks + return + } else { + // Handle key-based checks (e.g., for JSON files) + if (checkIfFileMissing(targetFile)) { + results[mapping.area][locale][targetFile].missing = [ + { + key: [sourceFile], // File path as a single element array + sourceValue: undefined, // Or perhaps a specific marker like "File missing" + }, + ] + return + } + + // Ensure sourceContent is parsed if it's a JSON file (already handled before calling this function for JSONs) + // The main check here is for targetContent and proceeding with key comparisons. + if (!sourceFile.endsWith(".json")) { + // This case should ideally not be hit if reportFileLevelOnly is false, + // as non-JSONs would typically have reportFileLevelOnly = true. + // However, keeping a safeguard. + // If source is not JSON, but we are in this branch, it implies a configuration mismatch + // or that sourceContent might be raw text content for a non-JSON file that somehow + // needs key-based comparison (which is unlikely with current setup). + // For now, if it's not JSON, we can't do key-based comparison. + return + } + + const targetContent = parseJsonContent(loadFileContent(targetFile), targetFile) + if (!targetContent) { + results[mapping.area][locale][targetFile].error = `Failed to load or parse target file: ${targetFile}` + return + } + + if (checksToRun.includes("missing") || checksToRun.includes("all")) { + results[mapping.area][locale][targetFile].missing = checkMissingTranslations(sourceContent, targetContent) + } + + if (checksToRun.includes("extra") || checksToRun.includes("all")) { + results[mapping.area][locale][targetFile].extra = checkExtraTranslations(sourceContent, targetContent) + } + } +} + +interface ExtraFileIssue { + extraFilePath: string + key: string[] // Typically ["EXTRA_FILE_MARKER"] + localeValue: string // The path of the extra file itself +} + +function identifyExtraFiles( + mapping: PathMapping, + locale: Language, + allSourceFileBasenamesForMapping: Set, // A Set of basenames like {"common.json", "tools.json"} or {"README.md"} +): ExtraFileIssue[] { + const targetDir = mapping.targetTemplate.replace("", locale) + const foundExtraFiles: ExtraFileIssue[] = [] + + if (!fs.existsSync(targetDir)) { + return foundExtraFiles // No target directory, so no extra files to check + } + + let actualTargetFilesDirents: fs.Dirent[] + try { + actualTargetFilesDirents = fs.readdirSync(targetDir, { withFileTypes: true }) + } catch (e: any) { + // This case should be rare if existsSync passed, but good to handle + bufferLog(`Error reading target directory ${targetDir} for locale ${locale}: ${e.message}`) + return foundExtraFiles // Return empty or perhaps an issue indicating directory read error + } + + for (const actualTargetFileDirent of actualTargetFilesDirents) { + if (actualTargetFileDirent.isFile() && !actualTargetFileDirent.name.startsWith(".")) { + const actualTargetFilename = actualTargetFileDirent.name + let derivedSourceBasename: string + + // Derive Corresponding Source Basename + if (mapping.targetTemplate.endsWith("..json")) { + derivedSourceBasename = actualTargetFilename.replace(`.${locale}.json`, ".json") + } else if (mapping.targetTemplate.endsWith("/")) { + derivedSourceBasename = actualTargetFilename + } else { + const langPattern = `.${locale}.` + if (actualTargetFilename.includes(langPattern)) { + derivedSourceBasename = actualTargetFilename.replace(langPattern, ".") + } else { + derivedSourceBasename = actualTargetFilename + } + } + + if (!allSourceFileBasenamesForMapping.has(derivedSourceBasename)) { + const fullPathToActualTargetFile = path.join(targetDir, actualTargetFilename) + foundExtraFiles.push({ + extraFilePath: fullPathToActualTargetFile, + key: ["EXTRA_FILE_MARKER"], // Standardized marker + localeValue: fullPathToActualTargetFile, // The path of the extra file itself + }) + } + } + } + return foundExtraFiles +} + +function formatResults(results: Results, checkTypes: string[], options: LintOptions, mappings: PathMapping[]): boolean { + let hasIssues = false + + clearLogs() // Clear buffer at start + seenErrors.clear() // Clear error tracking + bufferLog("=== Translation Results ===") + + // Group errors by type for summary (excluding size warnings initially) + const errorsByType = new Map() + + for (const [area, areaResults] of Object.entries(results)) { + let areaHasIssues = false + const extraByLocale = new Map>() + let missingCount = 0 + // missingByFile: key is targetFilePath:locale, value is { keys: Set, sourceFile: string } + // No, missingByFile is `sourceFile:locale` -> Set + // We need to adjust how missingKeysByLocale and missingFilesByLocale are built or used. + const missingByFile = new Map>() // Stores sourceFile:locale -> Set of stringified keys + + for (const [locale, localeResults] of Object.entries(areaResults)) { + let localeMissingCount = 0 + let localeExtraCount = 0 + let localeErrorCount = 0 + + for (const [targetFilePath, fileResult] of Object.entries(localeResults)) { + // file is targetFilePath + if (fileResult.error) { + localeErrorCount++ + const errorType = fileResult.error.split(":")[0] // Basic error type + if (!errorsByType.has(errorType)) errorsByType.set(errorType, []) + errorsByType.get(errorType)?.push(`${locale} - ${targetFilePath} (Error: ${fileResult.error})`) + areaHasIssues = true + } + + if (fileResult.sizeWarning) { + areaHasIssues = true // Ensure area is reported if there's a size warning + } + + if (fileResult.error && !fileResult.sizeWarning) { + continue + } + + if (checkTypes.includes("missing") && fileResult.missing.length > 0) { + localeMissingCount += fileResult.missing.length + missingCount += fileResult.missing.length + areaHasIssues = true + + // Populate missingByFile (sourceFile:locale -> keys) + // This requires knowing the sourceFile for this targetFilePath. + // This information should ideally be in fileResult if we modify it. + // For now, we'll build missingFilesByLocale and missingKeysByLocale more directly later. + const missingFileKey = `${targetFilePath}:${locale}` // Using targetFilePath for now, will refine + if (!missingByFile.has(missingFileKey)) { + missingByFile.set(missingFileKey, new Set()) + } + fileResult.missing.forEach(({ key: pathArray }) => { + missingByFile.get(missingFileKey)?.add(pathArray.join("\u0000")) + }) + } + + if (checkTypes.includes("extra") && fileResult.extra.length > 0) { + localeExtraCount += fileResult.extra.length + areaHasIssues = true + // Populate extraByLocale (locale -> targetFilePath -> {issues, sourceFile}) + // Requires sourceFile for targetFilePath. + // Assuming fileResult.sourceFilePath exists (hypothetical change) + // const sourceFileForExtra = (fileResult as any).sourceFilePath || "unknown_source_for_extra"; + // For now, we'll handle sourceFile derivation during display. + if (!extraByLocale.has(locale)) { + extraByLocale.set(locale, new Map()) + } + // Storing raw extras for now, sourceFile to be derived in display loop + extraByLocale + .get(locale) + ?.set(targetFilePath, { issues: fileResult.extra, sourceFile: "DERIVE_LATER" }) + } + } + hasIssues ||= localeErrorCount > 0 || localeMissingCount > 0 || localeExtraCount > 0 || areaHasIssues + } + + if (areaHasIssues) { + bufferLog(`\n${area.toUpperCase()} Translations:`) + const mappingForArea = mappings.find((m) => m.area === area) + if (!mappingForArea) continue + + // Show error summaries (excluding size warnings) + errorsByType.forEach((files, errorType) => { + if (errorType === "SizeWarning") return // Skip size warnings here + + const areaFiles = files.filter((fileEntry) => { + // fileEntry is "locale - targetFilePath (Error: message)" + // Check if targetFilePath belongs to the current area's mapping + const targetFileFromEntry = fileEntry.split(" - ")[1]?.split(" (Error:")[0] + if (!targetFileFromEntry) return false + + // A simple check: does the targetFileFromEntry look like it came from this mapping? + // This is hard without knowing the source file. + // For now, assume if the error was logged under this area, it belongs. + return true // Simplified, as errors are already grouped by area in `results` + }) + + if (areaFiles.length > 0) { + bufferLog(` ❌ ${errorType}:`) + bufferLog(` Affected files: ${areaFiles.length}`) + if (options?.verbose) { + areaFiles.forEach((file) => bufferLog(` ${file}`)) + } + } + }) + errorsByType.clear() // Clear after processing for an area to avoid carry-over + + // Display Size Warnings for the area + const sizeWarningMessages: string[] = [] + for (const [_locale, localeResults] of Object.entries(areaResults)) { + for (const [targetFilePath, fileRes] of Object.entries(localeResults)) { + if (fileRes.sizeWarning) { + sizeWarningMessages.push(` ⚠️ Size Warning for ${targetFilePath}: ${fileRes.sizeWarning}`) + } + } + } + if (sizeWarningMessages.length > 0) { + sizeWarningMessages.sort().forEach((msg) => bufferLog(msg)) + } + + // Show missing translations summary + if (missingCount > 0 && (checkTypes.includes("missing") || checkTypes.includes("all"))) { + bufferLog(` 📝 Missing translations (${missingCount} total):`) + + const missingFilesByLocaleDisplay = new Map() // lang -> [targetFilePath] + const missingKeysByLocaleDisplay = new Map< + string, + Map; sourceFile: string }> + >() // lang -> targetFilePath -> {keys, sourceFile} + + // Re-iterate results to correctly categorize and get sourceFile + for (const [locale, localeResults] of Object.entries(areaResults)) { + for (const [targetFilePath, fileRes] of Object.entries(localeResults)) { + if (fileRes.missing && fileRes.missing.length > 0) { + // Try to find the original source file for this targetFilePath + let sourceFileAssociatedWithTarget = "unknown_source.file" // Default + // Heuristic: iterate all source files for this mapping, resolve their target, and see if it matches. + const currentMapping = mappings.find((m) => m.area === area) + if (currentMapping) { + const allSourcesForMapping = enumerateSourceFiles(currentMapping.source) + for (const sf of allSourcesForMapping) { + if ( + resolveTargetPath(sf, currentMapping.targetTemplate, locale) === targetFilePath + ) { + sourceFileAssociatedWithTarget = sf + break + } + } + // If still unknown, and missing[0].key[0] looks like a source file (common for full file missing) + if ( + sourceFileAssociatedWithTarget === "unknown_source.file" && + fileRes.missing[0]?.key?.length === 1 + ) { + // This key is often the source file path when the entire file is missing + sourceFileAssociatedWithTarget = fileRes.missing[0].key[0] + } + } + + const isCompletelyMissingFile = + fileRes.missing.length === 1 && + fileRes.missing[0].key.length === 1 && + fileRes.missing[0].key[0] === sourceFileAssociatedWithTarget // Check against derived/found source + + if (isCompletelyMissingFile) { + if (!missingFilesByLocaleDisplay.has(locale)) + missingFilesByLocaleDisplay.set(locale, []) + missingFilesByLocaleDisplay.get(locale)?.push(targetFilePath) + } else { + if (!missingKeysByLocaleDisplay.has(locale)) + missingKeysByLocaleDisplay.set(locale, new Map()) + if (!missingKeysByLocaleDisplay.get(locale)?.has(targetFilePath)) { + missingKeysByLocaleDisplay.get(locale)?.set(targetFilePath, { + keys: new Set(), + sourceFile: sourceFileAssociatedWithTarget, + }) + } + fileRes.missing.forEach((issue) => { + missingKeysByLocaleDisplay + .get(locale) + ?.get(targetFilePath) + ?.keys.add(issue.key.join("\u0000")) + }) + } + } + } + } + + // Report missing files + missingFilesByLocaleDisplay.forEach((files, lang) => { + renderMissingFilesSection(lang, files, area, results, mappingForArea) + }) + + // Report files with missing keys + missingKeysByLocaleDisplay.forEach((fileMap, lang) => { + renderMissingKeysSection(lang, fileMap, mappingForArea) + }) + } + + // Show extra translations if any + if (extraByLocale.size > 0 && (checkTypes.includes("extra") || checkTypes.includes("all"))) { + bufferLog(` ⚠️ Extra translations:`) + extraByLocale.forEach((fileMap, locale) => { + renderExtraTranslationsSection(locale, fileMap, mappingForArea) + }) + } + + // if (!areaHasIssues) { // This check might be misleading now with how hasIssues is set + // bufferLog(` ✅ No issues found`); + // } + } + } + // Final check for overall issues to determine return value + let overallHasIssues = false + for (const areaRes of Object.values(results)) { + for (const localeRes of Object.values(areaRes)) { + for (const fileRes of Object.values(localeRes)) { + if ( + fileRes.error || + fileRes.sizeWarning || + (fileRes.missing && fileRes.missing.length > 0) || + (fileRes.extra && fileRes.extra.length > 0) + ) { + overallHasIssues = true + break + } + } + if (overallHasIssues) break + } + if (overallHasIssues) break + } + return overallHasIssues +} + +function formatSummary(results: Results): void { + bufferLog("\n======= SUMMARY =======") + let totalMissing = 0 + let totalExtra = 0 + let totalErrors = 0 + + for (const [area, areaResults] of Object.entries(results)) { + let areaMissing = 0 + let areaExtra = 0 + let areaErrors = 0 + + for (const [_locale, localeResults] of Object.entries(areaResults)) { + for (const [_file, fileResults] of Object.entries(localeResults)) { + if (fileResults.error) { + areaErrors++ + totalErrors++ + } else { + areaMissing += fileResults.missing.length + areaExtra += fileResults.extra.length + } + } + } + + totalMissing += areaMissing + totalExtra += areaExtra + + bufferLog(`${area.toUpperCase()}: ${areaMissing} missing, ${areaExtra} extra, ${areaErrors} errors`) + } + + bufferLog(`\nTOTAL: ${totalMissing} missing, ${totalExtra} extra, ${totalErrors} errors`) + + if (totalMissing === 0 && totalExtra === 0 && totalErrors === 0) { + bufferLog("\n✅ All translations are complete!") + } else { + bufferLog("\n⚠️ Some translation issues were found.") + + if (totalMissing > 0) { + bufferLog("\nFor missing translations:") + bufferLog("1. For .md files:") + bufferLog(" Create the missing translation files in the appropriate locale directory") + bufferLog(" from the English sources in the root of the repository.") + bufferLog(" Use for each file to maintain clear translation context.") + bufferLog("\n2. For .json files:") + bufferLog(" Add missing translations using manage-translations.js.") + bufferLog("\n Key Path Format:") + bufferLog(" - Use single dots (.) for nested paths: command.newTask.title") + bufferLog(" - Use double dots (..) for literal dots: settings..path.name") + bufferLog("\n Example adding translations:") + bufferLog(" # Using here document for volume changes to one file:") + bufferLog(" node scripts/manage-translations.js [-v] --stdin relative/path/to/settings.json << EOF") + bufferLog(' {"command.newTask.title": "Create New Task"}') + bufferLog(' {"settings..path.name": "Custom Path Setting"}') + bufferLog(" EOF") + bufferLog("\n # Or single key-value pairs:") + bufferLog( + ' node scripts/manage-translations.js [-v] relative/path/to/settings.json "command.newTask.title" "Create New Task" [key2 value2 ...]', + ) + } + + if (totalExtra > 0) { + bufferLog("\nFor extra translations:") + bufferLog("Remove translations not present in English (source of truth):") + bufferLog("\n# Using here document for volume changes:") + bufferLog( + "node scripts/manage-translations.js [-v] -d --stdin relative/path/to/settings.json [file2.json ...] << EOF", + ) + bufferLog('["command.oldTask.title"]') + bufferLog('["settings..old.path"]') + bufferLog("EOF") + bufferLog("\n# Or multiple files with specific keys:") + bufferLog( + 'node scripts/manage-translations.js [-v] -d file1.json file2.json -- "command.oldTask.title" "settings..old.path"', + ) + } + + bufferLog("\nNotes:") + bufferLog("- Always translate from the original English source documents") + bufferLog("- Use -v flag for verbose output showing each operation") + bufferLog("- Run manage-translations.js without arguments for full usage details") + } +} + +function printUsage(): void { + bufferLog("Usage: node lint-translations.js [options]") + bufferLog("\nDescription:") + bufferLog(" Lint translation files to find missing or extra translations across different locales.") + bufferLog("\nOptions:") + bufferLog(" --help Show this help message") + bufferLog(" --verbose Enable verbose output with detailed information") + bufferLog(" --locale= Filter by specific locales (comma-separated)") + bufferLog(" Example: --locale=fr,de,ja") + bufferLog(" Use 'all' for all supported locales") + bufferLog(" --file= Filter by specific files (comma-separated)") + bufferLog(" Example: --file=settings.json,commands.json") + bufferLog(" Use 'all' for all files") + bufferLog(" --area= Filter by specific areas (comma-separated)") + bufferLog(` Valid areas: ${PATH_MAPPINGS.map((m) => m.area).join(", ")}, all`) + bufferLog(" Example: --area=docs,core") + bufferLog(" --check= Specify which checks to run (comma-separated)") + bufferLog(" Valid checks: missing, extra, all") + bufferLog(" Example: --check=missing,extra") + bufferLog("\nExamples:") + bufferLog(" # Check all translations in all areas") + bufferLog(" node lint-translations.js") + bufferLog("\n # Check only missing translations for French locale") + bufferLog(" node lint-translations.js --locale=fr --check=missing") + bufferLog("\n # Check only documentation translations for German and Japanese") + bufferLog(" node lint-translations.js --area=docs --locale=de,ja") + bufferLog("\n # Verbose output for specific files in core area") + bufferLog(" node lint-translations.js --area=core --file=settings.json,commands.json --verbose") +} + +function parseArgs(args: string[] = process.argv.slice(2)): LintOptions { + const options: LintOptions = { + area: ["all"], + check: ["all"] as ("missing" | "extra" | "all")[], + verbose: false, + help: false, + } + + // Reset languages to original value at the start + languages = [...originalLanguages] + + for (const arg of args) { + if (arg === "--verbose") { + options.verbose = true + continue + } + if (arg === "--help") { + options.help = true + continue + } + + const match = arg.match(/^--([^=]+)=(.+)$/) + if (!match) continue + + const [, key, value] = match + const values = value.split(",") + + switch (key) { + case "locale": + options.locale = values + // Override the global languages array with the provided locales + // Add 'en' as it's always needed as the source language + languages = ["en", ...values] as unknown as Language[] + break + case "file": + options.file = values + break + case "area": { + const validAreas = [...PATH_MAPPINGS.map((m) => m.area), "all"] + for (const area of values) { + if (!validAreas.includes(area)) { + throw new Error(`Error: Invalid area '${area}'. Must be one of: ${validAreas.join(", ")}`) + } + } + options.area = values + break + } + case "check": { + const validChecks = ["missing", "extra", "all"] + for (const check of values) { + if (!validChecks.includes(check)) { + bufferLog(`Error: Invalid check '${check}'. Must be one of: ${validChecks.join(", ")}`) + process.exit(1) + } + } + options.check = values as ("missing" | "extra" | "all")[] + break + } + } + } + + return options +} + +function lintTranslations(args?: LintOptions): { output: string } { + clearLogs() // Clear the buffer at the start + const options = args || parseArgs() || { area: ["all"], check: ["all"] } + + // If help flag is set, print usage and return + if (options.help) { + printUsage() + return { output: printLogs() } + } + + const checksToRun = options.check?.includes("all") ? ["missing", "extra"] : options.check || ["all"] + + const filteredMappings = filterMappingsByArea(PATH_MAPPINGS, options.area) + const results: Results = {} + + for (const mapping of filteredMappings) { + let enumeratedSourceFilesFullPaths = enumerateSourceFiles(mapping.source) + enumeratedSourceFilesFullPaths = filterSourceFiles(enumeratedSourceFilesFullPaths, options.file) + + if (enumeratedSourceFilesFullPaths.length === 0) { + bufferLog(`No matching files found for area ${mapping.name} with current filters.`) + continue + } + + const allSourceFileBasenamesForMapping = new Set(enumeratedSourceFilesFullPaths.map((sf) => path.basename(sf))) + + const locales = getFilteredLocales(options.locale) + + if (locales.length === 0) { + bufferLog(`No matching locales found for area ${mapping.name} with current filters.`) + continue + } + + // Process each source file against each locale + for (const sourceFile of enumeratedSourceFilesFullPaths) { + let sourceContent: any = null + // Load and parse source content once per source file + if (sourceFile.endsWith(".json")) { + const content = loadFileContent(sourceFile) + if (!content) { + // Log error or handle missing source file appropriately + // This might already be handled by enumerateSourceFiles or loadFileContent + bufferLog(`Warning: Could not load source file: ${sourceFile}`) + continue + } + sourceContent = parseJsonContent(content, sourceFile) + if (!sourceContent) { + bufferLog(`Warning: Could not parse source JSON file: ${sourceFile}`) + continue + } + } else { + // For non-JSON files (like .md), sourceContent is the raw text + sourceContent = loadFileContent(sourceFile) // Assuming loadFileContent returns string or null + if (sourceContent === null || sourceContent === undefined) { + // Check for null or undefined explicitly + bufferLog(`Warning: Could not load source file content: ${sourceFile}`) + continue + } + } + + for (const locale of locales) { + processFileLocale(sourceFile, sourceContent, mapping, locale, checksToRun, results) + } + } + + // After processing all source files for a mapping, check for extra files in target directories + if (checksToRun.includes("extra") || checksToRun.includes("all")) { + for (const locale of locales) { + const extraFileIssues = identifyExtraFiles(mapping, locale, allSourceFileBasenamesForMapping) + for (const issue of extraFileIssues) { + results[mapping.area] = results[mapping.area] || {} + results[mapping.area][locale] = results[mapping.area][locale] || {} + results[mapping.area][locale][issue.extraFilePath] = results[mapping.area][locale][ + issue.extraFilePath + ] || { + missing: [], + extra: [], + error: undefined, + sizeWarning: undefined, + } + results[mapping.area][locale][issue.extraFilePath].extra.push({ + key: issue.key, + localeValue: issue.localeValue, + }) + } + } + } + } + + const hasIssues = formatResults(results, checksToRun, options, filteredMappings) + formatSummary(results) + if (!hasIssues) { + bufferLog("\nAll translations are complete") + } + const output = printLogs() + + return { output } +} + +// Export functions for use in other modules +export { + enumerateSourceFiles, + resolveTargetPath, + loadFileContent, + parseJsonContent, + fileExists, + PATH_MAPPINGS, + getValueAtPath, + checkMissingTranslations, + checkExtraTranslations, + getFilteredLocales, + filterMappingsByArea, + filterSourceFiles, + lintTranslations, +} + +describe("Translation Linting", () => { + test("Run translation linting", () => { + // Use the centralized parseArgs function to process Jest arguments + // Jest passes arguments after -- to the test + const options = parseArgs(process.argv) + + // If help flag is set, run with help option + if (options.help) { + const result = lintTranslations(options) + console.log(result.output) // Print help directly to console for visibility + expect(result.output).toContain("Usage: node lint-translations.js [options]") + return + } + + // Run with processed options + const result = lintTranslations(options) + + // MUST FAIL in ANY event where the output does not contain "All translations are complete" + // This will cause the test to fail for locales with missing or extra translations + expect(result.output).toContain("All translations are complete") + }) + + test("Filters mappings by area correctly", () => { + const filteredMappings = filterMappingsByArea(PATH_MAPPINGS, ["docs"]) + expect(filteredMappings).toHaveLength(1) + expect(filteredMappings[0].area).toBe("docs") + }) + + test("Displays help information when help flag is set", () => { + const result = lintTranslations({ + help: true, + area: ["all"], + check: ["all"], + }) + + // Verify help content + expect(result.output).toContain("Usage: node lint-translations.js [options]") + expect(result.output).toContain("Description:") + expect(result.output).toContain("Options:") + expect(result.output).toContain("Examples:") + + // Verify it doesn't run the linting process + expect(result.output).not.toContain("Translation Results") + }) + + test("Checks for missing translations", () => { + const source = { key1: "value1", key2: "value2" } + const target = { key1: "value1" } + const issues = checkMissingTranslations(source, target) + expect(issues).toHaveLength(1) + expect(issues[0].key).toEqual(["key2"]) + }) + + test("Checks for extra translations", () => { + const source = { key1: "value1" } + const target = { key1: "value1", extraKey: "extra" } + const issues = checkExtraTranslations(source, target) + expect(issues).toHaveLength(1) + expect(issues[0].key).toEqual(["extraKey"]) + }) +}) diff --git a/locales/__tests__/utils.ts b/locales/__tests__/utils.ts new file mode 100644 index 0000000000..6fb55d90dc --- /dev/null +++ b/locales/__tests__/utils.ts @@ -0,0 +1,77 @@ +const fs = require("fs") +const path = require("path") +import { languages as schemaLanguages } from "../../src/schemas/index" + +export const languages = schemaLanguages +export type Language = (typeof languages)[number] + +let logBuffer: string[] = [] + +export function bufferLog(message: string) { + logBuffer.push(message) +} + +export function printLogs(): string { + const output = logBuffer.join("\n") + logBuffer = [] + return output +} + +export function clearLogs(): void { + logBuffer = [] +} + +export function fileExists(filePath: string): boolean { + return fs.existsSync(path.join("./", filePath)) +} + +export function loadFileContent(filePath: string): string | null { + try { + return fs.readFileSync(filePath, "utf8") + } catch (error) { + return null + } +} + +// Track unique errors to avoid duplication +const seenErrors = new Set() + +export function parseJsonContent(content: string | null, filePath: string): any | null { + if (!content) return null + + try { + return JSON.parse(content) + } catch (error) { + // Only log first occurrence of each unique error + const errorKey = `${filePath}:${(error as Error).message}` + if (!seenErrors.has(errorKey)) { + seenErrors.add(errorKey) + bufferLog(`Error parsing ${path.basename(filePath)}: ${(error as Error).message}`) + } + return null + } +} + +export function getValueAtPath(obj: any, pathArray: string[]): any { + let current = obj + for (const part of pathArray) { + if ( + current === undefined || + current === null || + typeof current !== "object" || + !Object.prototype.hasOwnProperty.call(current, part) + ) { + return undefined + } + current = current[part] + } + return current +} + +// Utility function to escape dots in keys for display purposes +export function escapeDotsForDisplay(pathArray: string[]): string { + if (!pathArray || pathArray.length === 0) { + return "" + } + return pathArray.map((segment) => segment.replace(/\./g, "..")).join(".") +} diff --git a/locales/ca/PRIVACY.md b/locales/ca/PRIVACY.md new file mode 100644 index 0000000000..09266af0a1 --- /dev/null +++ b/locales/ca/PRIVACY.md @@ -0,0 +1,37 @@ +# Política de privadesa de Roo Code + +**Última actualització: 7 de març de 2025** + +Roo Code respecta la vostra privadesa i està compromès amb la transparència sobre com gestionem les vostres dades. A continuació trobareu un resum senzill d'on van les dades clau i, el que és més important, on no van. + +### **On van les vostres dades (i on no)** + +- **Codi i fitxers**: Roo Code accedeix als fitxers del vostre ordinador local quan és necessari per a les funcions assistides per IA. Quan envieu ordres a Roo Code, els fitxers rellevants es poden transmetre al vostre proveïdor de model d'IA escollit (per exemple, OpenAI, Anthropic, OpenRouter) per generar respostes. No tenim accés a aquestes dades, però els proveïdors d'IA poden emmagatzemar-les segons les seves polítiques de privadesa. +- **Ordres**: Qualsevol ordre executada a través de Roo Code es produeix al vostre entorn local. No obstant això, quan utilitzeu funcions basades en IA, el codi rellevant i el context de les vostres ordres es poden transmetre al vostre proveïdor de model d'IA escollit (per exemple, OpenAI, Anthropic, OpenRouter) per generar respostes. No tenim accés ni emmagatzemem aquestes dades, però els proveïdors d'IA poden processar-les segons les seves polítiques de privadesa. +- **Prompts i sol·licituds d'IA**: Quan utilitzeu funcions basades en IA, els vostres prompts i el context rellevant del projecte s'envien al vostre proveïdor de model d'IA escollit (per exemple, OpenAI, Anthropic, OpenRouter) per generar respostes. No emmagatzemem ni processem aquestes dades. Aquests proveïdors d'IA tenen les seves pròpies polítiques de privadesa i poden emmagatzemar dades segons els seus termes de servei. +- **Claus d'API i credencials**: Si introduïu una clau d'API (per exemple, per connectar un model d'IA), s'emmagatzema localment al vostre dispositiu i mai s'envia a nosaltres ni a tercers, excepte al proveïdor que heu escollit. +- **Telemetria (dades d'ús)**: Només recollim dades d'ús de funcions i errors si hi opteu explícitament. Aquesta telemetria està impulsada per PostHog i ens ajuda a entendre l'ús de les funcions per millorar Roo Code. Això inclou el vostre ID de màquina VS Code i els patrons d'ús de funcions i informes d'excepcions. **No** recollim informació personalment identificable, el vostre codi ni prompts d'IA. + +### **Com utilitzem les vostres dades (si es recullen)** + +- Si opteu per la telemetria, la utilitzem per entendre l'ús de les funcions i millorar Roo Code. +- **No** venem ni compartim les vostres dades. +- **No** entrenem cap model amb les vostres dades. + +### **Les vostres opcions i control** + +- Podeu executar models localment per evitar que s'enviïn dades a tercers. +- Per defecte, la recollida de telemetria està desactivada i si l'activeu, podeu desactivar-la en qualsevol moment. +- Podeu eliminar Roo Code per aturar tota la recollida de dades. + +### **Seguretat i actualitzacions** + +Prenem mesures raonables per protegir les vostres dades, però cap sistema és 100% segur. Si la nostra política de privadesa canvia, us ho notificarem dins de l'extensió. + +### **Contacteu-nos** + +Per a qualsevol pregunta relacionada amb la privadesa, contacteu-nos a support@roocode.com. + +--- + +En utilitzar Roo Code, accepteu aquesta Política de privadesa. diff --git a/locales/de/PRIVACY.md b/locales/de/PRIVACY.md new file mode 100644 index 0000000000..e2486fecce --- /dev/null +++ b/locales/de/PRIVACY.md @@ -0,0 +1,37 @@ +# Roo Code Datenschutzerklärung + +**Zuletzt aktualisiert: 7. März 2025** + +Roo Code respektiert deine Privatsphäre und verpflichtet sich zu Transparenz im Umgang mit deinen Daten. Hier findest du eine einfache Übersicht darüber, wohin wichtige Daten fließen – und vor allem, wohin nicht. + +### **Wohin deine Daten gehen (und wohin nicht)** + +- **Code & Dateien**: Roo Code greift bei Bedarf für KI-unterstützte Funktionen auf Dateien auf deinem lokalen Computer zu. Wenn du Befehle an Roo Code sendest, können relevante Dateien an deinen gewählten KI-Modellanbieter (z.B. OpenAI, Anthropic, OpenRouter) übertragen werden, um Antworten zu generieren. Wir haben keinen Zugriff auf diese Daten, aber KI-Anbieter können sie gemäß ihrer Datenschutzrichtlinien speichern. +- **Befehle**: Alle über Roo Code ausgeführten Befehle finden in deiner lokalen Umgebung statt. Wenn du jedoch KI-gestützte Funktionen nutzt, können der relevante Code und der Kontext deiner Befehle an deinen gewählten KI-Modellanbieter (z.B. OpenAI, Anthropic, OpenRouter) übertragen werden, um Antworten zu generieren. Wir haben keinen Zugriff auf diese Daten und speichern sie nicht, aber KI-Anbieter können sie gemäß ihrer Datenschutzrichtlinien verarbeiten. +- **Prompts & KI-Anfragen**: Wenn du KI-gestützte Funktionen nutzt, werden deine Prompts und der relevante Projektkontext an deinen gewählten KI-Modellanbieter (z.B. OpenAI, Anthropic, OpenRouter) gesendet, um Antworten zu generieren. Wir speichern oder verarbeiten diese Daten nicht. Diese KI-Anbieter haben ihre eigenen Datenschutzrichtlinien und können Daten gemäß ihrer Nutzungsbedingungen speichern. +- **API-Schlüssel & Zugangsdaten**: Wenn du einen API-Schlüssel eingibst (z.B. um ein KI-Modell zu verbinden), wird dieser lokal auf deinem Gerät gespeichert und niemals an uns oder Dritte gesendet, außer an den von dir gewählten Anbieter. +- **Telemetrie (Nutzungsdaten)**: Wir sammeln Funktionsnutzungs- und Fehlerdaten nur, wenn du ausdrücklich zustimmst. Diese Telemetrie wird von PostHog bereitgestellt und hilft uns, die Funktionsnutzung zu verstehen, um Roo Code zu verbessern. Dies umfasst deine VS Code Maschinen-ID, Funktionsnutzungsmuster und Ausnahmeberichte. Wir sammeln **keine** personenbezogenen Daten, deinen Code oder KI-Prompts. + +### **Wie wir deine Daten nutzen (falls erfasst)** + +- Wenn du der Telemetrie zustimmst, nutzen wir sie, um die Funktionsnutzung zu verstehen und Roo Code zu verbessern. +- Wir **verkaufen** oder **teilen** deine Daten nicht. +- Wir **trainieren** keine Modelle mit deinen Daten. + +### **Deine Wahlmöglichkeiten & Kontrolle** + +- Du kannst Modelle lokal ausführen, um die Übertragung von Daten an Dritte zu verhindern. +- Standardmäßig ist die Telemetrieerfassung deaktiviert, und wenn du sie aktivierst, kannst du sie jederzeit wieder deaktivieren. +- Du kannst Roo Code löschen, um die gesamte Datenerfassung zu stoppen. + +### **Sicherheit & Aktualisierungen** + +Wir ergreifen angemessene Maßnahmen, um deine Daten zu schützen, aber kein System ist zu 100% sicher. Wenn sich unsere Datenschutzrichtlinie ändert, werden wir dich innerhalb der Erweiterung benachrichtigen. + +### **Kontaktiere uns** + +Bei Fragen zum Datenschutz erreichst du uns unter support@roocode.com. + +--- + +Durch die Nutzung von Roo Code stimmst du dieser Datenschutzerklärung zu. diff --git a/locales/es/PRIVACY.md b/locales/es/PRIVACY.md new file mode 100644 index 0000000000..adc3f8b56a --- /dev/null +++ b/locales/es/PRIVACY.md @@ -0,0 +1,37 @@ +# Política de Privacidad de Roo Code + +**Última actualización: 7 de marzo de 2025** + +Roo Code respeta tu privacidad y está comprometido con la transparencia sobre cómo manejamos tus datos. A continuación, encontrarás un desglose simple de dónde van los datos importantes y, lo que es más importante, dónde no van. + +### **Dónde van tus datos (y dónde no)** + +- **Código y archivos**: Roo Code accede a los archivos en tu máquina local cuando es necesario para las funciones asistidas por IA. Cuando envías comandos a Roo Code, los archivos relevantes pueden ser transmitidos a tu proveedor de modelo de IA elegido (por ejemplo, OpenAI, Anthropic, OpenRouter) para generar respuestas. No tenemos acceso a estos datos, pero los proveedores de IA pueden almacenarlos según sus políticas de privacidad. +- **Comandos**: Cualquier comando ejecutado a través de Roo Code ocurre en tu entorno local. Sin embargo, cuando utilizas funciones basadas en IA, el código relevante y el contexto de tus comandos pueden ser transmitidos a tu proveedor de modelo de IA elegido (por ejemplo, OpenAI, Anthropic, OpenRouter) para generar respuestas. No tenemos acceso ni almacenamos estos datos, pero los proveedores de IA pueden procesarlos según sus políticas de privacidad. +- **Prompts y solicitudes de IA**: Cuando utilizas funciones basadas en IA, tus prompts y el contexto relevante del proyecto se envían a tu proveedor de modelo de IA elegido (por ejemplo, OpenAI, Anthropic, OpenRouter) para generar respuestas. No almacenamos ni procesamos estos datos. Estos proveedores de IA tienen sus propias políticas de privacidad y pueden almacenar datos según sus términos de servicio. +- **Claves API y credenciales**: Si ingresas una clave API (por ejemplo, para conectar un modelo de IA), se almacena localmente en tu dispositivo y nunca se envía a nosotros ni a terceros, excepto al proveedor que hayas elegido. +- **Telemetría (datos de uso)**: Solo recopilamos datos de uso de funciones y errores si optas explícitamente por participar. Esta telemetría está impulsada por PostHog y nos ayuda a entender el uso de las funciones para mejorar Roo Code. Esto incluye tu ID de máquina de VS Code y patrones de uso de funciones e informes de excepciones. **No** recopilamos información personalmente identificable, tu código o prompts de IA. + +### **Cómo usamos tus datos (si se recopilan)** + +- Si optas por la telemetría, la usamos para entender el uso de las funciones y mejorar Roo Code. +- **No** vendemos ni compartimos tus datos. +- **No** entrenamos ningún modelo con tus datos. + +### **Tus opciones y control** + +- Puedes ejecutar modelos localmente para evitar que se envíen datos a terceros. +- Por defecto, la recopilación de telemetría está desactivada y si la activas, puedes optar por no participar en cualquier momento. +- Puedes eliminar Roo Code para detener toda la recopilación de datos. + +### **Seguridad y actualizaciones** + +Tomamos medidas razonables para proteger tus datos, pero ningún sistema es 100% seguro. Si nuestra política de privacidad cambia, te lo notificaremos dentro de la extensión. + +### **Contáctanos** + +Para cualquier pregunta relacionada con la privacidad, contáctanos en support@roocode.com. + +--- + +Al usar Roo Code, aceptas esta Política de Privacidad. diff --git a/locales/fr/PRIVACY.md b/locales/fr/PRIVACY.md new file mode 100644 index 0000000000..a5ec0de19b --- /dev/null +++ b/locales/fr/PRIVACY.md @@ -0,0 +1,37 @@ +# Politique de confidentialité de Roo Code + +**Dernière mise à jour : 7 mars 2025** + +Roo Code respecte votre vie privée et s'engage à être transparent sur la manière dont nous gérons vos données. Voici une présentation simple de la destination des données clés - et surtout, de leur non-destination. + +### **Où vont vos données (et où elles ne vont pas)** + +- **Code et fichiers** : Roo Code accède aux fichiers de votre machine locale lorsque c'est nécessaire pour les fonctionnalités assistées par l'IA. Lorsque vous envoyez des commandes à Roo Code, les fichiers pertinents peuvent être transmis à votre fournisseur de modèle d'IA choisi (par exemple, OpenAI, Anthropic, OpenRouter) pour générer des réponses. Nous n'avons pas accès à ces données, mais les fournisseurs d'IA peuvent les stocker selon leurs politiques de confidentialité. +- **Commandes** : Toutes les commandes exécutées via Roo Code se déroulent dans votre environnement local. Cependant, lorsque vous utilisez des fonctionnalités basées sur l'IA, le code pertinent et le contexte de vos commandes peuvent être transmis à votre fournisseur de modèle d'IA choisi (par exemple, OpenAI, Anthropic, OpenRouter) pour générer des réponses. Nous n'avons pas accès à ces données et ne les stockons pas, mais les fournisseurs d'IA peuvent les traiter selon leurs politiques de confidentialité. +- **Prompts et requêtes IA** : Lorsque vous utilisez des fonctionnalités basées sur l'IA, vos prompts et le contexte pertinent du projet sont envoyés à votre fournisseur de modèle d'IA choisi (par exemple, OpenAI, Anthropic, OpenRouter) pour générer des réponses. Nous ne stockons ni ne traitons ces données. Ces fournisseurs d'IA ont leurs propres politiques de confidentialité et peuvent stocker des données selon leurs conditions de service. +- **Clés API et identifiants** : Si vous saisissez une clé API (par exemple, pour connecter un modèle d'IA), elle est stockée localement sur votre appareil et n'est jamais envoyée à nous ou à des tiers, sauf au fournisseur que vous avez choisi. +- **Télémétrie (données d'utilisation)** : Nous ne collectons les données d'utilisation des fonctionnalités et les erreurs que si vous y consentez explicitement. Cette télémétrie est gérée par PostHog et nous aide à comprendre l'utilisation des fonctionnalités pour améliorer Roo Code. Cela inclut votre ID machine VS Code et les modèles d'utilisation des fonctionnalités et les rapports d'exception. Nous ne collectons **pas** d'informations personnellement identifiables, votre code ou vos prompts IA. + +### **Comment nous utilisons vos données (si collectées)** + +- Si vous optez pour la télémétrie, nous l'utilisons pour comprendre l'utilisation des fonctionnalités et améliorer Roo Code. +- Nous ne **vendons** ni ne **partageons** vos données. +- Nous n'**entraînons** aucun modèle avec vos données. + +### **Vos choix et contrôle** + +- Vous pouvez exécuter des modèles localement pour éviter l'envoi de données à des tiers. +- Par défaut, la collecte de télémétrie est désactivée et si vous l'activez, vous pouvez vous en désinscrire à tout moment. +- Vous pouvez supprimer Roo Code pour arrêter toute collecte de données. + +### **Sécurité et mises à jour** + +Nous prenons des mesures raisonnables pour sécuriser vos données, mais aucun système n'est sécurisé à 100%. Si notre politique de confidentialité change, nous vous en informerons dans l'extension. + +### **Nous contacter** + +Pour toute question relative à la confidentialité, contactez-nous à support@roocode.com. + +--- + +En utilisant Roo Code, vous acceptez cette politique de confidentialité. diff --git a/locales/hi/PRIVACY.md b/locales/hi/PRIVACY.md new file mode 100644 index 0000000000..2d1809eb9e --- /dev/null +++ b/locales/hi/PRIVACY.md @@ -0,0 +1,37 @@ +# Roo Code गोपनीयता नीति + +**अंतिम अपडेट: 7 मार्च, 2025** + +Roo Code आपकी गोपनीयता का सम्मान करता है और आपके डेटा को कैसे संभालता है, इस बारे में पारदर्शिता के लिए प्रतिबद्ध है। नीचे एक सरल विवरण दिया गया है कि महत्वपूर्ण डेटा कहाँ जाता है—और, महत्वपूर्ण रूप से, कहाँ नहीं जाता। + +### **आपका डेटा कहाँ जाता है (और कहाँ नहीं)** + +- **कोड और फ़ाइलें**: Roo Code AI-सहायक सुविधाओं के लिए आवश्यक होने पर आपकी लोकल मशीन पर फ़ाइलों तक पहुंचता है। जब आप Roo Code को कमांड भेजते हैं, तो प्रासंगिक फ़ाइलें आपके चुने हुए AI मॉडल प्रदाता (जैसे, OpenAI, Anthropic, OpenRouter) को प्रतिक्रियाएं उत्पन्न करने के लिए भेजी जा सकती हैं। हमें इस डेटा तक पहुंच नहीं है, लेकिन AI प्रदाता अपनी गोपनीयता नीतियों के अनुसार इसे स्टोर कर सकते हैं। +- **कमांड**: Roo Code के माध्यम से निष्पादित की गई कोई भी कमांड आपके स्थानीय वातावरण में होती है। हालांकि, जब आप AI-आधारित सुविधाओं का उपयोग करते हैं, तो प्रासंगिक कोड और आपकी कमांड का संदर्भ प्रतिक्रियाएं उत्पन्न करने के लिए आपके चुने हुए AI मॉडल प्रदाता (जैसे, OpenAI, Anthropic, OpenRouter) को भेजा जा सकता है। हमें इस डेटा तक पहुंच नहीं है और न ही हम इसे स्टोर करते हैं, लेकिन AI प्रदाता अपनी गोपनीयता नीतियों के अनुसार इसे प्रोसेस कर सकते हैं। +- **प्रॉम्प्ट्स और AI अनुरोध**: जब आप AI-आधारित सुविधाओं का उपयोग करते हैं, तो आपके प्रॉम्प्ट्स और प्रोजेक्ट का प्रासंगिक संदर्भ प्रतिक्रियाएं उत्पन्न करने के लिए आपके चुने हुए AI मॉडल प्रदाता (जैसे, OpenAI, Anthropic, OpenRouter) को भेजा जाता है। हम इस डेटा को स्टोर या प्रोसेस नहीं करते हैं। इन AI प्रदाताओं की अपनी गोपनीयता नीतियां हैं और वे अपनी सेवा शर्तों के अनुसार डेटा स्टोर कर सकते हैं। +- **API कुंजियां और क्रेडेंशियल्स**: यदि आप कोई API कुंजी दर्ज करते हैं (जैसे, AI मॉडल को कनेक्ट करने के लिए), तो यह आपके डिवाइस पर स्थानीय रूप से स्टोर की जाती है और कभी भी हमें या किसी तीसरे पक्ष को नहीं भेजी जाती है, सिवाय आपके द्वारा चुने गए प्रदाता के। +- **टेलीमेट्री (उपयोग डेटा)**: हम केवल तभी फीचर उपयोग और त्रुटि डेटा एकत्र करते हैं जब आप स्पष्ट रूप से सहमति देते हैं। यह टेलीमेट्री PostHog द्वारा संचालित है और हमें Roo Code को बेहतर बनाने के लिए फीचर उपयोग को समझने में मदद करती है। इसमें आपकी VS Code मशीन ID और फीचर उपयोग पैटर्न और अपवाद रिपोर्ट शामिल हैं। हम व्यक्तिगत रूप से पहचान योग्य जानकारी, आपका कोड, या AI प्रॉम्प्ट्स **नहीं** एकत्र करते हैं। + +### **हम आपके डेटा का उपयोग कैसे करते हैं (यदि एकत्र किया गया है)** + +- यदि आप टेलीमेट्री के लिए सहमत होते हैं, तो हम इसका उपयोग फीचर उपयोग को समझने और Roo Code को बेहतर बनाने के लिए करते हैं। +- हम आपका डेटा **नहीं** बेचते या साझा करते हैं। +- हम आपके डेटा पर कोई मॉडल **नहीं** प्रशिक्षित करते हैं। + +### **आपके विकल्प और नियंत्रण** + +- आप तृतीय-पक्षों को डेटा भेजने से बचने के लिए मॉडल स्थानीय रूप से चला सकते हैं। +- डिफ़ॉल्ट रूप से, टेलीमेट्री संग्रह बंद है और यदि आप इसे चालू करते हैं, तो आप किसी भी समय टेलीमेट्री से बाहर निकल सकते हैं। +- आप सभी डेटा संग्रह को रोकने के लिए Roo Code को हटा सकते हैं। + +### **सुरक्षा और अपडेट** + +हम आपके डेटा को सुरक्षित करने के लिए उचित उपाय करते हैं, लेकिन कोई भी सिस्टम 100% सुरक्षित नहीं है। यदि हमारी गोपनीयता नीति में परिवर्तन होता है, तो हम आपको एक्सटेंशन के भीतर सूचित करेंगे। + +### **हमसे संपर्क करें** + +किसी भी गोपनीयता-संबंधित प्रश्नों के लिए, हमसे support@roocode.com पर संपर्क करें। + +--- + +Roo Code का उपयोग करके, आप इस गोपनीयता नीति से सहमत होते हैं। diff --git a/locales/it/PRIVACY.md b/locales/it/PRIVACY.md new file mode 100644 index 0000000000..648be7d9cd --- /dev/null +++ b/locales/it/PRIVACY.md @@ -0,0 +1,37 @@ +# Informativa sulla Privacy di Roo Code + +**Ultimo aggiornamento: 7 marzo 2025** + +Roo Code rispetta la tua privacy e si impegna alla trasparenza su come gestiamo i tuoi dati. Di seguito trovi una semplice spiegazione di dove vanno i dati importanti e, soprattutto, dove non vanno. + +### **Dove vanno i tuoi dati (e dove no)** + +- **Codice e file**: Roo Code accede ai file sul tuo computer locale quando necessario per le funzionalità assistite dall'IA. Quando invii comandi a Roo Code, i file pertinenti potrebbero essere trasmessi al fornitore di modelli IA da te scelto (ad esempio, OpenAI, Anthropic, OpenRouter) per generare risposte. Non abbiamo accesso a questi dati, ma i fornitori di IA potrebbero memorizzarli secondo le loro politiche sulla privacy. +- **Comandi**: Qualsiasi comando eseguito attraverso Roo Code avviene nel tuo ambiente locale. Tuttavia, quando utilizzi funzionalità basate sull'IA, il codice pertinente e il contesto dei tuoi comandi potrebbero essere trasmessi al fornitore di modelli IA da te scelto (ad esempio, OpenAI, Anthropic, OpenRouter) per generare risposte. Non abbiamo accesso né memorizziamo questi dati, ma i fornitori di IA potrebbero elaborarli secondo le loro politiche sulla privacy. +- **Prompt e richieste IA**: Quando utilizzi funzionalità basate sull'IA, i tuoi prompt e il contesto pertinente del progetto vengono inviati al fornitore di modelli IA da te scelto (ad esempio, OpenAI, Anthropic, OpenRouter) per generare risposte. Non memorizziamo né elaboriamo questi dati. Questi fornitori di IA hanno le proprie politiche sulla privacy e potrebbero memorizzare i dati secondo i loro termini di servizio. +- **Chiavi API e credenziali**: Se inserisci una chiave API (ad esempio, per connettere un modello IA), viene memorizzata localmente sul tuo dispositivo e non viene mai inviata a noi o a terze parti, eccetto al fornitore che hai scelto. +- **Telemetria (dati di utilizzo)**: Raccogliamo dati sull'utilizzo delle funzionalità e sugli errori solo se acconsenti esplicitamente. Questa telemetria è gestita da PostHog e ci aiuta a comprendere l'utilizzo delle funzionalità per migliorare Roo Code. Questo include il tuo ID macchina VS Code e i modelli di utilizzo delle funzionalità e i report delle eccezioni. **Non** raccogliamo informazioni personalmente identificabili, il tuo codice o i prompt IA. + +### **Come utilizziamo i tuoi dati (se raccolti)** + +- Se acconsenti alla telemetria, la utilizziamo per comprendere l'utilizzo delle funzionalità e migliorare Roo Code. +- **Non** vendiamo né condividiamo i tuoi dati. +- **Non** addestriamo alcun modello con i tuoi dati. + +### **Le tue scelte e il controllo** + +- Puoi eseguire i modelli localmente per evitare l'invio di dati a terze parti. +- Per impostazione predefinita, la raccolta della telemetria è disattivata e se la attivi, puoi disattivarla in qualsiasi momento. +- Puoi eliminare Roo Code per interrompere tutta la raccolta dati. + +### **Sicurezza e aggiornamenti** + +Adottiamo misure ragionevoli per proteggere i tuoi dati, ma nessun sistema è sicuro al 100%. Se la nostra politica sulla privacy cambia, te lo notificheremo all'interno dell'estensione. + +### **Contattaci** + +Per qualsiasi domanda sulla privacy, contattaci a support@roocode.com. + +--- + +Utilizzando Roo Code, accetti questa Informativa sulla Privacy. diff --git a/locales/ja/PRIVACY.md b/locales/ja/PRIVACY.md new file mode 100644 index 0000000000..390dcc233b --- /dev/null +++ b/locales/ja/PRIVACY.md @@ -0,0 +1,37 @@ +# Roo Code プライバシーポリシー + +**最終更新日:2025年3月7日** + +Roo Codeはあなたのプライバシーを尊重し、データの取り扱い方法について透明性を保つことを約束します。以下は、重要なデータがどこに行くのか、そしてより重要なことに、どこに行かないのかについての簡単な説明です。 + +### **あなたのデータの行き先(および行かない場所)** + +- **コードとファイル**:Roo CodeはAIアシスト機能のために必要な場合、ローカルマシン上のファイルにアクセスします。Roo Codeにコマンドを送信すると、関連するファイルが応答を生成するために選択したAIモデルプロバイダー(OpenAI、Anthropic、OpenRouterなど)に送信される場合があります。私たちはこのデータにアクセスできませんが、AIプロバイダーは自社のプライバシーポリシーに従ってデータを保存する場合があります。 +- **コマンド**:Roo Codeを通じて実行されるすべてのコマンドはローカル環境で実行されます。ただし、AI機能を使用する場合、関連するコードとコマンドのコンテキストは、応答を生成するために選択したAIモデルプロバイダー(OpenAI、Anthropic、OpenRouterなど)に送信される場合があります。私たちはこのデータにアクセスせず、保存もしませんが、AIプロバイダーは自社のプライバシーポリシーに従ってデータを処理する場合があります。 +- **プロンプトとAIリクエスト**:AI機能を使用する場合、プロンプトとプロジェクトの関連コンテキストは、応答を生成するために選択したAIモデルプロバイダー(OpenAI、Anthropic、OpenRouterなど)に送信されます。私たちはこのデータを保存または処理しません。これらのAIプロバイダーには独自のプライバシーポリシーがあり、利用規約に従ってデータを保存する場合があります。 +- **APIキーと認証情報**:APIキー(AIモデルを接続するためなど)を入力した場合、それはデバイスにローカルに保存され、選択したプロバイダーを除き、私たちや第三者に送信されることはありません。 +- **テレメトリ(使用状況データ)**:機能の使用状況とエラーデータは、明示的に同意した場合にのみ収集されます。このテレメトリはPostHogによって提供され、Roo Codeを改善するために機能の使用状況を理解するのに役立ちます。これにはVS Codeマシン ID、機能の使用パターン、例外レポートが含まれます。個人を特定できる情報、コード、AIプロンプトは収集**しません**。 + +### **データの使用方法(収集された場合)** + +- テレメトリに同意した場合、機能の使用状況を理解しRoo Codeを改善するために使用します。 +- データの**販売**や**共有**は行いません。 +- あなたのデータを使用してモデルを**トレーニング**することはありません。 + +### **選択肢とコントロール** + +- モデルをローカルで実行して、データが第三者に送信されるのを防ぐことができます。 +- デフォルトでは、テレメトリ収集はオフになっており、オンにした場合でもいつでもオプトアウトできます。 +- Roo Codeを削除することで、すべてのデータ収集を停止できます。 + +### **セキュリティとアップデート** + +私たちはあなたのデータを保護するための合理的な措置を講じていますが、100%安全なシステムは存在しません。プライバシーポリシーが変更された場合、拡張機能内で通知します。 + +### **お問い合わせ** + +プライバシーに関するご質問は、support@roocode.comまでお問い合わせください。 + +--- + +Roo Codeを使用することで、このプライバシーポリシーに同意したことになります。 diff --git a/locales/ko/PRIVACY.md b/locales/ko/PRIVACY.md new file mode 100644 index 0000000000..8ab8b5caaa --- /dev/null +++ b/locales/ko/PRIVACY.md @@ -0,0 +1,37 @@ +# Roo Code 개인정보 처리방침 + +**최종 업데이트: 2025년 3월 7일** + +Roo Code는 귀하의 개인정보를 존중하며 데이터 처리 방식에 대한 투명성을 약속합니다. 다음은 주요 데이터가 어디로 이동하는지, 그리고 더 중요하게는 어디로 이동하지 않는지에 대한 간단한 설명입니다. + +### **귀하의 데이터가 이동하는 곳 (및 이동하지 않는 곳)** + +- **코드 및 파일**: Roo Code는 AI 지원 기능을 위해 필요한 경우 로컬 머신의 파일에 접근합니다. Roo Code에 명령을 보낼 때, 관련 파일이 응답 생성을 위해 선택한 AI 모델 제공업체(예: OpenAI, Anthropic, OpenRouter)로 전송될 수 있습니다. 우리는 이 데이터에 접근할 수 없지만, AI 제공업체는 자체 개인정보 처리방침에 따라 데이터를 저장할 수 있습니다. +- **명령어**: Roo Code를 통해 실행되는 모든 명령어는 로컬 환경에서 실행됩니다. 하지만 AI 기반 기능을 사용할 때, 관련 코드와 명령어의 컨텍스트가 응답 생성을 위해 선택한 AI 모델 제공업체(예: OpenAI, Anthropic, OpenRouter)로 전송될 수 있습니다. 우리는 이 데이터에 접근하거나 저장하지 않지만, AI 제공업체는 자체 개인정보 처리방침에 따라 데이터를 처리할 수 있습니다. +- **프롬프트 및 AI 요청**: AI 기반 기능을 사용할 때, 귀하의 프롬프트와 프로젝트의 관련 컨텍스트가 응답 생성을 위해 선택한 AI 모델 제공업체(예: OpenAI, Anthropic, OpenRouter)로 전송됩니다. 우리는 이 데이터를 저장하거나 처리하지 않습니다. 이러한 AI 제공업체들은 자체 개인정보 처리방침을 가지고 있으며 서비스 약관에 따라 데이터를 저장할 수 있습니다. +- **API 키 및 자격 증명**: API 키(예: AI 모델 연결용)를 입력하면, 이는 귀하의 기기에 로컬로 저장되며 선택한 제공업체를 제외하고 우리나 제3자에게 전송되지 않습니다. +- **원격 측정(사용 데이터)**: 기능 사용 및 오류 데이터는 귀하가 명시적으로 동의한 경우에만 수집됩니다. 이 원격 측정은 PostHog에 의해 제공되며 Roo Code 개선을 위한 기능 사용 이해에 도움을 줍니다. 여기에는 VS Code 머신 ID와 기능 사용 패턴 및 예외 보고서가 포함됩니다. 우리는 개인 식별 정보, 코드 또는 AI 프롬프트를 수집하지 **않습니다**. + +### **데이터 사용 방법 (수집된 경우)** + +- 원격 측정에 동의하면, 기능 사용을 이해하고 Roo Code를 개선하는 데 사용됩니다. +- 귀하의 데이터를 **판매**하거나 **공유**하지 않습니다. +- 귀하의 데이터로 모델을 **학습**하지 않습니다. + +### **귀하의 선택 및 제어** + +- 제3자에게 데이터가 전송되는 것을 방지하기 위해 모델을 로컬에서 실행할 수 있습니다. +- 기본적으로 원격 측정 수집은 비활성화되어 있으며, 활성화하더라도 언제든지 옵트아웃할 수 있습니다. +- Roo Code를 삭제하여 모든 데이터 수집을 중단할 수 있습니다. + +### **보안 및 업데이트** + +우리는 귀하의 데이터를 보호하기 위해 합리적인 조치를 취하지만, 어떤 시스템도 100% 안전하지는 않습니다. 개인정보 처리방침이 변경되면 확장 프로그램 내에서 알려드립니다. + +### **연락처** + +개인정보 관련 문의사항은 support@roocode.com으로 연락해 주시기 바랍니다. + +--- + +Roo Code를 사용함으로써 이 개인정보 처리방침에 동의하게 됩니다. diff --git a/locales/nl/PRIVACY.md b/locales/nl/PRIVACY.md new file mode 100644 index 0000000000..f05efa4612 --- /dev/null +++ b/locales/nl/PRIVACY.md @@ -0,0 +1,37 @@ +# Roo Code Privacybeleid + +**Laatst bijgewerkt: 7 maart 2025** + +Roo Code respecteert je privacy en zet zich in voor transparantie over hoe we met je gegevens omgaan. Hieronder vind je een eenvoudige uitleg over waar belangrijke gegevens naartoe gaan - en belangrijker nog, waar ze niet naartoe gaan. + +### **Waar je gegevens naartoe gaan (en waar niet)** + +- **Code en bestanden**: Roo Code heeft toegang tot bestanden op je lokale machine wanneer dat nodig is voor AI-ondersteunde functies. Wanneer je opdrachten naar Roo Code stuurt, kunnen relevante bestanden worden verzonden naar je gekozen AI-modelprovider (bijvoorbeeld OpenAI, Anthropic, OpenRouter) om antwoorden te genereren. Wij hebben geen toegang tot deze gegevens, maar AI-providers kunnen ze opslaan volgens hun privacybeleid. +- **Opdrachten**: Alle opdrachten die via Roo Code worden uitgevoerd, gebeuren in je lokale omgeving. Echter, wanneer je AI-gestuurde functies gebruikt, kunnen de relevante code en context van je opdrachten worden verzonden naar je gekozen AI-modelprovider (bijvoorbeeld OpenAI, Anthropic, OpenRouter) om antwoorden te genereren. Wij hebben geen toegang tot deze gegevens en slaan ze niet op, maar AI-providers kunnen ze verwerken volgens hun privacybeleid. +- **Prompts en AI-verzoeken**: Wanneer je AI-gestuurde functies gebruikt, worden je prompts en relevante projectcontext verzonden naar je gekozen AI-modelprovider (bijvoorbeeld OpenAI, Anthropic, OpenRouter) om antwoorden te genereren. Wij slaan deze gegevens niet op en verwerken ze niet. Deze AI-providers hebben hun eigen privacybeleid en kunnen gegevens opslaan volgens hun servicevoorwaarden. +- **API-sleutels en inloggegevens**: Als je een API-sleutel invoert (bijvoorbeeld om een AI-model te verbinden), wordt deze lokaal op je apparaat opgeslagen en nooit naar ons of derden verzonden, behalve naar de provider die je hebt gekozen. +- **Telemetrie (gebruiksgegevens)**: We verzamelen alleen functiegebruik en foutgegevens als je hier expliciet voor kiest. Deze telemetrie wordt aangedreven door PostHog en helpt ons het functiegebruik te begrijpen om Roo Code te verbeteren. Dit omvat je VS Code machine-ID en patronen van functiegebruik en uitzonderingsrapporten. We verzamelen **geen** persoonlijk identificeerbare informatie, je code of AI-prompts. + +### **Hoe we je gegevens gebruiken (indien verzameld)** + +- Als je kiest voor telemetrie, gebruiken we deze om het functiegebruik te begrijpen en Roo Code te verbeteren. +- We **verkopen** of **delen** je gegevens niet. +- We **trainen** geen modellen met je gegevens. + +### **Je keuzes en controle** + +- Je kunt modellen lokaal uitvoeren om te voorkomen dat gegevens naar derden worden verzonden. +- Standaard staat telemetrieverzameling uit en als je het aanzet, kun je je op elk moment afmelden. +- Je kunt Roo Code verwijderen om alle gegevensverzameling te stoppen. + +### **Beveiliging en updates** + +We nemen redelijke maatregelen om je gegevens te beveiligen, maar geen enkel systeem is 100% veilig. Als ons privacybeleid verandert, zullen we je hiervan op de hoogte stellen binnen de extensie. + +### **Contact met ons opnemen** + +Voor privacy-gerelateerde vragen kun je contact met ons opnemen via support@roocode.com. + +--- + +Door Roo Code te gebruiken, ga je akkoord met dit Privacybeleid. diff --git a/locales/pl/PRIVACY.md b/locales/pl/PRIVACY.md new file mode 100644 index 0000000000..3b343600f9 --- /dev/null +++ b/locales/pl/PRIVACY.md @@ -0,0 +1,37 @@ +# Polityka Prywatności Roo Code + +**Ostatnia aktualizacja: 7 marca 2025** + +Roo Code szanuje Twoją prywatność i zobowiązuje się do przejrzystości w kwestii przetwarzania Twoich danych. Poniżej znajduje się proste wyjaśnienie, gdzie trafiają kluczowe dane - i co ważniejsze, gdzie nie trafiają. + +### **Gdzie trafiają Twoje dane (i gdzie nie)** + +- **Kod i pliki**: Roo Code uzyskuje dostęp do plików na Twoim lokalnym komputerze, gdy jest to potrzebne do funkcji wspomaganych przez AI. Gdy wysyłasz polecenia do Roo Code, odpowiednie pliki mogą być przesyłane do wybranego przez Ciebie dostawcy modelu AI (np. OpenAI, Anthropic, OpenRouter) w celu generowania odpowiedzi. Nie mamy dostępu do tych danych, ale dostawcy AI mogą je przechowywać zgodnie ze swoimi politykami prywatności. +- **Polecenia**: Wszystkie polecenia wykonywane przez Roo Code odbywają się w Twoim lokalnym środowisku. Jednak gdy korzystasz z funkcji opartych na AI, odpowiedni kod i kontekst Twoich poleceń mogą być przesyłane do wybranego przez Ciebie dostawcy modelu AI (np. OpenAI, Anthropic, OpenRouter) w celu generowania odpowiedzi. Nie mamy dostępu ani nie przechowujemy tych danych, ale dostawcy AI mogą je przetwarzać zgodnie ze swoimi politykami prywatności. +- **Prompty i zapytania AI**: Gdy korzystasz z funkcji opartych na AI, Twoje prompty i odpowiedni kontekst projektu są wysyłane do wybranego przez Ciebie dostawcy modelu AI (np. OpenAI, Anthropic, OpenRouter) w celu generowania odpowiedzi. Nie przechowujemy ani nie przetwarzamy tych danych. Ci dostawcy AI mają własne polityki prywatności i mogą przechowywać dane zgodnie ze swoimi warunkami świadczenia usług. +- **Klucze API i poświadczenia**: Jeśli wprowadzisz klucz API (np. do połączenia z modelem AI), jest on przechowywany lokalnie na Twoim urządzeniu i nigdy nie jest wysyłany do nas ani do stron trzecich, z wyjątkiem wybranego przez Ciebie dostawcy. +- **Telemetria (dane o użytkowaniu)**: Zbieramy dane o użyciu funkcji i błędach tylko wtedy, gdy wyraźnie wyrazisz na to zgodę. Ta telemetria jest obsługiwana przez PostHog i pomaga nam zrozumieć wykorzystanie funkcji w celu ulepszenia Roo Code. Obejmuje to Twój identyfikator maszyny VS Code oraz wzorce użycia funkcji i raporty o wyjątkach. **Nie** zbieramy danych osobowych, Twojego kodu ani promptów AI. + +### **Jak wykorzystujemy Twoje dane (jeśli są zbierane)** + +- Jeśli zgodzisz się na telemetrię, wykorzystujemy ją do zrozumienia użycia funkcji i ulepszenia Roo Code. +- **Nie** sprzedajemy ani nie udostępniamy Twoich danych. +- **Nie** trenujemy żadnych modeli na Twoich danych. + +### **Twoje wybory i kontrola** + +- Możesz uruchamiać modele lokalnie, aby zapobiec wysyłaniu danych do stron trzecich. +- Domyślnie zbieranie telemetrii jest wyłączone, a jeśli je włączysz, możesz zrezygnować w dowolnym momencie. +- Możesz usunąć Roo Code, aby zatrzymać całe zbieranie danych. + +### **Bezpieczeństwo i aktualizacje** + +Podejmujemy rozsądne środki, aby zabezpieczyć Twoje dane, ale żaden system nie jest w 100% bezpieczny. Jeśli nasza polityka prywatności ulegnie zmianie, powiadomimy Cię o tym w rozszerzeniu. + +### **Kontakt z nami** + +W przypadku pytań dotyczących prywatności, skontaktuj się z nami pod adresem support@roocode.com. + +--- + +Korzystając z Roo Code, zgadzasz się na tę Politykę Prywatności. diff --git a/locales/pt-BR/PRIVACY.md b/locales/pt-BR/PRIVACY.md new file mode 100644 index 0000000000..b61cb40dd0 --- /dev/null +++ b/locales/pt-BR/PRIVACY.md @@ -0,0 +1,37 @@ +# Política de Privacidade do Roo Code + +**Última atualização: 7 de março de 2025** + +O Roo Code respeita sua privacidade e está comprometido com a transparência sobre como lidamos com seus dados. Abaixo está uma explicação simples de para onde vão os dados importantes — e, mais importante ainda, para onde eles não vão. + +### **Para onde vão seus dados (e para onde não vão)** + +- **Código e arquivos**: O Roo Code acessa arquivos em sua máquina local quando necessário para recursos assistidos por IA. Quando você envia comandos para o Roo Code, arquivos relevantes podem ser transmitidos para seu provedor de modelo de IA escolhido (por exemplo, OpenAI, Anthropic, OpenRouter) para gerar respostas. Não temos acesso a esses dados, mas os provedores de IA podem armazená-los de acordo com suas políticas de privacidade. +- **Comandos**: Qualquer comando executado através do Roo Code acontece em seu ambiente local. No entanto, quando você usa recursos baseados em IA, o código relevante e o contexto de seus comandos podem ser transmitidos para seu provedor de modelo de IA escolhido (por exemplo, OpenAI, Anthropic, OpenRouter) para gerar respostas. Não temos acesso nem armazenamos esses dados, mas os provedores de IA podem processá-los de acordo com suas políticas de privacidade. +- **Prompts e solicitações de IA**: Quando você usa recursos baseados em IA, seus prompts e o contexto relevante do projeto são enviados para seu provedor de modelo de IA escolhido (por exemplo, OpenAI, Anthropic, OpenRouter) para gerar respostas. Não armazenamos nem processamos esses dados. Esses provedores de IA têm suas próprias políticas de privacidade e podem armazenar dados de acordo com seus termos de serviço. +- **Chaves de API e credenciais**: Se você inserir uma chave de API (por exemplo, para conectar um modelo de IA), ela é armazenada localmente em seu dispositivo e nunca é enviada para nós ou terceiros, exceto para o provedor que você escolheu. +- **Telemetria (dados de uso)**: Coletamos dados de uso de recursos e erros apenas se você optar explicitamente por participar. Essa telemetria é fornecida pelo PostHog e nos ajuda a entender o uso dos recursos para melhorar o Roo Code. Isso inclui seu ID de máquina do VS Code e padrões de uso de recursos e relatórios de exceções. **Não** coletamos informações pessoalmente identificáveis, seu código ou prompts de IA. + +### **Como usamos seus dados (se coletados)** + +- Se você optar pela telemetria, nós a usamos para entender o uso dos recursos e melhorar o Roo Code. +- **Não** vendemos nem compartilhamos seus dados. +- **Não** treinamos nenhum modelo com seus dados. + +### **Suas escolhas e controle** + +- Você pode executar modelos localmente para evitar que dados sejam enviados a terceiros. +- Por padrão, a coleta de telemetria está desativada e, se você ativá-la, pode optar por não participar a qualquer momento. +- Você pode excluir o Roo Code para interromper toda a coleta de dados. + +### **Segurança e atualizações** + +Tomamos medidas razoáveis para proteger seus dados, mas nenhum sistema é 100% seguro. Se nossa política de privacidade mudar, notificaremos você dentro da extensão. + +### **Contate-nos** + +Para quaisquer questões relacionadas à privacidade, entre em contato conosco em support@roocode.com. + +--- + +Ao usar o Roo Code, você concorda com esta Política de Privacidade. diff --git a/locales/ru/PRIVACY.md b/locales/ru/PRIVACY.md new file mode 100644 index 0000000000..74114847f2 --- /dev/null +++ b/locales/ru/PRIVACY.md @@ -0,0 +1,37 @@ +# Политика конфиденциальности Roo Code + +**Последнее обновление: 7 марта 2025 г.** + +Roo Code уважает вашу конфиденциальность и стремится к прозрачности в отношении того, как мы обрабатываем ваши данные. Ниже приведено простое объяснение того, куда поступают ключевые данные — и, что более важно, куда они не поступают. + +### **Куда поступают ваши данные (и куда нет)** + +- **Код и файлы**: Roo Code получает доступ к файлам на вашем локальном компьютере, когда это необходимо для функций с поддержкой ИИ. Когда вы отправляете команды в Roo Code, соответствующие файлы могут передаваться выбранному вами поставщику моделей ИИ (например, OpenAI, Anthropic, OpenRouter) для генерации ответов. У нас нет доступа к этим данным, но поставщики ИИ могут хранить их в соответствии со своими политиками конфиденциальности. +- **Команды**: Все команды, выполняемые через Roo Code, происходят в вашей локальной среде. Однако при использовании функций на основе ИИ соответствующий код и контекст ваших команд могут передаваться выбранному вами поставщику моделей ИИ (например, OpenAI, Anthropic, OpenRouter) для генерации ответов. У нас нет доступа к этим данным и мы их не храним, но поставщики ИИ могут обрабатывать их в соответствии со своими политиками конфиденциальности. +- **Промпты и запросы ИИ**: Когда вы используете функции на основе ИИ, ваши промпты и соответствующий контекст проекта отправляются выбранному вами поставщику моделей ИИ (например, OpenAI, Anthropic, OpenRouter) для генерации ответов. Мы не храним и не обрабатываем эти данные. У этих поставщиков ИИ есть свои политики конфиденциальности, и они могут хранить данные в соответствии со своими условиями предоставления услуг. +- **API-ключи и учетные данные**: Если вы вводите API-ключ (например, для подключения модели ИИ), он хранится локально на вашем устройстве и никогда не отправляется нам или третьим лицам, за исключением выбранного вами поставщика. +- **Телеметрия (данные об использовании)**: Мы собираем данные об использовании функций и ошибках только если вы явно дадите на это согласие. Эта телеметрия обеспечивается PostHog и помогает нам понять использование функций для улучшения Roo Code. Это включает ID вашей машины VS Code и шаблоны использования функций и отчеты об исключениях. Мы **не** собираем личную информацию, ваш код или промпты ИИ. + +### **Как мы используем ваши данные (если они собираются)** + +- Если вы соглашаетесь на телеметрию, мы используем ее для понимания использования функций и улучшения Roo Code. +- Мы **не** продаем и не передаем ваши данные. +- Мы **не** обучаем модели на ваших данных. + +### **Ваш выбор и контроль** + +- Вы можете запускать модели локально, чтобы предотвратить отправку данных третьим лицам. +- По умолчанию сбор телеметрии отключен, и если вы его включите, вы можете отказаться от него в любое время. +- Вы можете удалить Roo Code, чтобы прекратить весь сбор данных. + +### **Безопасность и обновления** + +Мы принимаем разумные меры для защиты ваших данных, но ни одна система не является на 100% безопасной. Если наша политика конфиденциальности изменится, мы уведомим вас об этом в расширении. + +### **Свяжитесь с нами** + +По любым вопросам, связанным с конфиденциальностью, обращайтесь к нам по адресу support@roocode.com. + +--- + +Используя Roo Code, вы соглашаетесь с этой Политикой конфиденциальности. diff --git a/locales/tr/PRIVACY.md b/locales/tr/PRIVACY.md new file mode 100644 index 0000000000..657d0463b0 --- /dev/null +++ b/locales/tr/PRIVACY.md @@ -0,0 +1,37 @@ +# Roo Code Gizlilik Politikası + +**Son Güncelleme: 7 Mart 2025** + +Roo Code gizliliğinize saygı duyar ve verilerinizi nasıl işlediğimiz konusunda şeffaflığa bağlıdır. Aşağıda, önemli verilerin nereye gittiğine - ve daha da önemlisi, nereye gitmediğine dair basit bir açıklama bulunmaktadır. + +### **Verileriniz Nereye Gider (Ve Nereye Gitmez)** + +- **Kod ve Dosyalar**: Roo Code, AI destekli özellikler için gerektiğinde yerel makinenizdeki dosyalara erişir. Roo Code'a komutlar gönderdiğinizde, ilgili dosyalar yanıt üretmek için seçtiğiniz AI model sağlayıcısına (örneğin, OpenAI, Anthropic, OpenRouter) iletilebilir. Bu verilere erişimimiz yoktur, ancak AI sağlayıcıları kendi gizlilik politikalarına göre bunları depolayabilir. +- **Komutlar**: Roo Code aracılığıyla yürütülen tüm komutlar yerel ortamınızda gerçekleşir. Ancak, AI destekli özellikleri kullandığınızda, ilgili kod ve komutlarınızın bağlamı yanıt üretmek için seçtiğiniz AI model sağlayıcısına (örneğin, OpenAI, Anthropic, OpenRouter) iletilebilir. Bu verilere erişimimiz yoktur ve bunları depolamayız, ancak AI sağlayıcıları kendi gizlilik politikalarına göre bunları işleyebilir. +- **Promptlar ve AI İstekleri**: AI destekli özellikleri kullandığınızda, promptlarınız ve projenin ilgili bağlamı yanıt üretmek için seçtiğiniz AI model sağlayıcısına (örneğin, OpenAI, Anthropic, OpenRouter) gönderilir. Bu verileri depolamaz veya işlemeyiz. Bu AI sağlayıcılarının kendi gizlilik politikaları vardır ve hizmet şartlarına göre verileri depolayabilirler. +- **API Anahtarları ve Kimlik Bilgileri**: Bir API anahtarı girerseniz (örneğin, bir AI modelini bağlamak için), bu anahtar cihazınızda yerel olarak depolanır ve seçtiğiniz sağlayıcı dışında bize veya üçüncü taraflara asla gönderilmez. +- **Telemetri (Kullanım Verileri)**: Özellik kullanımı ve hata verilerini yalnızca açıkça onay verdiğinizde toplarız. Bu telemetri PostHog tarafından sağlanır ve Roo Code'u geliştirmek için özellik kullanımını anlamamıza yardımcı olur. Bu, VS Code makine kimliğinizi ve özellik kullanım kalıplarını ve istisna raporlarını içerir. Kişisel olarak tanımlanabilir bilgileri, kodunuzu veya AI promptlarını **toplamayız**. + +### **Verilerinizi Nasıl Kullanırız (Eğer Toplanırsa)** + +- Telemetriye onay verirseniz, bunu özellik kullanımını anlamak ve Roo Code'u geliştirmek için kullanırız. +- Verilerinizi **satmaz** veya **paylaşmayız**. +- Verilerinizle herhangi bir model **eğitmeyiz**. + +### **Seçimleriniz ve Kontrolünüz** + +- Verilerin üçüncü taraflara gönderilmesini önlemek için modelleri yerel olarak çalıştırabilirsiniz. +- Varsayılan olarak, telemetri toplama kapalıdır ve açarsanız, istediğiniz zaman vazgeçebilirsiniz. +- Tüm veri toplamayı durdurmak için Roo Code'u silebilirsiniz. + +### **Güvenlik ve Güncellemeler** + +Verilerinizi korumak için makul önlemler alırız, ancak hiçbir sistem %100 güvenli değildir. Gizlilik politikamız değişirse, sizi uzantı içinde bilgilendireceğiz. + +### **Bize Ulaşın** + +Gizlilikle ilgili sorularınız için support@roocode.com adresinden bize ulaşabilirsiniz. + +--- + +Roo Code'u kullanarak bu Gizlilik Politikasını kabul etmiş olursunuz. diff --git a/locales/vi/PRIVACY.md b/locales/vi/PRIVACY.md new file mode 100644 index 0000000000..b03608cd32 --- /dev/null +++ b/locales/vi/PRIVACY.md @@ -0,0 +1,37 @@ +# Chính sách Bảo mật của Roo Code + +**Cập nhật lần cuối: 7 tháng 3, 2025** + +Roo Code tôn trọng quyền riêng tư của bạn và cam kết minh bạch về cách chúng tôi xử lý dữ liệu của bạn. Dưới đây là phần giải thích đơn giản về nơi dữ liệu quan trọng đi đến—và quan trọng hơn, nơi chúng không đi đến. + +### **Dữ liệu của bạn đi đến đâu (và không đi đến đâu)** + +- **Mã và Tệp tin**: Roo Code truy cập các tệp tin trên máy tính cục bộ của bạn khi cần thiết cho các tính năng hỗ trợ AI. Khi bạn gửi lệnh đến Roo Code, các tệp tin liên quan có thể được truyền đến nhà cung cấp mô hình AI mà bạn đã chọn (ví dụ: OpenAI, Anthropic, OpenRouter) để tạo phản hồi. Chúng tôi không có quyền truy cập vào dữ liệu này, nhưng các nhà cung cấp AI có thể lưu trữ chúng theo chính sách bảo mật của họ. +- **Lệnh**: Mọi lệnh thực thi thông qua Roo Code đều diễn ra trong môi trường cục bộ của bạn. Tuy nhiên, khi bạn sử dụng các tính năng dựa trên AI, mã liên quan và ngữ cảnh của lệnh của bạn có thể được truyền đến nhà cung cấp mô hình AI mà bạn đã chọn (ví dụ: OpenAI, Anthropic, OpenRouter) để tạo phản hồi. Chúng tôi không có quyền truy cập hoặc lưu trữ dữ liệu này, nhưng các nhà cung cấp AI có thể xử lý chúng theo chính sách bảo mật của họ. +- **Prompt và Yêu cầu AI**: Khi bạn sử dụng các tính năng dựa trên AI, prompt của bạn và ngữ cảnh dự án liên quan được gửi đến nhà cung cấp mô hình AI mà bạn đã chọn (ví dụ: OpenAI, Anthropic, OpenRouter) để tạo phản hồi. Chúng tôi không lưu trữ hoặc xử lý dữ liệu này. Các nhà cung cấp AI này có chính sách bảo mật riêng và có thể lưu trữ dữ liệu theo điều khoản dịch vụ của họ. +- **Khóa API và Thông tin Xác thực**: Nếu bạn nhập khóa API (ví dụ: để kết nối với mô hình AI), nó được lưu trữ cục bộ trên thiết bị của bạn và không bao giờ được gửi cho chúng tôi hoặc bên thứ ba, ngoại trừ nhà cung cấp mà bạn đã chọn. +- **Đo lường từ xa (Dữ liệu Sử dụng)**: Chúng tôi chỉ thu thập dữ liệu sử dụng tính năng và lỗi nếu bạn chọn tham gia một cách rõ ràng. Việc đo lường từ xa này được cung cấp bởi PostHog và giúp chúng tôi hiểu việc sử dụng tính năng để cải thiện Roo Code. Điều này bao gồm ID máy VS Code của bạn và mô hình sử dụng tính năng và báo cáo ngoại lệ. Chúng tôi **không** thu thập thông tin nhận dạng cá nhân, mã của bạn, hoặc prompt AI. + +### **Cách chúng tôi sử dụng dữ liệu của bạn (nếu được thu thập)** + +- Nếu bạn chọn tham gia đo lường từ xa, chúng tôi sử dụng nó để hiểu việc sử dụng tính năng và cải thiện Roo Code. +- Chúng tôi **không** bán hoặc chia sẻ dữ liệu của bạn. +- Chúng tôi **không** huấn luyện bất kỳ mô hình nào trên dữ liệu của bạn. + +### **Lựa chọn và Kiểm soát của bạn** + +- Bạn có thể chạy mô hình cục bộ để ngăn dữ liệu được gửi đến bên thứ ba. +- Theo mặc định, việc thu thập đo lường từ xa bị tắt và nếu bạn bật nó, bạn có thể từ chối bất kỳ lúc nào. +- Bạn có thể xóa Roo Code để dừng mọi việc thu thập dữ liệu. + +### **Bảo mật và Cập nhật** + +Chúng tôi thực hiện các biện pháp hợp lý để bảo vệ dữ liệu của bạn, nhưng không có hệ thống nào an toàn 100%. Nếu chính sách bảo mật của chúng tôi thay đổi, chúng tôi sẽ thông báo cho bạn trong tiện ích mở rộng. + +### **Liên hệ với chúng tôi** + +Đối với bất kỳ câu hỏi nào liên quan đến quyền riêng tư, hãy liên hệ với chúng tôi tại support@roocode.com. + +--- + +Bằng việc sử dụng Roo Code, bạn đồng ý với Chính sách Bảo mật này. diff --git a/locales/zh-CN/PRIVACY.md b/locales/zh-CN/PRIVACY.md new file mode 100644 index 0000000000..5aa45cd12e --- /dev/null +++ b/locales/zh-CN/PRIVACY.md @@ -0,0 +1,37 @@ +# Roo Code 隐私政策 + +**最后更新:2025年3月7日** + +Roo Code 尊重您的隐私,并致力于保持数据处理的透明度。以下是关于重要数据去向的简单说明——更重要的是,哪些数据不会被传输。 + +### **您的数据去向(及不会去向何处)** + +- **代码和文件**:Roo Code 在需要 AI 辅助功能时会访问您本地机器上的文件。当您向 Roo Code 发送命令时,相关文件可能会传输到您选择的 AI 模型提供商(如 OpenAI、Anthropic、OpenRouter)以生成响应。我们无法访问这些数据,但 AI 提供商可能会根据其隐私政策存储这些数据。 +- **命令**:通过 Roo Code 执行的所有命令都在您的本地环境中进行。但是,当您使用基于 AI 的功能时,相关代码和命令上下文可能会传输到您选择的 AI 模型提供商(如 OpenAI、Anthropic、OpenRouter)以生成响应。我们无法访问也不存储这些数据,但 AI 提供商可能会根据其隐私政策处理这些数据。 +- **提示词和 AI 请求**:当您使用基于 AI 的功能时,您的提示词和相关项目上下文会发送到您选择的 AI 模型提供商(如 OpenAI、Anthropic、OpenRouter)以生成响应。我们不存储或处理这些数据。这些 AI 提供商有自己的隐私政策,可能会根据其服务条款存储数据。 +- **API 密钥和凭证**:如果您输入 API 密钥(例如,用于连接 AI 模型),它会存储在您的设备本地,除了您选择的提供商外,永远不会发送给我们或任何第三方。 +- **遥测(使用数据)**:我们仅在您明确选择加入时才收集功能使用和错误数据。此遥测由 PostHog 提供支持,帮助我们了解功能使用情况以改进 Roo Code。这包括您的 VS Code 机器 ID 以及功能使用模式和异常报告。我们**不**收集个人身份信息、您的代码或 AI 提示词。 + +### **我们如何使用您的数据(如果收集)** + +- 如果您选择加入遥测,我们使用它来了解功能使用情况并改进 Roo Code。 +- 我们**不**出售或共享您的数据。 +- 我们**不**使用您的数据训练任何模型。 + +### **您的选择和控制** + +- 您可以在本地运行模型,以防止数据发送给第三方。 +- 默认情况下,遥测收集处于关闭状态,如果您开启它,可以随时选择退出。 +- 您可以删除 Roo Code 以停止所有数据收集。 + +### **安全性和更新** + +我们采取合理措施来保护您的数据,但没有任何系统是 100% 安全的。如果我们的隐私政策发生变化,我们会在扩展程序中通知您。 + +### **联系我们** + +如有任何隐私相关问题,请通过 support@roocode.com 联系我们。 + +--- + +使用 Roo Code 即表示您同意本隐私政策。 diff --git a/locales/zh-TW/PRIVACY.md b/locales/zh-TW/PRIVACY.md new file mode 100644 index 0000000000..792a03c77d --- /dev/null +++ b/locales/zh-TW/PRIVACY.md @@ -0,0 +1,37 @@ +# Roo Code 隱私權政策 + +**最後更新:2025年3月7日** + +Roo Code 尊重您的隱私,並致力於保持資料處理的透明度。以下是關於重要資料去向的簡單說明——更重要的是,哪些資料不會被傳輸。 + +### **您的資料去向(及不會去向何處)** + +- **程式碼和檔案**:Roo Code 在需要 AI 輔助功能時會存取您本機上的檔案。當您向 Roo Code 發送指令時,相關檔案可能會傳輸到您選擇的 AI 模型提供商(如 OpenAI、Anthropic、OpenRouter)以生成回應。我們無法存取這些資料,但 AI 提供商可能會根據其隱私權政策儲存這些資料。 +- **指令**:透過 Roo Code 執行的所有指令都在您的本機環境中進行。但是,當您使用基於 AI 的功能時,相關程式碼和指令上下文可能會傳輸到您選擇的 AI 模型提供商(如 OpenAI、Anthropic、OpenRouter)以生成回應。我們無法存取也不儲存這些資料,但 AI 提供商可能會根據其隱私權政策處理這些資料。 +- **提示詞和 AI 請求**:當您使用基於 AI 的功能時,您的提示詞和相關專案上下文會發送到您選擇的 AI 模型提供商(如 OpenAI、Anthropic、OpenRouter)以生成回應。我們不儲存或處理這些資料。這些 AI 提供商有自己的隱私權政策,可能會根據其服務條款儲存資料。 +- **API 金鑰和憑證**:如果您輸入 API 金鑰(例如,用於連接 AI 模型),它會儲存在您的裝置本機,除了您選擇的提供商外,永遠不會發送給我們或任何第三方。 +- **遙測(使用資料)**:我們僅在您明確選擇加入時才收集功能使用和錯誤資料。此遙測由 PostHog 提供支援,幫助我們了解功能使用情況以改進 Roo Code。這包括您的 VS Code 機器 ID 以及功能使用模式和異常報告。我們**不**收集個人身份資訊、您的程式碼或 AI 提示詞。 + +### **我們如何使用您的資料(如果收集)** + +- 如果您選擇加入遙測,我們使用它來了解功能使用情況並改進 Roo Code。 +- 我們**不**出售或分享您的資料。 +- 我們**不**使用您的資料訓練任何模型。 + +### **您的選擇和控制** + +- 您可以在本機執行模型,以防止資料發送給第三方。 +- 預設情況下,遙測收集處於關閉狀態,如果您開啟它,可以隨時選擇退出。 +- 您可以刪除 Roo Code 以停止所有資料收集。 + +### **安全性和更新** + +我們採取合理措施來保護您的資料,但沒有任何系統是 100% 安全的。如果我們的隱私權政策發生變化,我們會在擴充功能中通知您。 + +### **聯絡我們** + +如有任何隱私權相關問題,請透過 support@roocode.com 聯絡我們。 + +--- + +使用 Roo Code 即表示您同意本隱私權政策。 diff --git a/scripts/__tests__/manage-translations.test.ts b/scripts/__tests__/manage-translations.test.ts new file mode 100644 index 0000000000..54816b692e --- /dev/null +++ b/scripts/__tests__/manage-translations.test.ts @@ -0,0 +1,339 @@ +import * as fs from "fs" +import * as path from "path" +import * as os from "os" +// Mock the module +jest.mock("../manage-translations", () => { + const actualModule = jest.requireActual("../manage-translations") + return { + ...actualModule, + collectStdin: jest.fn(), + } +}) + +// Import after mocking +const manageTrans = require("../manage-translations") +const { getNestedValue, setNestedValue, deleteNestedValue, addTranslations, deleteTranslations, main } = manageTrans + +// Helper function to mock stdin with input data +function mockStdinWithData(inputData: string) { + const mockStdin: { + setEncoding: jest.Mock + on: jest.Mock + } = { + setEncoding: jest.fn(), + on: jest.fn().mockImplementation((event: string, callback: any) => { + if (event === "data") { + callback(inputData) + } + if (event === "end") { + callback() + } + return mockStdin + }), + } + Object.defineProperty(process, "stdin", { value: mockStdin }) +} + +describe("Translation Management", () => { + let testDir: string + let testFile: string + + beforeEach(async () => { + testDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "translations-test-")) + testFile = path.join(testDir, "test.json") + }) + + afterEach(async () => { + await fs.promises.rm(testDir, { recursive: true, force: true }) + }) + + describe("Nested Value Operations", () => { + test("handles nested paths and escaped dots", () => { + const obj = { + settings: { + "customStoragePath.description": "Storage path setting", + "vsCodeLmModelSelector.vendor.description": "Vendor setting", + nested: { key: "normal nested" }, + }, + } + + // Regular nested path + expect(getNestedValue(obj, "settings.nested.key")).toBe("normal nested") + + // Paths with dots + expect(getNestedValue(obj, "settings.customStoragePath..description")).toBe("Storage path setting") + expect(getNestedValue(obj, "settings.vsCodeLmModelSelector..vendor..description")).toBe("Vendor setting") + }) + + test("setNestedValue handles escaped dots", () => { + const obj = {} + + // Regular nested path + setNestedValue(obj, "settings.nested.key", "normal nested") + + // Paths with dots + setNestedValue(obj, "settings.customStoragePath..description", "Storage path setting") + setNestedValue(obj, "settings.vsCodeLmModelSelector..vendor..description", "Vendor setting") + + expect(obj).toEqual({ + settings: { + nested: { + key: "normal nested", + }, + "customStoragePath.description": "Storage path setting", + "vsCodeLmModelSelector.vendor.description": "Vendor setting", + }, + }) + }) + + test("deleteNestedValue handles escaped dots", () => { + const obj = { + settings: { + nested: { + key: "normal nested", + }, + "customStoragePath.description": "Storage path setting", + "vsCodeLmModelSelector.vendor.description": "Vendor setting", + }, + } + + // Delete regular nested path + expect(deleteNestedValue(obj, "settings.nested.key")).toBe(true) + + // Delete paths with dots + expect(deleteNestedValue(obj, "settings.customStoragePath..description")).toBe(true) + expect(deleteNestedValue(obj, "settings.vsCodeLmModelSelector..vendor..description")).toBe(true) + + expect(obj).toEqual({ + settings: { + nested: {}, + }, + }) + }) + }) + + describe("Translation Operations", () => { + test("addTranslations adds new translations", async () => { + const data = {} + const pairs: [string, string][] = [ + ["key1.nested", "value1"], + ["key2", "value2"], + ] + const modified = await addTranslations(data, pairs, testFile) + expect(modified).toBe(true) + expect(data).toEqual({ + key1: { nested: "value1" }, + key2: "value2", + }) + }) + + test("addTranslations skips existing translations", async () => { + const data = { key1: { nested: "existing" } } + const pairs: [string, string][] = [["key1.nested", "new value"]] + const modified = await addTranslations(data, pairs, testFile) + expect(modified).toBe(false) + expect(data).toEqual({ key1: { nested: "existing" } }) + }) + + test("deleteTranslations removes existing translations", async () => { + const data = { + key1: { nested: "value1" }, + key2: "value2", + } + const keys = ["key1.nested", "key2"] + const modified = await deleteTranslations(data, keys, testFile) + expect(modified).toBe(true) + expect(data).toEqual({ key1: {} }) + }) + + test("deleteTranslations handles non-existent keys", async () => { + const data = { key1: { nested: "value1" } } + const keys = ["key1.nonexistent", "key2"] + const modified = await deleteTranslations(data, keys, testFile) + expect(modified).toBe(false) + expect(data).toEqual({ key1: { nested: "value1" } }) + }) + }) + + describe("File Operations", () => { + test("addTranslations creates parent directories if needed", async () => { + const nestedFile = path.join(testDir, "nested", "test.json") + const data = {} + const pairs: [string, string][] = [["key", "value"]] + await addTranslations(data, pairs, nestedFile) + + const dirExists = await fs.promises + .access(path.dirname(nestedFile)) + .then(() => true) + .catch(() => false) + expect(dirExists).toBe(true) + }) + + test("addTranslations with verbose mode logs operations", async () => { + const consoleSpy = jest.spyOn(console, "log") + const data = {} + const pairs: [string, string][] = [["key", "value"]] + await addTranslations(data, pairs, testFile, true) + + expect(consoleSpy).toHaveBeenCalledWith("Created new key path: key") + expect(consoleSpy).toHaveBeenCalledWith(`Full path: ${testFile}`) + expect(consoleSpy).toHaveBeenCalledWith('Set value: "value"') + + consoleSpy.mockRestore() + }) + + test("deleteTranslations with verbose mode logs operations", async () => { + const consoleSpy = jest.spyOn(console, "log") + const data = { key: "value" } + const keys = ["key"] + await deleteTranslations(data, keys, testFile, true) + + expect(consoleSpy).toHaveBeenCalledWith("Deleted key: key") + expect(consoleSpy).toHaveBeenCalledWith(`From file: ${testFile}`) + + consoleSpy.mockRestore() + }) + }) + + describe("Main Function", () => { + beforeEach(() => { + // Reset process.argv before each test + process.argv = ["node", "script"] + }) + + describe("Stdin Operations", () => { + test("adds nested key paths via stdin", async () => { + const testFile = path.join(testDir, "test.json") + process.argv = ["node", "script", "--stdin", testFile] as any + mockStdinWithData('{"key.nested.path": "value1"}\n{"other.nested": "value2"}') + + await main() + + const data = JSON.parse(await fs.promises.readFile(testFile, "utf8")) + expect(data).toEqual({ + key: { + nested: { + path: "value1", + }, + }, + other: { + nested: "value2", + }, + }) + }) + + test("adds keys with dots using double-dot escaping via stdin", async () => { + const testFile = path.join(testDir, "test.json") + process.argv = ["node", "script", "--stdin", testFile] as any + mockStdinWithData('{"settings..path": "value1"}\n{"key..with..dots": "value2"}') + + await main() + + const data = JSON.parse(await fs.promises.readFile(testFile, "utf8")) + expect(data).toEqual({ + "settings.path": "value1", + "key.with.dots": "value2", + }) + }) + + test("deletes keys with dots using double-dot escaping via stdin", async () => { + const testFile = path.join(testDir, "test.json") + await fs.promises.writeFile( + testFile, + JSON.stringify({ + "settings.path": "value1", + "key.with.dots": "value2", + }), + ) + + process.argv = ["node", "script", "-d", "--stdin", testFile] as any + mockStdinWithData('["settings..path"]\n["key..with..dots"]') + + await main() + + const data = JSON.parse(await fs.promises.readFile(testFile, "utf8")) + expect(data).toEqual({}) + }) + + test("handles mixed nested paths and escaped dots via stdin", async () => { + const testFile = path.join(testDir, "test.json") + process.argv = ["node", "script", "--stdin", testFile] as any + mockStdinWithData('{"nested.key..with..dots": "value1"}\n' + '{"settings..path.sub.key": "value2"}') + + await main() + + const data = JSON.parse(await fs.promises.readFile(testFile, "utf8")) + expect(data).toEqual({ + nested: { + "key.with.dots": "value1", + }, + "settings.path": { + sub: { + key: "value2", + }, + }, + }) + }) + + test("deletes translations from multiple files via stdin", async () => { + mockStdinWithData('["key1"]\n["key2"]') + const testFile1 = path.join(testDir, "test1.json") + const testFile2 = path.join(testDir, "test2.json") + + // Create test files with initial content + await fs.promises.writeFile(testFile1, JSON.stringify({ key1: "value1", key2: "value2" })) + await fs.promises.writeFile(testFile2, JSON.stringify({ key1: "value1", key2: "value2" })) + + process.argv = ["node", "script", "-d", "--stdin", testFile1, testFile2] as any + + await main() + + const data1 = JSON.parse(await fs.promises.readFile(testFile1, "utf8")) + const data2 = JSON.parse(await fs.promises.readFile(testFile2, "utf8")) + + expect(data1).toEqual({}) + expect(data2).toEqual({}) + }) + }) + + test("main throws error for invalid JSON file", async () => { + const invalidJson = path.join(testDir, "invalid.json") + await fs.promises.writeFile(invalidJson, "invalid json content") + + process.argv = ["node", "script", invalidJson] + await expect(main()).rejects.toThrow("Invalid JSON in translation file") + }) + + test("main handles missing file in non-verbose mode", async () => { + const nonExistentFile = path.join(testDir, "nonexistent.json") + process.argv = ["node", "script", nonExistentFile, "key", "value"] + await main() + + const fileContent = await fs.promises.readFile(nonExistentFile, "utf8") + const data = JSON.parse(fileContent) + expect(data).toEqual({ key: "value" }) + }) + + test("main adds translations from command line", async () => { + const testFile = path.join(testDir, "test.json") + process.argv = ["node", "script", testFile, "key1", "value1", "key2", "value2"] + await main() + + const fileContent = await fs.promises.readFile(testFile, "utf8") + const data = JSON.parse(fileContent) + expect(data).toEqual({ key1: "value1", key2: "value2" }) + }) + + test("main deletes translations in delete mode", async () => { + const testFile = path.join(testDir, "test.json") + await fs.promises.writeFile(testFile, JSON.stringify({ key1: "value1", key2: "value2" })) + + process.argv = ["node", "script", "-d", testFile, "--", "key1"] + await main() + + const fileContent = await fs.promises.readFile(testFile, "utf8") + const data = JSON.parse(fileContent) + expect(data).toEqual({ key2: "value2" }) + }) + }) +}) diff --git a/scripts/find-missing-i18n-key.js b/scripts/find-missing-i18n-key.js deleted file mode 100644 index 87d160d490..0000000000 --- a/scripts/find-missing-i18n-key.js +++ /dev/null @@ -1,204 +0,0 @@ -const fs = require("fs") -const path = require("path") - -// Parse 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] - } - return acc -}, {}) - -// Display help information -if (args.help) { - console.log(` -Find missing i18n translations - -A useful script to identify whether the i18n keys used in component files exist in all language files. - -Usage: - node scripts/find-missing-i18n-key.js [options] - -Options: - --locale= Only check a specific language (e.g., --locale=de) - --file= Only check a specific file (e.g., --file=chat.json) - --help Display help information - -Output: - - Generate a report of missing translations - `) - process.exit(0) -} - -// Directories to traverse and their corresponding locales -const DIRS = { - components: { - path: path.join(__dirname, "../webview-ui/src/components"), - localesDir: path.join(__dirname, "../webview-ui/src/i18n/locales"), - }, - src: { - path: path.join(__dirname, "../src"), - localesDir: path.join(__dirname, "../src/i18n/locales"), - }, -} - -// Regular expressions to match i18n keys -const i18nPatterns = [ - /{t\("([^"]+)"\)}/g, // Match {t("key")} format - /i18nKey="([^"]+)"/g, // Match i18nKey="key" format - /t\("([a-zA-Z][a-zA-Z0-9_]*[:.][a-zA-Z0-9_.]+)"\)/g, // Match t("key") format, where key contains a colon or dot -] - -// Get all language directories for a specific locales directory -function getLocaleDirs(localesDir) { - try { - const allLocales = fs.readdirSync(localesDir).filter((file) => { - const stats = fs.statSync(path.join(localesDir, file)) - return stats.isDirectory() // Do not exclude any language directories - }) - - // Filter to a specific language if specified - return args.locale ? allLocales.filter((locale) => locale === args.locale) : allLocales - } catch (error) { - if (error.code === "ENOENT") { - console.warn(`Warning: Locales directory not found: ${localesDir}`) - return [] - } - throw error - } -} - -// Get the value from JSON by path -function getValueByPath(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 -} - -// Check if the key exists in all language files, return a list of missing language files -function checkKeyInLocales(key, localeDirs, localesDir) { - const [file, ...pathParts] = key.split(":") - const jsonPath = pathParts.join(".") - - const missingLocales = [] - - localeDirs.forEach((locale) => { - const filePath = path.join(localesDir, locale, `${file}.json`) - if (!fs.existsSync(filePath)) { - missingLocales.push(`${locale}/${file}.json`) - return - } - - const json = JSON.parse(fs.readFileSync(filePath, "utf8")) - if (getValueByPath(json, jsonPath) === undefined) { - missingLocales.push(`${locale}/${file}.json`) - } - }) - - return missingLocales -} - -// Recursively traverse the directory -function findMissingI18nKeys() { - const results = [] - - function walk(dir, baseDir, localeDirs, localesDir) { - 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, localeDirs, localesDir) // Recursively traverse subdirectories - } else if (stat.isFile() && [".ts", ".tsx", ".js", ".jsx"].includes(path.extname(filePath))) { - const content = fs.readFileSync(filePath, "utf8") - - // Match all i18n keys - for (const pattern of i18nPatterns) { - let match - while ((match = pattern.exec(content)) !== null) { - const key = match[1] - const missingLocales = checkKeyInLocales(key, localeDirs, localesDir) - if (missingLocales.length > 0) { - results.push({ - key, - missingLocales, - file: path.relative(baseDir, filePath), - }) - } - } - } - } - } - } - - // Walk through all directories - Object.entries(DIRS).forEach(([name, config]) => { - const localeDirs = getLocaleDirs(config.localesDir) - if (localeDirs.length > 0) { - console.log(`\nChecking ${name} directory with ${localeDirs.length} languages: ${localeDirs.join(", ")}`) - walk(config.path, config.path, localeDirs, config.localesDir) - } - }) - - return results -} - -// Execute and output the results -function main() { - try { - if (args.locale) { - // Check if the specified locale exists in any of the locales directories - const localeExists = Object.values(DIRS).some((config) => { - const localeDirs = getLocaleDirs(config.localesDir) - return localeDirs.includes(args.locale) - }) - - if (!localeExists) { - console.error(`Error: Language '${args.locale}' not found in any locales directory`) - process.exit(1) - } - } - - const missingKeys = findMissingI18nKeys() - - if (missingKeys.length === 0) { - console.log("\n✅ All i18n keys are present!") - return - } - - console.log("\nMissing i18n keys:\n") - missingKeys.forEach(({ key, missingLocales, file }) => { - console.log(`File: ${file}`) - console.log(`Key: ${key}`) - console.log("Missing in:") - missingLocales.forEach((file) => console.log(` - ${file}`)) - console.log("-------------------") - }) - - // Exit code 1 indicates missing keys - process.exit(1) - } catch (error) { - console.error("Error:", error.message) - console.error(error.stack) - process.exit(1) - } -} - -main() diff --git a/scripts/find-missing-translations.js b/scripts/find-missing-translations.js deleted file mode 100755 index 9277d935ba..0000000000 --- a/scripts/find-missing-translations.js +++ /dev/null @@ -1,279 +0,0 @@ -/** - * Script to find missing translations in locale files - * - * Usage: - * node scripts/find-missing-translations.js [options] - * - * Options: - * --locale= Only check a specific locale (e.g. --locale=fr) - * --file= Only check a specific file (e.g. --file=chat.json) - * --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" }, -) // Default to checking both 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= Only check a specific locale (e.g. --locale=fr) - --file= Only check a specific file (e.g. --file=chat.json) - --area= Only check a specific area (core, webview, or both) - 'core' = Backend (src/i18n/locales) - 'webview' = Frontend UI (webview-ui/src/i18n/locales) - 'both' = Check both 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 === "both" ? ["core", "webview"] : [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 -} - -// Function to check translations for a specific area -function checkAreaTranslations(area) { - const LOCALES_DIR = LOCALES_DIRS[area] - - // Get all locale directories (or filter to the specified locale) - const allLocales = fs.readdirSync(LOCALES_DIR).filter((item) => { - const stats = fs.statSync(path.join(LOCALES_DIR, item)) - return stats.isDirectory() && item !== "en" // Exclude English as it's our source - }) - - // Filter to the specified locale if provided - 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 === "core" ? "BACKEND" : "FRONTEND"} - Checking ${locales.length} non-English locale(s): ${locales.join(", ")}`, - ) - - // Get all English JSON files - const englishDir = path.join(LOCALES_DIR, "en") - let englishFiles = fs.readdirSync(englishDir).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 - let englishFileContents - - try { - englishFileContents = englishFiles.map((file) => ({ - name: file, - content: JSON.parse(fs.readFileSync(path.join(englishDir, file), "utf8")), - })) - } catch (e) { - console.error(`Error: File '${englishDir}' is not a valid JSON file`) - process.exit(1) - } - - console.log( - `Checking ${englishFileContents.length} translation file(s): ${englishFileContents.map((f) => f.name).join(", ")}`, - ) - - // Results object to store missing translations - const missingTranslations = {} - - // For each locale, check for missing translations - for (const locale of locales) { - missingTranslations[locale] = {} - - for (const { name, content: englishContent } of englishFileContents) { - const localeFilePath = path.join(LOCALES_DIR, locale, name) - - // Check if the file exists in the locale - if (!fs.existsSync(localeFilePath)) { - missingTranslations[locale][name] = { file: "File is missing entirely" } - continue - } - - // Load the locale file - let localeContent - - try { - localeContent = JSON.parse(fs.readFileSync(localeFilePath, "utf8")) - } catch (e) { - console.error(`Error: File '${localeFilePath}' is not a valid JSON file`) - process.exit(1) - } - - // Find all keys in the English file - const englishKeys = findKeys(englishContent) - - // 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 -} - -// Main function to find missing translations -function findMissingTranslations() { - try { - console.log("Starting translation check...") - - let anyAreaMissingTranslations = false - - // Check each requested area - for (const area of areasToCheck) { - const { hasMissingTranslations } = 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() diff --git a/scripts/manage-translations.d.ts b/scripts/manage-translations.d.ts new file mode 100644 index 0000000000..600b588867 --- /dev/null +++ b/scripts/manage-translations.d.ts @@ -0,0 +1,13 @@ +export function getNestedValue(obj: any, keyPath: string): any +export function setNestedValue(obj: any, keyPath: string, value: any): void +export function deleteNestedValue(obj: any, keyPath: string): boolean +export function collectStdin(): Promise +export function parseInputLines(inputText: string): [string, string][] | [string][] +export function processStdin(): Promise<[string, string][]> +export function addTranslations( + data: any, + pairs: [string, string][], + filePath: string, + verbose?: boolean, +): Promise +export function deleteTranslations(data: any, keys: string[], filePath: string, verbose?: boolean): Promise diff --git a/scripts/manage-translations.js b/scripts/manage-translations.js new file mode 100755 index 0000000000..54c3ad6c8c --- /dev/null +++ b/scripts/manage-translations.js @@ -0,0 +1,395 @@ +#!/usr/bin/env node + +const fs = require("fs") +const path = require("path") + +// Export functions for testing +module.exports = { + getNestedValue, + setNestedValue, + deleteNestedValue, + processStdin, + addTranslations, + deleteTranslations, +} + +function splitPath(keyPath) { + // First replace double dots with a placeholder + const placeholder = "\u0000" + const escaped = keyPath.replace(/\.\./g, placeholder) + + // Split on single dots + const parts = escaped.split(".") + + // Restore dots in each part + return parts.map((part) => part.replace(new RegExp(placeholder, "g"), ".")) +} + +function unescapeKey(key) { + return key.replace(/\.\./g, ".") +} + +function getNestedValue(obj, keyPath) { + // Try nested path first + const nestedValue = splitPath(keyPath).reduce((current, key) => { + return current && typeof current === "object" ? current[unescapeKey(key)] : undefined + }, obj) + + // If nested path doesn't exist, check for exact key match + if (nestedValue === undefined && keyPath in obj) { + return obj[keyPath] + } + + return nestedValue +} + +function setNestedValue(obj, keyPath, value) { + const keys = splitPath(keyPath) + const lastKey = keys.pop() + const target = keys.reduce((current, key) => { + const unescapedKey = unescapeKey(key) + if (!(unescapedKey in current)) { + current[unescapedKey] = {} + } + return current[unescapedKey] + }, obj) + target[unescapeKey(lastKey)] = value +} + +function deleteNestedValue(obj, keyPath) { + // First check if the exact key exists + if (keyPath in obj) { + delete obj[keyPath] + return true + } + + // Then try nested path + const keys = splitPath(keyPath) + const lastKey = keys.pop() + const target = keys.reduce((current, key) => { + const unescapedKey = unescapeKey(key) + return current && typeof current === "object" ? current[unescapedKey] : undefined + }, obj) + + if (target && typeof target === "object") { + const unescapedLastKey = unescapeKey(lastKey) + if (unescapedLastKey in target) { + delete target[unescapedLastKey] + return true + } + } + return false +} + +async function collectStdin() { + return new Promise((resolve, reject) => { + let buffer = "" + process.stdin.setEncoding("utf8") + + process.stdin.on("data", (chunk) => { + buffer += chunk + }) + + process.stdin.on("end", () => { + resolve(buffer) + }) + + process.stdin.on("error", reject) + }) +} + +function parseInputLines(inputText) { + const pairs = [] + const lines = inputText.split("\n") + + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed) continue + + try { + const data = JSON.parse(trimmed) + if (Array.isArray(data)) { + // In delete mode, allow multiple elements in array + data.forEach((key) => pairs.push([key])) + } else if (typeof data === "object" && data !== null) { + const entries = Object.entries(data) + if (entries.length !== 1) { + throw new Error("Each line must contain a single key-value pair") + } + pairs.push(entries[0]) + } else { + throw new Error("Each line must be a JSON object or array") + } + } catch (err) { + if (err.message.startsWith("Each line must")) { + throw err + } + throw new Error(`Invalid JSON on line: ${trimmed}`) + } + } + + return pairs +} + +async function processStdin() { + const inputText = await collectStdin() + return parseInputLines(inputText) +} + +async function addTranslations(data, pairs, filePath, verbose = false) { + let modified = false + + // Create parent directories if they don't exist + const directory = path.dirname(filePath) + await fs.promises.mkdir(directory, { recursive: true }) + + for (const [keyPath, value] of pairs) { + const currentValue = getNestedValue(data, keyPath) + if (currentValue === undefined) { + setNestedValue(data, keyPath, value) + if (verbose) { + console.log(`Created new key path: ${keyPath}`) + console.log(`Full path: ${filePath}`) + console.log(`Set value: "${value}"`) + } + modified = true + } else if (verbose) { + console.log(`Key exists: ${keyPath}`) + console.log(`Full path: ${filePath}`) + console.log(`Current value: "${currentValue}"`) + } + } + return modified +} + +async function deleteTranslations(data, keys, filePath, verbose = false) { + let modified = false + for (const keyPath of keys) { + if (deleteNestedValue(data, keyPath)) { + if (verbose) { + console.log(`Deleted key: ${keyPath}`) + console.log(`From file: ${filePath}`) + } + modified = true + } else if (verbose) { + console.log(`Key not found: ${keyPath}`) + console.log(`In file: ${filePath}`) + } + } + return modified +} + +async function main() { + const args = process.argv.slice(2) + const verbose = args.includes("-v") + const deleteMode = args.includes("-d") + const stdinMode = args.includes("--stdin") + + if (verbose) args.splice(args.indexOf("-v"), 1) + if (deleteMode) args.splice(args.indexOf("-d"), 1) + if (stdinMode) args.splice(args.indexOf("--stdin"), 1) + + if (args.length < 1) { + console.log("Usage:") + console.log("Command Line Mode:") + console.log(" Add/update translations:") + console.log(" node scripts/manage-translations.js [-v] TRANSLATION_FILE KEY_PATH VALUE [KEY_PATH VALUE...]") + console.log(" Delete translations:") + console.log( + " node scripts/manage-translations.js [-v] -d TRANSLATION_FILE1 [TRANSLATION_FILE2 ...] [ -- KEY1 ...]", + ) + console.log("") + console.log("Key Path Format:") + console.log(" - Use single dot (.) for nested paths: 'command.newTask.title'") + console.log(" - Use double dots (..) to include a literal dot in key names (like SMTP byte stuffing):") + console.log(" 'settings..path' -> { 'settings.path': 'value' }") + console.log("") + console.log(" Examples:") + console.log(" 'command.newTask.title' -> { command: { newTask: { title: 'value' } } }") + console.log(" 'settings..path' -> { 'settings.path': 'value' }") + console.log(" 'nested.key..with..dots' -> { nested: { 'key.with.dots': 'value' } }") + console.log("") + console.log("Line-by-Line JSON Mode (--stdin):") + console.log(" Each line must be a complete, single JSON object/array") + console.log(" Multi-line or combined JSON is not supported") + console.log("") + console.log(" Add/update translations:") + console.log(" node scripts/manage-translations.js [-v] --stdin TRANSLATION_FILE") + console.log(" Format: One object per line with exactly one key-value pair:") + console.log(' {"command.newTask.title": "New Task"}') + console.log(' {"settings..path": "Custom Path"}') + console.log(' {"nested.key..with..dots": "Value with dots in key"}') + console.log("") + console.log(" Delete translations:") + console.log(" node scripts/manage-translations.js [-v] -d --stdin TRANSLATION_FILE") + console.log(" Format: One array per line with exactly one key:") + console.log(' ["command.newTask.title"]') + console.log(' ["settings..path"]') + console.log(' ["nested.key..with..dots"]') + console.log("") + console.log("Options:") + console.log(" -v Enable verbose output (shows operations)") + console.log(" -d Delete mode - remove keys instead of setting them") + console.log(" --stdin Read line-by-line JSON from stdin") + console.log("") + console.log("Examples:") + console.log(" # Add via command line:") + console.log(' node scripts/manage-translations.js package.nls.json command.newTask.title "New Task"') + console.log(' node scripts/manage-translations.js package.nls.json settings..path "Custom Path"') + console.log(' node scripts/manage-translations.js package.nls.json nested.key..with..dots "Value with dots"') + console.log("") + console.log(" # Add multiple translations (one JSON object per line):") + console.log(" translations.txt:") + console.log(' {"command.newTask.title": "New Task"}') + console.log(' {"settings..path": "Custom Path"}') + console.log(" node scripts/manage-translations.js --stdin package.nls.json < translations.txt") + console.log("") + console.log(" # Delete multiple keys (one JSON array per line):") + console.log(" delete_keys.txt:") + console.log(' ["command.newTask.title"]') + console.log(' ["settings..path"]') + console.log(' ["nested.key..with..dots"]') + console.log(" node scripts/manage-translations.js -d --stdin package.nls.json < delete_keys.txt") + console.log("") + console.log(" # Using here document for batching:") + console.log(" node scripts/manage-translations.js --stdin package.nls.json << EOF") + console.log(' {"command.newTask.title": "New Task"}') + console.log(' {"settings..path": "Custom Path"}') + console.log(" EOF") + console.log("") + console.log(" # Delete using here document:") + console.log(" node scripts/manage-translations.js -d --stdin package.nls.json << EOF") + console.log(' ["command.newTask.title"]') + console.log(' ["settings..path"]') + console.log(' ["nested.key..with..dots"]') + console.log(" EOF") + process.exit(1) + } + + let modified = false + + try { + if (stdinMode && deleteMode) { + const files = args + // Check if all files exist first + for (const filePath of files) { + try { + await fs.promises.access(filePath) + } catch (err) { + if (err.code === "ENOENT") { + throw new Error(`File not found: ${filePath}`) + } + throw err + } + } + + const input = await processStdin() + const keys = input.map(([key]) => key) + + // Process each file + for (const filePath of files) { + const data = JSON.parse(await fs.promises.readFile(filePath, "utf8")) + if (await deleteTranslations(data, keys, filePath, verbose)) { + await fs.promises.writeFile(filePath, JSON.stringify(data, null, "\t") + "\n") + modified = true + } + } + return + } else if (deleteMode) { + const separatorIndex = args.indexOf("--") + const files = separatorIndex === -1 ? args : args.slice(0, separatorIndex) + const keys = separatorIndex === -1 ? [] : args.slice(separatorIndex + 1) + + // Check if all files exist first + for (const filePath of files) { + try { + await fs.promises.access(filePath) + } catch (err) { + if (err.code === "ENOENT") { + throw new Error(`File not found: ${filePath}`) + } + throw err + } + } + + // Process each file + for (const filePath of files) { + const data = JSON.parse(await fs.promises.readFile(filePath, "utf8")) + if (await deleteTranslations(data, keys, filePath, verbose)) { + await fs.promises.writeFile(filePath, JSON.stringify(data, null, "\t") + "\n") + modified = true + } + } + return + } + + // Original non-delete mode code + const filePath = args[0] + let data = {} + try { + data = JSON.parse(await fs.promises.readFile(filePath, "utf8")) + } catch (err) { + if (err.code === "ENOENT") { + if (verbose) { + console.log(`File not found: ${filePath}`) + console.log("Creating new file") + } + const directory = path.dirname(filePath) + await fs.promises.mkdir(directory, { recursive: true }) + } else { + throw err + } + } + + if (stdinMode) { + const pairs = await processStdin() + modified = await addTranslations(data, pairs, filePath, verbose) + } else if (args.length >= 3 && args.length % 2 === 1) { + // Process key-value pairs from command line + const pairs = [] + for (let i = 1; i < args.length; i += 2) { + pairs.push([args[i], args[i + 1]]) + } + modified = await addTranslations(data, pairs, filePath, verbose) + } else { + console.log("Invalid number of arguments") + process.exit(1) + } + + // Write back if modified + if (modified) { + await fs.promises.writeFile(filePath, JSON.stringify(data, null, "\t") + "\n") + if (verbose) { + console.log("File updated successfully") + } + } + } catch (err) { + if (err instanceof SyntaxError) { + throw new Error("Invalid JSON in translation file") + } else if (err.code !== "ENOENT") { + // ENOENT is handled above + throw err + } + } +} + +// Only run main when called directly +if (require.main === module) { + main().catch((err) => { + console.error("Error:", err.message) + process.exit(1) + }) +} + +// Export functions for testing +module.exports = { + getNestedValue, + setNestedValue, + deleteNestedValue, + parseInputLines, + collectStdin, + processStdin, + addTranslations, + deleteTranslations, + main, +} diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 2bd61dfd93..633b90ec3d 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -89,11 +89,5 @@ "path_placeholder": "D:\\RooCodeStorage", "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" - } } } diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 5eb6b05e8f..ceda64bad7 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -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" - } } } diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index beea1ba3a9..2bfb43055a 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -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" - } } } diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index c34d0105a4..7399432c6a 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -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" - } } } diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 07438c873b..bf5421eff8 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -89,11 +89,5 @@ "input": { "task_prompt": "Roo को क्या करना है?", "task_placeholder": "अपना कार्य यहाँ लिखें" - }, - "settings": { - "providers": { - "groqApiKey": "ग्रोक एपीआई कुंजी", - "getGroqApiKey": "ग्रोक एपीआई कुंजी प्राप्त करें" - } } } diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 476285169f..69e2c2123f 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -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" - } } } diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index f44469d0c9..6f40c8e03d 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -89,11 +89,5 @@ "input": { "task_prompt": "Rooにどんなことをさせますか?", "task_placeholder": "タスクをここに入力してください" - }, - "settings": { - "providers": { - "groqApiKey": "Groq APIキー", - "getGroqApiKey": "Groq APIキーを取得" - } } } diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 944d9ba19b..9026315da2 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -89,11 +89,5 @@ "input": { "task_prompt": "Roo에게 무엇을 시킬까요?", "task_placeholder": "여기에 작업을 입력하세요" - }, - "settings": { - "providers": { - "groqApiKey": "Groq API 키", - "getGroqApiKey": "Groq API 키 받기" - } } } diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 46ed243b49..49f51cefff 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -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" - } } } diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index a6588b2fda..80112a91ab 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -89,11 +89,5 @@ "path_placeholder": "D:\\RooCodeStorage", "enter_absolute_path": "Por favor, digite um caminho absoluto (ex: D:\\RooCodeStorage ou /home/user/storage)", "enter_valid_path": "Por favor, digite um caminho válido" - }, - "settings": { - "providers": { - "groqApiKey": "Chave de API Groq", - "getGroqApiKey": "Obter chave de API Groq" - } } } diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index baef76ea78..80829e138c 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -89,11 +89,5 @@ "input": { "task_prompt": "Что должен сделать Roo?", "task_placeholder": "Введите вашу задачу здесь" - }, - "settings": { - "providers": { - "groqApiKey": "Ключ API Groq", - "getGroqApiKey": "Получить ключ API Groq" - } } } diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 2c4ec8b354..61b8e12fb5 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -89,11 +89,5 @@ "input": { "task_prompt": "Roo ne yapsın?", "task_placeholder": "Görevini buraya yaz" - }, - "settings": { - "providers": { - "groqApiKey": "Groq API Anahtarı", - "getGroqApiKey": "Groq API Anahtarı Al" - } } } diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 499309df75..8945e9e098 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -89,11 +89,5 @@ "input": { "task_prompt": "Bạn muốn Roo làm gì?", "task_placeholder": "Nhập nhiệm vụ của bạn ở đây" - }, - "settings": { - "providers": { - "groqApiKey": "Khóa API Groq", - "getGroqApiKey": "Lấy khóa API Groq" - } } } diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index ac3754ccd6..2fc49c9b37 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -89,11 +89,5 @@ "input": { "task_prompt": "让Roo做什么?", "task_placeholder": "在这里输入任务" - }, - "settings": { - "providers": { - "groqApiKey": "Groq API 密钥", - "getGroqApiKey": "获取 Groq API 密钥" - } } } diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 93c59acbbb..a51cfa0e9a 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -89,11 +89,5 @@ "input": { "task_prompt": "讓 Roo 做什麼?", "task_placeholder": "在這裡輸入工作" - }, - "settings": { - "providers": { - "groqApiKey": "Groq API 金鑰", - "getGroqApiKey": "取得 Groq API 金鑰" - } } } diff --git a/webview-ui/src/i18n/locales/ca/prompts.json b/webview-ui/src/i18n/locales/ca/prompts.json index 01f34f3398..5392816020 100644 --- a/webview-ui/src/i18n/locales/ca/prompts.json +++ b/webview-ui/src/i18n/locales/ca/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "Editar modes globals", "editProjectModes": "Editar modes de projecte (.roomodes)", "createModeHelpText": "Feu clic a + per crear un nou mode personalitzat, o simplement demaneu a Roo al xat que en creï un per a vostè!", - "selectMode": "Cerqueu modes" + "selectMode": "Cerqueu modes", + "noMatchFound": "No s'han trobat modes coincidents" }, "apiConfiguration": { "title": "Configuració d'API", diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index c9e1cb5606..4935dbaeef 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -458,7 +458,6 @@ }, "footer": { "feedback": "Si teniu qualsevol pregunta o comentari, no dubteu a obrir un issue a github.com/RooVetGit/Roo-Code o unir-vos a reddit.com/r/RooCode o discord.gg/roocode", - "version": "Roo Code v{{version}}", "telemetry": { "label": "Permetre informes anònims d'errors i ús", "description": "Ajudeu a millorar Roo Code enviant dades d'ús anònimes i informes d'errors. Mai s'envia codi, prompts o informació personal. Vegeu la nostra política de privacitat per a més detalls." diff --git a/webview-ui/src/i18n/locales/de/prompts.json b/webview-ui/src/i18n/locales/de/prompts.json index 98280a5e83..6043ada683 100644 --- a/webview-ui/src/i18n/locales/de/prompts.json +++ b/webview-ui/src/i18n/locales/de/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "Globale Modi bearbeiten", "editProjectModes": "Projektmodi bearbeiten (.roomodes)", "createModeHelpText": "Klicke auf +, um einen neuen benutzerdefinierten Modus zu erstellen, oder bitte Roo einfach im Chat, einen für dich zu erstellen!", - "selectMode": "Modi suchen" + "selectMode": "Modi suchen", + "noMatchFound": "Keine passenden Modi gefunden" }, "apiConfiguration": { "title": "API-Konfiguration", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index aac308c4a1..20f9344db7 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -458,7 +458,6 @@ }, "footer": { "feedback": "Wenn du Fragen oder Feedback hast, kannst du gerne ein Issue auf github.com/RooVetGit/Roo-Code öffnen oder reddit.com/r/RooCode oder discord.gg/roocode beitreten", - "version": "Roo Code v{{version}}", "telemetry": { "label": "Anonyme Fehler- und Nutzungsberichte zulassen", "description": "Helfen Sie, Roo Code zu verbessern, indem Sie anonyme Nutzungsdaten und Fehlerberichte senden. Es werden niemals Code, Prompts oder persönliche Informationen gesendet. Weitere Details finden Sie in unserer Datenschutzrichtlinie." diff --git a/webview-ui/src/i18n/locales/en/prompts.json b/webview-ui/src/i18n/locales/en/prompts.json index c1d2a671fb..c7fbbea56f 100644 --- a/webview-ui/src/i18n/locales/en/prompts.json +++ b/webview-ui/src/i18n/locales/en/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "Edit Global Modes", "editProjectModes": "Edit Project Modes (.roomodes)", "createModeHelpText": "Hit the + to create a new custom mode, or just ask Roo in chat to create one for you!", - "selectMode": "Search modes" + "selectMode": "Search modes", + "noMatchFound": "No matching modes found" }, "apiConfiguration": { "title": "API Configuration", diff --git a/webview-ui/src/i18n/locales/es/prompts.json b/webview-ui/src/i18n/locales/es/prompts.json index baf7246f25..9616c29c8e 100644 --- a/webview-ui/src/i18n/locales/es/prompts.json +++ b/webview-ui/src/i18n/locales/es/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "Editar modos globales", "editProjectModes": "Editar modos del proyecto (.roomodes)", "createModeHelpText": "¡Haz clic en + para crear un nuevo modo personalizado, o simplemente pídele a Roo en el chat que te cree uno!", - "selectMode": "Buscar modos" + "selectMode": "Buscar modos", + "noMatchFound": "No se encontraron modos coincidentes" }, "apiConfiguration": { "title": "Configuración de API", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index f862f13405..cbaa7b2686 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -458,7 +458,6 @@ }, "footer": { "feedback": "Si tiene alguna pregunta o comentario, no dude en abrir un issue en github.com/RooVetGit/Roo-Code o unirse a reddit.com/r/RooCode o discord.gg/roocode", - "version": "Roo Code v{{version}}", "telemetry": { "label": "Permitir informes anónimos de errores y uso", "description": "Ayude a mejorar Roo Code enviando datos de uso anónimos e informes de errores. Nunca se envía código, prompts o información personal. Consulte nuestra política de privacidad para más detalles." diff --git a/webview-ui/src/i18n/locales/fr/prompts.json b/webview-ui/src/i18n/locales/fr/prompts.json index 2a8e410cd5..825df19d2e 100644 --- a/webview-ui/src/i18n/locales/fr/prompts.json +++ b/webview-ui/src/i18n/locales/fr/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "Modifier les modes globaux", "editProjectModes": "Modifier les modes du projet (.roomodes)", "createModeHelpText": "Cliquez sur + pour créer un nouveau mode personnalisé, ou demandez simplement à Roo dans le chat de vous en créer un !", - "selectMode": "Rechercher les modes" + "selectMode": "Rechercher les modes", + "noMatchFound": "Aucun mode correspondant trouvé" }, "apiConfiguration": { "title": "Configuration API", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 9f694c5aa3..5b28473fab 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -458,7 +458,6 @@ }, "footer": { "feedback": "Si vous avez des questions ou des commentaires, n'hésitez pas à ouvrir un problème sur github.com/RooVetGit/Roo-Code ou à rejoindre reddit.com/r/RooCode ou discord.gg/roocode", - "version": "Roo Code v{{version}}", "telemetry": { "label": "Autoriser les rapports anonymes d'erreurs et d'utilisation", "description": "Aidez à améliorer Roo Code en envoyant des données d'utilisation anonymes et des rapports d'erreurs. Aucun code, prompt ou information personnelle n'est jamais envoyé. Consultez notre politique de confidentialité pour plus de détails." diff --git a/webview-ui/src/i18n/locales/hi/prompts.json b/webview-ui/src/i18n/locales/hi/prompts.json index de31c41356..e8f8360909 100644 --- a/webview-ui/src/i18n/locales/hi/prompts.json +++ b/webview-ui/src/i18n/locales/hi/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "ग्लोबल मोड्स संपादित करें", "editProjectModes": "प्रोजेक्ट मोड्स संपादित करें (.roomodes)", "createModeHelpText": "नया कस्टम मोड बनाने के लिए + पर क्लिक करें, या बस चैट में Roo से आपके लिए एक बनाने को कहें!", - "selectMode": "मोड खोजें" + "selectMode": "मोड खोजें", + "noMatchFound": "कोई मिलान करने वाला मोड नहीं मिला" }, "apiConfiguration": { "title": "API कॉन्फ़िगरेशन", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 0b21ed906b..38d726fd9c 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -458,7 +458,6 @@ }, "footer": { "feedback": "यदि आपके कोई प्रश्न या प्रतिक्रिया है, तो github.com/RooVetGit/Roo-Code पर एक मुद्दा खोलने या reddit.com/r/RooCode या discord.gg/roocode में शामिल होने में संकोच न करें", - "version": "Roo Code v{{version}}", "telemetry": { "label": "गुमनाम त्रुटि और उपयोग रिपोर्टिंग की अनुमति दें", "description": "गुमनाम उपयोग डेटा और त्रुटि रिपोर्ट भेजकर Roo Code को बेहतर बनाने में मदद करें। कोड, प्रॉम्प्ट, या व्यक्तिगत जानकारी कभी भी नहीं भेजी जाती है। अधिक विवरण के लिए हमारी गोपनीयता नीति देखें।" diff --git a/webview-ui/src/i18n/locales/it/prompts.json b/webview-ui/src/i18n/locales/it/prompts.json index 1146f7012f..31159555c1 100644 --- a/webview-ui/src/i18n/locales/it/prompts.json +++ b/webview-ui/src/i18n/locales/it/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "Modifica modalità globali", "editProjectModes": "Modifica modalità di progetto (.roomodes)", "createModeHelpText": "Clicca sul + per creare una nuova modalità personalizzata, o chiedi semplicemente a Roo nella chat di crearne una per te!", - "selectMode": "Cerca modalità" + "selectMode": "Cerca modalità", + "noMatchFound": "Nessuna modalità corrispondente trovata" }, "apiConfiguration": { "title": "Configurazione API", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index a3e210c480..5f7a2d786d 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -458,7 +458,6 @@ }, "footer": { "feedback": "Se hai domande o feedback, sentiti libero di aprire un issue su github.com/RooVetGit/Roo-Code o unirti a reddit.com/r/RooCode o discord.gg/roocode", - "version": "Roo Code v{{version}}", "telemetry": { "label": "Consenti segnalazioni anonime di errori e utilizzo", "description": "Aiuta a migliorare Roo Code inviando dati di utilizzo anonimi e segnalazioni di errori. Non vengono mai inviati codice, prompt o informazioni personali. Consulta la nostra politica sulla privacy per maggiori dettagli." diff --git a/webview-ui/src/i18n/locales/ja/prompts.json b/webview-ui/src/i18n/locales/ja/prompts.json index 82f2bae20b..836e9980f4 100644 --- a/webview-ui/src/i18n/locales/ja/prompts.json +++ b/webview-ui/src/i18n/locales/ja/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "グローバルモードを編集", "editProjectModes": "プロジェクトモードを編集 (.roomodes)", "createModeHelpText": "+ をクリックして新しいカスタムモードを作成するか、チャットで Roo に作成を依頼してください!", - "selectMode": "モードを検索" + "selectMode": "モードを検索", + "noMatchFound": "一致するモードが見つかりません" }, "apiConfiguration": { "title": "API設定", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 1f1ec12c54..a44448a222 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -458,7 +458,6 @@ }, "footer": { "feedback": "質問やフィードバックがある場合は、github.com/RooVetGit/Roo-Codeで問題を開くか、reddit.com/r/RooCodediscord.gg/roocodeに参加してください", - "version": "Roo Code v{{version}}", "telemetry": { "label": "匿名のエラーと使用状況レポートを許可", "description": "匿名の使用データとエラーレポートを送信してRoo Codeの改善にご協力ください。コード、プロンプト、個人情報が送信されることはありません。詳細については、プライバシーポリシーをご覧ください。" diff --git a/webview-ui/src/i18n/locales/ko/prompts.json b/webview-ui/src/i18n/locales/ko/prompts.json index 04be16e560..d6288c1fbc 100644 --- a/webview-ui/src/i18n/locales/ko/prompts.json +++ b/webview-ui/src/i18n/locales/ko/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "전역 모드 편집", "editProjectModes": "프로젝트 모드 편집 (.roomodes)", "createModeHelpText": "새 커스텀 모드를 만들려면 + 버튼을 클릭하거나, 채팅에서 Roo에게 만들어달라고 요청하세요!", - "selectMode": "모드 검색" + "selectMode": "모드 검색", + "noMatchFound": "일치하는 모드를 찾을 수 없습니다" }, "apiConfiguration": { "title": "API 구성", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 95195ff0a2..b896c16645 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -458,7 +458,6 @@ }, "footer": { "feedback": "질문이나 피드백이 있으시면 github.com/RooVetGit/Roo-Code에서 이슈를 열거나 reddit.com/r/RooCode 또는 discord.gg/roocode에 가입하세요", - "version": "Roo Code v{{version}}", "telemetry": { "label": "익명 오류 및 사용 보고 허용", "description": "익명 사용 데이터 및 오류 보고서를 보내 Roo Code 개선에 도움을 주세요. 코드, 프롬프트 또는 개인 정보는 절대 전송되지 않습니다. 자세한 내용은 개인정보 보호정책을 참조하세요." diff --git a/webview-ui/src/i18n/locales/nl/prompts.json b/webview-ui/src/i18n/locales/nl/prompts.json index c7d7c5ea0b..93bb413b15 100644 --- a/webview-ui/src/i18n/locales/nl/prompts.json +++ b/webview-ui/src/i18n/locales/nl/prompts.json @@ -1,149 +1,150 @@ { - "title": "Prompts", - "done": "Gereed", - "modes": { - "title": "Modi", - "createNewMode": "Nieuwe modus aanmaken", - "editModesConfig": "Modusconfiguratie bewerken", - "editGlobalModes": "Globale modi bewerken", - "editProjectModes": "Projectmodi bewerken (.roomodes)", - "createModeHelpText": "Klik op + om een nieuwe aangepaste modus te maken, of vraag Roo in de chat om er een voor je te maken!", - "selectMode": "Modus zoeken" - }, - "apiConfiguration": { - "title": "API-configuratie", - "select": "Selecteer welke API-configuratie voor deze modus gebruikt moet worden" - }, - "tools": { - "title": "Beschikbare tools", - "builtInModesText": "Tools voor ingebouwde modi kunnen niet worden aangepast", - "editTools": "Tools bewerken", - "doneEditing": "Bewerken voltooid", - "allowedFiles": "Toegestane bestanden:", - "toolNames": { - "read": "Bestanden lezen", - "edit": "Bestanden bewerken", - "browser": "Browser gebruiken", - "command": "Commando's uitvoeren", - "mcp": "MCP gebruiken" - }, - "noTools": "Geen" - }, - "roleDefinition": { - "title": "Roldefinitie", - "resetToDefault": "Terugzetten naar standaard", - "description": "Definieer Roo's expertise en persoonlijkheid voor deze modus. Deze beschrijving bepaalt hoe Roo zich presenteert en taken benadert." - }, - "customInstructions": { - "title": "Modusspecifieke instructies (optioneel)", - "resetToDefault": "Terugzetten naar standaard", - "description": "Voeg gedragsrichtlijnen toe die specifiek zijn voor de modus {{modeName}}.", - "loadFromFile": "Modusspecifieke instructies voor {{mode}} kunnen ook worden geladen uit de map .roo/rules-{{slug}}/ in je werkruimte (.roorules-{{slug}} en .clinerules-{{slug}} zijn verouderd en werken binnenkort niet meer)." - }, - "globalCustomInstructions": { - "title": "Aangepaste instructies voor alle modi", - "description": "Deze instructies gelden voor alle modi. Ze bieden een basisset aan gedragingen die kunnen worden uitgebreid met modusspecifieke instructies hieronder.\nWil je dat Roo in een andere taal denkt en spreekt dan de weergavetaal van je editor ({{language}}), dan kun je dat hier aangeven.", - "loadFromFile": "Instructies kunnen ook worden geladen uit de map .roo/rules/ in je werkruimte (.roorules en .clinerules zijn verouderd en werken binnenkort niet meer)." - }, - "systemPrompt": { - "preview": "Systeemprompt bekijken", - "copy": "Systeemprompt kopiëren naar klembord", - "title": "Systeemprompt ({{modeName}} modus)" - }, - "supportPrompts": { - "title": "Ondersteuningsprompts", - "resetPrompt": "Reset {{promptType}} prompt naar standaard", - "prompt": "Prompt", - "enhance": { - "apiConfiguration": "API-configuratie", - "apiConfigDescription": "Je kunt een API-configuratie selecteren die altijd wordt gebruikt voor het verbeteren van prompts, of gewoon de huidige selectie gebruiken", - "useCurrentConfig": "Huidige API-configuratie gebruiken", - "testPromptPlaceholder": "Voer een prompt in om de verbetering te testen", - "previewButton": "Voorbeeld promptverbetering" - }, - "types": { - "ENHANCE": { - "label": "Prompt verbeteren", - "description": "Gebruik promptverbetering om op maat gemaakte suggesties of verbeteringen voor je invoer te krijgen. Zo begrijpt Roo je intentie en krijg je de best mogelijke antwoorden. Beschikbaar via het ✨-icoon in de chat." - }, - "EXPLAIN": { - "label": "Code uitleggen", - "description": "Krijg gedetailleerde uitleg over codefragmenten, functies of hele bestanden. Handig om complexe code te begrijpen of nieuwe patronen te leren. Beschikbaar via codeacties (lampje in de editor) en het contextmenu (rechtsklik op geselecteerde code)." - }, - "FIX": { - "label": "Problemen oplossen", - "description": "Krijg hulp bij het identificeren en oplossen van bugs, fouten of codekwaliteitsproblemen. Biedt stapsgewijze begeleiding bij het oplossen van problemen. Beschikbaar via codeacties (lampje in de editor) en het contextmenu (rechtsklik op geselecteerde code)." - }, - "IMPROVE": { - "label": "Code verbeteren", - "description": "Ontvang suggesties voor codeoptimalisatie, betere praktijken en architecturale verbeteringen met behoud van functionaliteit. Beschikbaar via codeacties (lampje in de editor) en het contextmenu (rechtsklik op geselecteerde code)." - }, - "ADD_TO_CONTEXT": { - "label": "Aan context toevoegen", - "description": "Voeg context toe aan je huidige taak of gesprek. Handig voor extra informatie of verduidelijkingen. Beschikbaar via codeacties (lampje in de editor) en het contextmenu (rechtsklik op geselecteerde code)." - }, - "TERMINAL_ADD_TO_CONTEXT": { - "label": "Terminalinhoud aan context toevoegen", - "description": "Voeg terminaluitvoer toe aan je huidige taak of gesprek. Handig voor commando-uitvoer of logboeken. Beschikbaar in het terminalcontextmenu (rechtsklik op geselecteerde terminalinhoud)." - }, - "TERMINAL_FIX": { - "label": "Terminalcommando repareren", - "description": "Krijg hulp bij het repareren van terminalcommando's die zijn mislukt of verbetering nodig hebben. Beschikbaar in het terminalcontextmenu (rechtsklik op geselecteerde terminalinhoud)." - }, - "TERMINAL_EXPLAIN": { - "label": "Terminalcommando uitleggen", - "description": "Krijg gedetailleerde uitleg over terminalcommando's en hun uitvoer. Beschikbaar in het terminalcontextmenu (rechtsklik op geselecteerde terminalinhoud)." - }, - "NEW_TASK": { - "label": "Nieuwe taak starten", - "description": "Start een nieuwe taak met gebruikersinvoer. Beschikbaar via de Command Palette." - } - } - }, - "advancedSystemPrompt": { - "title": "Geavanceerd: Systeemprompt overschrijven", - "description": "Je kunt de systeemprompt voor deze modus volledig vervangen (behalve de roldefinitie en aangepaste instructies) door een bestand aan te maken op .roo/system-prompt-{{slug}} in je werkruimte. Dit is een zeer geavanceerde functie die ingebouwde beveiligingen en consistentiecontroles omzeilt (vooral rond toolgebruik), dus wees voorzichtig!" - }, - "createModeDialog": { - "title": "Nieuwe modus aanmaken", - "close": "Sluiten", - "name": { - "label": "Naam", - "placeholder": "Voer de naam van de modus in" - }, - "slug": { - "label": "Slug", - "description": "De slug wordt gebruikt in URL's en bestandsnamen. Moet kleine letters, cijfers en koppeltekens bevatten." - }, - "saveLocation": { - "label": "Opslaglocatie", - "description": "Kies waar je deze modus wilt opslaan. Projectspecifieke modi hebben voorrang op globale modi.", - "global": { - "label": "Globaal", - "description": "Beschikbaar in alle werkruimtes" - }, - "project": { - "label": "Projectspecifiek (.roomodes)", - "description": "Alleen beschikbaar in deze werkruimte, heeft voorrang op globaal" - } - }, - "roleDefinition": { - "label": "Roldefinitie", - "description": "Definieer Roo's expertise en persoonlijkheid voor deze modus." - }, - "tools": { - "label": "Beschikbare tools", - "description": "Selecteer welke tools deze modus kan gebruiken." - }, - "customInstructions": { - "label": "Aangepaste instructies (optioneel)", - "description": "Voeg gedragsrichtlijnen toe die specifiek zijn voor deze modus." - }, - "buttons": { - "cancel": "Annuleren", - "create": "Modus aanmaken" - }, - "deleteMode": "Modus verwijderen" - }, - "allFiles": "alle bestanden" + "title": "Prompts", + "done": "Gereed", + "modes": { + "title": "Modi", + "createNewMode": "Nieuwe modus aanmaken", + "editModesConfig": "Modusconfiguratie bewerken", + "editGlobalModes": "Globale modi bewerken", + "editProjectModes": "Projectmodi bewerken (.roomodes)", + "createModeHelpText": "Klik op + om een nieuwe aangepaste modus te maken, of vraag Roo in de chat om er een voor je te maken!", + "selectMode": "Modus zoeken", + "noMatchFound": "Geen overeenkomende modi gevonden" + }, + "apiConfiguration": { + "title": "API-configuratie", + "select": "Selecteer welke API-configuratie voor deze modus gebruikt moet worden" + }, + "tools": { + "title": "Beschikbare tools", + "builtInModesText": "Tools voor ingebouwde modi kunnen niet worden aangepast", + "editTools": "Tools bewerken", + "doneEditing": "Bewerken voltooid", + "allowedFiles": "Toegestane bestanden:", + "toolNames": { + "read": "Bestanden lezen", + "edit": "Bestanden bewerken", + "browser": "Browser gebruiken", + "command": "Commando's uitvoeren", + "mcp": "MCP gebruiken" + }, + "noTools": "Geen" + }, + "roleDefinition": { + "title": "Roldefinitie", + "resetToDefault": "Terugzetten naar standaard", + "description": "Definieer Roo's expertise en persoonlijkheid voor deze modus. Deze beschrijving bepaalt hoe Roo zich presenteert en taken benadert." + }, + "customInstructions": { + "title": "Modusspecifieke instructies (optioneel)", + "resetToDefault": "Terugzetten naar standaard", + "description": "Voeg gedragsrichtlijnen toe die specifiek zijn voor de modus {{modeName}}.", + "loadFromFile": "Modusspecifieke instructies voor {{mode}} kunnen ook worden geladen uit de map .roo/rules-{{slug}}/ in je werkruimte (.roorules-{{slug}} en .clinerules-{{slug}} zijn verouderd en werken binnenkort niet meer)." + }, + "globalCustomInstructions": { + "title": "Aangepaste instructies voor alle modi", + "description": "Deze instructies gelden voor alle modi. Ze bieden een basisset aan gedragingen die kunnen worden uitgebreid met modusspecifieke instructies hieronder.\nWil je dat Roo in een andere taal denkt en spreekt dan de weergavetaal van je editor ({{language}}), dan kun je dat hier aangeven.", + "loadFromFile": "Instructies kunnen ook worden geladen uit de map .roo/rules/ in je werkruimte (.roorules en .clinerules zijn verouderd en werken binnenkort niet meer)." + }, + "systemPrompt": { + "preview": "Systeemprompt bekijken", + "copy": "Systeemprompt kopiëren naar klembord", + "title": "Systeemprompt ({{modeName}} modus)" + }, + "supportPrompts": { + "title": "Ondersteuningsprompts", + "resetPrompt": "Reset {{promptType}} prompt naar standaard", + "prompt": "Prompt", + "enhance": { + "apiConfiguration": "API-configuratie", + "apiConfigDescription": "Je kunt een API-configuratie selecteren die altijd wordt gebruikt voor het verbeteren van prompts, of gewoon de huidige selectie gebruiken", + "useCurrentConfig": "Huidige API-configuratie gebruiken", + "testPromptPlaceholder": "Voer een prompt in om de verbetering te testen", + "previewButton": "Voorbeeld promptverbetering" + }, + "types": { + "ENHANCE": { + "label": "Prompt verbeteren", + "description": "Gebruik promptverbetering om op maat gemaakte suggesties of verbeteringen voor je invoer te krijgen. Zo begrijpt Roo je intentie en krijg je de best mogelijke antwoorden. Beschikbaar via het ✨-icoon in de chat." + }, + "EXPLAIN": { + "label": "Code uitleggen", + "description": "Krijg gedetailleerde uitleg over codefragmenten, functies of hele bestanden. Handig om complexe code te begrijpen of nieuwe patronen te leren. Beschikbaar via codeacties (lampje in de editor) en het contextmenu (rechtsklik op geselecteerde code)." + }, + "FIX": { + "label": "Problemen oplossen", + "description": "Krijg hulp bij het identificeren en oplossen van bugs, fouten of codekwaliteitsproblemen. Biedt stapsgewijze begeleiding bij het oplossen van problemen. Beschikbaar via codeacties (lampje in de editor) en het contextmenu (rechtsklik op geselecteerde code)." + }, + "IMPROVE": { + "label": "Code verbeteren", + "description": "Ontvang suggesties voor codeoptimalisatie, betere praktijken en architecturale verbeteringen met behoud van functionaliteit. Beschikbaar via codeacties (lampje in de editor) en het contextmenu (rechtsklik op geselecteerde code)." + }, + "ADD_TO_CONTEXT": { + "label": "Aan context toevoegen", + "description": "Voeg context toe aan je huidige taak of gesprek. Handig voor extra informatie of verduidelijkingen. Beschikbaar via codeacties (lampje in de editor) en het contextmenu (rechtsklik op geselecteerde code)." + }, + "TERMINAL_ADD_TO_CONTEXT": { + "label": "Terminalinhoud aan context toevoegen", + "description": "Voeg terminaluitvoer toe aan je huidige taak of gesprek. Handig voor commando-uitvoer of logboeken. Beschikbaar in het terminalcontextmenu (rechtsklik op geselecteerde terminalinhoud)." + }, + "TERMINAL_FIX": { + "label": "Terminalcommando repareren", + "description": "Krijg hulp bij het repareren van terminalcommando's die zijn mislukt of verbetering nodig hebben. Beschikbaar in het terminalcontextmenu (rechtsklik op geselecteerde terminalinhoud)." + }, + "TERMINAL_EXPLAIN": { + "label": "Terminalcommando uitleggen", + "description": "Krijg gedetailleerde uitleg over terminalcommando's en hun uitvoer. Beschikbaar in het terminalcontextmenu (rechtsklik op geselecteerde terminalinhoud)." + }, + "NEW_TASK": { + "label": "Nieuwe taak starten", + "description": "Start een nieuwe taak met gebruikersinvoer. Beschikbaar via de Command Palette." + } + } + }, + "advancedSystemPrompt": { + "title": "Geavanceerd: Systeemprompt overschrijven", + "description": "Je kunt de systeemprompt voor deze modus volledig vervangen (behalve de roldefinitie en aangepaste instructies) door een bestand aan te maken op .roo/system-prompt-{{slug}} in je werkruimte. Dit is een zeer geavanceerde functie die ingebouwde beveiligingen en consistentiecontroles omzeilt (vooral rond toolgebruik), dus wees voorzichtig!" + }, + "createModeDialog": { + "title": "Nieuwe modus aanmaken", + "close": "Sluiten", + "name": { + "label": "Naam", + "placeholder": "Voer de naam van de modus in" + }, + "slug": { + "label": "Slug", + "description": "De slug wordt gebruikt in URL's en bestandsnamen. Moet kleine letters, cijfers en koppeltekens bevatten." + }, + "saveLocation": { + "label": "Opslaglocatie", + "description": "Kies waar je deze modus wilt opslaan. Projectspecifieke modi hebben voorrang op globale modi.", + "global": { + "label": "Globaal", + "description": "Beschikbaar in alle werkruimtes" + }, + "project": { + "label": "Projectspecifiek (.roomodes)", + "description": "Alleen beschikbaar in deze werkruimte, heeft voorrang op globaal" + } + }, + "roleDefinition": { + "label": "Roldefinitie", + "description": "Definieer Roo's expertise en persoonlijkheid voor deze modus." + }, + "tools": { + "label": "Beschikbare tools", + "description": "Selecteer welke tools deze modus kan gebruiken." + }, + "customInstructions": { + "label": "Aangepaste instructies (optioneel)", + "description": "Voeg gedragsrichtlijnen toe die specifiek zijn voor deze modus." + }, + "buttons": { + "cancel": "Annuleren", + "create": "Modus aanmaken" + }, + "deleteMode": "Modus verwijderen" + }, + "allFiles": "alle bestanden" } diff --git a/webview-ui/src/i18n/locales/pl/prompts.json b/webview-ui/src/i18n/locales/pl/prompts.json index 13bd766d31..8ce1edba35 100644 --- a/webview-ui/src/i18n/locales/pl/prompts.json +++ b/webview-ui/src/i18n/locales/pl/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "Edytuj tryby globalne", "editProjectModes": "Edytuj tryby projektu (.roomodes)", "createModeHelpText": "Kliknij +, aby utworzyć nowy niestandardowy tryb, lub po prostu poproś Roo w czacie, aby utworzył go dla Ciebie!", - "selectMode": "Szukaj trybów" + "selectMode": "Szukaj trybów", + "noMatchFound": "Nie znaleziono pasujących trybów" }, "apiConfiguration": { "title": "Konfiguracja API", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 343ce01397..7b8e771c86 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -458,7 +458,6 @@ }, "footer": { "feedback": "Jeśli masz jakiekolwiek pytania lub opinie, śmiało otwórz zgłoszenie na github.com/RooVetGit/Roo-Code lub dołącz do reddit.com/r/RooCode lub discord.gg/roocode", - "version": "Roo Code v{{version}}", "telemetry": { "label": "Zezwól na anonimowe raportowanie błędów i użycia", "description": "Pomóż ulepszyć Roo Code, wysyłając anonimowe dane o użytkowaniu i raporty o błędach. Nigdy nie są wysyłane kod, podpowiedzi ani informacje osobiste. Zobacz naszą politykę prywatności, aby uzyskać więcej szczegółów." diff --git a/webview-ui/src/i18n/locales/pt-BR/prompts.json b/webview-ui/src/i18n/locales/pt-BR/prompts.json index 38abe83e99..c38ad22241 100644 --- a/webview-ui/src/i18n/locales/pt-BR/prompts.json +++ b/webview-ui/src/i18n/locales/pt-BR/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "Editar modos globais", "editProjectModes": "Editar modos do projeto (.roomodes)", "createModeHelpText": "Clique em + para criar um novo modo personalizado, ou simplesmente peça ao Roo no chat para criar um para você!", - "selectMode": "Buscar modos" + "selectMode": "Buscar modos", + "noMatchFound": "Nenhum modo correspondente encontrado" }, "apiConfiguration": { "title": "Configuração de API", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 0c66a21847..3efd39964f 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -458,7 +458,6 @@ }, "footer": { "feedback": "Se tiver alguma dúvida ou feedback, sinta-se à vontade para abrir um problema em github.com/RooVetGit/Roo-Code ou juntar-se a reddit.com/r/RooCode ou discord.gg/roocode", - "version": "Roo Code v{{version}}", "telemetry": { "label": "Permitir relatórios anônimos de erros e uso", "description": "Ajude a melhorar o Roo Code enviando dados de uso anônimos e relatórios de erros. Nunca são enviados código, prompts ou informações pessoais. Consulte nossa política de privacidade para mais detalhes." diff --git a/webview-ui/src/i18n/locales/ru/prompts.json b/webview-ui/src/i18n/locales/ru/prompts.json index 6e1c10adb8..f50e70c8d1 100644 --- a/webview-ui/src/i18n/locales/ru/prompts.json +++ b/webview-ui/src/i18n/locales/ru/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "Редактировать глобальные режимы", "editProjectModes": "Редактировать режимы проекта (.roomodes)", "createModeHelpText": "Нажмите +, чтобы создать новый пользовательский режим, или просто попросите Roo в чате создать его для вас!", - "selectMode": "Поиск режимов" + "selectMode": "Поиск режимов", + "noMatchFound": "Соответствующие режимы не найдены" }, "apiConfiguration": { "title": "Конфигурация API", diff --git a/webview-ui/src/i18n/locales/tr/prompts.json b/webview-ui/src/i18n/locales/tr/prompts.json index 27fb89c091..45df205997 100644 --- a/webview-ui/src/i18n/locales/tr/prompts.json +++ b/webview-ui/src/i18n/locales/tr/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "Global modları düzenle", "editProjectModes": "Proje modlarını düzenle (.roomodes)", "createModeHelpText": "Yeni bir özel mod oluşturmak için + düğmesine tıklayın veya sohbette Roo'dan sizin için bir tane oluşturmasını isteyin!", - "selectMode": "Modları Ara" + "selectMode": "Modları Ara", + "noMatchFound": "Eşleşen mod bulunamadı" }, "apiConfiguration": { "title": "API Yapılandırması", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 1d23b7e04e..2859ef6bcc 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -458,7 +458,6 @@ }, "footer": { "feedback": "Herhangi bir sorunuz veya geri bildiriminiz varsa, github.com/RooVetGit/Roo-Code adresinde bir konu açmaktan veya reddit.com/r/RooCode ya da discord.gg/roocode'a katılmaktan çekinmeyin", - "version": "Roo Code v{{version}}", "telemetry": { "label": "Anonim hata ve kullanım raporlamaya izin ver", "description": "Anonim kullanım verileri ve hata raporları göndererek Roo Code'u geliştirmeye yardımcı olun. Hiçbir kod, istem veya kişisel bilgi asla gönderilmez. Daha fazla ayrıntı için gizlilik politikamıza bakın." diff --git a/webview-ui/src/i18n/locales/vi/mcp.json b/webview-ui/src/i18n/locales/vi/mcp.json index c7c10be655..c83e623b58 100644 --- a/webview-ui/src/i18n/locales/vi/mcp.json +++ b/webview-ui/src/i18n/locales/vi/mcp.json @@ -12,7 +12,6 @@ }, "editGlobalMCP": "Chỉnh sửa MCP toàn cục", "editProjectMCP": "Chỉnh sửa MCP dự án", - "editSettings": "Chỉnh sửa cài đặt MCP", "tool": { "alwaysAllow": "Luôn cho phép", "parameters": "Tham số", diff --git a/webview-ui/src/i18n/locales/vi/prompts.json b/webview-ui/src/i18n/locales/vi/prompts.json index c0d88f9007..2c9eccb9d5 100644 --- a/webview-ui/src/i18n/locales/vi/prompts.json +++ b/webview-ui/src/i18n/locales/vi/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "Chỉnh sửa chế độ toàn cục", "editProjectModes": "Chỉnh sửa chế độ dự án (.roomodes)", "createModeHelpText": "Nhấn + để tạo chế độ tùy chỉnh mới, hoặc chỉ cần yêu cầu Roo trong chat tạo một chế độ cho bạn!", - "selectMode": "Tìm kiếm chế độ" + "selectMode": "Tìm kiếm chế độ", + "noMatchFound": "Không tìm thấy chế độ phù hợp" }, "apiConfiguration": { "title": "Cấu hình API", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index f964bf4b7a..bd022a52be 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -458,7 +458,6 @@ }, "footer": { "feedback": "Nếu bạn có bất kỳ câu hỏi hoặc phản hồi nào, vui lòng mở một vấn đề tại github.com/RooVetGit/Roo-Code hoặc tham gia reddit.com/r/RooCode hoặc discord.gg/roocode", - "version": "Roo Code v{{version}}", "telemetry": { "label": "Cho phép báo cáo lỗi và sử dụng ẩn danh", "description": "Giúp cải thiện Roo Code bằng cách gửi dữ liệu sử dụng ẩn danh và báo cáo lỗi. Không bao giờ gửi mã, lời nhắc hoặc thông tin cá nhân. Xem chính sách bảo mật của chúng tôi để biết thêm chi tiết." diff --git a/webview-ui/src/i18n/locales/zh-CN/mcp.json b/webview-ui/src/i18n/locales/zh-CN/mcp.json index 8e1d1dbe8a..514a2b57e0 100644 --- a/webview-ui/src/i18n/locales/zh-CN/mcp.json +++ b/webview-ui/src/i18n/locales/zh-CN/mcp.json @@ -12,7 +12,6 @@ }, "editGlobalMCP": "编辑全局配置", "editProjectMCP": "编辑项目配置", - "editSettings": "参数设置", "tool": { "alwaysAllow": "始终允许", "parameters": "参数", diff --git a/webview-ui/src/i18n/locales/zh-CN/prompts.json b/webview-ui/src/i18n/locales/zh-CN/prompts.json index b2afe66f40..b3d0af4f8d 100644 --- a/webview-ui/src/i18n/locales/zh-CN/prompts.json +++ b/webview-ui/src/i18n/locales/zh-CN/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "修改全局模式", "editProjectModes": "编辑项目模式 (.roomodes)", "createModeHelpText": "点击 + 创建模式,或在对话时让Roo创建一个新模式。", - "selectMode": "搜索模式" + "selectMode": "搜索模式", + "noMatchFound": "未找到匹配的模式" }, "apiConfiguration": { "title": "API配置", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index ba14e48762..6ad116a5d7 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -458,7 +458,6 @@ }, "footer": { "feedback": "如果您有任何问题或反馈,请随时在 github.com/RooVetGit/Roo-Code 上提出问题或加入 reddit.com/r/RooCodediscord.gg/roocode", - "version": "Roo Code v{{version}}", "telemetry": { "label": "允许匿名数据收集", "description": "匿名收集错误报告和使用数据(不含代码/提示/个人信息),详情见隐私政策" diff --git a/webview-ui/src/i18n/locales/zh-TW/mcp.json b/webview-ui/src/i18n/locales/zh-TW/mcp.json index 67cc0bde4d..48fe770d11 100644 --- a/webview-ui/src/i18n/locales/zh-TW/mcp.json +++ b/webview-ui/src/i18n/locales/zh-TW/mcp.json @@ -12,7 +12,6 @@ }, "editGlobalMCP": "編輯全域 MCP", "editProjectMCP": "編輯專案 MCP", - "editSettings": "編輯 MCP 設定", "tool": { "alwaysAllow": "總是允許", "parameters": "參數", diff --git a/webview-ui/src/i18n/locales/zh-TW/prompts.json b/webview-ui/src/i18n/locales/zh-TW/prompts.json index c0365bf278..c5106ef621 100644 --- a/webview-ui/src/i18n/locales/zh-TW/prompts.json +++ b/webview-ui/src/i18n/locales/zh-TW/prompts.json @@ -8,7 +8,8 @@ "editGlobalModes": "編輯全域模式", "editProjectModes": "編輯專案模式 (.roomodes)", "createModeHelpText": "點選 + 建立新的自訂模式,或者在聊天中直接請 Roo 為您建立!", - "selectMode": "搜尋模式" + "selectMode": "搜尋模式", + "noMatchFound": "未找到符合的模式" }, "apiConfiguration": { "title": "API 設定",