Compare commits

...

38 Commits

Author SHA1 Message Date
GitHub Action
18b5210e43 fix(i18n): Auto update translations for PR #10912 2025-10-28 11:29:04 +00:00
icarus
2db55a1304 Merge branch 'v2' of github.com:CherryHQ/cherry-studio into refactor/i18n 2025-10-28 19:28:33 +08:00
icarus
b18c00e38b docs: handle language preference change cleanup comment
Add comment explaining why cleanup is not needed for language preference subscription
2025-10-23 23:35:21 +08:00
icarus
9b1ccb60aa feat(i18n): add language preference change subscription
Add subscription to language preference changes to dynamically update i18n language. Also change warning to error log when no language preference is found
2025-10-23 23:31:17 +08:00
icarus
7e5e3786cf feat(i18n): add language preference handling and improve initialization
Initialize i18n with user's preferred language from preferences or fallback to default
Standardize locale file imports naming convention and improve i18n initialization flow
2025-10-23 23:08:47 +08:00
icarus
57206dd3b1 refactor(i18n): restructure translation resources and update logger context
Move translation files to new location and update resource structure to include translation namespace
Update logger context to include module prefix for better debugging
2025-10-23 22:46:24 +08:00
icarus
b02678a714 refactor(i18n): simplify i18n implementation and cleanup unused locales
- Rename i18next import to i18n for consistency
- Standardize locale keys to lowercase format
- Remove commented out locale imports and unused translations
- Replace getI18n with direct t function usage in services
- Clean up VS Code settings and commented i18n config
2025-10-23 22:39:40 +08:00
icarus
5b4f15d355 refactor(sort): remove unused lexicalSort function 2025-10-23 22:24:16 +08:00
icarus
262b0aeeb6 refactor(i18n): simplify language resources and integrate i18next
Remove unused language resources and rename locales to resources for clarity
Add i18next initialization with missing key logging
2025-10-23 22:19:40 +08:00
icarus
b181902183 refactor(i18n): restore main to avoid conflict before merge 2025-10-23 21:51:12 +08:00
icarus
bfbd934fdc feat(i18n): add progress tracking and post-processing to translation
- Add postProcess callback to translateConcurrent and translateRecursively
- Implement progress bar for individual file translation counts
- Improve console output formatting with carriage returns
- Move progress bar initialization inside file processing loop
2025-10-23 21:51:12 +08:00
icarus
75041ce952 feat(i18n): enhance translation script with concurrency and config
- Add concurrent translation with configurable max requests
- Implement automatic retry and rate limiting
- Improve error handling and progress tracking
- Add detailed configuration options and usage instructions
2025-10-23 21:51:12 +08:00
icarus
dc469b6112 chore: remove i18next-scanner and related dependencies
Remove i18next-scanner package and its configuration file along with unused dependencies that were only required by it.
2025-10-23 21:51:12 +08:00
icarus
5efce861a9 feat(i18n): add new supported locales and clean up translation script
- Add 8 new locales (de-de, el-gr, es-es, fr-fr, ja-jp, pt-pt, ru-ru)
- Remove unused translate directory references from auto-translate script
2025-10-23 21:50:53 +08:00
icarus
112d735659 refactor(i18n): reorganize translation files and update paths
Move machine-translated files from translate/ to locales/ directory
Update README location and clarify translation maintenance status
2025-10-23 21:50:53 +08:00
icarus
8f2b3f0bdc feat(i18n): update translation files and add missing plural forms
- Update README to clarify translation maintenance status
- Add missing plural forms in en-us, zh-cn, and zh-tw locales
- Complete machine translations for de-de, ja-jp, ru-ru, pt-pt, es-es, fr-fr, and el-gr
- Convert array to object format for accessibility description in de-de and zh-cn
2025-10-23 21:50:21 +08:00
icarus
91704f2ee9 refactor(translation): simplify translation prompt and function parameters
Remove redundant <translate_input> tags and pass text directly to translate function
Improve logging by showing both original and translated text
2025-10-23 21:50:21 +08:00
icarus
e1aa223e5d feat(i18n): add Simplified Chinese and German language support
Add 'zh-cn' and 'de-de' to language map and log system prompt for debugging
2025-10-23 21:50:21 +08:00
icarus
74eb3141cd feat(i18n): add missing pluralization strings for citations and search results 2025-10-23 21:50:21 +08:00
icarus
9dcaf84da6 refactor(i18n): simplify default translation placeholder
Remove language parameter from default value function since it's unused and simplify the placeholder text
2025-10-23 21:50:21 +08:00
icarus
66e48dbba9 chore: update i18n sync command and vscode settings
- Replace custom i18n sync script with i18next-cli sync command
- Update json sorting setting to use biome formatter
- Format i18n-ally config arrays for better readability
2025-10-23 21:50:21 +08:00
icarus
534459dd13 refactor(sort): replace lexicalSort with naturalSort for better string comparison
Use localeCompare with numeric sensitivity for more natural string sorting behavior
2025-10-23 21:50:21 +08:00
icarus
b2e2acebb1 feat(i18n): add pluralization support for translation strings
- Add singular/plural forms for count-based translation strings
- Update VS Code settings to enable JSON sorting
- Remove unused i18n-ally sortKeys setting
2025-10-23 21:50:21 +08:00
icarus
6b3828f189 fix: correct translation keys and remove unused code
- Fix incorrect translation key paths in MessageVideo component
- Remove commented-out code for unused OCR options in PreprocessProviderSettings
- Simplify success toast message in NotesSidebar by removing count parameter
2025-10-23 21:50:21 +08:00
icarus
5f02822ef2 fix(i18n): remove count interpolation from tool labels
The count values were moved outside the translated strings to simplify localization and improve consistency across languages. The counts are now displayed separately after the translated labels in the UI.
2025-10-23 21:50:21 +08:00
icarus
c0fe0a7774 fix(i18n): improve default translation value handling
Use a function to generate more informative default values for missing translations, showing target language and original value
2025-10-23 21:50:21 +08:00
icarus
c61aec34af chore: update i18next-cli dependency to v1.12.0 2025-10-23 21:50:21 +08:00
icarus
3e990dddb5 feat(i18n): enhance i18next configuration and vscode settings
- Add new i18next config options including defaultValue, primaryLanguage, and types generation
- Update vscode settings for better i18n-ally integration and namespace support
- Reorder search exclude patterns for consistency
2025-10-23 21:50:21 +08:00
icarus
cf0aa49427 refactor(i18n): add translation namespace to locale json instead of adding namespace in code 2025-10-23 21:50:20 +08:00
icarus
311a229ab7 style: change biome sort order to natural and clean up i18next config
- Update biome.jsonc to use natural sort order for keys
- Clean up i18next.config.ts formatting and remove trailing newline
2025-10-23 21:50:00 +08:00
icarus
3677a34ceb feat(i18n): add i18next config and update extract commands
Add configuration file for i18next-cli with supported locales and extraction settings
Update package.json scripts to use i18next-cli commands for status and extraction
2025-10-23 21:50:00 +08:00
icarus
43fcfa6c95 build: add i18next-cli dependency for translation management
Add i18next-cli package to support translation file generation and management. This enables better i18n workflow integration.
2025-10-23 21:50:00 +08:00
icarus
3b97142361 fix(i18n): split fetch_complete into singular and plural forms
Update translation files to properly handle singular/plural cases for search results display
2025-10-23 21:49:49 +08:00
icarus
2e60db80df feat(i18n): add i18next scanner configuration and script
Add i18next-scanner configuration file to automate translation key extraction
Include new 'i18n:scan' script in package.json to run the scanner
Update tsconfig and oxlintrc to include the new config file
2025-10-23 21:49:49 +08:00
icarus
e5232b1fbb docs(i18n): update i18n documentation to reflect english as source language
- Change source language from Chinese to English in both docs
- Update script names from check:i18n/sync:i18n/auto:i18n to i18n:check/i18n:sync/i18n:auto
- Remove deprecated update:i18n script references
- Clarify base locale behavior and environment variable usage
2025-10-23 21:49:16 +08:00
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
icarus
16a69e240b refactor(i18n): standardize i18n script names and update base locale
- Rename i18n-related scripts to follow consistent naming pattern (i18n:check, i18n:sync, i18n:auto)
- Change default base locale from zh-cn to en-us
- Remove unused update-i18n.ts script
- Update documentation and CI workflow to reflect script name changes
2025-10-23 21:49:16 +08:00
icarus
9c43bb07c0 build: add i18next-scanner dependency for localization support
Add i18next-scanner package to automate extraction and management of localization strings
2025-10-23 21:48:26 +08:00
33 changed files with 1136 additions and 448 deletions

View File

@@ -58,7 +58,7 @@ jobs:
run: yarn typecheck
- name: i18n Check
run: yarn check:i18n
run: yarn i18n:check
- name: Test
run: yarn test

35
.vscode/settings.json vendored
View File

@@ -27,27 +27,40 @@
"source.fixAll.biome": "explicit",
"source.fixAll.eslint": "explicit",
"source.fixAll.oxc": "explicit",
"source.organizeImports": "never"
"source.organizeImports": "never",
"source.sort.json.biome": "always"
},
"editor.formatOnSave": true,
"files.associations": {
"*.css": "tailwindcss",
".oxlintrc.json": "jsonc"
".oxlintrc.json": "jsonc",
"*.css": "tailwindcss"
},
"files.eol": "\n",
// "i18n-ally.defaultNamespace": "translation",
// "i18n-ally.displayLanguage": "zh-cn", // 界面显示语言
"i18n-ally.enabledFrameworks": ["react-i18next", "i18next"],
"i18n-ally.enabledParsers": ["ts", "js", "json"], // 解析语言
"i18n-ally.enabledFrameworks": [
"react-i18next",
"i18next"
],
"i18n-ally.enabledParsers": [
"ts",
"js",
"json"
], // 解析语言
"i18n-ally.fullReloadOnChanged": true,
"i18n-ally.keystyle": "nested", // 翻译路径格式
"i18n-ally.localesPaths": ["src/renderer/src/i18n/locales"],
// "i18n-ally.namespace": true, // 开启命名空间
"i18n-ally.sortKeys": true, // 排序
"i18n-ally.localesPaths": [
"src/renderer/src/i18n/locales"
],
"i18n-ally.namespace": true, // 开启命名空间
"i18n-ally.sourceLanguage": "zh-cn", // 翻译源语言
"i18n-ally.usage.derivedKeyRules": ["{key}_one", "{key}_other"], // 标记单复数形式的键为已翻译
"i18n-ally.usage.derivedKeyRules": [
"{key}_one",
"{key}_other"
], // 标记单复数形式的键为已翻译
"search.exclude": {
"**/dist/**": true,
".yarn/releases/**": true
".yarn/releases/**": true,
"**/dist/**": true
},
"tailwindCSS.classAttributes": [
"className",

View File

@@ -19,7 +19,7 @@ This file provides guidance to AI coding assistants when working with code in th
- **Development**: `yarn dev` - Runs Electron app in development mode with hot reload
- **Debug**: `yarn debug` - Starts with debugging enabled, use `chrome://inspect` to attach debugger
- **Build Check**: `yarn build:check` - **REQUIRED** before commits (lint + test + typecheck)
- If having i18n sort issues, run `yarn sync:i18n` first to sync template
- If having i18n sort issues, run `yarn i18n:sync` first to sync template
- If having formatting issues, run `yarn format` first
- **Test**: `yarn test` - Run all tests (Vitest) across main and renderer processes
- **Single Test**:

View File

@@ -8,7 +8,7 @@
"useSortedKeys": {
"level": "on",
"options": {
"sortOrder": "lexicographic"
"sortOrder": "natural"
}
}
}

View File

@@ -107,7 +107,7 @@ By avoiding template strings, you gain better developer experience, more reliabl
The project includes several scripts to automate i18n-related tasks:
### `check:i18n` - Validate i18n Structure
### `i18n:check` - Validate i18n Structure
This script checks:
@@ -116,28 +116,30 @@ This script checks:
- Whether keys are properly sorted
```bash
yarn check:i18n
yarn i18n:check
```
### `sync:i18n` - Synchronize JSON Structure and Sort Order
### `i18n:sync` - Synchronize JSON Structure and Sort Order
This script uses `zh-cn.json` as the source of truth to sync structure across all language files, including:
By default, this script uses `en-us.json` as the source of truth to sync structure across all language files, including:
1. Adding missing keys, with placeholder `[to be translated]`
2. Removing obsolete keys
3. Sorting keys automatically
You can override this behavior by setting the `BASE_LOCALE` environment variable.
```bash
yarn sync:i18n
yarn i18n:sync
```
### `auto:i18n` - Automatically Translate Pending Texts
### `i18n:auto` - Automatically Translate Pending Texts
This script fills in texts marked as `[to be translated]` using machine translation.
This script automatically translates texts marked as `[to be translated]` using machine translation. Similar to `i18n:sync`, it defaults to using `en-us.json` as the base, but you can override this behavior by setting the `BASE_LOCALE` environment variable.
Typically, after adding new texts in `zh-cn.json`, run `sync:i18n`, then `auto:i18n` to complete translations.
Typically, after adding required texts to `en-us.json`, running `i18n:sync && i18n:auto` will automatically complete the translations.
Before using this script, set the required environment variables:
Before using this script, you need to configure environment variables, for example:
```bash
API_KEY="sk-xxx"
@@ -145,33 +147,23 @@ BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1/"
MODEL="qwen-plus-latest"
```
Alternatively, add these variables directly to your `.env` file.
You can also add environment variables by directly editing the `.env` file.
```bash
yarn auto:i18n
```
### `update:i18n` - Object-level Translation Update
Updates translations in language files under `src/renderer/src/i18n/translate` at the object level, preserving existing translations and only updating new content.
**Not recommended** — prefer `auto:i18n` for translation tasks.
```bash
yarn update:i18n
yarn i18n:auto
```
### Workflow
1. During development, first add the required text in `zh-cn.json`
2. Confirm it displays correctly in the Chinese environment
3. Run `yarn sync:i18n` to propagate the keys to other language files
4. Run `yarn auto:i18n` to perform machine translation
5. Grab a coffee and let the magic happen!
1. During development, first add the required text in `en-us.json`. You can use the quick fix functionality provided by the i18n-ally plugin to easily accomplish this.
2. Confirm the text displays correctly in the UI
3. Use `yarn i18n:sync` to sync the text to other language files
4. Use `yarn i18n:auto` to perform automatic translation
5. Grab a coffee and wait for the translation to complete!
## Best Practices
1. **Use Chinese as Source Language**: All development starts in Chinese, then translates to other languages.
2. **Run Check Script Before Commit**: Use `yarn check:i18n` to catch i18n issues early.
1. **Use English as Source Language**: All development starts in English, then translates to other languages.
2. **Run Check Script Before Commit**: Use `yarn i18n:check` to catch i18n issues early.
3. **Translate in Small Increments**: Avoid accumulating a large backlog of untranslated content.
4. **Keep Keys Semantically Clear**: Keys should clearly express their purpose, e.g., `user.profile.avatar.upload.error`

View File

@@ -23,9 +23,9 @@ i18n ally是一个强大的VSCode插件它能在开发阶段提供实时反
## i18n 约定
### **绝对避免使用flat格式**
### **避免使用flat格式**
绝对避免使用flat格式`"add.button.tip": "添加"`。应采用清晰的嵌套结构:
避免使用flat格式`"add.button.tip": "添加"`。应采用清晰的嵌套结构:
```json
// 错误示例 - flat结构
@@ -101,7 +101,7 @@ export const getThemeModeLabel = (key: string): string => {
项目中有一系列脚本来自动化i18n相关任务
### `check:i18n` - 检查i18n结构
### `i18n:check` - 检查i18n结构
此脚本会检查:
@@ -111,26 +111,28 @@ export const getThemeModeLabel = (key: string): string => {
- 是否已经有序
```bash
yarn check:i18n
yarn i18n:check
```
### `sync:i18n` - 同步json结构与排序
### `i18n:sync` - 同步json结构与排序
此脚本以`zh-cn.json`文件为基准,将结构同步到其他语言文件,包括:
此脚本默认以`en-us.json`文件为基准,将结构同步到其他语言文件,包括:
1. 添加缺失的键。缺少的翻译内容会以`[to be translated]`标记
2. 删除多余的键
3. 自动排序
你也可以设置环境变量`BASE_LOCALE`来覆盖这一行为。
```bash
yarn sync:i18n
yarn i18n:auto
```
### `auto:i18n` - 自动翻译待翻译文本
### `i18n:auto` - 自动翻译待翻译文本
脚本自动将标记为待翻译的文本通过机器翻译填充。
脚本自动将标记为待翻译的文本通过机器翻译填充。与 `i18n:sync` 相同,默认以`en-us.json`文件为基准,也可以设置环境变量`BASE_LOCALE`来覆盖这一行为。
通常,在`zh-cn.json`中添加所需文案后,执行`sync:i18n`即可自动完成翻译。
通常,在`en-us.json`中添加所需文案后,执行`i18n:sync && i18n:auto`即可自动完成翻译。
使用该脚本前,需要配置环境变量,例如:
@@ -143,29 +145,19 @@ MODEL="qwen-plus-latest"
你也可以通过直接编辑`.env`文件来添加环境变量。
```bash
yarn auto:i18n
```
### `update:i18n` - 对象级别翻译更新
对`src/renderer/src/i18n/translate`中的语言文件进行对象级别的翻译更新,保留已有翻译,只更新新增内容。
**不建议**使用该脚本,更推荐使用`auto:i18n`进行翻译。
```bash
yarn update:i18n
yarn i18n:auto
```
### 工作流
1. 开发阶段,先在`zh-cn.json`中添加所需文案
2. 确认在中文环境下显示无误后,使用`yarn sync:i18n`将文案同步到其他语言文件
3. 使用`yarn auto:i18n`进行自动翻译
1. 开发阶段,先在`en-us.json`中添加所需文案。你可以利用 i18n-ally 插件提供的快速修复功能轻松完成这一点。
2. 确认文案在 UI 中显示无误后,使用`yarn i18n:sync`将文案同步到其他语言文件
3. 使用`yarn i18n:auto`进行自动翻译
4. 喝杯咖啡,等翻译完成吧!
## 最佳实践
1. **以文为源语言**:所有开发首先使用文,再翻译为其他语言
2. **提交前运行检查脚本**:使用`yarn check:i18n`检查i18n是否有问题
1. **以文为源语言**:所有开发首先使用文,再翻译为其他语言
2. **提交前运行检查脚本**:使用`yarn i18n:check`检查i18n是否有问题
3. **小步提交翻译**:避免积累大量未翻译文本
4. **保持key语义明确**key应能清晰表达其用途如`user.profile.avatar.upload.error`

19
i18next.config.ts Normal file
View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'i18next-cli'
/** @see https://github.com/i18next/i18next-cli */
export default defineConfig({
locales: ['en-us', 'zh-cn', 'zh-tw', 'de-de', 'el-gr', 'es-es', 'fr-fr', 'ja-jp', 'pt-pt', 'ru-ru'],
extract: {
input: 'src/renderer/src/**/*.{ts,tsx}',
output: 'src/renderer/src/i18n/locales/{{language}}.json',
defaultValue: (_1, _2, _3, value) => `[to be translated]${value}`,
primaryLanguage: 'en-us',
removeUnusedKeys: false
},
types: {
input: ['src/renderer/src/i18n/locales/en-us.json'],
output: 'src/renderer/src/i18n/i18next.d.ts',
resourcesFile: 'src/renderer/src/i18n/resources.d.ts',
enableSelector: true
}
})

View File

@@ -54,10 +54,11 @@
"typecheck:node": "tsgo --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsgo --noEmit -p tsconfig.web.json --composite false",
"typecheck:ui": "cd packages/ui && npm run type-check",
"check:i18n": "dotenv -e .env -- tsx scripts/check-i18n.ts",
"sync:i18n": "dotenv -e .env -- tsx scripts/sync-i18n.ts",
"update:i18n": "dotenv -e .env -- tsx scripts/update-i18n.ts",
"auto:i18n": "dotenv -e .env -- tsx scripts/auto-translate-i18n.ts",
"i18n:check": "dotenv -e .env -- tsx scripts/check-i18n.ts",
"i18n:sync": "i18next-cli sync",
"i18n:auto": "dotenv -e .env -- tsx scripts/auto-translate-i18n.ts",
"i18n:status": "i18next-cli status",
"i18n:extract": "i18next-cli extract",
"update:languages": "tsx scripts/update-languages.ts",
"test": "vitest run --silent",
"test:main": "vitest run --project main",
@@ -69,7 +70,7 @@
"test:e2e": "yarn playwright test",
"test:lint": "oxlint --deny-warnings && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --cache",
"test:scripts": "vitest scripts",
"lint": "oxlint --fix && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --cache && biome lint --write && biome format --write && yarn typecheck && yarn check:i18n && yarn format:check",
"lint": "oxlint --fix && eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --cache && biome lint --write && biome format --write && yarn typecheck && yarn i18n:check && yarn format:check",
"lint:ox": "oxlint --fix && biome lint --write && biome format --write",
"format": "biome format --write && biome lint --write",
"format:check": "biome format && biome lint",
@@ -284,6 +285,7 @@
"htmlparser2": "^10.0.0",
"husky": "^9.1.7",
"i18next": "^23.11.5",
"i18next-cli": "^1.12.0",
"iconv-lite": "^0.6.3",
"ipaddr.js": "^2.2.0",
"isbinaryfile": "5.0.4",

View File

@@ -48,7 +48,7 @@ Usage Instructions:
- pt-pt (Portuguese)
Run Command:
yarn auto:i18n
yarn i18n:auto
Performance Optimization Recommendations:
- For stable API services: MAX_CONCURRENT_TRANSLATIONS=8, TRANSLATION_DELAY_MS=50
@@ -257,7 +257,6 @@ const main = async () => {
validateConfig()
const localesDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
const translateDir = path.join(__dirname, '../src/renderer/src/i18n/translate')
const baseLocale = process.env.TRANSLATION_BASE_LOCALE ?? 'en-us'
const baseFileName = `${baseLocale}.json`
const baseLocalePath = path.join(__dirname, '../src/renderer/src/i18n/locales', baseFileName)
@@ -272,19 +271,16 @@ const main = async () => {
console.log('')
// Process files using ES6+ array methods
const getFiles = (dir: string) =>
fs
.readdirSync(dir)
.filter((file) => {
const filename = file.replace('.json', '')
return file.endsWith('.json') && file !== baseFileName && !SCRIPT_CONFIG.SKIP_LANGUAGES.includes(filename)
})
.map((filename) => path.join(dir, filename))
const localeFiles = getFiles(localesDir)
const translateFiles = getFiles(translateDir)
const files = [...localeFiles, ...translateFiles]
const files = fs
.readdirSync(localesDir)
.filter((file) => {
const filename = file.replace('.json', '')
return file.endsWith('.json') && file !== baseFileName && !SCRIPT_CONFIG.SKIP_LANGUAGES.includes(filename)
})
.map((filename) => path.join(localesDir, filename))
console.info(`📂 Base Locale: ${baseLocale}`)
console.info('📂 Files to translate:')
files.forEach((filePath) => {
const filename = path.basename(filePath, '.json')

View File

@@ -4,7 +4,7 @@ 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 ?? 'zh-cn'
const baseLocale = process.env.BASE_LOCALE ?? 'en-us'
const baseFileName = `${baseLocale}.json`
const baseFilePath = path.join(translationsDir, baseFileName)
@@ -12,39 +12,41 @@ type I18NValue = string | { [key: string]: I18NValue }
type I18N = { [key: string]: I18NValue }
/**
* 递归检查并同步目标对象与模板对象的键值结构
* 1. 如果目标对象缺少模板对象中的键,抛出错误
* 2. 如果目标对象存在模板对象中不存在的键,抛出错误
* 3. 对于嵌套对象,递归执行同步操作
* 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 需要检查的目标翻译对象
* @param template 作为基准的模板对象(通常是中文翻译文件)
* @throws {Error} 当发现键值结构不匹配时抛出错误
* @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(`缺少属性 ${key}`)
throw new Error(`Missing property ${key}`)
}
if (key.includes('.')) {
throw new Error(`应该使用严格嵌套结构 ${key}`)
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(`属性 ${key} 不是对象`)
throw new Error(`Property ${key} is not an object`)
}
// 递归检查子对象
// Recursively check child objects
checkRecursively(target[key], template[key])
}
}
// 删除 target 中存在但 template 中没有的 key
// Remove keys that exist in target but not in template
for (const targetKey in target) {
if (!(targetKey in template)) {
throw new Error(`多余属性 ${targetKey}`)
throw new Error(`Extra property ${targetKey}`)
}
}
}
@@ -56,9 +58,9 @@ function isSortedI18N(obj: I18N): boolean {
}
/**
* 检查 JSON 对象中是否存在重复键,并收集所有重复键
* @param obj 要检查的对象
* @returns 返回重复键的数组(若无重复则返回空数组)
* 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>()
@@ -69,7 +71,7 @@ function checkDuplicateKeys(obj: I18N): string[] {
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)
}
@@ -77,7 +79,7 @@ function checkDuplicateKeys(obj: I18N): string[] {
keys.add(fullPath)
}
// 递归检查子对象
// Recursively check child objects
if (typeof obj[key] === 'object' && obj[key] !== null) {
checkObject(obj[key], fullPath)
}
@@ -90,7 +92,7 @@ function checkDuplicateKeys(obj: I18N): string[] {
function checkTranslations() {
if (!fs.existsSync(baseFilePath)) {
throw new Error(`主模板文件 ${baseFileName} 不存在,请检查路径或文件名`)
throw new Error(`Base template file ${baseFileName} does not exist, please check path or filename`)
}
const baseContent = fs.readFileSync(baseFilePath, 'utf-8')
@@ -98,23 +100,23 @@ function checkTranslations() {
try {
baseJson = JSON.parse(baseContent)
} catch (error) {
throw new Error(`解析 ${baseFileName} 出错。${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(`主模板文件 ${baseFileName} 存在以下重复键:\n${duplicateKeys.join('\n')}`)
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(`主模板文件 ${baseFileName} 的键值未按字典序排序。`)
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 = {}
@@ -122,19 +124,19 @@ function checkTranslations() {
const fileContent = fs.readFileSync(filePath, 'utf-8')
targetJson = JSON.parse(fileContent)
} catch (error) {
throw new Error(`解析 ${file} 出错。`)
throw new Error(`Error parsing ${file}.`)
}
// 检查有序性
// Check if sorted
if (!isSortedI18N(targetJson)) {
throw new Error(`翻译文件 ${file} 的键值未按字典序排序。`)
throw new Error(`Translation file ${file} keys are not sorted.`)
}
try {
checkRecursively(targetJson, baseJson)
} catch (e) {
console.error(e)
throw new Error(`在检查 ${filePath} 时出错`)
throw new Error(`Error while checking ${filePath}`)
}
}
}
@@ -142,10 +144,10 @@ function checkTranslations() {
export function main() {
try {
checkTranslations()
console.log('i18n 检查已通过')
console.log('i18n check passed')
} catch (e) {
console.error(e)
throw new Error(`检查未通过。尝试运行 yarn sync:i18n 以解决问题。`)
throw new Error(`Check failed. Try running yarn i18n:sync to fix the issue.`)
}
}

View File

@@ -1,34 +1,27 @@
// 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
* Natural 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: string, b: string): number {
if (a > b) {
return 1
}
if (a < b) {
return -1
}
return 0
function naturalSort(a: string, b: string): number {
return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' })
}
/**
* 对对象的键按照字典序进行排序(支持嵌套对象)
* @param obj 需要排序的对象
* @returns 返回排序后的新对象
* Sort object keys in dictionary order (supports nested objects)
* @param obj The object to be sorted
* @returns A new object with sorted keys
*/
export function sortedObjectByKeys(obj: object): object {
const sortedKeys = Object.keys(obj).sort(lexicalSort)
const sortedKeys = Object.keys(obj).sort(naturalSort)
const sortedObj = {}
for (const key of sortedKeys) {
let value = obj[key]
// 如果值是对象,递归排序
// If the value is an object, sort it recursively
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
value = sortedObjectByKeys(value)
}

View File

@@ -1,147 +0,0 @@
/**
* 使用 OpenAI 兼容的模型生成 i18n 文本,并更新到 translate 目录
*
* API_KEY=sk-xxxx BASE_URL=xxxx MODEL=xxxx ts-node scripts/update-i18n.ts
*/
import OpenAI from '@cherrystudio/openai'
import cliProgress from 'cli-progress'
import fs from 'fs'
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://dashscope.aliyuncs.com/compatible-mode/v1/'
const MODEL = process.env.MODEL || 'qwen-plus-latest'
const INDEX = [
// 语言的名称代码用来翻译的模型
{ name: 'France', code: 'fr-fr', model: MODEL },
{ name: 'Spanish', code: 'es-es', model: MODEL },
{ name: 'Portuguese', code: 'pt-pt', model: MODEL },
{ name: 'Greek', code: 'el-gr', model: MODEL }
]
const zh = JSON.parse(fs.readFileSync('src/renderer/src/i18n/locales/zh-cn.json', 'utf8')) as I18N
const openai = new OpenAI({
apiKey: API_KEY,
baseURL: BASE_URL
})
// 递归遍历翻译
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 (!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]'))
) {
// 加入到本层待翻译列表
toTranslateTexts[key] = baseObj[key]
}
}
if (Object.keys(toTranslateTexts).length > 0) {
const completion = await openai.chat.completions.create({
model: model,
response_format: { type: 'json_object' },
messages: [
{
role: 'user',
content: `
You are a robot specifically designed for translation tasks. As a model that has been extensively fine-tuned on Russian language corpora, you are proficient in using the Russian 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 Russian 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({
confirm: '确定要备份数据吗?',
select_model: '选择模型',
title: '文件',
deeply_thought: '已深度思考(用时 {{seconds}} 秒)'
})}
######################################################
MAKE SURE TO OUTPUT IN Russian. DO NOT OUTPUT IN UNSPECIFIED LANGUAGE.
######################################################
`
},
{
role: 'assistant',
content: JSON.stringify({
confirm: 'Подтвердите резервное копирование данных?',
select_model: 'Выберите Модель',
title: 'Файл',
deeply_thought: 'Глубоко продумано (заняло {{seconds}} секунд)'
})
},
{
role: 'user',
content: `
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(toTranslateTexts)}
######################################################
MAKE SURE TO OUTPUT IN ${targetLang}. DO NOT OUTPUT IN UNSPECIFIED LANGUAGE.
######################################################
`
}
]
})
// 添加翻译后的键值,并打印错译漏译内容
try {
const result = JSON.parse(completion.choices[0].message.content!)
// console.debug('result', result)
for (const e in toTranslateTexts) {
if (result[e] && typeof result[e] === 'string') {
targetObj[e] = result[e]
} else {
console.warn(`missing value "${e}" in ${targetLang} translation`)
}
}
} catch (e) {
console.error(e)
for (const e in toTranslateTexts) {
console.warn(`missing value "${e}" in ${targetLang} translation`)
}
}
}
// 删除多余的键值
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')) 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

@@ -36,6 +36,8 @@ import { dataRefactorMigrateService } from './data/migrate/dataRefactor/DataRefa
import { dataApiService } from '@data/DataApiService'
import { cacheService } from '@data/CacheService'
import { initWebviewHotkeys } from './services/WebviewService'
import { i18n } from './utils/language'
import { defaultLanguage } from '@shared/config/constant'
const logger = loggerService.withContext('MainEntry')
@@ -169,6 +171,22 @@ if (!app.requestSingleInstanceLock()) {
await preferenceService.initialize()
const userLanguage = preferenceService.get('app.language')
if (userLanguage) {
i18n.changeLanguage(userLanguage)
// Do not care about cleanup because it spans the whole lifecyle of the app
preferenceService.subscribeChange('app.language', (newLang) => {
if (newLang) {
i18n.changeLanguage(newLang)
} else {
logger.error('New langauge is null, skip.')
}
})
} else {
logger.error('No user language preference found, falling back to default language')
i18n.changeLanguage(defaultLanguage)
}
// Initialize DataApiService
await dataApiService.initialize()

View File

@@ -1,4 +1,4 @@
import { getI18n } from '@main/utils/language'
import { t } from '@main/utils/language'
import type { MenuItemConstructorOptions } from 'electron'
import { Menu } from 'electron'
@@ -26,12 +26,10 @@ class ContextMenu {
}
private createInspectMenuItems(w: Electron.WebContents): MenuItemConstructorOptions[] {
const i18n = getI18n()
const { common } = i18n.translation
const template: MenuItemConstructorOptions[] = [
{
id: 'inspect',
label: common.inspect,
label: t('common.inspect'),
click: () => {
w.toggleDevTools()
},
@@ -43,29 +41,27 @@ class ContextMenu {
}
private createEditMenuItems(properties: Electron.ContextMenuParams): MenuItemConstructorOptions[] {
const i18n = getI18n()
const { common } = i18n.translation
const hasText = properties.selectionText.trim().length > 0
const can = (type: string) => properties.editFlags[`can${type}`] && hasText
const template: MenuItemConstructorOptions[] = [
{
id: 'copy',
label: common.copy,
label: t('common.copy'),
role: 'copy',
enabled: can('Copy'),
visible: properties.isEditable || hasText
},
{
id: 'paste',
label: common.paste,
label: t('common.paste'),
role: 'paste',
enabled: properties.editFlags.canPaste,
visible: properties.isEditable
},
{
id: 'cut',
label: common.cut,
label: t('common.cut'),
role: 'cut',
enabled: can('Cut'),
visible: properties.isEditable

View File

@@ -1,6 +1,6 @@
import { preferenceService } from '@data/PreferenceService'
import { isLinux, isMac, isWin } from '@main/constant'
import { getI18n } from '@main/utils/language'
import { t } from '@main/utils/language'
import type { MenuItemConstructorOptions } from 'electron'
import { app, Menu, nativeImage, nativeTheme, Tray } from 'electron'
@@ -72,23 +72,20 @@ export class TrayService {
}
private updateContextMenu() {
const i18n = getI18n()
const { tray: trayLocale, selection: selectionLocale } = i18n.translation
const quickAssistantEnabled = preferenceService.get('feature.quick_assistant.enabled')
const selectionAssistantEnabled = preferenceService.get('feature.selection.enabled')
const template = [
{
label: trayLocale.show_window,
label: t('tray.show_window'),
click: () => windowService.showMainWindow()
},
quickAssistantEnabled && {
label: trayLocale.show_mini_window,
label: t('tray.show_mini_window'),
click: () => windowService.showMiniWindow()
},
(isWin || isMac) && {
label: selectionLocale.name + (selectionAssistantEnabled ? ' - On' : ' - Off'),
label: t('selection.name') + (selectionAssistantEnabled ? ' - On' : ' - Off'),
click: () => {
if (selectionService) {
selectionService.toggleEnabled()
@@ -98,7 +95,7 @@ export class TrayService {
},
{ type: 'separator' },
{
label: trayLocale.quit,
label: t('tray.quit'),
click: () => this.quit()
}
].filter(Boolean) as MenuItemConstructorOptions[]

View File

@@ -1,33 +1,57 @@
import { preferenceService } from '@data/PreferenceService'
import { loggerService } from '@logger'
import { defaultLanguage } from '@shared/config/constant'
import type { LanguageVarious } from '@shared/data/preference/preferenceTypes'
import { app } from 'electron'
import i18n from 'i18next'
import EnUs from '../../renderer/src/i18n/locales/en-us.json'
import ZhCn from '../../renderer/src/i18n/locales/zh-cn.json'
import ZhTw from '../../renderer/src/i18n/locales/zh-tw.json'
// Machine translation
import enUS from '../../renderer/src/i18n/locales/en-us.json'
import zhCN from '../../renderer/src/i18n/locales/zh-cn.json'
import zhTW from '../../renderer/src/i18n/locales/zh-tw.json'
// import deDE from '../../renderer/src/i18n/locales/de-de.json'
// import elGR from '../../renderer/src/i18n/locales/el-gr.json'
// import esES from '../../renderer/src/i18n/locales/es-es.json'
// import frFR from '../../renderer/src/i18n/locales/fr-fr.json'
// import JaJP from '../../renderer/src/i18n/locales/ja-jp.json'
// import ptPT from '../../renderer/src/i18n/locales/pt-pt.json'
// import RuRu from '../../renderer/src/i18n/locales/ru-ru.json'
import deDE from '../../renderer/src/i18n/translate/de-de.json'
import elGR from '../../renderer/src/i18n/translate/el-gr.json'
import esES from '../../renderer/src/i18n/translate/es-es.json'
import frFR from '../../renderer/src/i18n/translate/fr-fr.json'
import JaJP from '../../renderer/src/i18n/translate/ja-jp.json'
import jaJP from '../../renderer/src/i18n/translate/ja-jp.json'
import ptPT from '../../renderer/src/i18n/translate/pt-pt.json'
import RuRu from '../../renderer/src/i18n/translate/ru-ru.json'
import ruRU from '../../renderer/src/i18n/translate/ru-ru.json'
export const locales = Object.fromEntries(
[
['en-US', EnUs],
['zh-CN', ZhCn],
['zh-TW', ZhTw],
['ja-JP', JaJP],
['ru-RU', RuRu],
['de-DE', deDE],
['el-GR', elGR],
['es-ES', esES],
['fr-FR', frFR],
['pt-PT', ptPT]
].map(([locale, translation]) => [locale, { translation }])
const logger = loggerService.withContext('main:i18n')
// const resources = Object.fromEntries([
// ['en-US', enUS],
// ['zh-CN', zhCN],
// ['zh-TW', zhTW],
// ['de-DE', deDE],
// ['el-GR', elGR],
// ['es-ES', esES],
// ['fr-FR', frFR],
// ['ja-JP', jaJP],
// ['pt-PT', ptPT],
// ['ru-RU', ruRU]
// ] as const)
const resources = Object.fromEntries(
(
[
['en-US', enUS],
['zh-CN', zhCN],
['zh-TW', zhTW],
['de-DE', deDE],
['el-GR', elGR],
['es-ES', esES],
['fr-FR', frFR],
['ja-JP', jaJP],
['pt-PT', ptPT],
['ru-RU', ruRU]
] as const
).map(([key, translation]) => [key, { translation }])
)
export const getAppLanguage = (): LanguageVarious => {
@@ -38,10 +62,44 @@ export const getAppLanguage = (): LanguageVarious => {
return language
}
return (Object.keys(locales).includes(appLocale) ? appLocale : defaultLanguage) as LanguageVarious
return (Object.keys(resources).includes(appLocale) ? appLocale : defaultLanguage) as LanguageVarious
}
export const getI18n = (): Record<string, any> => {
const language = getAppLanguage()
return locales[language]
return resources[language]
}
let t: (key: string) => string = () => {
logger.error('i18n not inialized')
return ''
}
let changeLang: (lang: LanguageVarious) => void = () => {
logger.error('i18n not inialized')
}
i18n
.init({
resources,
lng: getAppLanguage(),
fallbackLng: defaultLanguage,
ns: 'translation',
interpolation: {
escapeValue: false
},
saveMissing: true,
missingKeyHandler: (_1, _2, key) => {
logger.error(`Missing key: ${key}`)
}
})
.then((tfn) => {
changeLang = (lang: LanguageVarious) => {
i18n.changeLanguage(lang)
}
t = (key: string) => tfn(key)
const lng = getAppLanguage()
logger.debug('i18n context', { lng, resource: resources[lng] })
})
export { changeLang, i18n, t }

View File

@@ -4,11 +4,16 @@ import { defaultLanguage } from '@shared/config/constant'
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
// Original translation
import enUS from './locales/en-us.json'
import zhCN from './locales/zh-cn.json'
import zhTW from './locales/zh-tw.json'
// Machine translation
// import deDE from './locales/de-de.json'
// import elGR from './locales/el-gr.json'
// import esES from './locales/es-es.json'
// import frFR from './locales/fr-fr.json'
// import jaJP from './locales/ja-jp.json'
// import ptPT from './locales/pt-pt.json'
// import ruRU from './locales/ru-ru.json'
import deDE from './translate/de-de.json'
import elGR from './translate/el-gr.json'
import esES from './translate/es-es.json'
@@ -17,21 +22,35 @@ import jaJP from './translate/ja-jp.json'
import ptPT from './translate/pt-pt.json'
import ruRU from './translate/ru-ru.json'
const logger = loggerService.withContext('I18N')
const logger = loggerService.withContext('renderer:i18n')
// const resources = Object.fromEntries([
// ['en-US', enUS],
// ['zh-CN', zhCN],
// ['zh-TW', zhTW],
// ['de-DE', deDE],
// ['el-GR', elGR],
// ['es-ES', esES],
// ['fr-FR', frFR],
// ['ja-JP', jaJP],
// ['pt-PT', ptPT],
// ['ru-RU', ruRU]
// ])
const resources = Object.fromEntries(
[
['en-US', enUS],
['ja-JP', jaJP],
['ru-RU', ruRU],
['zh-CN', zhCN],
['zh-TW', zhTW],
['de-DE', deDE],
['el-GR', elGR],
['es-ES', esES],
['fr-FR', frFR],
['pt-PT', ptPT]
].map(([locale, translation]) => [locale, { translation }])
(
[
['en-US', enUS],
['zh-CN', zhCN],
['zh-TW', zhTW],
['de-DE', deDE],
['el-GR', elGR],
['es-ES', esES],
['fr-FR', frFR],
['ja-JP', jaJP],
['pt-PT', ptPT],
['ru-RU', ruRU]
] as const
).map(([key, translation]) => [key, { translation }])
)
export const getLanguage = async () => {

View File

@@ -0,0 +1,3 @@
仅 en-us, zh-cn, zh-tw 经过人工确认,其他翻译文件由机器翻译生成
Only en-us, zh-cn, zh-tw are manually maintained; other translation files are machine-translated.

View File

@@ -1129,8 +1129,8 @@
"pause_placeholder": "Paused",
"prompt": "Prompt",
"provider": "Provider",
"providerId": "Provider ID",
"provider_disabled": "Model provider is not enabled",
"providerId": "Provider ID",
"reason": "Reason",
"render": {
"description": "Failed to render message content. Please check if the message content format is correct",
@@ -1780,9 +1780,9 @@
"goBack": "Go Back",
"goForward": "Go Forward",
"minimize": "Minimize MinApp",
"openExternal": "Open in Browser",
"open_link_external_off": "Current: Open links in default window",
"open_link_external_on": "Current: Open links in browser",
"openExternal": "Open in Browser",
"refresh": "Refresh",
"rightclick_copyurl": "Right-click to copy URL"
},
@@ -2570,9 +2570,9 @@
"uploadError": "Image upload failed",
"uploadFile": "Upload file",
"uploadHint": "Supports JPG, PNG, GIF and other formats, max 10MB",
"uploading": "Uploading image",
"uploadSuccess": "Image uploaded successfully",
"uploadText": "Click or drag image here to upload",
"uploading": "Uploading image",
"urlPlaceholder": "Paste image link",
"urlRequired": "Please enter image URL"
},
@@ -2803,11 +2803,11 @@
},
"settings": {
"about": {
"checkingUpdate": "Checking for updates...",
"checkUpdate": {
"available": "Update",
"label": "Check Update"
},
"checkingUpdate": "Checking for updates...",
"contact": {
"button": "Email",
"title": "Contact"

View File

@@ -1129,8 +1129,8 @@
"pause_placeholder": "已中断",
"prompt": "提示词",
"provider": "提供商",
"providerId": "提供商 ID",
"provider_disabled": "模型提供商未启用",
"providerId": "提供商 ID",
"reason": "原因",
"render": {
"description": "消息内容渲染失败,请检查消息内容格式是否正确",
@@ -1780,9 +1780,9 @@
"goBack": "后退",
"goForward": "前进",
"minimize": "最小化小程序",
"openExternal": "在浏览器中打开",
"open_link_external_off": "当前:使用默认窗口打开链接",
"open_link_external_on": "当前:在浏览器中打开链接",
"openExternal": "在浏览器中打开",
"refresh": "刷新",
"rightclick_copyurl": "右键复制 URL"
},
@@ -2570,9 +2570,9 @@
"uploadError": "图片上传失败",
"uploadFile": "上传文件",
"uploadHint": "支持 JPG、PNG、GIF 等格式,最大 10MB",
"uploading": "正在上传图片",
"uploadSuccess": "图片上传成功",
"uploadText": "点击或拖拽图片到此处上传",
"uploading": "正在上传图片",
"urlPlaceholder": "粘贴图片链接地址",
"urlRequired": "请输入图片链接地址"
},
@@ -2803,11 +2803,11 @@
},
"settings": {
"about": {
"checkingUpdate": "正在检查更新...",
"checkUpdate": {
"available": "立即更新",
"label": "检查更新"
},
"checkingUpdate": "正在检查更新...",
"contact": {
"button": "邮件",
"title": "邮件联系"

View File

@@ -1129,8 +1129,8 @@
"pause_placeholder": "回應已暫停",
"prompt": "提示詞",
"provider": "提供商",
"providerId": "提供者 ID",
"provider_disabled": "模型供應商未啟用",
"providerId": "提供者 ID",
"reason": "原因",
"render": {
"description": "消息內容渲染失敗,請檢查消息內容格式是否正確",
@@ -1780,9 +1780,9 @@
"goBack": "上一頁",
"goForward": "下一頁",
"minimize": "最小化小工具",
"openExternal": "在瀏覽器中開啟",
"open_link_external_off": "当前:使用預設視窗開啟連結",
"open_link_external_on": "当前:在瀏覽器中開啟連結",
"openExternal": "在瀏覽器中開啟",
"refresh": "重新整理",
"rightclick_copyurl": "右鍵複製 URL"
},
@@ -2570,9 +2570,9 @@
"uploadError": "圖片上傳失敗",
"uploadFile": "上傳檔案",
"uploadHint": "支援 JPG、PNG、GIF 等格式,最大 10MB",
"uploading": "正在上傳圖片",
"uploadSuccess": "圖片上傳成功",
"uploadText": "點擊或拖拽圖片到此處上傳",
"uploading": "正在上傳圖片",
"urlPlaceholder": "貼上圖片連結地址",
"urlRequired": "請輸入圖片連結地址"
},
@@ -2803,11 +2803,11 @@
},
"settings": {
"about": {
"checkingUpdate": "正在檢查更新...",
"checkUpdate": {
"available": "立即更新",
"label": "檢查更新"
},
"checkingUpdate": "正在檢查更新...",
"contact": {
"button": "電子郵件",
"title": "聯絡方式"

View File

@@ -1129,8 +1129,8 @@
"pause_placeholder": "Unterbrochen",
"prompt": "Prompt",
"provider": "Anbieter",
"providerId": "Anbieter-ID",
"provider_disabled": "Modellanbieter nicht aktiviert",
"providerId": "Anbieter-ID",
"reason": "Grund",
"render": {
"description": "Rendering der Nachricht fehlgeschlagen. Bitte überprüfen Sie das Format des Nachrichteninhalts",
@@ -1780,9 +1780,9 @@
"goBack": "Zurück",
"goForward": "Vorwärts",
"minimize": "Mini-App minimieren",
"openExternal": "In Browser öffnen",
"open_link_external_off": "Aktuell: Links im Standardfenster öffnen",
"open_link_external_on": "Aktuell: Links im Browser öffnen",
"openExternal": "In Browser öffnen",
"refresh": "Aktualisieren",
"rightclick_copyurl": "Rechtsklick zum Kopieren der URL"
},
@@ -2570,9 +2570,9 @@
"uploadError": "Bild-Upload fehlgeschlagen",
"uploadFile": "Datei hochladen",
"uploadHint": "Unterstützt JPG, PNG, GIF usw., maximal 10 MB",
"uploading": "Bild wird hochgeladen",
"uploadSuccess": "Bild erfolgreich hochgeladen",
"uploadText": "Klicken oder Bild hierher ziehen zum Hochladen",
"uploading": "Bild wird hochgeladen",
"urlPlaceholder": "Bildlink-Adresse einfügen",
"urlRequired": "Bitte Bildlink-Adresse eingeben"
},
@@ -2803,11 +2803,11 @@
},
"settings": {
"about": {
"checkingUpdate": "Sucht nach Updates...",
"checkUpdate": {
"available": "Jetzt aktualisieren",
"label": "Auf Updates prüfen"
},
"checkingUpdate": "Sucht nach Updates...",
"contact": {
"button": "E-Mail",
"title": "E-Mail-Kontakt"

View File

@@ -1129,8 +1129,8 @@
"pause_placeholder": "Διακόπηκε",
"prompt": "συμβουλές",
"provider": "πάροχος",
"providerId": "Αναγνωριστικό παρόχου",
"provider_disabled": "Ο παρεχόμενος παροχός του μοντέλου δεν είναι ενεργοποιημένος",
"providerId": "Αναγνωριστικό παρόχου",
"reason": "αιτία",
"render": {
"description": "Απέτυχε η ώθηση της εξίσωσης, παρακαλώ ελέγξτε το σωστό μορφάτι της",
@@ -1780,9 +1780,9 @@
"goBack": "Πίσω",
"goForward": "Μπροστά",
"minimize": "Ελαχιστοποίηση της εφαρμογής",
"openExternal": "Άνοιγμα στον περιηγητή",
"open_link_external_off": "Τρέχον: Άνοιγμα συνδέσμου χρησιμοποιώντας το προεπιλεγμένο παράθυρο",
"open_link_external_on": "Τρέχον: Άνοιγμα συνδέσμου στον περιηγητή",
"openExternal": "Άνοιγμα στον περιηγητή",
"refresh": "Ανανέωση",
"rightclick_copyurl": "Αντιγραφή URL με δεξί κλικ"
},
@@ -2570,9 +2570,9 @@
"uploadError": "Η μεταφόρτωση της εικόνας απέτυχε",
"uploadFile": "Ανέβασμα αρχείου",
"uploadHint": "Υποστηρίζει μορφές όπως JPG, PNG, GIF, μέγιστο μέγεθος 10MB",
"uploading": "Ανεβάζει εικόνα",
"uploadSuccess": "Η εικόνα ανέβηκε με επιτυχία",
"uploadText": "Κάντε κλικ ή σύρετε την εικόνα εδώ για μεταφόρτωση",
"uploading": "Ανεβάζει εικόνα",
"urlPlaceholder": "Επικολλήστε τη διεύθυνση συνδέσμου της εικόνας",
"urlRequired": "Παρακαλώ εισαγάγετε τη διεύθυνση σύνδεσης της εικόνας"
},
@@ -2803,11 +2803,11 @@
},
"settings": {
"about": {
"checkingUpdate": "Ελέγχω ενημερώσεις...",
"checkUpdate": {
"available": "Άμεση ενημέρωση",
"label": "Έλεγχος ενημερώσεων"
},
"checkingUpdate": "Ελέγχω ενημερώσεις...",
"contact": {
"button": "Ταχυδρομείο",
"title": "Επικοινωνία μέσω ταχυδρομείου"

View File

@@ -1129,8 +1129,8 @@
"pause_placeholder": "Interrumpido",
"prompt": "prompt",
"provider": "proveedor",
"providerId": "ID del proveedor",
"provider_disabled": "El proveedor de modelos no está habilitado",
"providerId": "ID del proveedor",
"reason": "causa",
"render": {
"description": "Error al renderizar la fórmula, por favor, compruebe si el formato de la fórmula es correcto",
@@ -1780,9 +1780,9 @@
"goBack": "Retroceder",
"goForward": "Avanzar",
"minimize": "Minimizar la aplicación",
"openExternal": "Abrir en el navegador",
"open_link_external_off": "Actual: Abrir enlaces en ventana predeterminada",
"open_link_external_on": "Actual: Abrir enlaces en el navegador",
"openExternal": "Abrir en el navegador",
"refresh": "Actualizar",
"rightclick_copyurl": "Copiar URL con clic derecho"
},
@@ -2570,9 +2570,9 @@
"uploadError": "La subida de la imagen falló",
"uploadFile": "subir archivo",
"uploadHint": "Admite formatos como JPG, PNG, GIF, entre otros, con un tamaño máximo de 10MB",
"uploading": "Subiendo imágenes",
"uploadSuccess": "Imagen subida con éxito",
"uploadText": "Haz clic o arrastra la imagen aquí para subirla",
"uploading": "Subiendo imágenes",
"urlPlaceholder": "pegar el enlace de la imagen",
"urlRequired": "Por favor, introduce la dirección del enlace de la imagen"
},
@@ -2803,11 +2803,11 @@
},
"settings": {
"about": {
"checkingUpdate": "Verificando actualizaciones...",
"checkUpdate": {
"available": "Actualizar ahora",
"label": "Comprobar actualizaciones"
},
"checkingUpdate": "Verificando actualizaciones...",
"contact": {
"button": "Correo electrónico",
"title": "Contacto por correo electrónico"

View File

@@ -1129,8 +1129,8 @@
"pause_placeholder": "Прервано",
"prompt": "mot-clé",
"provider": "fournisseur",
"providerId": "ID du fournisseur",
"provider_disabled": "Le fournisseur de modèles n'est pas activé",
"providerId": "ID du fournisseur",
"reason": "raison",
"render": {
"description": "La formule n'a pas été rendue avec succès, veuillez vérifier si le format de la formule est correct",
@@ -1780,9 +1780,9 @@
"goBack": "Reculer",
"goForward": "Avancer",
"minimize": "Свернуть мини-программу",
"openExternal": "Открыть в браузере",
"open_link_external_off": "Текущий: открывать ссылки в окне по умолчанию",
"open_link_external_on": "Текущий: открывать ссылки в браузере",
"openExternal": "Открыть в браузере",
"refresh": "Обновить",
"rightclick_copyurl": "Скопировать URL через правую кнопку мыши"
},
@@ -2570,9 +2570,9 @@
"uploadError": "Échec du téléversement de l'image",
"uploadFile": "télécharger un fichier",
"uploadHint": "prend en charge les formats JPG, PNG, GIF, etc., jusqu'à 10 Mo max.",
"uploading": "Téléchargement de l'image en cours",
"uploadSuccess": "L'image a été téléchargée avec succès",
"uploadText": "Cliquez ou faites glisser l'image ici pour la télécharger",
"uploading": "Téléchargement de l'image en cours",
"urlPlaceholder": "coller l'URL de l'image",
"urlRequired": "Veuillez entrer l'URL de l'image"
},
@@ -2803,11 +2803,11 @@
},
"settings": {
"about": {
"checkingUpdate": "Vérification des mises à jour en cours...",
"checkUpdate": {
"available": "Mettre à jour maintenant",
"label": "Vérifier les mises à jour"
},
"checkingUpdate": "Vérification des mises à jour en cours...",
"contact": {
"button": "Courriel",
"title": "Contactez-nous par courriel"

View File

@@ -1129,8 +1129,8 @@
"pause_placeholder": "応答を一時停止しました",
"prompt": "プロンプトを表示する",
"provider": "プロバイダー",
"providerId": "プロバイダーID",
"provider_disabled": "モデルプロバイダーが有効になっていません",
"providerId": "プロバイダーID",
"reason": "原因",
"render": {
"description": "メッセージの内容のレンダリングに失敗しました。メッセージの内容の形式が正しいか確認してください",
@@ -1780,9 +1780,9 @@
"goBack": "戻る",
"goForward": "進む",
"minimize": "ミニアプリを最小化",
"openExternal": "ブラウザで開く",
"open_link_external_off": "現在:デフォルトのウィンドウで開く",
"open_link_external_on": "現在:ブラウザで開く",
"openExternal": "ブラウザで開く",
"refresh": "更新",
"rightclick_copyurl": "右クリックでURLをコピー"
},
@@ -2570,9 +2570,9 @@
"uploadError": "画像のアップロードに失敗しました",
"uploadFile": "ファイルをアップロード",
"uploadHint": "JPG、PNG、GIFおよびその他の形式をサポートし、最大10MB",
"uploading": "写真のアップロード",
"uploadSuccess": "画像アップロードに正常にアップロードします",
"uploadText": "画像をクリックまたはドラッグしてここにアップロードします",
"uploading": "写真のアップロード",
"urlPlaceholder": "画像リンクアドレスを貼り付けます",
"urlRequired": "画像リンクアドレスを入力してください"
},
@@ -2803,11 +2803,11 @@
},
"settings": {
"about": {
"checkingUpdate": "更新を確認中...",
"checkUpdate": {
"available": "今すぐ更新",
"label": "更新を確認"
},
"checkingUpdate": "更新を確認中...",
"contact": {
"button": "メール",
"title": "連絡先"

View File

@@ -1129,8 +1129,8 @@
"pause_placeholder": "Interrompido",
"prompt": "prompt",
"provider": "fornecedor",
"providerId": "ID do fornecedor",
"provider_disabled": "O provedor de modelos está desativado",
"providerId": "ID do fornecedor",
"reason": "causa",
"render": {
"description": "Falha ao renderizar a fórmula, por favor verifique se o formato da fórmula está correto",
@@ -1780,9 +1780,9 @@
"goBack": "Voltar",
"goForward": "Avançar",
"minimize": "Minimizar aplicativo",
"openExternal": "Abrir no navegador",
"open_link_external_off": "Atual: Abrir links em janela padrão",
"open_link_external_on": "Atual: Abrir links no navegador",
"openExternal": "Abrir no navegador",
"refresh": "Atualizar",
"rightclick_copyurl": "Copiar URL com botão direito"
},
@@ -2570,9 +2570,9 @@
"uploadError": "Falha no carregamento da imagem",
"uploadFile": "enviar arquivo",
"uploadHint": "Compatível com formatos como JPG, PNG, GIF, etc., tamanho máximo de 10MB",
"uploading": "enviando imagem",
"uploadSuccess": "Imagem enviada com sucesso",
"uploadText": "Clique ou arraste a imagem aqui para enviar",
"uploading": "enviando imagem",
"urlPlaceholder": "colar o endereço do link da imagem",
"urlRequired": "Por favor, insira o endereço do link da imagem"
},
@@ -2803,11 +2803,11 @@
},
"settings": {
"about": {
"checkingUpdate": "Verificando atualizações...",
"checkUpdate": {
"available": "Atualizar agora",
"label": "Verificar atualizações"
},
"checkingUpdate": "Verificando atualizações...",
"contact": {
"button": "E-mail",
"title": "Contato por e-mail"

View File

@@ -1129,8 +1129,8 @@
"pause_placeholder": "Получение ответа приостановлено",
"prompt": "подсказка",
"provider": "поставщик",
"providerId": "ID поставщика",
"provider_disabled": "Провайдер моделей не включен",
"providerId": "ID поставщика",
"reason": "причина",
"render": {
"description": "Не удалось рендерить содержимое сообщения. Пожалуйста, проверьте, правильно ли формат содержимого сообщения",
@@ -1780,9 +1780,9 @@
"goBack": "Назад",
"goForward": "Вперед",
"minimize": "Свернуть встроенное приложение",
"openExternal": "Открыть в браузере",
"open_link_external_off": "Текущий: Открыть ссылки в окне по умолчанию",
"open_link_external_on": "Текущий: Открыть ссылки в браузере",
"openExternal": "Открыть в браузере",
"refresh": "Обновить",
"rightclick_copyurl": "ПКМ → Копировать URL"
},
@@ -2570,9 +2570,9 @@
"uploadError": "Загрузка изображения не удалась",
"uploadFile": "Загрузить файл",
"uploadHint": "Поддерживает JPG, PNG, GIF и другие форматы, до 10 МБ",
"uploading": "Загрузка изображений",
"uploadSuccess": "Загрузка изображения успешно",
"uploadText": "Нажмите или перетащите изображение, чтобы загрузить здесь",
"uploading": "Загрузка изображений",
"urlPlaceholder": "Вставьте адрес ссылки изображения",
"urlRequired": "Пожалуйста, введите адрес ссылки изображения"
},
@@ -2803,11 +2803,11 @@
},
"settings": {
"about": {
"checkingUpdate": "Проверка обновлений...",
"checkUpdate": {
"available": "Обновить",
"label": "Проверить обновления"
},
"checkingUpdate": "Проверка обновлений...",
"contact": {
"button": "Электронная почта",
"title": "Контакты"

View File

@@ -27,7 +27,7 @@ const MessageVideo: FC<Props> = ({ block }) => {
const renderLocalVideo = () => {
if (!block.filePath) {
logger.warn('Local video was requested but block.filePath is missing.')
return <div>{t('message.video.error.local_file_missing')}</div>
return <div>{t('message.message.video.error.local_file_missing')}</div>
}
const videoSrc = `file://${block.metadata?.video.path}`
@@ -67,7 +67,7 @@ const MessageVideo: FC<Props> = ({ block }) => {
}
logger.warn(`Unsupported video type: ${block.metadata?.type} or missing necessary data.`)
return <div>{t('message.video.error.unsupported_type')}</div>
return <div>{t('message.message.video.error.unsupported_type')}</div>
}
}

View File

@@ -490,7 +490,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
const result = await SaveToKnowledgePopup.showForNote(note)
if (result?.success) {
window.toast.success(t('notes.export_success', { count: result.savedCount }))
window.toast.success(t('notes.export_success'))
}
} catch (error) {
window.toast.error(t('notes.export_failed'))

View File

@@ -502,22 +502,13 @@ export const ToolingSettings: FC<AgentToolingSettingsProps> = ({ agentBase, upda
})}
</Chip>
<Chip variant="flat" color="default">
{t('agent.settings.tooling.review.autoTools', {
defaultValue: `Auto: ${autoCount}`,
count: autoCount
})}
{t('agent.settings.tooling.review.autoTools')}: {autoCount}
</Chip>
<Chip variant="flat" color="success">
{t('agent.settings.tooling.review.customTools', {
defaultValue: `Custom: ${customCount}`,
count: customCount
})}
{t('agent.settings.tooling.review.customTools')}: {customCount}
</Chip>
<Chip variant="flat" color="warning">
{t('agent.settings.tooling.review.mcp', {
defaultValue: `MCP: ${agentSummary.mcps}`,
count: agentSummary.mcps
})}
{t('agent.settings.tooling.review.mcp')}: {agentSummary.mcps}
</Chip>
</div>
<span className="text-foreground-500 text-xs">

View File

@@ -141,41 +141,6 @@ const PreprocessProviderSettings: FC<Props> = ({ provider: _provider }) => {
</Flex>
</>
)}
{/* 这部分看起来暂时用不上了 */}
{/* {hasObjectKey(preprocessProvider, 'options') && preprocessProvider.id === 'system' && (
<>
<SettingDivider style={{ marginTop: 15, marginBottom: 12 }} />
<SettingRow>
<SettingRowTitle>{t('settings.tool.preprocess.mac_system_ocr_options.mode.title')}</SettingRowTitle>
<Segmented
options={[
{
label: t('settings.tool.preprocess.mac_system_ocr_options.mode.accurate'),
value: 1
},
{
label: t('settings.tool.preprocess.mac_system_ocr_options.mode.fast'),
value: 0
}
]}
value={options.recognitionLevel}
onChange={(value) => onUpdateOptions('recognitionLevel', value)}
/>
</SettingRow>
<SettingDivider style={{ marginTop: 15, marginBottom: 12 }} />
<SettingRow>
<SettingRowTitle>{t('settings.tool.preprocess.mac_system_ocr_options.min_confidence')}</SettingRowTitle>
<InputNumber
value={options.minConfidence}
onChange={(value) => onUpdateOptions('minConfidence', value)}
min={0}
max={1}
step={0.1}
/>
</SettingRow>
</>
)} */}
</>
)
}

825
yarn.lock

File diff suppressed because it is too large Load Diff