chore(i18n): improve i18n translation scripts (#8441)

* refactor(i18n): 迁移i18n脚本至TypeScript并添加进度条

- 将check-i18n.js和sync-i18n.js迁移为TypeScript版本
- 添加cli-progress依赖以显示翻译进度条
- 更新package.json中的i18n相关脚本
- 移除不再使用的sort.js工具文件

* refactor(i18n): 重构翻译同步脚本以支持多目录

将翻译文件目录变量重命名为更清晰的名称,并添加对translate目录的支持
优化文件路径处理逻辑,使用path.basename获取文件名

* chore: update i18n

* docs(i18n): 更新翻译目录的README说明

更新README文件以更清晰地说明翻译文件的生成方式和使用注意事项

* style(DMXAPISettings): 添加关于国际化的FIXME注释

在PlatformOptions上方添加注释,提醒此处需要国际化处理
This commit is contained in:
Phantom 2025-07-24 10:21:48 +08:00 committed by GitHub
parent 185045f805
commit 52c087fd22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 4295 additions and 804 deletions

View File

@ -50,8 +50,9 @@
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"check:i18n": "node scripts/check-i18n.js",
"sync:i18n": "node scripts/sync-i18n.js",
"check:i18n": "tsx scripts/check-i18n.ts",
"sync:i18n": "tsx scripts/sync-i18n.ts",
"update:i18n": "dotenv -e .env -- tsx scripts/update-i18n.ts",
"test": "vitest run --silent",
"test:main": "vitest run --project main",
"test:renderer": "vitest run --project renderer",
@ -132,6 +133,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@tryfabric/martian": "^1.2.4",
"@types/cli-progress": "^3",
"@types/diff": "^7",
"@types/fs-extra": "^11",
"@types/lodash": "^4.17.5",
@ -161,6 +163,7 @@
"async-mutex": "^0.5.0",
"axios": "^1.7.3",
"browser-image-compression": "^2.0.2",
"cli-progress": "^3.12.0",
"code-inspector-plugin": "^0.20.14",
"color": "^5.0.0",
"country-flag-emoji-polyfill": "0.1.8",
@ -247,6 +250,7 @@
"tar": "^7.4.3",
"tiny-pinyin": "^1.3.2",
"tokenx": "^1.1.0",
"tsx": "^4.20.3",
"typescript": "^5.6.2",
"undici": "6.21.2",
"unified": "^11.0.5",

View File

@ -1,151 +0,0 @@
'use strict'
Object.defineProperty(exports, '__esModule', { value: true })
exports.main = main
var fs = require('fs')
var path = require('path')
var sort_1 = require('./sort')
var translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
var baseLocale = 'zh-cn'
var baseFileName = ''.concat(baseLocale, '.json')
var baseFilePath = path.join(translationsDir, baseFileName)
/**
* 递归检查并同步目标对象与模板对象的键值结构
* 1. 如果目标对象缺少模板对象中的键抛出错误
* 2. 如果目标对象存在模板对象中不存在的键抛出错误
* 3. 对于嵌套对象递归执行同步操作
*
* 该函数用于确保所有翻译文件与基准模板通常是中文翻译文件保持完全一致的键值结构
* 任何结构上的差异都会导致错误被抛出以便及时发现和修复翻译文件中的问题
*
* @param target 需要检查的目标翻译对象
* @param template 作为基准的模板对象通常是中文翻译文件
* @throws {Error} 当发现键值结构不匹配时抛出错误
*/
function checkRecursively(target, template) {
for (var key in template) {
if (!(key in target)) {
throw new Error('\u7F3A\u5C11\u5C5E\u6027 '.concat(key))
}
if (typeof template[key] === 'object' && template[key] !== null) {
if (typeof target[key] !== 'object' || target[key] === null) {
throw new Error('\u5C5E\u6027 '.concat(key, ' \u4E0D\u662F\u5BF9\u8C61'))
}
// 递归检查子对象
checkRecursively(target[key], template[key])
}
}
// 删除 target 中存在但 template 中没有的 key
for (var targetKey in target) {
if (!(targetKey in template)) {
throw new Error('\u591A\u4F59\u5C5E\u6027 '.concat(targetKey))
}
}
}
function isSortedI18N(obj) {
// fs.writeFileSync('./test_origin.json', JSON.stringify(obj))
// fs.writeFileSync('./test_sorted.json', JSON.stringify(sortedObjectByKeys(obj)))
return JSON.stringify(obj) === JSON.stringify((0, sort_1.sortedObjectByKeys)(obj))
}
/**
* 检查 JSON 对象中是否存在重复键并收集所有重复键
* @param obj 要检查的对象
* @returns 返回重复键的数组若无重复则返回空数组
*/
function checkDuplicateKeys(obj) {
var keys = new Set()
var duplicateKeys = []
var checkObject = function (obj, path) {
if (path === void 0) {
path = ''
}
for (var key in obj) {
var fullPath = path ? ''.concat(path, '.').concat(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(
'\u4E3B\u6A21\u677F\u6587\u4EF6 '.concat(
baseFileName,
' \u4E0D\u5B58\u5728\uFF0C\u8BF7\u68C0\u67E5\u8DEF\u5F84\u6216\u6587\u4EF6\u540D'
)
)
}
var baseContent = fs.readFileSync(baseFilePath, 'utf-8')
var baseJson = {}
try {
baseJson = JSON.parse(baseContent)
} catch (error) {
throw new Error('\u89E3\u6790 '.concat(baseFileName, ' \u51FA\u9519\u3002').concat(error))
}
// 检查主模板是否存在重复键
var duplicateKeys = checkDuplicateKeys(baseJson)
if (duplicateKeys.length > 0) {
throw new Error(
'\u4E3B\u6A21\u677F\u6587\u4EF6 '
.concat(baseFileName, ' \u5B58\u5728\u4EE5\u4E0B\u91CD\u590D\u952E\uFF1A\n')
.concat(duplicateKeys.join('\n'))
)
}
// 检查主模板是否有序
if (!isSortedI18N(baseJson)) {
throw new Error(
'\u4E3B\u6A21\u677F\u6587\u4EF6 '.concat(
baseFileName,
' \u7684\u952E\u503C\u672A\u6309\u5B57\u5178\u5E8F\u6392\u5E8F\u3002'
)
)
}
var files = fs.readdirSync(translationsDir).filter(function (file) {
return file.endsWith('.json') && file !== baseFileName
})
// 同步键
for (var _i = 0, files_1 = files; _i < files_1.length; _i++) {
var file = files_1[_i]
var filePath = path.join(translationsDir, file)
var targetJson = {}
try {
var fileContent = fs.readFileSync(filePath, 'utf-8')
targetJson = JSON.parse(fileContent)
} catch (error) {
throw new Error('\u89E3\u6790 '.concat(file, ' \u51FA\u9519\u3002'))
}
// 检查有序性
if (!isSortedI18N(targetJson)) {
throw new Error(
'\u7FFB\u8BD1\u6587\u4EF6 '.concat(file, ' \u7684\u952E\u503C\u672A\u6309\u5B57\u5178\u5E8F\u6392\u5E8F\u3002')
)
}
try {
checkRecursively(targetJson, baseJson)
} catch (e) {
throw new Error('\u5728\u68C0\u67E5 '.concat(filePath, ' \u65F6\u51FA\u9519\uFF1A').concat(e))
}
}
}
function main() {
try {
checkTranslations()
} catch (e) {
console.error(e)
throw new Error(
'\u68C0\u67E5\u672A\u901A\u8FC7\u3002\u5C1D\u8BD5\u8FD0\u884C yarn sync:i18n \u4EE5\u89E3\u51B3\u95EE\u9898\u3002'
)
}
}
main()

View File

@ -1,40 +0,0 @@
'use strict'
Object.defineProperty(exports, '__esModule', { value: true })
exports.sortedObjectByKeys = sortedObjectByKeys
// https://github.com/Gudahtt/prettier-plugin-sort-json/blob/main/src/index.ts
/**
* Lexical sort function for strings, meant to be used as the sort
* function for `Array.prototype.sort`.
*
* @param a - First element to compare.
* @param b - Second element to compare.
* @returns A number indicating which element should come first.
*/
function lexicalSort(a, b) {
if (a > b) {
return 1
}
if (a < b) {
return -1
}
return 0
}
/**
* 对对象的键按照字典序进行排序支持嵌套对象
* @param obj 需要排序的对象
* @returns 返回排序后的新对象
*/
function sortedObjectByKeys(obj) {
var sortedKeys = Object.keys(obj).sort(lexicalSort)
var sortedObj = {}
for (var _i = 0, sortedKeys_1 = sortedKeys; _i < sortedKeys_1.length; _i++) {
var key = sortedKeys_1[_i]
var value = obj[key]
// 如果值是对象,递归排序
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
value = sortedObjectByKeys(value)
}
sortedObj[key] = value
}
return sortedObj
}

View File

@ -1,143 +0,0 @@
'use strict'
Object.defineProperty(exports, '__esModule', { value: true })
var fs = require('fs')
var path = require('path')
var sort_1 = require('./sort')
var translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
var baseLocale = 'zh-cn'
var baseFileName = ''.concat(baseLocale, '.json')
var baseFilePath = path.join(translationsDir, baseFileName)
/**
* 递归同步 target 对象使其与 template 对象保持一致
* 1. 如果 template 中存在 target 中缺少的 key则添加'[to be translated]'
* 2. 如果 target 中存在 template 中不存在的 key则删除
* 3. 对于子对象递归同步
*
* @param target 目标对象需要更新的语言对象
* @param template 主模板对象中文
* @returns 返回是否对 target 进行了更新
*/
function syncRecursively(target, template) {
// 添加 template 中存在但 target 中缺少的 key
for (var key in template) {
if (!(key in target)) {
target[key] =
typeof template[key] === 'object' && template[key] !== null ? {} : '[to be translated]:'.concat(template[key])
console.log('\u6DFB\u52A0\u65B0\u5C5E\u6027\uFF1A'.concat(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 (var targetKey in target) {
if (!(targetKey in template)) {
console.log('\u79FB\u9664\u591A\u4F59\u5C5E\u6027\uFF1A'.concat(targetKey))
delete target[targetKey]
}
}
}
/**
* 检查 JSON 对象中是否存在重复键并收集所有重复键
* @param obj 要检查的对象
* @returns 返回重复键的数组若无重复则返回空数组
*/
function checkDuplicateKeys(obj) {
var keys = new Set()
var duplicateKeys = []
var checkObject = function (obj, path) {
if (path === void 0) {
path = ''
}
for (var key in obj) {
var fullPath = path ? ''.concat(path, '.').concat(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(
'\u4E3B\u6A21\u677F\u6587\u4EF6 '.concat(
baseFileName,
' \u4E0D\u5B58\u5728\uFF0C\u8BF7\u68C0\u67E5\u8DEF\u5F84\u6216\u6587\u4EF6\u540D'
)
)
return
}
var baseContent = fs.readFileSync(baseFilePath, 'utf-8')
var baseJson = {}
try {
baseJson = JSON.parse(baseContent)
} catch (error) {
console.error('\u89E3\u6790 '.concat(baseFileName, ' \u51FA\u9519\u3002').concat(error))
return
}
// 检查主模板是否存在重复键
var duplicateKeys = checkDuplicateKeys(baseJson)
if (duplicateKeys.length > 0) {
throw new Error(
'\u4E3B\u6A21\u677F\u6587\u4EF6 '
.concat(baseFileName, ' \u5B58\u5728\u4EE5\u4E0B\u91CD\u590D\u952E\uFF1A\n')
.concat(duplicateKeys.join('\n'))
)
}
// 为主模板排序
var sortedJson = (0, sort_1.sortedObjectByKeys)(baseJson)
if (JSON.stringify(baseJson) !== JSON.stringify(sortedJson)) {
try {
fs.writeFileSync(baseFilePath, JSON.stringify(sortedJson, null, 2) + '\n', 'utf-8')
console.log('\u4E3B\u6A21\u677F\u5DF2\u6392\u5E8F')
} catch (error) {
console.error('\u5199\u5165 '.concat(baseFilePath, ' \u51FA\u9519\u3002'), error)
return
}
}
var files = fs.readdirSync(translationsDir).filter(function (file) {
return file.endsWith('.json') && file !== baseFileName
})
// 同步键
for (var _i = 0, files_1 = files; _i < files_1.length; _i++) {
var file = files_1[_i]
var filePath = path.join(translationsDir, file)
var targetJson = {}
try {
var fileContent = fs.readFileSync(filePath, 'utf-8')
targetJson = JSON.parse(fileContent)
} catch (error) {
console.error('\u89E3\u6790 '.concat(file, ' \u51FA\u9519\uFF0C\u8DF3\u8FC7\u6B64\u6587\u4EF6\u3002'), error)
continue
}
syncRecursively(targetJson, baseJson)
var sortedJson_1 = (0, sort_1.sortedObjectByKeys)(targetJson)
try {
fs.writeFileSync(filePath, JSON.stringify(sortedJson_1, null, 2) + '\n', 'utf-8')
console.log(
'\u6587\u4EF6 '.concat(
file,
' \u5DF2\u6392\u5E8F\u5E76\u540C\u6B65\u66F4\u65B0\u4E3A\u4E3B\u6A21\u677F\u7684\u5185\u5BB9'
)
)
} catch (error) {
console.error('\u5199\u5165 '.concat(file, ' \u51FA\u9519\u3002').concat(error))
}
}
}
syncTranslations()

View File

@ -3,10 +3,11 @@ import * as path from 'path'
import { sortedObjectByKeys } from './sort'
const translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
const localesDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
const translateDir = path.join(__dirname, '../src/renderer/src/i18n/translate')
const baseLocale = 'zh-cn'
const baseFileName = `${baseLocale}.json`
const baseFilePath = path.join(translationsDir, baseFileName)
const baseFilePath = path.join(localesDir, baseFileName)
type I18NValue = string | { [key: string]: I18NValue }
type I18N = { [key: string]: I18NValue }
@ -113,17 +114,25 @@ function syncTranslations() {
}
}
const files = fs.readdirSync(translationsDir).filter((file) => file.endsWith('.json') && file !== baseFileName)
const localeFiles = fs
.readdirSync(localesDir)
.filter((file) => file.endsWith('.json') && file !== baseFileName)
.map((filename) => path.join(localesDir, filename))
const translateFiles = fs
.readdirSync(translateDir)
.filter((file) => file.endsWith('.json') && file !== baseFileName)
.map((filename) => path.join(translateDir, filename))
const files = [...localeFiles, ...translateFiles]
// 同步键
for (const file of files) {
const filePath = path.join(translationsDir, file)
for (const filePath of files) {
const filename = path.basename(filePath)
let targetJson: I18N = {}
try {
const fileContent = fs.readFileSync(filePath, 'utf-8')
targetJson = JSON.parse(fileContent)
} catch (error) {
console.error(`解析 ${file} 出错,跳过此文件。`, error)
console.error(`解析 ${filename} 出错,跳过此文件。`, error)
continue
}
@ -133,9 +142,9 @@ function syncTranslations() {
try {
fs.writeFileSync(filePath, JSON.stringify(sortedJson, null, 2) + '\n', 'utf-8')
console.log(`文件 ${file} 已排序并同步更新为主模板的内容`)
console.log(`文件 ${filename} 已排序并同步更新为主模板的内容`)
} catch (error) {
console.error(`写入 ${file} 出错。${error}`)
console.error(`写入 ${filename} 出错。${error}`)
}
}
}

View File

@ -4,9 +4,16 @@
* API_KEY=sk-xxxx BASE_URL=xxxx MODEL=xxxx ts-node scripts/update-i18n.ts
*/
import cliProgress from 'cli-progress'
import fs from 'fs'
import OpenAI from 'openai'
type I18NValue = string | { [key: string]: I18NValue }
type I18N = { [key: string]: I18NValue }
const API_KEY = process.env.API_KEY
const BASE_URL = process.env.BASE_URL || 'https://llmapi.paratera.com/v1'
const MODEL = process.env.MODEL || 'Qwen3-235B-A22B'
const BASE_URL = process.env.BASE_URL || 'https://dashscope.aliyuncs.com/compatible-mode/v1/'
const MODEL = process.env.MODEL || 'qwen-plus-latest'
const INDEX = [
// 语言的名称代码用来翻译的模型
@ -16,10 +23,7 @@ const INDEX = [
{ name: 'Greek', code: 'el-gr', model: MODEL }
]
const fs = require('fs')
import OpenAI from 'openai'
const zh = JSON.parse(fs.readFileSync('src/renderer/src/i18n/locales/zh-cn.json', 'utf8')) as object
const zh = JSON.parse(fs.readFileSync('src/renderer/src/i18n/locales/zh-cn.json', 'utf8')) as I18N
const openai = new OpenAI({
apiKey: API_KEY,
@ -27,21 +31,23 @@ const openai = new OpenAI({
})
// 递归遍历翻译
async function translate(zh: object, obj: object, target: string, model: string, updateFile) {
const texts: { [key: string]: string } = {}
for (const e in zh) {
if (typeof zh[e] == 'object') {
async function translate(baseObj: I18N, targetObj: I18N, targetLang: string, model: string, updateFile) {
const toTranslateTexts: { [key: string]: string } = {}
for (const key in baseObj) {
if (typeof baseObj[key] == 'object') {
// 遍历下一层
if (!obj[e] || typeof obj[e] != 'object') obj[e] = {}
await translate(zh[e], obj[e], target, model, updateFile)
} else {
if (!targetObj[key] || typeof targetObj[key] != 'object') targetObj[key] = {}
await translate(baseObj[key], targetObj[key], targetLang, model, updateFile)
} else if (
!targetObj[key] ||
typeof targetObj[key] != 'string' ||
(typeof targetObj[key] === 'string' && targetObj[key].startsWith('[to be translated]'))
) {
// 加入到本层待翻译列表
if (!obj[e] || typeof obj[e] != 'string') {
texts[e] = zh[e]
}
toTranslateTexts[key] = baseObj[key]
}
}
if (Object.keys(texts).length > 0) {
if (Object.keys(toTranslateTexts).length > 0) {
const completion = await openai.chat.completions.create({
model: model,
response_format: { type: 'json_object' },
@ -79,16 +85,16 @@ MAKE SURE TO OUTPUT IN Russian. DO NOT OUTPUT IN UNSPECIFIED LANGUAGE.
{
role: 'user',
content: `
You are a robot specifically designed for translation tasks. As a model that has been extensively fine-tuned on ${target} language corpora, you are proficient in using the ${target} language.
Now, please output the translation based on the input content. The input will include both Chinese and English key values, and you should output the corresponding key values in the ${target} language.
You are a robot specifically designed for translation tasks. As a model that has been extensively fine-tuned on ${targetLang} language corpora, you are proficient in using the ${targetLang} language.
Now, please output the translation based on the input content. The input will include both Chinese and English key values, and you should output the corresponding key values in the ${targetLang} language.
When translating, ensure that no key value is omitted, and maintain the accuracy and fluency of the translation. Pay attention to the capitalization rules in the output to match the source text, and especially pay attention to whether to capitalize the first letter of each word except for prepositions. For strings containing \`{{value}}\`, ensure that the format is not disrupted.
Output in JSON.
######################################################
INPUT
######################################################
${JSON.stringify(texts)}
${JSON.stringify(toTranslateTexts)}
######################################################
MAKE SURE TO OUTPUT IN ${target}. DO NOT OUTPUT IN UNSPECIFIED LANGUAGE.
MAKE SURE TO OUTPUT IN ${targetLang}. DO NOT OUTPUT IN UNSPECIFIED LANGUAGE.
######################################################
`
}
@ -97,37 +103,45 @@ MAKE SURE TO OUTPUT IN ${target}. DO NOT OUTPUT IN UNSPECIFIED LANGUAGE.
// 添加翻译后的键值,并打印错译漏译内容
try {
const result = JSON.parse(completion.choices[0].message.content!)
for (const e in texts) {
// console.debug('result', result)
for (const e in toTranslateTexts) {
if (result[e] && typeof result[e] === 'string') {
obj[e] = result[e]
targetObj[e] = result[e]
} else {
console.log('[warning]', `missing value "${e}" in ${target} translation`)
console.warn(`missing value "${e}" in ${targetLang} translation`)
}
}
} catch (e) {
console.log('[error]', e)
for (const e in texts) {
console.log('[warning]', `missing value "${e}" in ${target} translation`)
console.error(e)
for (const e in toTranslateTexts) {
console.warn(`missing value "${e}" in ${targetLang} translation`)
}
}
}
// 删除多余的键值
for (const e in obj) {
if (!zh[e]) {
delete obj[e]
for (const e in targetObj) {
if (!baseObj[e]) {
delete targetObj[e]
}
}
// 更新文件
updateFile()
}
let count = 0
;(async () => {
const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic)
bar.start(INDEX.length, 0)
for (const { name, code, model } of INDEX) {
const obj = fs.existsSync(`src/renderer/src/i18n/translate/${code}.json`)
? JSON.parse(fs.readFileSync(`src/renderer/src/i18n/translate/${code}.json`, 'utf8'))
? (JSON.parse(fs.readFileSync(`src/renderer/src/i18n/translate/${code}.json`, 'utf8')) as I18N)
: {}
await translate(zh, obj, name, model, () => {
fs.writeFileSync(`src/renderer/src/i18n/translate/${code}.json`, JSON.stringify(obj, null, 2), 'utf8')
})
count += 1
bar.update(count)
}
bar.stop()
})()

View File

@ -1,2 +1,2 @@
本目录文件使用机器翻译,请勿编辑
This directory file is machine translated, please do not edit
本目录文件使用`yarn update:i18n`机器翻译生成,请勿手动编辑
This directory contains machine translated files generated by `yarn update:i18n`. Please do not edit manually.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@ enum PlatformType {
OVERSEA = 'https://ssvip.DMXAPI.com'
}
// FIXME: always Chinese. take consider of i18n
const PlatformOptions = [
{
label: 'www.DMXAPI.cn 人民币站',

View File

@ -5596,6 +5596,15 @@ __metadata:
languageName: node
linkType: hard
"@types/cli-progress@npm:^3":
version: 3.11.6
resolution: "@types/cli-progress@npm:3.11.6"
dependencies:
"@types/node": "npm:*"
checksum: 10c0/d9a2d60b8fc6ccef73368fa20a23d5b16506808a81ec65f7e8eedf58d236ebaf2ab46578936c000c8e39dde825cb48a3cf9195c8b410177efd5388bcf9d07370
languageName: node
linkType: hard
"@types/d3-array@npm:*":
version: 3.2.1
resolution: "@types/d3-array@npm:3.2.1"
@ -7175,6 +7184,7 @@ __metadata:
"@testing-library/jest-dom": "npm:^6.6.3"
"@testing-library/react": "npm:^16.3.0"
"@tryfabric/martian": "npm:^1.2.4"
"@types/cli-progress": "npm:^3"
"@types/diff": "npm:^7"
"@types/fs-extra": "npm:^11"
"@types/lodash": "npm:^4.17.5"
@ -7204,6 +7214,7 @@ __metadata:
async-mutex: "npm:^0.5.0"
axios: "npm:^1.7.3"
browser-image-compression: "npm:^2.0.2"
cli-progress: "npm:^3.12.0"
code-inspector-plugin: "npm:^0.20.14"
color: "npm:^5.0.0"
country-flag-emoji-polyfill: "npm:0.1.8"
@ -7295,6 +7306,7 @@ __metadata:
tar: "npm:^7.4.3"
tiny-pinyin: "npm:^1.3.2"
tokenx: "npm:^1.1.0"
tsx: "npm:^4.20.3"
turndown: "npm:7.2.0"
typescript: "npm:^5.6.2"
undici: "npm:6.21.2"
@ -8597,6 +8609,15 @@ __metadata:
languageName: node
linkType: hard
"cli-progress@npm:^3.12.0":
version: 3.12.0
resolution: "cli-progress@npm:3.12.0"
dependencies:
string-width: "npm:^4.2.3"
checksum: 10c0/f464cb19ebde2f3880620a2adfaeeefaec6cb15c8e610c8a659ca1047ee90d69f3bf2fdabbb1fe33ac408678e882e3e0eecdb84ab5df0edf930b269b8a72682d
languageName: node
linkType: hard
"cli-spinners@npm:^2.5.0":
version: 2.9.2
resolution: "cli-spinners@npm:2.9.2"
@ -10486,7 +10507,7 @@ __metadata:
languageName: node
linkType: hard
"esbuild@npm:^0.25.0, esbuild@npm:^0.25.5":
"esbuild@npm:^0.25.0, esbuild@npm:^0.25.5, esbuild@npm:~0.25.0":
version: 0.25.8
resolution: "esbuild@npm:0.25.8"
dependencies:
@ -11927,6 +11948,15 @@ __metadata:
languageName: node
linkType: hard
"get-tsconfig@npm:^4.7.5":
version: 4.10.1
resolution: "get-tsconfig@npm:4.10.1"
dependencies:
resolve-pkg-maps: "npm:^1.0.0"
checksum: 10c0/7f8e3dabc6a49b747920a800fb88e1952fef871cdf51b79e98db48275a5de6cdaf499c55ee67df5fa6fe7ce65f0063e26de0f2e53049b408c585aa74d39ffa21
languageName: node
linkType: hard
"get-uri@npm:^6.0.1":
version: 6.0.4
resolution: "get-uri@npm:6.0.4"
@ -18111,6 +18141,13 @@ __metadata:
languageName: node
linkType: hard
"resolve-pkg-maps@npm:^1.0.0":
version: 1.0.0
resolution: "resolve-pkg-maps@npm:1.0.0"
checksum: 10c0/fb8f7bbe2ca281a73b7ef423a1cbc786fb244bd7a95cbe5c3fba25b27d327150beca8ba02f622baea65919a57e061eb5005204daa5f93ed590d9b77463a567ab
languageName: node
linkType: hard
"responselike@npm:^2.0.0":
version: 2.0.1
resolution: "responselike@npm:2.0.1"
@ -19730,6 +19767,22 @@ __metadata:
languageName: node
linkType: hard
"tsx@npm:^4.20.3":
version: 4.20.3
resolution: "tsx@npm:4.20.3"
dependencies:
esbuild: "npm:~0.25.0"
fsevents: "npm:~2.3.3"
get-tsconfig: "npm:^4.7.5"
dependenciesMeta:
fsevents:
optional: true
bin:
tsx: dist/cli.mjs
checksum: 10c0/6ff0d91ed046ec743fac7ed60a07f3c025e5b71a5aaf58f3d2a6b45e4db114c83e59ebbb078c8e079e48d3730b944a02bc0de87695088aef4ec8bbc705dc791b
languageName: node
linkType: hard
"tunnel-agent@npm:^0.6.0":
version: 0.6.0
resolution: "tunnel-agent@npm:0.6.0"