mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 22:52:08 +08:00
* chore(i18n): 更新i18n文件为嵌套结构以适应插件 * feat(i18n): 添加自动翻译脚本处理待翻译文本 添加自动翻译脚本auto-translate-i18n.ts,用于处理以[to be translated]开头的待翻译文本 在package.json中添加对应的运行命令auto:i18n * chore(i18n): 更新嵌套结构 * chore(i18n): 更新多语言翻译文件并改进翻译逻辑 更新了多个语言的翻译文件,替换了"[to be translated]"标记为实际翻译内容 改进auto-translate-i18n.ts中的翻译逻辑,添加错误处理和日志输出 部分数组格式的翻译描述自动改为对象格式 * fix(i18n): 修复嵌套结构检查并改进错误处理 添加对嵌套结构中使用点符号的检查,确保使用严格嵌套结构 改进错误处理,在检查失败时输出更清晰的错误信息 * fix(测试): 更新下载失败测试中的翻译键名 * test(下载): 移除重复的下载失败翻译并更新测试 * feat(eslint): 添加规则,警告不建议在t()函数中使用模板字符串 * style: 使用单引号替换模板字符串中的反引号 * docs(.vscode): 添加i18n-ally扩展推荐到vscode配置 * fix: 在自动翻译脚本中停止进度条显示 确保在脚本执行完成后正确停止进度条,避免控制台输出混乱 * fix(i18n): 修复模型列表添加确认对话框的翻译键名 更新多语言文件中模型管理部分的翻译结构,将"add_listed"从字符串改为包含"confirm"和"key"的对象 同时修正EditModelsPopup组件中对应的翻译键引用 * chore: 注释掉i18n-ally命名空间配置 * docs: 添加国际化(i18n)最佳实践文档 添加中英文双语的技术文档,详细介绍项目中的i18n实现方案、工具链和最佳实践 包含i18n ally插件使用指南、自动化脚本说明以及代码规范要求 * docs(国际化): 更新i18n文档中的键名格式示例 将文档中错误的flat格式示例从下划线命名改为点分隔命名,以保持一致性 * refactor(i18n): 统一翻译键名从.key后缀改为.label后缀 * chore(i18n): sort * refactor(locales): 使用 Object.fromEntries 重构 locales 对象 * feat(i18n): 添加机器翻译的语言支持 新增希腊语、西班牙语、法语和葡萄牙语的机器翻译支持,并调整语言资源加载顺序
153 lines
4.7 KiB
TypeScript
153 lines
4.7 KiB
TypeScript
import * as fs from 'fs'
|
|
import * as path from 'path'
|
|
|
|
import { sortedObjectByKeys } from './sort'
|
|
|
|
const translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
|
|
const baseLocale = 'zh-cn'
|
|
const baseFileName = `${baseLocale}.json`
|
|
const baseFilePath = path.join(translationsDir, baseFileName)
|
|
|
|
type I18NValue = string | { [key: string]: I18NValue }
|
|
type I18N = { [key: string]: I18NValue }
|
|
|
|
/**
|
|
* 递归检查并同步目标对象与模板对象的键值结构
|
|
* 1. 如果目标对象缺少模板对象中的键,抛出错误
|
|
* 2. 如果目标对象存在模板对象中不存在的键,抛出错误
|
|
* 3. 对于嵌套对象,递归执行同步操作
|
|
*
|
|
* 该函数用于确保所有翻译文件与基准模板(通常是中文翻译文件)保持完全一致的键值结构。
|
|
* 任何结构上的差异都会导致错误被抛出,以便及时发现和修复翻译文件中的问题。
|
|
*
|
|
* @param target 需要检查的目标翻译对象
|
|
* @param template 作为基准的模板对象(通常是中文翻译文件)
|
|
* @throws {Error} 当发现键值结构不匹配时抛出错误
|
|
*/
|
|
function checkRecursively(target: I18N, template: I18N): void {
|
|
for (const key in template) {
|
|
if (!(key in target)) {
|
|
throw new Error(`缺少属性 ${key}`)
|
|
}
|
|
if (key.includes('.')) {
|
|
throw new Error(`应该使用严格嵌套结构 ${key}`)
|
|
}
|
|
if (typeof template[key] === 'object' && template[key] !== null) {
|
|
if (typeof target[key] !== 'object' || target[key] === null) {
|
|
throw new Error(`属性 ${key} 不是对象`)
|
|
}
|
|
// 递归检查子对象
|
|
checkRecursively(target[key], template[key])
|
|
}
|
|
}
|
|
|
|
// 删除 target 中存在但 template 中没有的 key
|
|
for (const targetKey in target) {
|
|
if (!(targetKey in template)) {
|
|
throw new Error(`多余属性 ${targetKey}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
function isSortedI18N(obj: I18N): boolean {
|
|
// fs.writeFileSync('./test_origin.json', JSON.stringify(obj))
|
|
// fs.writeFileSync('./test_sorted.json', JSON.stringify(sortedObjectByKeys(obj)))
|
|
return JSON.stringify(obj) === JSON.stringify(sortedObjectByKeys(obj))
|
|
}
|
|
|
|
/**
|
|
* 检查 JSON 对象中是否存在重复键,并收集所有重复键
|
|
* @param obj 要检查的对象
|
|
* @returns 返回重复键的数组(若无重复则返回空数组)
|
|
*/
|
|
function checkDuplicateKeys(obj: I18N): string[] {
|
|
const keys = new Set<string>()
|
|
const duplicateKeys: string[] = []
|
|
|
|
const checkObject = (obj: I18N, path: string = '') => {
|
|
for (const key in obj) {
|
|
const fullPath = path ? `${path}.${key}` : key
|
|
|
|
if (keys.has(fullPath)) {
|
|
// 发现重复键时,添加到数组中(避免重复添加)
|
|
if (!duplicateKeys.includes(fullPath)) {
|
|
duplicateKeys.push(fullPath)
|
|
}
|
|
} else {
|
|
keys.add(fullPath)
|
|
}
|
|
|
|
// 递归检查子对象
|
|
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
checkObject(obj[key], fullPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
checkObject(obj)
|
|
return duplicateKeys
|
|
}
|
|
|
|
function checkTranslations() {
|
|
if (!fs.existsSync(baseFilePath)) {
|
|
throw new Error(`主模板文件 ${baseFileName} 不存在,请检查路径或文件名`)
|
|
}
|
|
|
|
const baseContent = fs.readFileSync(baseFilePath, 'utf-8')
|
|
let baseJson: I18N = {}
|
|
try {
|
|
baseJson = JSON.parse(baseContent)
|
|
} catch (error) {
|
|
throw new Error(`解析 ${baseFileName} 出错。${error}`)
|
|
}
|
|
|
|
// 检查主模板是否存在重复键
|
|
const duplicateKeys = checkDuplicateKeys(baseJson)
|
|
if (duplicateKeys.length > 0) {
|
|
throw new Error(`主模板文件 ${baseFileName} 存在以下重复键:\n${duplicateKeys.join('\n')}`)
|
|
}
|
|
|
|
// 检查主模板是否有序
|
|
if (!isSortedI18N(baseJson)) {
|
|
throw new Error(`主模板文件 ${baseFileName} 的键值未按字典序排序。`)
|
|
}
|
|
|
|
const files = fs.readdirSync(translationsDir).filter((file) => file.endsWith('.json') && file !== baseFileName)
|
|
|
|
// 同步键
|
|
for (const file of files) {
|
|
const filePath = path.join(translationsDir, file)
|
|
let targetJson: I18N = {}
|
|
try {
|
|
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
|
targetJson = JSON.parse(fileContent)
|
|
} catch (error) {
|
|
throw new Error(`解析 ${file} 出错。`)
|
|
}
|
|
|
|
// 检查有序性
|
|
if (!isSortedI18N(targetJson)) {
|
|
throw new Error(`翻译文件 ${file} 的键值未按字典序排序。`)
|
|
}
|
|
|
|
try {
|
|
checkRecursively(targetJson, baseJson)
|
|
} catch (e) {
|
|
console.error(e)
|
|
throw new Error(`在检查 ${filePath} 时出错`)
|
|
}
|
|
}
|
|
}
|
|
|
|
export function main() {
|
|
try {
|
|
checkTranslations()
|
|
console.log('i18n 检查已通过')
|
|
} catch (e) {
|
|
console.error(e)
|
|
throw new Error(`检查未通过。尝试运行 yarn sync:i18n 以解决问题。`)
|
|
}
|
|
}
|
|
|
|
main()
|