From f4d4a32af2efe4ea9416046f3c90df36bb35de3a Mon Sep 17 00:00:00 2001 From: lyzno1 <92089059+lyzno1@users.noreply.github.com> Date: Tue, 29 Jul 2025 18:24:57 +0800 Subject: [PATCH] Feat/enhance i18n scripts (#23114) --- web/__tests__/check-i18n.test.ts | 569 ++++++++++++++++++++++++ web/i18n-config/check-i18n.js | 223 +++++++++- web/i18n/zh-Hans/app-annotation.ts | 3 - web/i18n/zh-Hans/app-debug.ts | 1 - web/i18n/zh-Hans/app.ts | 2 - web/i18n/zh-Hans/login.ts | 1 - web/i18n/zh-Hans/time.ts | 1 - web/i18n/zh-Hans/workflow.ts | 1 - web/i18n/zh-Hant/app-annotation.ts | 3 - web/i18n/zh-Hant/app.ts | 14 - web/i18n/zh-Hant/billing.ts | 26 -- web/i18n/zh-Hant/common.ts | 1 - web/i18n/zh-Hant/dataset-creation.ts | 2 - web/i18n/zh-Hant/dataset-documents.ts | 1 - web/i18n/zh-Hant/dataset-hit-testing.ts | 1 - web/i18n/zh-Hant/login.ts | 1 - web/i18n/zh-Hant/tools.ts | 1 - web/i18n/zh-Hant/workflow.ts | 3 - 18 files changed, 783 insertions(+), 71 deletions(-) create mode 100644 web/__tests__/check-i18n.test.ts diff --git a/web/__tests__/check-i18n.test.ts b/web/__tests__/check-i18n.test.ts new file mode 100644 index 000000000..173aa9611 --- /dev/null +++ b/web/__tests__/check-i18n.test.ts @@ -0,0 +1,569 @@ +import fs from 'node:fs' +import path from 'node:path' + +// Mock functions to simulate the check-i18n functionality +const vm = require('node:vm') +const transpile = require('typescript').transpile + +describe('check-i18n script functionality', () => { + const testDir = path.join(__dirname, '../i18n-test') + const testEnDir = path.join(testDir, 'en-US') + const testZhDir = path.join(testDir, 'zh-Hans') + + // Helper function that replicates the getKeysFromLanguage logic + async function getKeysFromLanguage(language: string, testPath = testDir): Promise { + return new Promise((resolve, reject) => { + const folderPath = path.resolve(testPath, language) + const allKeys: string[] = [] + + if (!fs.existsSync(folderPath)) { + resolve([]) + return + } + + fs.readdir(folderPath, (err, files) => { + if (err) { + reject(err) + return + } + + const translationFiles = files.filter(file => /\.(ts|js)$/.test(file)) + + translationFiles.forEach((file) => { + const filePath = path.join(folderPath, file) + const fileName = file.replace(/\.[^/.]+$/, '') + const camelCaseFileName = fileName.replace(/[-_](.)/g, (_, c) => + c.toUpperCase(), + ) + + try { + const content = fs.readFileSync(filePath, 'utf8') + const moduleExports = {} + const context = { + exports: moduleExports, + module: { exports: moduleExports }, + require, + console, + __filename: filePath, + __dirname: folderPath, + } + + vm.runInNewContext(transpile(content), context) + const translationObj = moduleExports.default || moduleExports + + if(!translationObj || typeof translationObj !== 'object') + throw new Error(`Error parsing file: ${filePath}`) + + const nestedKeys: string[] = [] + const iterateKeys = (obj: any, prefix = '') => { + for (const key in obj) { + const nestedKey = prefix ? `${prefix}.${key}` : key + if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) { + // This is an object (but not array), recurse into it but don't add it as a key + iterateKeys(obj[key], nestedKey) + } + else { + // This is a leaf node (string, number, boolean, array, etc.), add it as a key + nestedKeys.push(nestedKey) + } + } + } + iterateKeys(translationObj) + + const fileKeys = nestedKeys.map(key => `${camelCaseFileName}.${key}`) + allKeys.push(...fileKeys) + } + catch (error) { + reject(error) + } + }) + resolve(allKeys) + }) + }) + } + + beforeEach(() => { + // Clean up and create test directories + if (fs.existsSync(testDir)) + fs.rmSync(testDir, { recursive: true }) + + fs.mkdirSync(testDir, { recursive: true }) + fs.mkdirSync(testEnDir, { recursive: true }) + fs.mkdirSync(testZhDir, { recursive: true }) + }) + + afterEach(() => { + // Clean up test files + if (fs.existsSync(testDir)) + fs.rmSync(testDir, { recursive: true }) + }) + + describe('Key extraction logic', () => { + it('should extract only leaf node keys, not intermediate objects', async () => { + const testContent = `const translation = { + simple: 'Simple Value', + nested: { + level1: 'Level 1 Value', + deep: { + level2: 'Level 2 Value' + } + }, + array: ['not extracted'], + number: 42, + boolean: true +} + +export default translation +` + + fs.writeFileSync(path.join(testEnDir, 'test.ts'), testContent) + + const keys = await getKeysFromLanguage('en-US') + + expect(keys).toEqual([ + 'test.simple', + 'test.nested.level1', + 'test.nested.deep.level2', + 'test.array', + 'test.number', + 'test.boolean', + ]) + + // Should not include intermediate object keys + expect(keys).not.toContain('test.nested') + expect(keys).not.toContain('test.nested.deep') + }) + + it('should handle camelCase file name conversion correctly', async () => { + const testContent = `const translation = { + key: 'value' +} + +export default translation +` + + fs.writeFileSync(path.join(testEnDir, 'app-debug.ts'), testContent) + fs.writeFileSync(path.join(testEnDir, 'user_profile.ts'), testContent) + + const keys = await getKeysFromLanguage('en-US') + + expect(keys).toContain('appDebug.key') + expect(keys).toContain('userProfile.key') + }) + }) + + describe('Missing keys detection', () => { + it('should detect missing keys in target language', async () => { + const enContent = `const translation = { + common: { + save: 'Save', + cancel: 'Cancel', + delete: 'Delete' + }, + app: { + title: 'My App', + version: '1.0' + } +} + +export default translation +` + + const zhContent = `const translation = { + common: { + save: '保存', + cancel: '取消' + // missing 'delete' + }, + app: { + title: '我的应用' + // missing 'version' + } +} + +export default translation +` + + fs.writeFileSync(path.join(testEnDir, 'test.ts'), enContent) + fs.writeFileSync(path.join(testZhDir, 'test.ts'), zhContent) + + const enKeys = await getKeysFromLanguage('en-US') + const zhKeys = await getKeysFromLanguage('zh-Hans') + + const missingKeys = enKeys.filter(key => !zhKeys.includes(key)) + + expect(missingKeys).toContain('test.common.delete') + expect(missingKeys).toContain('test.app.version') + expect(missingKeys).toHaveLength(2) + }) + }) + + describe('Extra keys detection', () => { + it('should detect extra keys in target language', async () => { + const enContent = `const translation = { + common: { + save: 'Save', + cancel: 'Cancel' + } +} + +export default translation +` + + const zhContent = `const translation = { + common: { + save: '保存', + cancel: '取消', + delete: '删除', // extra key + extra: '额外的' // another extra key + }, + newSection: { + someKey: '某个值' // extra section + } +} + +export default translation +` + + fs.writeFileSync(path.join(testEnDir, 'test.ts'), enContent) + fs.writeFileSync(path.join(testZhDir, 'test.ts'), zhContent) + + const enKeys = await getKeysFromLanguage('en-US') + const zhKeys = await getKeysFromLanguage('zh-Hans') + + const extraKeys = zhKeys.filter(key => !enKeys.includes(key)) + + expect(extraKeys).toContain('test.common.delete') + expect(extraKeys).toContain('test.common.extra') + expect(extraKeys).toContain('test.newSection.someKey') + expect(extraKeys).toHaveLength(3) + }) + }) + + describe('File filtering logic', () => { + it('should filter keys by specific file correctly', async () => { + // Create multiple files + const file1Content = `const translation = { + button: 'Button', + text: 'Text' +} + +export default translation +` + + const file2Content = `const translation = { + title: 'Title', + description: 'Description' +} + +export default translation +` + + fs.writeFileSync(path.join(testEnDir, 'components.ts'), file1Content) + fs.writeFileSync(path.join(testEnDir, 'pages.ts'), file2Content) + fs.writeFileSync(path.join(testZhDir, 'components.ts'), file1Content) + fs.writeFileSync(path.join(testZhDir, 'pages.ts'), file2Content) + + const allEnKeys = await getKeysFromLanguage('en-US') + const allZhKeys = await getKeysFromLanguage('zh-Hans') + + // Test file filtering logic + const targetFile = 'components' + const filteredEnKeys = allEnKeys.filter(key => + key.startsWith(targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase())), + ) + const filteredZhKeys = allZhKeys.filter(key => + key.startsWith(targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase())), + ) + + expect(allEnKeys).toHaveLength(4) // 2 keys from each file + expect(filteredEnKeys).toHaveLength(2) // only components keys + expect(filteredEnKeys).toContain('components.button') + expect(filteredEnKeys).toContain('components.text') + expect(filteredEnKeys).not.toContain('pages.title') + expect(filteredEnKeys).not.toContain('pages.description') + }) + }) + + describe('Complex nested structure handling', () => { + it('should handle deeply nested objects correctly', async () => { + const complexContent = `const translation = { + level1: { + level2: { + level3: { + level4: { + deepValue: 'Deep Value' + }, + anotherValue: 'Another Value' + }, + simpleValue: 'Simple Value' + }, + directValue: 'Direct Value' + }, + rootValue: 'Root Value' +} + +export default translation +` + + fs.writeFileSync(path.join(testEnDir, 'complex.ts'), complexContent) + + const keys = await getKeysFromLanguage('en-US') + + expect(keys).toContain('complex.level1.level2.level3.level4.deepValue') + expect(keys).toContain('complex.level1.level2.level3.anotherValue') + expect(keys).toContain('complex.level1.level2.simpleValue') + expect(keys).toContain('complex.level1.directValue') + expect(keys).toContain('complex.rootValue') + + // Should not include intermediate objects + expect(keys).not.toContain('complex.level1') + expect(keys).not.toContain('complex.level1.level2') + expect(keys).not.toContain('complex.level1.level2.level3') + expect(keys).not.toContain('complex.level1.level2.level3.level4') + }) + }) + + describe('Edge cases', () => { + it('should handle empty objects', async () => { + const emptyContent = `const translation = { + empty: {}, + withValue: 'value' +} + +export default translation +` + + fs.writeFileSync(path.join(testEnDir, 'empty.ts'), emptyContent) + + const keys = await getKeysFromLanguage('en-US') + + expect(keys).toContain('empty.withValue') + expect(keys).not.toContain('empty.empty') + }) + + it('should handle special characters in keys', async () => { + const specialContent = `const translation = { + 'key-with-dash': 'value1', + 'key_with_underscore': 'value2', + 'key.with.dots': 'value3', + normalKey: 'value4' +} + +export default translation +` + + fs.writeFileSync(path.join(testEnDir, 'special.ts'), specialContent) + + const keys = await getKeysFromLanguage('en-US') + + expect(keys).toContain('special.key-with-dash') + expect(keys).toContain('special.key_with_underscore') + expect(keys).toContain('special.key.with.dots') + expect(keys).toContain('special.normalKey') + }) + + it('should handle different value types', async () => { + const typesContent = `const translation = { + stringValue: 'string', + numberValue: 42, + booleanValue: true, + nullValue: null, + undefinedValue: undefined, + arrayValue: ['array', 'values'], + objectValue: { + nested: 'nested value' + } +} + +export default translation +` + + fs.writeFileSync(path.join(testEnDir, 'types.ts'), typesContent) + + const keys = await getKeysFromLanguage('en-US') + + expect(keys).toContain('types.stringValue') + expect(keys).toContain('types.numberValue') + expect(keys).toContain('types.booleanValue') + expect(keys).toContain('types.nullValue') + expect(keys).toContain('types.undefinedValue') + expect(keys).toContain('types.arrayValue') + expect(keys).toContain('types.objectValue.nested') + expect(keys).not.toContain('types.objectValue') + }) + }) + + describe('Real-world scenario tests', () => { + it('should handle app-debug structure like real files', async () => { + const appDebugEn = `const translation = { + pageTitle: { + line1: 'Prompt', + line2: 'Engineering' + }, + operation: { + applyConfig: 'Publish', + resetConfig: 'Reset', + debugConfig: 'Debug' + }, + generate: { + instruction: 'Instructions', + generate: 'Generate', + resTitle: 'Generated Prompt', + noDataLine1: 'Describe your use case on the left,', + noDataLine2: 'the orchestration preview will show here.' + } +} + +export default translation +` + + const appDebugZh = `const translation = { + pageTitle: { + line1: '提示词', + line2: '编排' + }, + operation: { + applyConfig: '发布', + resetConfig: '重置', + debugConfig: '调试' + }, + generate: { + instruction: '指令', + generate: '生成', + resTitle: '生成的提示词', + noData: '在左侧描述您的用例,编排预览将在此处显示。' // This is extra + } +} + +export default translation +` + + fs.writeFileSync(path.join(testEnDir, 'app-debug.ts'), appDebugEn) + fs.writeFileSync(path.join(testZhDir, 'app-debug.ts'), appDebugZh) + + const enKeys = await getKeysFromLanguage('en-US') + const zhKeys = await getKeysFromLanguage('zh-Hans') + + const missingKeys = enKeys.filter(key => !zhKeys.includes(key)) + const extraKeys = zhKeys.filter(key => !enKeys.includes(key)) + + expect(missingKeys).toContain('appDebug.generate.noDataLine1') + expect(missingKeys).toContain('appDebug.generate.noDataLine2') + expect(extraKeys).toContain('appDebug.generate.noData') + + expect(missingKeys).toHaveLength(2) + expect(extraKeys).toHaveLength(1) + }) + + it('should handle time structure with operation nested keys', async () => { + const timeEn = `const translation = { + months: { + January: 'January', + February: 'February' + }, + operation: { + now: 'Now', + ok: 'OK', + cancel: 'Cancel', + pickDate: 'Pick Date' + }, + title: { + pickTime: 'Pick Time' + }, + defaultPlaceholder: 'Pick a time...' +} + +export default translation +` + + const timeZh = `const translation = { + months: { + January: '一月', + February: '二月' + }, + operation: { + now: '此刻', + ok: '确定', + cancel: '取消', + pickDate: '选择日期' + }, + title: { + pickTime: '选择时间' + }, + pickDate: '选择日期', // This is extra - duplicates operation.pickDate + defaultPlaceholder: '请选择时间...' +} + +export default translation +` + + fs.writeFileSync(path.join(testEnDir, 'time.ts'), timeEn) + fs.writeFileSync(path.join(testZhDir, 'time.ts'), timeZh) + + const enKeys = await getKeysFromLanguage('en-US') + const zhKeys = await getKeysFromLanguage('zh-Hans') + + const missingKeys = enKeys.filter(key => !zhKeys.includes(key)) + const extraKeys = zhKeys.filter(key => !enKeys.includes(key)) + + expect(missingKeys).toHaveLength(0) // No missing keys + expect(extraKeys).toContain('time.pickDate') // Extra root-level pickDate + expect(extraKeys).toHaveLength(1) + + // Should have both keys available + expect(zhKeys).toContain('time.operation.pickDate') // Correct nested key + expect(zhKeys).toContain('time.pickDate') // Extra duplicate key + }) + }) + + describe('Statistics calculation', () => { + it('should calculate correct difference statistics', async () => { + const enContent = `const translation = { + key1: 'value1', + key2: 'value2', + key3: 'value3' +} + +export default translation +` + + const zhContentMissing = `const translation = { + key1: 'value1', + key2: 'value2' + // missing key3 +} + +export default translation +` + + const zhContentExtra = `const translation = { + key1: 'value1', + key2: 'value2', + key3: 'value3', + key4: 'extra', + key5: 'extra2' +} + +export default translation +` + + fs.writeFileSync(path.join(testEnDir, 'stats.ts'), enContent) + + // Test missing keys scenario + fs.writeFileSync(path.join(testZhDir, 'stats.ts'), zhContentMissing) + + const enKeys = await getKeysFromLanguage('en-US') + const zhKeysMissing = await getKeysFromLanguage('zh-Hans') + + expect(enKeys.length - zhKeysMissing.length).toBe(1) // +1 means 1 missing key + + // Test extra keys scenario + fs.writeFileSync(path.join(testZhDir, 'stats.ts'), zhContentExtra) + + const zhKeysExtra = await getKeysFromLanguage('zh-Hans') + + expect(enKeys.length - zhKeysExtra.length).toBe(-2) // -2 means 2 extra keys + }) + }) +}) diff --git a/web/i18n-config/check-i18n.js b/web/i18n-config/check-i18n.js index 7e3b725c9..edc2566a3 100644 --- a/web/i18n-config/check-i18n.js +++ b/web/i18n-config/check-i18n.js @@ -58,9 +58,14 @@ async function getKeysFromLanguage(language) { const iterateKeys = (obj, prefix = '') => { for (const key in obj) { const nestedKey = prefix ? `${prefix}.${key}` : key - nestedKeys.push(nestedKey) - if (typeof obj[key] === 'object' && obj[key] !== null) + if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) { + // This is an object (but not array), recurse into it but don't add it as a key iterateKeys(obj[key], nestedKey) + } + else { + // This is a leaf node (string, number, boolean, array, etc.), add it as a key + nestedKeys.push(nestedKey) + } } } iterateKeys(translationObj) @@ -79,15 +84,176 @@ async function getKeysFromLanguage(language) { }) } +function removeKeysFromObject(obj, keysToRemove, prefix = '') { + let modified = false + for (const key in obj) { + const fullKey = prefix ? `${prefix}.${key}` : key + + if (keysToRemove.includes(fullKey)) { + delete obj[key] + modified = true + console.log(`🗑️ Removed key: ${fullKey}`) + } + else if (typeof obj[key] === 'object' && obj[key] !== null) { + const subModified = removeKeysFromObject(obj[key], keysToRemove, fullKey) + modified = modified || subModified + } + } + return modified +} + +async function removeExtraKeysFromFile(language, fileName, extraKeys) { + const filePath = path.resolve(__dirname, '../i18n', language, `${fileName}.ts`) + + if (!fs.existsSync(filePath)) { + console.log(`⚠️ File not found: ${filePath}`) + return false + } + + try { + // Filter keys that belong to this file + const camelCaseFileName = fileName.replace(/[-_](.)/g, (_, c) => c.toUpperCase()) + const fileSpecificKeys = extraKeys + .filter(key => key.startsWith(`${camelCaseFileName}.`)) + .map(key => key.substring(camelCaseFileName.length + 1)) // Remove file prefix + + if (fileSpecificKeys.length === 0) + return false + + console.log(`🔄 Processing file: ${filePath}`) + + // Read the original file content + const content = fs.readFileSync(filePath, 'utf8') + const lines = content.split('\n') + + let modified = false + const linesToRemove = [] + + // Find lines to remove for each key + for (const keyToRemove of fileSpecificKeys) { + const keyParts = keyToRemove.split('.') + let targetLineIndex = -1 + + // Build regex pattern for the exact key path + if (keyParts.length === 1) { + // Simple key at root level like "pickDate: 'value'" + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const simpleKeyPattern = new RegExp(`^\\s*${keyParts[0]}\\s*:`) + if (simpleKeyPattern.test(line)) { + targetLineIndex = i + break + } + } + } + else { + // Nested key - need to find the exact path + const currentPath = [] + let braceDepth = 0 + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const trimmedLine = line.trim() + + // Track current object path + const keyMatch = trimmedLine.match(/^(\w+)\s*:\s*{/) + if (keyMatch) { + currentPath.push(keyMatch[1]) + braceDepth++ + } + else if (trimmedLine === '},' || trimmedLine === '}') { + if (braceDepth > 0) { + braceDepth-- + currentPath.pop() + } + } + + // Check if this line matches our target key + const leafKeyMatch = trimmedLine.match(/^(\w+)\s*:/) + if (leafKeyMatch) { + const fullPath = [...currentPath, leafKeyMatch[1]] + const fullPathString = fullPath.join('.') + + if (fullPathString === keyToRemove) { + targetLineIndex = i + break + } + } + } + } + + if (targetLineIndex !== -1) { + linesToRemove.push(targetLineIndex) + console.log(`🗑️ Found key to remove: ${keyToRemove} at line ${targetLineIndex + 1}`) + modified = true + } + else { + console.log(`⚠️ Could not find key: ${keyToRemove}`) + } + } + + if (modified) { + // Remove lines in reverse order to maintain correct indices + linesToRemove.sort((a, b) => b - a) + + for (const lineIndex of linesToRemove) { + const line = lines[lineIndex] + console.log(`🗑️ Removing line ${lineIndex + 1}: ${line.trim()}`) + lines.splice(lineIndex, 1) + + // Also remove trailing comma from previous line if it exists and the next line is a closing brace + if (lineIndex > 0 && lineIndex < lines.length) { + const prevLine = lines[lineIndex - 1] + const nextLine = lines[lineIndex] ? lines[lineIndex].trim() : '' + + if (prevLine.trim().endsWith(',') && (nextLine.startsWith('}') || nextLine === '')) + lines[lineIndex - 1] = prevLine.replace(/,\s*$/, '') + } + } + + // Write back to file + const newContent = lines.join('\n') + fs.writeFileSync(filePath, newContent) + console.log(`💾 Updated file: ${filePath}`) + return true + } + + return false + } + catch (error) { + console.error(`Error processing file ${filePath}:`, error.message) + return false + } +} + +// Add command line argument support +const targetFile = process.argv.find(arg => arg.startsWith('--file='))?.split('=')[1] +const targetLang = process.argv.find(arg => arg.startsWith('--lang='))?.split('=')[1] +const autoRemove = process.argv.includes('--auto-remove') + async function main() { const compareKeysCount = async () => { - const targetKeys = await getKeysFromLanguage(targetLanguage) - const languagesKeys = await Promise.all(languages.map(language => getKeysFromLanguage(language))) + const allTargetKeys = await getKeysFromLanguage(targetLanguage) + + // Filter target keys by file if specified + const targetKeys = targetFile + ? allTargetKeys.filter(key => key.startsWith(targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase()))) + : allTargetKeys + + // Filter languages by target language if specified + const languagesToProcess = targetLang ? [targetLang] : languages + + const allLanguagesKeys = await Promise.all(languagesToProcess.map(language => getKeysFromLanguage(language))) + + // Filter language keys by file if specified + const languagesKeys = targetFile + ? allLanguagesKeys.map(keys => keys.filter(key => key.startsWith(targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase())))) + : allLanguagesKeys const keysCount = languagesKeys.map(keys => keys.length) const targetKeysCount = targetKeys.length - const comparison = languages.reduce((result, language, index) => { + const comparison = languagesToProcess.reduce((result, language, index) => { const languageKeysCount = keysCount[index] const difference = targetKeysCount - languageKeysCount result[language] = difference @@ -96,13 +262,52 @@ async function main() { console.log(comparison) - // Print missing keys - languages.forEach((language, index) => { - const missingKeys = targetKeys.filter(key => !languagesKeys[index].includes(key)) + // Print missing keys and extra keys + for (let index = 0; index < languagesToProcess.length; index++) { + const language = languagesToProcess[index] + const languageKeys = languagesKeys[index] + const missingKeys = targetKeys.filter(key => !languageKeys.includes(key)) + const extraKeys = languageKeys.filter(key => !targetKeys.includes(key)) + console.log(`Missing keys in ${language}:`, missingKeys) - }) + + // Show extra keys only when there are extra keys (negative difference) + if (extraKeys.length > 0) { + console.log(`Extra keys in ${language} (not in ${targetLanguage}):`, extraKeys) + + // Auto-remove extra keys if flag is set + if (autoRemove) { + console.log(`\n🤖 Auto-removing extra keys from ${language}...`) + + // Get all translation files + const i18nFolder = path.resolve(__dirname, '../i18n', language) + const files = fs.readdirSync(i18nFolder) + .filter(file => /\.ts$/.test(file)) + .map(file => file.replace(/\.ts$/, '')) + .filter(f => !targetFile || f === targetFile) // Filter by target file if specified + + let totalRemoved = 0 + for (const fileName of files) { + const removed = await removeExtraKeysFromFile(language, fileName, extraKeys) + if (removed) totalRemoved++ + } + + console.log(`✅ Auto-removal completed for ${language}. Modified ${totalRemoved} files.`) + } + } + } } + console.log('🚀 Starting check-i18n script...') + if (targetFile) + console.log(`📁 Checking file: ${targetFile}`) + + if (targetLang) + console.log(`🌍 Checking language: ${targetLang}`) + + if (autoRemove) + console.log('🤖 Auto-remove mode: ENABLED') + compareKeysCount() } diff --git a/web/i18n/zh-Hans/app-annotation.ts b/web/i18n/zh-Hans/app-annotation.ts index 44d075715..cb2d3be0c 100644 --- a/web/i18n/zh-Hans/app-annotation.ts +++ b/web/i18n/zh-Hans/app-annotation.ts @@ -9,8 +9,6 @@ const translation = { table: { header: { question: '提问', - match: '匹配', - response: '回复', answer: '答案', createdAt: '创建时间', hits: '命中次数', @@ -71,7 +69,6 @@ const translation = { noHitHistory: '没有命中历史', }, hitHistoryTable: { - question: '问题', query: '提问', match: '匹配', response: '回复', diff --git a/web/i18n/zh-Hans/app-debug.ts b/web/i18n/zh-Hans/app-debug.ts index 8bdb56ac6..b58eedb5b 100644 --- a/web/i18n/zh-Hans/app-debug.ts +++ b/web/i18n/zh-Hans/app-debug.ts @@ -254,7 +254,6 @@ const translation = { noDataLine1: '在左侧描述您的用例,', noDataLine2: '编排预览将在此处显示。', apply: '应用', - noData: '在左侧描述您的用例,编排预览将在此处显示。', loading: '为您编排应用程序中…', overwriteTitle: '覆盖现有配置?', overwriteMessage: '应用此提示将覆盖现有配置。', diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index 9e577a360..7c8b292ce 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -35,7 +35,6 @@ const translation = { learnMore: '了解更多', startFromBlank: '创建空白应用', startFromTemplate: '从应用模版创建', - captionAppType: '想要哪种应用类型?', foundResult: '{{count}} 个结果', foundResults: '{{count}} 个结果', noAppsFound: '未找到应用', @@ -45,7 +44,6 @@ const translation = { chatbotUserDescription: '通过简单的配置快速搭建一个基于 LLM 的对话机器人。支持切换为 Chatflow 编排。', completionShortDescription: '用于文本生成任务的 AI 助手', completionUserDescription: '通过简单的配置快速搭建一个面向文本生成类任务的 AI 助手。', - completionWarning: '该类型不久后将不再支持创建', agentShortDescription: '具备推理与自主工具调用的智能助手', agentUserDescription: '能够迭代式的规划推理、自主工具调用,直至完成任务目标的智能助手。', workflowShortDescription: '面向单轮自动化任务的编排工作流', diff --git a/web/i18n/zh-Hans/login.ts b/web/i18n/zh-Hans/login.ts index b63630e28..2276436d0 100644 --- a/web/i18n/zh-Hans/login.ts +++ b/web/i18n/zh-Hans/login.ts @@ -77,7 +77,6 @@ const translation = { activated: '现在登录', adminInitPassword: '管理员初始化密码', validate: '验证', - sso: '使用 SSO 继续', checkCode: { checkYourEmail: '验证您的电子邮件', tips: '验证码已经发送到您的邮箱 {{email}}', diff --git a/web/i18n/zh-Hans/time.ts b/web/i18n/zh-Hans/time.ts index 5158a710b..8a223d9dd 100644 --- a/web/i18n/zh-Hans/time.ts +++ b/web/i18n/zh-Hans/time.ts @@ -26,7 +26,6 @@ const translation = { now: '此刻', ok: '确定', cancel: '取消', - pickDate: '选择日期', }, title: { pickTime: '选择时间', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 81e207f67..1f0300ae2 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -213,7 +213,6 @@ const translation = { startRun: '开始运行', running: '运行中', testRunIteration: '测试运行迭代', - testRunLoop: '测试运行循环', back: '返回', iteration: '迭代', loop: '循环', diff --git a/web/i18n/zh-Hant/app-annotation.ts b/web/i18n/zh-Hant/app-annotation.ts index 02eb98f5d..538546928 100644 --- a/web/i18n/zh-Hant/app-annotation.ts +++ b/web/i18n/zh-Hant/app-annotation.ts @@ -9,8 +9,6 @@ const translation = { table: { header: { question: '提問', - match: '匹配', - response: '回覆', answer: '答案', createdAt: '建立時間', hits: '命中次數', @@ -71,7 +69,6 @@ const translation = { noHitHistory: '沒有命中歷史', }, hitHistoryTable: { - question: '問題', query: '提問', match: '匹配', response: '回覆', diff --git a/web/i18n/zh-Hant/app.ts b/web/i18n/zh-Hant/app.ts index e6a3a0b57..0bf99d506 100644 --- a/web/i18n/zh-Hant/app.ts +++ b/web/i18n/zh-Hant/app.ts @@ -26,21 +26,7 @@ const translation = { newApp: { startFromBlank: '建立空白應用', startFromTemplate: '從應用模版建立', - captionAppType: '想要哪種應用類型?', - chatbotDescription: '使用大型語言模型構建聊天助手', - completionDescription: '構建一個根據提示生成高品質文字的應用程式,例如生成文章、摘要、翻譯等。', - completionWarning: '該類型不久後將不再支援建立', - agentDescription: '構建一個智慧 Agent,可以自主選擇工具來完成任務', - workflowDescription: '以工作流的形式編排生成型應用,提供更多的自訂設定。它適合有經驗的使用者。', workflowWarning: '正在進行 Beta 測試', - chatbotType: '聊天助手編排方法', - basic: '基礎編排', - basicTip: '新手適用,可以切換成工作流編排', - basicFor: '新手適用', - basicDescription: '基本編排允許使用簡單的設定編排聊天機器人應用程式,而無需修改內建提示。它適合初學者。', - advanced: '工作流編排', - advancedFor: '進階使用者適用', - advancedDescription: '工作流編排以工作流的形式編排聊天機器人,提供自訂設定,包括編輯內建提示的能力。它適合有經驗的使用者。', captionName: '應用名稱 & 圖示', appNamePlaceholder: '給你的應用起個名字', captionDescription: '描述', diff --git a/web/i18n/zh-Hant/billing.ts b/web/i18n/zh-Hant/billing.ts index 6ede2c621..f957bc4ea 100644 --- a/web/i18n/zh-Hant/billing.ts +++ b/web/i18n/zh-Hant/billing.ts @@ -23,18 +23,13 @@ const translation = { contractOwner: '聯絡團隊管理員', free: '免費', startForFree: '免費開始', - getStartedWith: '開始使用', contactSales: '聯絡銷售', talkToSales: '聯絡銷售', modelProviders: '支援的模型提供商', - teamMembers: '團隊成員', buildApps: '構建應用程式數', vectorSpace: '向量空間', vectorSpaceTooltip: '向量空間是 LLMs 理解您的資料所需的長期記憶系統。', - vectorSpaceBillingTooltip: '向量儲存是將知識庫向量化處理後為讓 LLMs 理解資料而使用的長期記憶儲存,1MB 大約能滿足 1.2 million character 的向量化後資料儲存(以 OpenAI Embedding 模型估算,不同模型計算方式有差異)。在向量化過程中,實際的壓縮或尺寸減小取決於內容的複雜性和冗餘性。', - documentsUploadQuota: '文件上傳配額', documentProcessingPriority: '文件處理優先順序', - documentProcessingPriorityTip: '如需更高的文件處理優先順序,請升級您的套餐', documentProcessingPriorityUpgrade: '以更快的速度、更高的精度處理更多的資料。', priority: { 'standard': '標準', @@ -103,19 +98,16 @@ const translation = { sandbox: { name: 'Sandbox', description: '200 次 GPT 免費試用', - includesTitle: '包括:', for: '核心功能免費試用', }, professional: { name: 'Professional', description: '讓個人和小團隊能夠以經濟實惠的方式釋放更多能力。', - includesTitle: 'Sandbox 計劃中的一切,加上:', for: '適合獨立開發者/小型團隊', }, team: { name: 'Team', description: '協作無限制並享受頂級效能。', - includesTitle: 'Professional 計劃中的一切,加上:', for: '適用於中型團隊', }, enterprise: { @@ -123,15 +115,6 @@ const translation = { description: '獲得大規模關鍵任務系統的完整功能和支援。', includesTitle: 'Team 計劃中的一切,加上:', features: { - 1: '商業許可證授權', - 6: '先進安全與控制', - 3: '多個工作區及企業管理', - 2: '專屬企業功能', - 4: '單一登入', - 8: '專業技術支援', - 0: '企業級可擴展部署解決方案', - 7: 'Dify 官方的更新和維護', - 5: '由 Dify 合作夥伴協商的服務水平協議', }, price: '自訂', btnText: '聯繫銷售', @@ -140,9 +123,6 @@ const translation = { }, community: { features: { - 0: '所有核心功能均在公共存儲庫下釋出', - 2: '遵循 Dify 開源許可證', - 1: '單一工作區域', }, includesTitle: '免費功能:', btnText: '開始使用社區', @@ -153,10 +133,6 @@ const translation = { }, premium: { features: { - 2: '網頁應用程序標誌及品牌自定義', - 0: '各種雲端服務提供商的自我管理可靠性', - 1: '單一工作區域', - 3: '優先電子郵件及聊天支持', }, for: '適用於中型組織和團隊', comingSoon: '微軟 Azure 與 Google Cloud 支持即將推出', @@ -173,8 +149,6 @@ const translation = { fullSolution: '升級您的套餐以獲得更多空間。', }, apps: { - fullTipLine1: '升級您的套餐以', - fullTipLine2: '構建更多的程式。', fullTip1: '升級以創建更多應用程序', fullTip2des: '建議清除不活躍的應用程式以釋放使用空間,或聯繫我們。', contactUs: '聯繫我們', diff --git a/web/i18n/zh-Hant/common.ts b/web/i18n/zh-Hant/common.ts index 6404d0e00..ccfca85bf 100644 --- a/web/i18n/zh-Hant/common.ts +++ b/web/i18n/zh-Hant/common.ts @@ -197,7 +197,6 @@ const translation = { showAppLength: '顯示 {{length}} 個應用', delete: '刪除帳戶', deleteTip: '刪除您的帳戶將永久刪除您的所有資料並且無法恢復。', - deleteConfirmTip: '請將以下內容從您的註冊電子郵件發送至 ', account: '帳戶', myAccount: '我的帳戶', studio: '工作室', diff --git a/web/i18n/zh-Hant/dataset-creation.ts b/web/i18n/zh-Hant/dataset-creation.ts index fca1ff651..e99fb0c32 100644 --- a/web/i18n/zh-Hant/dataset-creation.ts +++ b/web/i18n/zh-Hant/dataset-creation.ts @@ -1,8 +1,6 @@ const translation = { steps: { header: { - creation: '建立知識庫', - update: '上傳檔案', fallbackRoute: '知識', }, one: '選擇資料來源', diff --git a/web/i18n/zh-Hant/dataset-documents.ts b/web/i18n/zh-Hant/dataset-documents.ts index b04a33907..1b482f181 100644 --- a/web/i18n/zh-Hant/dataset-documents.ts +++ b/web/i18n/zh-Hant/dataset-documents.ts @@ -341,7 +341,6 @@ const translation = { keywords: '關鍵詞', addKeyWord: '新增關鍵詞', keywordError: '關鍵詞最大長度為 20', - characters: '字元', hitCount: '召回次數', vectorHash: '向量雜湊:', questionPlaceholder: '在這裡新增問題', diff --git a/web/i18n/zh-Hant/dataset-hit-testing.ts b/web/i18n/zh-Hant/dataset-hit-testing.ts index 0dbe14902..4b8cc5150 100644 --- a/web/i18n/zh-Hant/dataset-hit-testing.ts +++ b/web/i18n/zh-Hant/dataset-hit-testing.ts @@ -2,7 +2,6 @@ const translation = { title: '召回測試', desc: '基於給定的查詢文字測試知識庫的召回效果。', dateTimeFormat: 'YYYY-MM-DD HH:mm', - recents: '最近查詢', table: { header: { source: '資料來源', diff --git a/web/i18n/zh-Hant/login.ts b/web/i18n/zh-Hant/login.ts index ae617cb5c..818732327 100644 --- a/web/i18n/zh-Hant/login.ts +++ b/web/i18n/zh-Hant/login.ts @@ -70,7 +70,6 @@ const translation = { activated: '現在登入', adminInitPassword: '管理員初始化密碼', validate: '驗證', - sso: '繼續使用 SSO', checkCode: { verify: '驗證', resend: '發送', diff --git a/web/i18n/zh-Hant/tools.ts b/web/i18n/zh-Hant/tools.ts index fbfb09e32..9dad3a74c 100644 --- a/web/i18n/zh-Hant/tools.ts +++ b/web/i18n/zh-Hant/tools.ts @@ -54,7 +54,6 @@ const translation = { keyTooltip: 'HTTP 頭部名稱,如果你不知道是什麼,可以將其保留為 Authorization 或設定為自定義值', types: { none: '無', - api_key: 'API Key', apiKeyPlaceholder: 'HTTP 頭部名稱,用於傳遞 API Key', apiValuePlaceholder: '輸入 API Key', api_key_query: '查詢參數', diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index 935d042fa..bcdfbb81d 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -107,10 +107,8 @@ const translation = { loadMore: '載入更多工作流', noHistory: '無歷史記錄', publishUpdate: '發布更新', - referenceVar: '參考變量', exportSVG: '匯出為 SVG', exportPNG: '匯出為 PNG', - noExist: '沒有這個變數', versionHistory: '版本歷史', exitVersions: '退出版本', exportImage: '匯出圖像', @@ -610,7 +608,6 @@ const translation = { }, select: '選擇', addSubVariable: '子變數', - condition: '條件', }, variableAssigner: { title: '變量賦值',