Feat/enhance i18n scripts (#23114)

This commit is contained in:
lyzno1
2025-07-29 18:24:57 +08:00
committed by GitHub
parent 1bf0df03b5
commit f4d4a32af2
18 changed files with 783 additions and 71 deletions

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

View File

@@ -58,9 +58,14 @@ async function getKeysFromLanguage(language) {
const iterateKeys = (obj, prefix = '') => {
for (const key in obj) {
const nestedKey = prefix ? `${prefix}.${key}` : key
nestedKeys.push(nestedKey)
if (typeof obj[key] === 'object' && obj[key] !== null)
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
// This is an object (but not array), recurse into it but don't add it as a key
iterateKeys(obj[key], nestedKey)
}
else {
// This is a leaf node (string, number, boolean, array, etc.), add it as a key
nestedKeys.push(nestedKey)
}
}
}
iterateKeys(translationObj)
@@ -79,15 +84,176 @@ async function getKeysFromLanguage(language) {
})
}
function removeKeysFromObject(obj, keysToRemove, prefix = '') {
let modified = false
for (const key in obj) {
const fullKey = prefix ? `${prefix}.${key}` : key
if (keysToRemove.includes(fullKey)) {
delete obj[key]
modified = true
console.log(`🗑️ Removed key: ${fullKey}`)
}
else if (typeof obj[key] === 'object' && obj[key] !== null) {
const subModified = removeKeysFromObject(obj[key], keysToRemove, fullKey)
modified = modified || subModified
}
}
return modified
}
async function removeExtraKeysFromFile(language, fileName, extraKeys) {
const filePath = path.resolve(__dirname, '../i18n', language, `${fileName}.ts`)
if (!fs.existsSync(filePath)) {
console.log(`⚠️ File not found: ${filePath}`)
return false
}
try {
// Filter keys that belong to this file
const camelCaseFileName = fileName.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
const fileSpecificKeys = extraKeys
.filter(key => key.startsWith(`${camelCaseFileName}.`))
.map(key => key.substring(camelCaseFileName.length + 1)) // Remove file prefix
if (fileSpecificKeys.length === 0)
return false
console.log(`🔄 Processing file: ${filePath}`)
// Read the original file content
const content = fs.readFileSync(filePath, 'utf8')
const lines = content.split('\n')
let modified = false
const linesToRemove = []
// Find lines to remove for each key
for (const keyToRemove of fileSpecificKeys) {
const keyParts = keyToRemove.split('.')
let targetLineIndex = -1
// Build regex pattern for the exact key path
if (keyParts.length === 1) {
// Simple key at root level like "pickDate: 'value'"
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const simpleKeyPattern = new RegExp(`^\\s*${keyParts[0]}\\s*:`)
if (simpleKeyPattern.test(line)) {
targetLineIndex = i
break
}
}
}
else {
// Nested key - need to find the exact path
const currentPath = []
let braceDepth = 0
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const trimmedLine = line.trim()
// Track current object path
const keyMatch = trimmedLine.match(/^(\w+)\s*:\s*{/)
if (keyMatch) {
currentPath.push(keyMatch[1])
braceDepth++
}
else if (trimmedLine === '},' || trimmedLine === '}') {
if (braceDepth > 0) {
braceDepth--
currentPath.pop()
}
}
// Check if this line matches our target key
const leafKeyMatch = trimmedLine.match(/^(\w+)\s*:/)
if (leafKeyMatch) {
const fullPath = [...currentPath, leafKeyMatch[1]]
const fullPathString = fullPath.join('.')
if (fullPathString === keyToRemove) {
targetLineIndex = i
break
}
}
}
}
if (targetLineIndex !== -1) {
linesToRemove.push(targetLineIndex)
console.log(`🗑️ Found key to remove: ${keyToRemove} at line ${targetLineIndex + 1}`)
modified = true
}
else {
console.log(`⚠️ Could not find key: ${keyToRemove}`)
}
}
if (modified) {
// Remove lines in reverse order to maintain correct indices
linesToRemove.sort((a, b) => b - a)
for (const lineIndex of linesToRemove) {
const line = lines[lineIndex]
console.log(`🗑️ Removing line ${lineIndex + 1}: ${line.trim()}`)
lines.splice(lineIndex, 1)
// Also remove trailing comma from previous line if it exists and the next line is a closing brace
if (lineIndex > 0 && lineIndex < lines.length) {
const prevLine = lines[lineIndex - 1]
const nextLine = lines[lineIndex] ? lines[lineIndex].trim() : ''
if (prevLine.trim().endsWith(',') && (nextLine.startsWith('}') || nextLine === ''))
lines[lineIndex - 1] = prevLine.replace(/,\s*$/, '')
}
}
// Write back to file
const newContent = lines.join('\n')
fs.writeFileSync(filePath, newContent)
console.log(`💾 Updated file: ${filePath}`)
return true
}
return false
}
catch (error) {
console.error(`Error processing file ${filePath}:`, error.message)
return false
}
}
// Add command line argument support
const targetFile = process.argv.find(arg => arg.startsWith('--file='))?.split('=')[1]
const targetLang = process.argv.find(arg => arg.startsWith('--lang='))?.split('=')[1]
const autoRemove = process.argv.includes('--auto-remove')
async function main() {
const compareKeysCount = async () => {
const targetKeys = await getKeysFromLanguage(targetLanguage)
const languagesKeys = await Promise.all(languages.map(language => getKeysFromLanguage(language)))
const allTargetKeys = await getKeysFromLanguage(targetLanguage)
// Filter target keys by file if specified
const targetKeys = targetFile
? allTargetKeys.filter(key => key.startsWith(targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase())))
: allTargetKeys
// Filter languages by target language if specified
const languagesToProcess = targetLang ? [targetLang] : languages
const allLanguagesKeys = await Promise.all(languagesToProcess.map(language => getKeysFromLanguage(language)))
// Filter language keys by file if specified
const languagesKeys = targetFile
? allLanguagesKeys.map(keys => keys.filter(key => key.startsWith(targetFile.replace(/[-_](.)/g, (_, c) => c.toUpperCase()))))
: allLanguagesKeys
const keysCount = languagesKeys.map(keys => keys.length)
const targetKeysCount = targetKeys.length
const comparison = languages.reduce((result, language, index) => {
const comparison = languagesToProcess.reduce((result, language, index) => {
const languageKeysCount = keysCount[index]
const difference = targetKeysCount - languageKeysCount
result[language] = difference
@@ -96,13 +262,52 @@ async function main() {
console.log(comparison)
// Print missing keys
languages.forEach((language, index) => {
const missingKeys = targetKeys.filter(key => !languagesKeys[index].includes(key))
// Print missing keys and extra keys
for (let index = 0; index < languagesToProcess.length; index++) {
const language = languagesToProcess[index]
const languageKeys = languagesKeys[index]
const missingKeys = targetKeys.filter(key => !languageKeys.includes(key))
const extraKeys = languageKeys.filter(key => !targetKeys.includes(key))
console.log(`Missing keys in ${language}:`, missingKeys)
})
// Show extra keys only when there are extra keys (negative difference)
if (extraKeys.length > 0) {
console.log(`Extra keys in ${language} (not in ${targetLanguage}):`, extraKeys)
// Auto-remove extra keys if flag is set
if (autoRemove) {
console.log(`\n🤖 Auto-removing extra keys from ${language}...`)
// Get all translation files
const i18nFolder = path.resolve(__dirname, '../i18n', language)
const files = fs.readdirSync(i18nFolder)
.filter(file => /\.ts$/.test(file))
.map(file => file.replace(/\.ts$/, ''))
.filter(f => !targetFile || f === targetFile) // Filter by target file if specified
let totalRemoved = 0
for (const fileName of files) {
const removed = await removeExtraKeysFromFile(language, fileName, extraKeys)
if (removed) totalRemoved++
}
console.log(`✅ Auto-removal completed for ${language}. Modified ${totalRemoved} files.`)
}
}
}
}
console.log('🚀 Starting check-i18n script...')
if (targetFile)
console.log(`📁 Checking file: ${targetFile}`)
if (targetLang)
console.log(`🌍 Checking language: ${targetLang}`)
if (autoRemove)
console.log('🤖 Auto-remove mode: ENABLED')
compareKeysCount()
}

View File

@@ -9,8 +9,6 @@ const translation = {
table: {
header: {
question: '提问',
match: '匹配',
response: '回复',
answer: '答案',
createdAt: '创建时间',
hits: '命中次数',
@@ -71,7 +69,6 @@ const translation = {
noHitHistory: '没有命中历史',
},
hitHistoryTable: {
question: '问题',
query: '提问',
match: '匹配',
response: '回复',

View File

@@ -254,7 +254,6 @@ const translation = {
noDataLine1: '在左侧描述您的用例,',
noDataLine2: '编排预览将在此处显示。',
apply: '应用',
noData: '在左侧描述您的用例,编排预览将在此处显示。',
loading: '为您编排应用程序中…',
overwriteTitle: '覆盖现有配置?',
overwriteMessage: '应用此提示将覆盖现有配置。',

View File

@@ -35,7 +35,6 @@ const translation = {
learnMore: '了解更多',
startFromBlank: '创建空白应用',
startFromTemplate: '从应用模版创建',
captionAppType: '想要哪种应用类型?',
foundResult: '{{count}} 个结果',
foundResults: '{{count}} 个结果',
noAppsFound: '未找到应用',
@@ -45,7 +44,6 @@ const translation = {
chatbotUserDescription: '通过简单的配置快速搭建一个基于 LLM 的对话机器人。支持切换为 Chatflow 编排。',
completionShortDescription: '用于文本生成任务的 AI 助手',
completionUserDescription: '通过简单的配置快速搭建一个面向文本生成类任务的 AI 助手。',
completionWarning: '该类型不久后将不再支持创建',
agentShortDescription: '具备推理与自主工具调用的智能助手',
agentUserDescription: '能够迭代式的规划推理、自主工具调用,直至完成任务目标的智能助手。',
workflowShortDescription: '面向单轮自动化任务的编排工作流',

View File

@@ -77,7 +77,6 @@ const translation = {
activated: '现在登录',
adminInitPassword: '管理员初始化密码',
validate: '验证',
sso: '使用 SSO 继续',
checkCode: {
checkYourEmail: '验证您的电子邮件',
tips: '验证码已经发送到您的邮箱 <strong>{{email}}</strong>',

View File

@@ -26,7 +26,6 @@ const translation = {
now: '此刻',
ok: '确定',
cancel: '取消',
pickDate: '选择日期',
},
title: {
pickTime: '选择时间',

View File

@@ -213,7 +213,6 @@ const translation = {
startRun: '开始运行',
running: '运行中',
testRunIteration: '测试运行迭代',
testRunLoop: '测试运行循环',
back: '返回',
iteration: '迭代',
loop: '循环',

View File

@@ -9,8 +9,6 @@ const translation = {
table: {
header: {
question: '提問',
match: '匹配',
response: '回覆',
answer: '答案',
createdAt: '建立時間',
hits: '命中次數',
@@ -71,7 +69,6 @@ const translation = {
noHitHistory: '沒有命中歷史',
},
hitHistoryTable: {
question: '問題',
query: '提問',
match: '匹配',
response: '回覆',

View File

@@ -26,21 +26,7 @@ const translation = {
newApp: {
startFromBlank: '建立空白應用',
startFromTemplate: '從應用模版建立',
captionAppType: '想要哪種應用類型?',
chatbotDescription: '使用大型語言模型構建聊天助手',
completionDescription: '構建一個根據提示生成高品質文字的應用程式,例如生成文章、摘要、翻譯等。',
completionWarning: '該類型不久後將不再支援建立',
agentDescription: '構建一個智慧 Agent可以自主選擇工具來完成任務',
workflowDescription: '以工作流的形式編排生成型應用,提供更多的自訂設定。它適合有經驗的使用者。',
workflowWarning: '正在進行 Beta 測試',
chatbotType: '聊天助手編排方法',
basic: '基礎編排',
basicTip: '新手適用,可以切換成工作流編排',
basicFor: '新手適用',
basicDescription: '基本編排允許使用簡單的設定編排聊天機器人應用程式,而無需修改內建提示。它適合初學者。',
advanced: '工作流編排',
advancedFor: '進階使用者適用',
advancedDescription: '工作流編排以工作流的形式編排聊天機器人,提供自訂設定,包括編輯內建提示的能力。它適合有經驗的使用者。',
captionName: '應用名稱 & 圖示',
appNamePlaceholder: '給你的應用起個名字',
captionDescription: '描述',

View File

@@ -23,18 +23,13 @@ const translation = {
contractOwner: '聯絡團隊管理員',
free: '免費',
startForFree: '免費開始',
getStartedWith: '開始使用',
contactSales: '聯絡銷售',
talkToSales: '聯絡銷售',
modelProviders: '支援的模型提供商',
teamMembers: '團隊成員',
buildApps: '構建應用程式數',
vectorSpace: '向量空間',
vectorSpaceTooltip: '向量空間是 LLMs 理解您的資料所需的長期記憶系統。',
vectorSpaceBillingTooltip: '向量儲存是將知識庫向量化處理後為讓 LLMs 理解資料而使用的長期記憶儲存1MB 大約能滿足 1.2 million character 的向量化後資料儲存(以 OpenAI Embedding 模型估算,不同模型計算方式有差異)。在向量化過程中,實際的壓縮或尺寸減小取決於內容的複雜性和冗餘性。',
documentsUploadQuota: '文件上傳配額',
documentProcessingPriority: '文件處理優先順序',
documentProcessingPriorityTip: '如需更高的文件處理優先順序,請升級您的套餐',
documentProcessingPriorityUpgrade: '以更快的速度、更高的精度處理更多的資料。',
priority: {
'standard': '標準',
@@ -103,19 +98,16 @@ const translation = {
sandbox: {
name: 'Sandbox',
description: '200 次 GPT 免費試用',
includesTitle: '包括:',
for: '核心功能免費試用',
},
professional: {
name: 'Professional',
description: '讓個人和小團隊能夠以經濟實惠的方式釋放更多能力。',
includesTitle: 'Sandbox 計劃中的一切,加上:',
for: '適合獨立開發者/小型團隊',
},
team: {
name: 'Team',
description: '協作無限制並享受頂級效能。',
includesTitle: 'Professional 計劃中的一切,加上:',
for: '適用於中型團隊',
},
enterprise: {
@@ -123,15 +115,6 @@ const translation = {
description: '獲得大規模關鍵任務系統的完整功能和支援。',
includesTitle: 'Team 計劃中的一切,加上:',
features: {
1: '商業許可證授權',
6: '先進安全與控制',
3: '多個工作區及企業管理',
2: '專屬企業功能',
4: '單一登入',
8: '專業技術支援',
0: '企業級可擴展部署解決方案',
7: 'Dify 官方的更新和維護',
5: '由 Dify 合作夥伴協商的服務水平協議',
},
price: '自訂',
btnText: '聯繫銷售',
@@ -140,9 +123,6 @@ const translation = {
},
community: {
features: {
0: '所有核心功能均在公共存儲庫下釋出',
2: '遵循 Dify 開源許可證',
1: '單一工作區域',
},
includesTitle: '免費功能:',
btnText: '開始使用社區',
@@ -153,10 +133,6 @@ const translation = {
},
premium: {
features: {
2: '網頁應用程序標誌及品牌自定義',
0: '各種雲端服務提供商的自我管理可靠性',
1: '單一工作區域',
3: '優先電子郵件及聊天支持',
},
for: '適用於中型組織和團隊',
comingSoon: '微軟 Azure 與 Google Cloud 支持即將推出',
@@ -173,8 +149,6 @@ const translation = {
fullSolution: '升級您的套餐以獲得更多空間。',
},
apps: {
fullTipLine1: '升級您的套餐以',
fullTipLine2: '構建更多的程式。',
fullTip1: '升級以創建更多應用程序',
fullTip2des: '建議清除不活躍的應用程式以釋放使用空間,或聯繫我們。',
contactUs: '聯繫我們',

View File

@@ -197,7 +197,6 @@ const translation = {
showAppLength: '顯示 {{length}} 個應用',
delete: '刪除帳戶',
deleteTip: '刪除您的帳戶將永久刪除您的所有資料並且無法恢復。',
deleteConfirmTip: '請將以下內容從您的註冊電子郵件發送至 ',
account: '帳戶',
myAccount: '我的帳戶',
studio: '工作室',

View File

@@ -1,8 +1,6 @@
const translation = {
steps: {
header: {
creation: '建立知識庫',
update: '上傳檔案',
fallbackRoute: '知識',
},
one: '選擇資料來源',

View File

@@ -341,7 +341,6 @@ const translation = {
keywords: '關鍵詞',
addKeyWord: '新增關鍵詞',
keywordError: '關鍵詞最大長度為 20',
characters: '字元',
hitCount: '召回次數',
vectorHash: '向量雜湊:',
questionPlaceholder: '在這裡新增問題',

View File

@@ -2,7 +2,6 @@ const translation = {
title: '召回測試',
desc: '基於給定的查詢文字測試知識庫的召回效果。',
dateTimeFormat: 'YYYY-MM-DD HH:mm',
recents: '最近查詢',
table: {
header: {
source: '資料來源',

View File

@@ -70,7 +70,6 @@ const translation = {
activated: '現在登入',
adminInitPassword: '管理員初始化密碼',
validate: '驗證',
sso: '繼續使用 SSO',
checkCode: {
verify: '驗證',
resend: '發送',

View File

@@ -54,7 +54,6 @@ const translation = {
keyTooltip: 'HTTP 頭部名稱,如果你不知道是什麼,可以將其保留為 Authorization 或設定為自定義值',
types: {
none: '無',
api_key: 'API Key',
apiKeyPlaceholder: 'HTTP 頭部名稱,用於傳遞 API Key',
apiValuePlaceholder: '輸入 API Key',
api_key_query: '查詢參數',

View File

@@ -107,10 +107,8 @@ const translation = {
loadMore: '載入更多工作流',
noHistory: '無歷史記錄',
publishUpdate: '發布更新',
referenceVar: '參考變量',
exportSVG: '匯出為 SVG',
exportPNG: '匯出為 PNG',
noExist: '沒有這個變數',
versionHistory: '版本歷史',
exitVersions: '退出版本',
exportImage: '匯出圖像',
@@ -610,7 +608,6 @@ const translation = {
},
select: '選擇',
addSubVariable: '子變數',
condition: '條件',
},
variableAssigner: {
title: '變量賦值',