Feat/enhance i18n scripts (#23114)
This commit is contained in:
569
web/__tests__/check-i18n.test.ts
Normal file
569
web/__tests__/check-i18n.test.ts
Normal file
@@ -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<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 = 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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@@ -58,10 +58,15 @@ async function getKeysFromLanguage(language) {
|
|||||||
const iterateKeys = (obj, prefix = '') => {
|
const iterateKeys = (obj, prefix = '') => {
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
const nestedKey = prefix ? `${prefix}.${key}` : key
|
const nestedKey = prefix ? `${prefix}.${key}` : key
|
||||||
nestedKeys.push(nestedKey)
|
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
||||||
if (typeof obj[key] === 'object' && obj[key] !== null)
|
// This is an object (but not array), recurse into it but don't add it as a key
|
||||||
iterateKeys(obj[key], nestedKey)
|
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)
|
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() {
|
async function main() {
|
||||||
const compareKeysCount = async () => {
|
const compareKeysCount = async () => {
|
||||||
const targetKeys = await getKeysFromLanguage(targetLanguage)
|
const allTargetKeys = await getKeysFromLanguage(targetLanguage)
|
||||||
const languagesKeys = await Promise.all(languages.map(language => getKeysFromLanguage(language)))
|
|
||||||
|
// 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 keysCount = languagesKeys.map(keys => keys.length)
|
||||||
const targetKeysCount = targetKeys.length
|
const targetKeysCount = targetKeys.length
|
||||||
|
|
||||||
const comparison = languages.reduce((result, language, index) => {
|
const comparison = languagesToProcess.reduce((result, language, index) => {
|
||||||
const languageKeysCount = keysCount[index]
|
const languageKeysCount = keysCount[index]
|
||||||
const difference = targetKeysCount - languageKeysCount
|
const difference = targetKeysCount - languageKeysCount
|
||||||
result[language] = difference
|
result[language] = difference
|
||||||
@@ -96,13 +262,52 @@ async function main() {
|
|||||||
|
|
||||||
console.log(comparison)
|
console.log(comparison)
|
||||||
|
|
||||||
// Print missing keys
|
// Print missing keys and extra keys
|
||||||
languages.forEach((language, index) => {
|
for (let index = 0; index < languagesToProcess.length; index++) {
|
||||||
const missingKeys = targetKeys.filter(key => !languagesKeys[index].includes(key))
|
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)
|
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()
|
compareKeysCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -9,8 +9,6 @@ const translation = {
|
|||||||
table: {
|
table: {
|
||||||
header: {
|
header: {
|
||||||
question: '提问',
|
question: '提问',
|
||||||
match: '匹配',
|
|
||||||
response: '回复',
|
|
||||||
answer: '答案',
|
answer: '答案',
|
||||||
createdAt: '创建时间',
|
createdAt: '创建时间',
|
||||||
hits: '命中次数',
|
hits: '命中次数',
|
||||||
@@ -71,7 +69,6 @@ const translation = {
|
|||||||
noHitHistory: '没有命中历史',
|
noHitHistory: '没有命中历史',
|
||||||
},
|
},
|
||||||
hitHistoryTable: {
|
hitHistoryTable: {
|
||||||
question: '问题',
|
|
||||||
query: '提问',
|
query: '提问',
|
||||||
match: '匹配',
|
match: '匹配',
|
||||||
response: '回复',
|
response: '回复',
|
||||||
|
@@ -254,7 +254,6 @@ const translation = {
|
|||||||
noDataLine1: '在左侧描述您的用例,',
|
noDataLine1: '在左侧描述您的用例,',
|
||||||
noDataLine2: '编排预览将在此处显示。',
|
noDataLine2: '编排预览将在此处显示。',
|
||||||
apply: '应用',
|
apply: '应用',
|
||||||
noData: '在左侧描述您的用例,编排预览将在此处显示。',
|
|
||||||
loading: '为您编排应用程序中…',
|
loading: '为您编排应用程序中…',
|
||||||
overwriteTitle: '覆盖现有配置?',
|
overwriteTitle: '覆盖现有配置?',
|
||||||
overwriteMessage: '应用此提示将覆盖现有配置。',
|
overwriteMessage: '应用此提示将覆盖现有配置。',
|
||||||
|
@@ -35,7 +35,6 @@ const translation = {
|
|||||||
learnMore: '了解更多',
|
learnMore: '了解更多',
|
||||||
startFromBlank: '创建空白应用',
|
startFromBlank: '创建空白应用',
|
||||||
startFromTemplate: '从应用模版创建',
|
startFromTemplate: '从应用模版创建',
|
||||||
captionAppType: '想要哪种应用类型?',
|
|
||||||
foundResult: '{{count}} 个结果',
|
foundResult: '{{count}} 个结果',
|
||||||
foundResults: '{{count}} 个结果',
|
foundResults: '{{count}} 个结果',
|
||||||
noAppsFound: '未找到应用',
|
noAppsFound: '未找到应用',
|
||||||
@@ -45,7 +44,6 @@ const translation = {
|
|||||||
chatbotUserDescription: '通过简单的配置快速搭建一个基于 LLM 的对话机器人。支持切换为 Chatflow 编排。',
|
chatbotUserDescription: '通过简单的配置快速搭建一个基于 LLM 的对话机器人。支持切换为 Chatflow 编排。',
|
||||||
completionShortDescription: '用于文本生成任务的 AI 助手',
|
completionShortDescription: '用于文本生成任务的 AI 助手',
|
||||||
completionUserDescription: '通过简单的配置快速搭建一个面向文本生成类任务的 AI 助手。',
|
completionUserDescription: '通过简单的配置快速搭建一个面向文本生成类任务的 AI 助手。',
|
||||||
completionWarning: '该类型不久后将不再支持创建',
|
|
||||||
agentShortDescription: '具备推理与自主工具调用的智能助手',
|
agentShortDescription: '具备推理与自主工具调用的智能助手',
|
||||||
agentUserDescription: '能够迭代式的规划推理、自主工具调用,直至完成任务目标的智能助手。',
|
agentUserDescription: '能够迭代式的规划推理、自主工具调用,直至完成任务目标的智能助手。',
|
||||||
workflowShortDescription: '面向单轮自动化任务的编排工作流',
|
workflowShortDescription: '面向单轮自动化任务的编排工作流',
|
||||||
|
@@ -77,7 +77,6 @@ const translation = {
|
|||||||
activated: '现在登录',
|
activated: '现在登录',
|
||||||
adminInitPassword: '管理员初始化密码',
|
adminInitPassword: '管理员初始化密码',
|
||||||
validate: '验证',
|
validate: '验证',
|
||||||
sso: '使用 SSO 继续',
|
|
||||||
checkCode: {
|
checkCode: {
|
||||||
checkYourEmail: '验证您的电子邮件',
|
checkYourEmail: '验证您的电子邮件',
|
||||||
tips: '验证码已经发送到您的邮箱 <strong>{{email}}</strong>',
|
tips: '验证码已经发送到您的邮箱 <strong>{{email}}</strong>',
|
||||||
|
@@ -26,7 +26,6 @@ const translation = {
|
|||||||
now: '此刻',
|
now: '此刻',
|
||||||
ok: '确定',
|
ok: '确定',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
pickDate: '选择日期',
|
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
pickTime: '选择时间',
|
pickTime: '选择时间',
|
||||||
|
@@ -213,7 +213,6 @@ const translation = {
|
|||||||
startRun: '开始运行',
|
startRun: '开始运行',
|
||||||
running: '运行中',
|
running: '运行中',
|
||||||
testRunIteration: '测试运行迭代',
|
testRunIteration: '测试运行迭代',
|
||||||
testRunLoop: '测试运行循环',
|
|
||||||
back: '返回',
|
back: '返回',
|
||||||
iteration: '迭代',
|
iteration: '迭代',
|
||||||
loop: '循环',
|
loop: '循环',
|
||||||
|
@@ -9,8 +9,6 @@ const translation = {
|
|||||||
table: {
|
table: {
|
||||||
header: {
|
header: {
|
||||||
question: '提問',
|
question: '提問',
|
||||||
match: '匹配',
|
|
||||||
response: '回覆',
|
|
||||||
answer: '答案',
|
answer: '答案',
|
||||||
createdAt: '建立時間',
|
createdAt: '建立時間',
|
||||||
hits: '命中次數',
|
hits: '命中次數',
|
||||||
@@ -71,7 +69,6 @@ const translation = {
|
|||||||
noHitHistory: '沒有命中歷史',
|
noHitHistory: '沒有命中歷史',
|
||||||
},
|
},
|
||||||
hitHistoryTable: {
|
hitHistoryTable: {
|
||||||
question: '問題',
|
|
||||||
query: '提問',
|
query: '提問',
|
||||||
match: '匹配',
|
match: '匹配',
|
||||||
response: '回覆',
|
response: '回覆',
|
||||||
|
@@ -26,21 +26,7 @@ const translation = {
|
|||||||
newApp: {
|
newApp: {
|
||||||
startFromBlank: '建立空白應用',
|
startFromBlank: '建立空白應用',
|
||||||
startFromTemplate: '從應用模版建立',
|
startFromTemplate: '從應用模版建立',
|
||||||
captionAppType: '想要哪種應用類型?',
|
|
||||||
chatbotDescription: '使用大型語言模型構建聊天助手',
|
|
||||||
completionDescription: '構建一個根據提示生成高品質文字的應用程式,例如生成文章、摘要、翻譯等。',
|
|
||||||
completionWarning: '該類型不久後將不再支援建立',
|
|
||||||
agentDescription: '構建一個智慧 Agent,可以自主選擇工具來完成任務',
|
|
||||||
workflowDescription: '以工作流的形式編排生成型應用,提供更多的自訂設定。它適合有經驗的使用者。',
|
|
||||||
workflowWarning: '正在進行 Beta 測試',
|
workflowWarning: '正在進行 Beta 測試',
|
||||||
chatbotType: '聊天助手編排方法',
|
|
||||||
basic: '基礎編排',
|
|
||||||
basicTip: '新手適用,可以切換成工作流編排',
|
|
||||||
basicFor: '新手適用',
|
|
||||||
basicDescription: '基本編排允許使用簡單的設定編排聊天機器人應用程式,而無需修改內建提示。它適合初學者。',
|
|
||||||
advanced: '工作流編排',
|
|
||||||
advancedFor: '進階使用者適用',
|
|
||||||
advancedDescription: '工作流編排以工作流的形式編排聊天機器人,提供自訂設定,包括編輯內建提示的能力。它適合有經驗的使用者。',
|
|
||||||
captionName: '應用名稱 & 圖示',
|
captionName: '應用名稱 & 圖示',
|
||||||
appNamePlaceholder: '給你的應用起個名字',
|
appNamePlaceholder: '給你的應用起個名字',
|
||||||
captionDescription: '描述',
|
captionDescription: '描述',
|
||||||
|
@@ -23,18 +23,13 @@ const translation = {
|
|||||||
contractOwner: '聯絡團隊管理員',
|
contractOwner: '聯絡團隊管理員',
|
||||||
free: '免費',
|
free: '免費',
|
||||||
startForFree: '免費開始',
|
startForFree: '免費開始',
|
||||||
getStartedWith: '開始使用',
|
|
||||||
contactSales: '聯絡銷售',
|
contactSales: '聯絡銷售',
|
||||||
talkToSales: '聯絡銷售',
|
talkToSales: '聯絡銷售',
|
||||||
modelProviders: '支援的模型提供商',
|
modelProviders: '支援的模型提供商',
|
||||||
teamMembers: '團隊成員',
|
|
||||||
buildApps: '構建應用程式數',
|
buildApps: '構建應用程式數',
|
||||||
vectorSpace: '向量空間',
|
vectorSpace: '向量空間',
|
||||||
vectorSpaceTooltip: '向量空間是 LLMs 理解您的資料所需的長期記憶系統。',
|
vectorSpaceTooltip: '向量空間是 LLMs 理解您的資料所需的長期記憶系統。',
|
||||||
vectorSpaceBillingTooltip: '向量儲存是將知識庫向量化處理後為讓 LLMs 理解資料而使用的長期記憶儲存,1MB 大約能滿足 1.2 million character 的向量化後資料儲存(以 OpenAI Embedding 模型估算,不同模型計算方式有差異)。在向量化過程中,實際的壓縮或尺寸減小取決於內容的複雜性和冗餘性。',
|
|
||||||
documentsUploadQuota: '文件上傳配額',
|
|
||||||
documentProcessingPriority: '文件處理優先順序',
|
documentProcessingPriority: '文件處理優先順序',
|
||||||
documentProcessingPriorityTip: '如需更高的文件處理優先順序,請升級您的套餐',
|
|
||||||
documentProcessingPriorityUpgrade: '以更快的速度、更高的精度處理更多的資料。',
|
documentProcessingPriorityUpgrade: '以更快的速度、更高的精度處理更多的資料。',
|
||||||
priority: {
|
priority: {
|
||||||
'standard': '標準',
|
'standard': '標準',
|
||||||
@@ -103,19 +98,16 @@ const translation = {
|
|||||||
sandbox: {
|
sandbox: {
|
||||||
name: 'Sandbox',
|
name: 'Sandbox',
|
||||||
description: '200 次 GPT 免費試用',
|
description: '200 次 GPT 免費試用',
|
||||||
includesTitle: '包括:',
|
|
||||||
for: '核心功能免費試用',
|
for: '核心功能免費試用',
|
||||||
},
|
},
|
||||||
professional: {
|
professional: {
|
||||||
name: 'Professional',
|
name: 'Professional',
|
||||||
description: '讓個人和小團隊能夠以經濟實惠的方式釋放更多能力。',
|
description: '讓個人和小團隊能夠以經濟實惠的方式釋放更多能力。',
|
||||||
includesTitle: 'Sandbox 計劃中的一切,加上:',
|
|
||||||
for: '適合獨立開發者/小型團隊',
|
for: '適合獨立開發者/小型團隊',
|
||||||
},
|
},
|
||||||
team: {
|
team: {
|
||||||
name: 'Team',
|
name: 'Team',
|
||||||
description: '協作無限制並享受頂級效能。',
|
description: '協作無限制並享受頂級效能。',
|
||||||
includesTitle: 'Professional 計劃中的一切,加上:',
|
|
||||||
for: '適用於中型團隊',
|
for: '適用於中型團隊',
|
||||||
},
|
},
|
||||||
enterprise: {
|
enterprise: {
|
||||||
@@ -123,15 +115,6 @@ const translation = {
|
|||||||
description: '獲得大規模關鍵任務系統的完整功能和支援。',
|
description: '獲得大規模關鍵任務系統的完整功能和支援。',
|
||||||
includesTitle: 'Team 計劃中的一切,加上:',
|
includesTitle: 'Team 計劃中的一切,加上:',
|
||||||
features: {
|
features: {
|
||||||
1: '商業許可證授權',
|
|
||||||
6: '先進安全與控制',
|
|
||||||
3: '多個工作區及企業管理',
|
|
||||||
2: '專屬企業功能',
|
|
||||||
4: '單一登入',
|
|
||||||
8: '專業技術支援',
|
|
||||||
0: '企業級可擴展部署解決方案',
|
|
||||||
7: 'Dify 官方的更新和維護',
|
|
||||||
5: '由 Dify 合作夥伴協商的服務水平協議',
|
|
||||||
},
|
},
|
||||||
price: '自訂',
|
price: '自訂',
|
||||||
btnText: '聯繫銷售',
|
btnText: '聯繫銷售',
|
||||||
@@ -140,9 +123,6 @@ const translation = {
|
|||||||
},
|
},
|
||||||
community: {
|
community: {
|
||||||
features: {
|
features: {
|
||||||
0: '所有核心功能均在公共存儲庫下釋出',
|
|
||||||
2: '遵循 Dify 開源許可證',
|
|
||||||
1: '單一工作區域',
|
|
||||||
},
|
},
|
||||||
includesTitle: '免費功能:',
|
includesTitle: '免費功能:',
|
||||||
btnText: '開始使用社區',
|
btnText: '開始使用社區',
|
||||||
@@ -153,10 +133,6 @@ const translation = {
|
|||||||
},
|
},
|
||||||
premium: {
|
premium: {
|
||||||
features: {
|
features: {
|
||||||
2: '網頁應用程序標誌及品牌自定義',
|
|
||||||
0: '各種雲端服務提供商的自我管理可靠性',
|
|
||||||
1: '單一工作區域',
|
|
||||||
3: '優先電子郵件及聊天支持',
|
|
||||||
},
|
},
|
||||||
for: '適用於中型組織和團隊',
|
for: '適用於中型組織和團隊',
|
||||||
comingSoon: '微軟 Azure 與 Google Cloud 支持即將推出',
|
comingSoon: '微軟 Azure 與 Google Cloud 支持即將推出',
|
||||||
@@ -173,8 +149,6 @@ const translation = {
|
|||||||
fullSolution: '升級您的套餐以獲得更多空間。',
|
fullSolution: '升級您的套餐以獲得更多空間。',
|
||||||
},
|
},
|
||||||
apps: {
|
apps: {
|
||||||
fullTipLine1: '升級您的套餐以',
|
|
||||||
fullTipLine2: '構建更多的程式。',
|
|
||||||
fullTip1: '升級以創建更多應用程序',
|
fullTip1: '升級以創建更多應用程序',
|
||||||
fullTip2des: '建議清除不活躍的應用程式以釋放使用空間,或聯繫我們。',
|
fullTip2des: '建議清除不活躍的應用程式以釋放使用空間,或聯繫我們。',
|
||||||
contactUs: '聯繫我們',
|
contactUs: '聯繫我們',
|
||||||
|
@@ -197,7 +197,6 @@ const translation = {
|
|||||||
showAppLength: '顯示 {{length}} 個應用',
|
showAppLength: '顯示 {{length}} 個應用',
|
||||||
delete: '刪除帳戶',
|
delete: '刪除帳戶',
|
||||||
deleteTip: '刪除您的帳戶將永久刪除您的所有資料並且無法恢復。',
|
deleteTip: '刪除您的帳戶將永久刪除您的所有資料並且無法恢復。',
|
||||||
deleteConfirmTip: '請將以下內容從您的註冊電子郵件發送至 ',
|
|
||||||
account: '帳戶',
|
account: '帳戶',
|
||||||
myAccount: '我的帳戶',
|
myAccount: '我的帳戶',
|
||||||
studio: '工作室',
|
studio: '工作室',
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
const translation = {
|
const translation = {
|
||||||
steps: {
|
steps: {
|
||||||
header: {
|
header: {
|
||||||
creation: '建立知識庫',
|
|
||||||
update: '上傳檔案',
|
|
||||||
fallbackRoute: '知識',
|
fallbackRoute: '知識',
|
||||||
},
|
},
|
||||||
one: '選擇資料來源',
|
one: '選擇資料來源',
|
||||||
|
@@ -341,7 +341,6 @@ const translation = {
|
|||||||
keywords: '關鍵詞',
|
keywords: '關鍵詞',
|
||||||
addKeyWord: '新增關鍵詞',
|
addKeyWord: '新增關鍵詞',
|
||||||
keywordError: '關鍵詞最大長度為 20',
|
keywordError: '關鍵詞最大長度為 20',
|
||||||
characters: '字元',
|
|
||||||
hitCount: '召回次數',
|
hitCount: '召回次數',
|
||||||
vectorHash: '向量雜湊:',
|
vectorHash: '向量雜湊:',
|
||||||
questionPlaceholder: '在這裡新增問題',
|
questionPlaceholder: '在這裡新增問題',
|
||||||
|
@@ -2,7 +2,6 @@ const translation = {
|
|||||||
title: '召回測試',
|
title: '召回測試',
|
||||||
desc: '基於給定的查詢文字測試知識庫的召回效果。',
|
desc: '基於給定的查詢文字測試知識庫的召回效果。',
|
||||||
dateTimeFormat: 'YYYY-MM-DD HH:mm',
|
dateTimeFormat: 'YYYY-MM-DD HH:mm',
|
||||||
recents: '最近查詢',
|
|
||||||
table: {
|
table: {
|
||||||
header: {
|
header: {
|
||||||
source: '資料來源',
|
source: '資料來源',
|
||||||
|
@@ -70,7 +70,6 @@ const translation = {
|
|||||||
activated: '現在登入',
|
activated: '現在登入',
|
||||||
adminInitPassword: '管理員初始化密碼',
|
adminInitPassword: '管理員初始化密碼',
|
||||||
validate: '驗證',
|
validate: '驗證',
|
||||||
sso: '繼續使用 SSO',
|
|
||||||
checkCode: {
|
checkCode: {
|
||||||
verify: '驗證',
|
verify: '驗證',
|
||||||
resend: '發送',
|
resend: '發送',
|
||||||
|
@@ -54,7 +54,6 @@ const translation = {
|
|||||||
keyTooltip: 'HTTP 頭部名稱,如果你不知道是什麼,可以將其保留為 Authorization 或設定為自定義值',
|
keyTooltip: 'HTTP 頭部名稱,如果你不知道是什麼,可以將其保留為 Authorization 或設定為自定義值',
|
||||||
types: {
|
types: {
|
||||||
none: '無',
|
none: '無',
|
||||||
api_key: 'API Key',
|
|
||||||
apiKeyPlaceholder: 'HTTP 頭部名稱,用於傳遞 API Key',
|
apiKeyPlaceholder: 'HTTP 頭部名稱,用於傳遞 API Key',
|
||||||
apiValuePlaceholder: '輸入 API Key',
|
apiValuePlaceholder: '輸入 API Key',
|
||||||
api_key_query: '查詢參數',
|
api_key_query: '查詢參數',
|
||||||
|
@@ -107,10 +107,8 @@ const translation = {
|
|||||||
loadMore: '載入更多工作流',
|
loadMore: '載入更多工作流',
|
||||||
noHistory: '無歷史記錄',
|
noHistory: '無歷史記錄',
|
||||||
publishUpdate: '發布更新',
|
publishUpdate: '發布更新',
|
||||||
referenceVar: '參考變量',
|
|
||||||
exportSVG: '匯出為 SVG',
|
exportSVG: '匯出為 SVG',
|
||||||
exportPNG: '匯出為 PNG',
|
exportPNG: '匯出為 PNG',
|
||||||
noExist: '沒有這個變數',
|
|
||||||
versionHistory: '版本歷史',
|
versionHistory: '版本歷史',
|
||||||
exitVersions: '退出版本',
|
exitVersions: '退出版本',
|
||||||
exportImage: '匯出圖像',
|
exportImage: '匯出圖像',
|
||||||
@@ -610,7 +608,6 @@ const translation = {
|
|||||||
},
|
},
|
||||||
select: '選擇',
|
select: '選擇',
|
||||||
addSubVariable: '子變數',
|
addSubVariable: '子變數',
|
||||||
condition: '條件',
|
|
||||||
},
|
},
|
||||||
variableAssigner: {
|
variableAssigner: {
|
||||||
title: '變量賦值',
|
title: '變量賦值',
|
||||||
|
Reference in New Issue
Block a user