| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566 |
- 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<string[]> {
- 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 = (context.module.exports as any).default || context.module.exports
-
- 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())),
- )
-
- 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
- })
- })
- })
|