Compare commits
38 Commits
copilot/fi
...
refactor/i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18b5210e43 | ||
|
|
2db55a1304 | ||
|
|
b18c00e38b | ||
|
|
9b1ccb60aa | ||
|
|
7e5e3786cf | ||
|
|
57206dd3b1 | ||
|
|
b02678a714 | ||
|
|
5b4f15d355 | ||
|
|
262b0aeeb6 | ||
|
|
b181902183 | ||
|
|
bfbd934fdc | ||
|
|
75041ce952 | ||
|
|
dc469b6112 | ||
|
|
5efce861a9 | ||
|
|
112d735659 | ||
|
|
8f2b3f0bdc | ||
|
|
91704f2ee9 | ||
|
|
e1aa223e5d | ||
|
|
74eb3141cd | ||
|
|
9dcaf84da6 | ||
|
|
66e48dbba9 | ||
|
|
534459dd13 | ||
|
|
b2e2acebb1 | ||
|
|
6b3828f189 | ||
|
|
5f02822ef2 | ||
|
|
c0fe0a7774 | ||
|
|
c61aec34af | ||
|
|
3e990dddb5 | ||
|
|
cf0aa49427 | ||
|
|
311a229ab7 | ||
|
|
3677a34ceb | ||
|
|
43fcfa6c95 | ||
|
|
3b97142361 | ||
|
|
2e60db80df | ||
|
|
e5232b1fbb | ||
|
|
7365c1ca1a | ||
|
|
16a69e240b | ||
|
|
9c43bb07c0 |
2
.github/workflows/pr-ci.yml
vendored
2
.github/workflows/pr-ci.yml
vendored
@@ -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
35
.vscode/settings.json
vendored
@@ -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",
|
||||
|
||||
@@ -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**:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"useSortedKeys": {
|
||||
"level": "on",
|
||||
"options": {
|
||||
"sortOrder": "lexicographic"
|
||||
"sortOrder": "natural"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
19
i18next.config.ts
Normal 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
|
||||
}
|
||||
})
|
||||
12
package.json
12
package.json
@@ -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",
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})()
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
3
src/renderer/src/i18n/locales/README.md
Normal file
3
src/renderer/src/i18n/locales/README.md
Normal 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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "邮件联系"
|
||||
|
||||
@@ -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": "聯絡方式"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "Επικοινωνία μέσω ταχυδρομείου"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "連絡先"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "Контакты"
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)} */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user