mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 22:52:08 +08:00
* feat(i18n): 优化i18n check脚本 * fix: 移除重复的代码行和错误信息 * fix: 重新排序 * fix: i18n sort * test * test * test * test * test * test * feat(i18n): 添加同步翻译脚本并重构检查脚本 重构 check-i18n 脚本为纯检查功能,不再自动修改翻译文件 新增 sync-i18n 脚本用于自动同步翻译文件结构 更新 package.json 添加 sync:i18n 命令 移除不再需要的 husky pre-commit.js 钩子 * docs(i18n): 移除未使用的测试翻译文本 * feat(scripts): 添加对象键名排序功能及测试 添加 lexicalSort 函数和 sortedObjectByKeys 函数用于按字典序排序对象键名 新增测试用例验证排序功能 * feat(i18n): 添加翻译文件键值排序检查功能 添加对i18n翻译文件键值字典序的检查功能,确保翻译文件保持一致的排序结构 * refactor(i18n): 优化同步逻辑并添加键排序功能 移除syncRecursively的返回值检查,简化同步流程 添加对翻译文件的键排序功能,使用sortedObjectByKeys工具 确保主模板和翻译文件在同步后都保持有序 * fix(i18n): 重新排序翻译文件 * style(scripts): 格式化sort.js * chore: 将 test:sort 重命名为 test:scripts
144 lines
4.4 KiB
TypeScript
144 lines
4.4 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 }
|
||
|
||
/**
|
||
* 递归同步 target 对象,使其与 template 对象保持一致
|
||
* 1. 如果 template 中存在 target 中缺少的 key,则添加('[to be translated]')
|
||
* 2. 如果 target 中存在 template 中不存在的 key,则删除
|
||
* 3. 对于子对象,递归同步
|
||
*
|
||
* @param target 目标对象(需要更新的语言对象)
|
||
* @param template 主模板对象(中文)
|
||
* @returns 返回是否对 target 进行了更新
|
||
*/
|
||
function syncRecursively(target: I18N, template: I18N): void {
|
||
// 添加 template 中存在但 target 中缺少的 key
|
||
for (const key in template) {
|
||
if (!(key in target)) {
|
||
target[key] =
|
||
typeof template[key] === 'object' && template[key] !== null ? {} : `[to be translated]:${template[key]}`
|
||
console.log(`添加新属性:${key}`)
|
||
}
|
||
if (typeof template[key] === 'object' && template[key] !== null) {
|
||
if (typeof target[key] !== 'object' || target[key] === null) {
|
||
target[key] = {}
|
||
}
|
||
// 递归同步子对象
|
||
syncRecursively(target[key], template[key])
|
||
}
|
||
}
|
||
|
||
// 删除 target 中存在但 template 中没有的 key
|
||
for (const targetKey in target) {
|
||
if (!(targetKey in template)) {
|
||
console.log(`移除多余属性:${targetKey}`)
|
||
delete target[targetKey]
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查 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 syncTranslations() {
|
||
if (!fs.existsSync(baseFilePath)) {
|
||
console.error(`主模板文件 ${baseFileName} 不存在,请检查路径或文件名`)
|
||
return
|
||
}
|
||
|
||
const baseContent = fs.readFileSync(baseFilePath, 'utf-8')
|
||
let baseJson: I18N = {}
|
||
try {
|
||
baseJson = JSON.parse(baseContent)
|
||
} catch (error) {
|
||
console.error(`解析 ${baseFileName} 出错。${error}`)
|
||
return
|
||
}
|
||
|
||
// 检查主模板是否存在重复键
|
||
const duplicateKeys = checkDuplicateKeys(baseJson)
|
||
if (duplicateKeys.length > 0) {
|
||
throw new Error(`主模板文件 ${baseFileName} 存在以下重复键:\n${duplicateKeys.join('\n')}`)
|
||
}
|
||
|
||
// 为主模板排序
|
||
const sortedJson = sortedObjectByKeys(baseJson)
|
||
if (JSON.stringify(baseJson) !== JSON.stringify(sortedJson)) {
|
||
try {
|
||
fs.writeFileSync(baseFilePath, JSON.stringify(sortedJson, null, 2) + '\n', 'utf-8')
|
||
console.log(`主模板已排序`)
|
||
} catch (error) {
|
||
console.error(`写入 ${baseFilePath} 出错。`, error)
|
||
return
|
||
}
|
||
}
|
||
|
||
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) {
|
||
console.error(`解析 ${file} 出错,跳过此文件。`, error)
|
||
continue
|
||
}
|
||
|
||
syncRecursively(targetJson, baseJson)
|
||
|
||
const sortedJson = sortedObjectByKeys(targetJson)
|
||
|
||
try {
|
||
fs.writeFileSync(filePath, JSON.stringify(sortedJson, null, 2) + '\n', 'utf-8')
|
||
console.log(`文件 ${file} 已排序并同步更新为主模板的内容`)
|
||
} catch (error) {
|
||
console.error(`写入 ${file} 出错。${error}`)
|
||
}
|
||
}
|
||
}
|
||
|
||
syncTranslations()
|