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 }) }) })