cherry-studio/scripts/check-i18n.ts
icarus 7365c1ca1a docs(scripts): translate comments and error messages to english
Update documentation and error messages in i18n-related scripts from Chinese to English to improve codebase accessibility for international teams.
2025-10-23 21:49:16 +08:00

155 lines
4.9 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 = process.env.BASE_LOCALE ?? 'en-us'
const baseFileName = `${baseLocale}.json`
const baseFilePath = path.join(translationsDir, baseFileName)
type I18NValue = string | { [key: string]: I18NValue }
type I18N = { [key: string]: I18NValue }
/**
* Recursively check and synchronize the key-value structure of target object with template object
* 1. If target object is missing keys from template object, throw error
* 2. If target object has keys that don't exist in template object, throw error
* 3. For nested objects, recursively perform synchronization operation
*
* This function ensures all translation files maintain completely consistent key-value structure
* with the base template (usually the base translation file).
* Any structural differences will cause errors to be thrown for timely detection and fixing
* of translation file issues.
*
* @param target The target translation object to check
* @param template The template object used as base (usually the base translation file)
* @throws {Error} Thrown when key-value structure mismatch is found
*/
function checkRecursively(target: I18N, template: I18N): void {
for (const key in template) {
if (!(key in target)) {
throw new Error(`Missing property ${key}`)
}
if (key.includes('.')) {
throw new Error(`Should use strict nested structure for key ${key}`)
}
if (typeof template[key] === 'object' && template[key] !== null) {
if (typeof target[key] !== 'object' || target[key] === null) {
throw new Error(`Property ${key} is not an object`)
}
// Recursively check child objects
checkRecursively(target[key], template[key])
}
}
// Remove keys that exist in target but not in template
for (const targetKey in target) {
if (!(targetKey in template)) {
throw new Error(`Extra property ${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))
}
/**
* Check for duplicate keys in JSON object and collect all duplicate keys
* @param obj The object to check
* @returns Array of duplicate keys (returns empty array if no duplicates)
*/
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)) {
// When duplicate key is found, add to array (avoid duplicate additions)
if (!duplicateKeys.includes(fullPath)) {
duplicateKeys.push(fullPath)
}
} else {
keys.add(fullPath)
}
// Recursively check child objects
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(`Base template file ${baseFileName} does not exist, please check path or filename`)
}
const baseContent = fs.readFileSync(baseFilePath, 'utf-8')
let baseJson: I18N = {}
try {
baseJson = JSON.parse(baseContent)
} catch (error) {
throw new Error(`Error parsing ${baseFileName}. ${error}`)
}
// Check if base template has duplicate keys
const duplicateKeys = checkDuplicateKeys(baseJson)
if (duplicateKeys.length > 0) {
throw new Error(`Base template file ${baseFileName} has the following duplicate keys:\n${duplicateKeys.join('\n')}`)
}
// Check if base template is sorted
if (!isSortedI18N(baseJson)) {
throw new Error(`Base template file ${baseFileName} keys are not sorted in dictionary order.`)
}
const files = fs.readdirSync(translationsDir).filter((file) => file.endsWith('.json') && file !== baseFileName)
// Sync keys
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(`Error parsing ${file}.`)
}
// Check if sorted
if (!isSortedI18N(targetJson)) {
throw new Error(`Translation file ${file} keys are not sorted.`)
}
try {
checkRecursively(targetJson, baseJson)
} catch (e) {
console.error(e)
throw new Error(`Error while checking ${filePath}`)
}
}
}
export function main() {
try {
checkTranslations()
console.log('i18n check passed')
} catch (e) {
console.error(e)
throw new Error(`Check failed. Try running yarn i18n:sync to fix the issue.`)
}
}
main()