mirror of
https://github.com/langgenius/dify.git
synced 2025-08-12 11:17:28 +00:00
763 lines
23 KiB
TypeScript
763 lines
23 KiB
TypeScript
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')
|
||
|
||
// 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
|
||
})
|
||
})
|
||
|
||
describe('Auto-remove multiline key-value pairs', () => {
|
||
// Helper function to simulate removeExtraKeysFromFile logic
|
||
function removeExtraKeysFromFile(content: string, keysToRemove: string[]): string {
|
||
const lines = content.split('\n')
|
||
const linesToRemove: number[] = []
|
||
|
||
for (const keyToRemove of keysToRemove) {
|
||
let targetLineIndex = -1
|
||
const linesToRemoveForKey: number[] = []
|
||
|
||
// Find the key line (simplified for single-level keys in test)
|
||
for (let i = 0; i < lines.length; i++) {
|
||
const line = lines[i]
|
||
const keyPattern = new RegExp(`^\\s*${keyToRemove}\\s*:`)
|
||
if (keyPattern.test(line)) {
|
||
targetLineIndex = i
|
||
break
|
||
}
|
||
}
|
||
|
||
if (targetLineIndex !== -1) {
|
||
linesToRemoveForKey.push(targetLineIndex)
|
||
|
||
// Check if this is a multiline key-value pair
|
||
const keyLine = lines[targetLineIndex]
|
||
const trimmedKeyLine = keyLine.trim()
|
||
|
||
// If key line ends with ":" (not complete value), it's likely multiline
|
||
if (trimmedKeyLine.endsWith(':') && !trimmedKeyLine.includes('{') && !trimmedKeyLine.match(/:\s*['"`]/)) {
|
||
// Find the value lines that belong to this key
|
||
let currentLine = targetLineIndex + 1
|
||
let foundValue = false
|
||
|
||
while (currentLine < lines.length) {
|
||
const line = lines[currentLine]
|
||
const trimmed = line.trim()
|
||
|
||
// Skip empty lines
|
||
if (trimmed === '') {
|
||
currentLine++
|
||
continue
|
||
}
|
||
|
||
// Check if this line starts a new key (indicates end of current value)
|
||
if (trimmed.match(/^\w+\s*:/))
|
||
break
|
||
|
||
// Check if this line is part of the value
|
||
if (trimmed.startsWith('\'') || trimmed.startsWith('"') || trimmed.startsWith('`') || foundValue) {
|
||
linesToRemoveForKey.push(currentLine)
|
||
foundValue = true
|
||
|
||
// Check if this line ends the value (ends with quote and comma/no comma)
|
||
if ((trimmed.endsWith('\',') || trimmed.endsWith('",') || trimmed.endsWith('`,')
|
||
|| trimmed.endsWith('\'') || trimmed.endsWith('"') || trimmed.endsWith('`'))
|
||
&& !trimmed.startsWith('//'))
|
||
break
|
||
}
|
||
else {
|
||
break
|
||
}
|
||
|
||
currentLine++
|
||
}
|
||
}
|
||
|
||
linesToRemove.push(...linesToRemoveForKey)
|
||
}
|
||
}
|
||
|
||
// Remove duplicates and sort in reverse order
|
||
const uniqueLinesToRemove = [...new Set(linesToRemove)].sort((a, b) => b - a)
|
||
|
||
for (const lineIndex of uniqueLinesToRemove)
|
||
lines.splice(lineIndex, 1)
|
||
|
||
return lines.join('\n')
|
||
}
|
||
|
||
it('should remove single-line key-value pairs correctly', () => {
|
||
const content = `const translation = {
|
||
keepThis: 'This should stay',
|
||
removeThis: 'This should be removed',
|
||
alsoKeep: 'This should also stay',
|
||
}
|
||
|
||
export default translation`
|
||
|
||
const result = removeExtraKeysFromFile(content, ['removeThis'])
|
||
|
||
expect(result).toContain('keepThis: \'This should stay\'')
|
||
expect(result).toContain('alsoKeep: \'This should also stay\'')
|
||
expect(result).not.toContain('removeThis: \'This should be removed\'')
|
||
})
|
||
|
||
it('should remove multiline key-value pairs completely', () => {
|
||
const content = `const translation = {
|
||
keepThis: 'This should stay',
|
||
removeMultiline:
|
||
'This is a multiline value that should be removed completely',
|
||
alsoKeep: 'This should also stay',
|
||
}
|
||
|
||
export default translation`
|
||
|
||
const result = removeExtraKeysFromFile(content, ['removeMultiline'])
|
||
|
||
expect(result).toContain('keepThis: \'This should stay\'')
|
||
expect(result).toContain('alsoKeep: \'This should also stay\'')
|
||
expect(result).not.toContain('removeMultiline:')
|
||
expect(result).not.toContain('This is a multiline value that should be removed completely')
|
||
})
|
||
|
||
it('should handle mixed single-line and multiline removals', () => {
|
||
const content = `const translation = {
|
||
keepThis: 'Keep this',
|
||
removeSingle: 'Remove this single line',
|
||
removeMultiline:
|
||
'Remove this multiline value',
|
||
anotherMultiline:
|
||
'Another multiline that spans multiple lines',
|
||
keepAnother: 'Keep this too',
|
||
}
|
||
|
||
export default translation`
|
||
|
||
const result = removeExtraKeysFromFile(content, ['removeSingle', 'removeMultiline', 'anotherMultiline'])
|
||
|
||
expect(result).toContain('keepThis: \'Keep this\'')
|
||
expect(result).toContain('keepAnother: \'Keep this too\'')
|
||
expect(result).not.toContain('removeSingle:')
|
||
expect(result).not.toContain('removeMultiline:')
|
||
expect(result).not.toContain('anotherMultiline:')
|
||
expect(result).not.toContain('Remove this single line')
|
||
expect(result).not.toContain('Remove this multiline value')
|
||
expect(result).not.toContain('Another multiline that spans multiple lines')
|
||
})
|
||
|
||
it('should properly detect multiline vs single-line patterns', () => {
|
||
const multilineContent = `const translation = {
|
||
singleLine: 'This is single line',
|
||
multilineKey:
|
||
'This is multiline',
|
||
keyWithColon: 'Value with: colon inside',
|
||
objectKey: {
|
||
nested: 'value'
|
||
},
|
||
}
|
||
|
||
export default translation`
|
||
|
||
// Test that single line with colon in value is not treated as multiline
|
||
const result1 = removeExtraKeysFromFile(multilineContent, ['keyWithColon'])
|
||
expect(result1).not.toContain('keyWithColon:')
|
||
expect(result1).not.toContain('Value with: colon inside')
|
||
|
||
// Test that true multiline is handled correctly
|
||
const result2 = removeExtraKeysFromFile(multilineContent, ['multilineKey'])
|
||
expect(result2).not.toContain('multilineKey:')
|
||
expect(result2).not.toContain('This is multiline')
|
||
|
||
// Test that object key removal works (note: this is a simplified test)
|
||
// In real scenario, object removal would be more complex
|
||
const result3 = removeExtraKeysFromFile(multilineContent, ['objectKey'])
|
||
expect(result3).not.toContain('objectKey: {')
|
||
// Note: Our simplified test function doesn't handle nested object removal perfectly
|
||
// This is acceptable as it's testing the main multiline string removal functionality
|
||
})
|
||
|
||
it('should handle real-world Polish translation structure', () => {
|
||
const polishContent = `const translation = {
|
||
createApp: 'UTWÓRZ APLIKACJĘ',
|
||
newApp: {
|
||
captionAppType: 'Jaki typ aplikacji chcesz stworzyć?',
|
||
chatbotDescription:
|
||
'Zbuduj aplikację opartą na czacie. Ta aplikacja używa formatu pytań i odpowiedzi.',
|
||
agentDescription:
|
||
'Zbuduj inteligentnego agenta, który może autonomicznie wybierać narzędzia.',
|
||
basic: 'Podstawowy',
|
||
},
|
||
}
|
||
|
||
export default translation`
|
||
|
||
const result = removeExtraKeysFromFile(polishContent, ['captionAppType', 'chatbotDescription', 'agentDescription'])
|
||
|
||
expect(result).toContain('createApp: \'UTWÓRZ APLIKACJĘ\'')
|
||
expect(result).toContain('basic: \'Podstawowy\'')
|
||
expect(result).not.toContain('captionAppType:')
|
||
expect(result).not.toContain('chatbotDescription:')
|
||
expect(result).not.toContain('agentDescription:')
|
||
expect(result).not.toContain('Jaki typ aplikacji')
|
||
expect(result).not.toContain('Zbuduj aplikację opartą na czacie')
|
||
expect(result).not.toContain('Zbuduj inteligentnego agenta')
|
||
})
|
||
})
|
||
})
|