Skip to content

Commit 350453c

Browse files
committed
feat(i18n): 添加多语言支持,包括中英文切换功能
实现应用的多语言支持,添加i18next和react-i18next库,创建语言资源文件,并在各组件中集成翻译功能。新增语言切换下拉菜单,支持中英文切换。修改相关组件使用翻译文本替代硬编码字符串。 [可选正文] - 添加i18next和react-i18next依赖 - 创建en和zh-CN语言资源文件 - 实现主进程和渲染进程的i18n初始化 - 在设置面板添加语言切换下拉菜单 - 修改Workspace、GlobalInputBar等组件使用翻译文本 - 添加IPC通道处理语言切换事件
1 parent 0a188e8 commit 350453c

File tree

16 files changed

+719
-264
lines changed

16 files changed

+719
-264
lines changed

package-lock.json

Lines changed: 81 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,12 @@
116116
"electron-log": "^5.3.2",
117117
"electron-store": "^11.0.2",
118118
"electron-updater": "^6.3.9",
119+
"i18next": "^25.6.0",
119120
"lucide-react": "^0.545.0",
120121
"next-themes": "^0.4.6",
121122
"react": "^19.0.0",
122123
"react-dom": "^19.0.0",
124+
"react-i18next": "^16.1.0",
123125
"react-router-dom": "^7.3.0",
124126
"sonner": "^2.0.7",
125127
"tailwind-merge": "^3.3.1"

src/main/main.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ import Store from 'electron-store';
1515
import { autoUpdater } from 'electron-updater';
1616
import log from 'electron-log';
1717
import { resolveHtmlPath } from './util';
18+
import i18next from 'i18next';
19+
import enCommon from '../shared/locales/en/common.json';
20+
import enMenu from '../shared/locales/en/menu.json';
21+
import zhCommon from '../shared/locales/zh-CN/common.json';
22+
import zhMenu from '../shared/locales/zh-CN/menu.json';
1823

1924
class AppUpdater {
2025
constructor() {
@@ -175,6 +180,7 @@ app.on('window-all-closed', () => {
175180
app
176181
.whenReady()
177182
.then(() => {
183+
initI18n();
178184
createWindow();
179185
app.on('activate', () => {
180186
// On macOS it's common to re-create a window in the app when the
@@ -244,18 +250,75 @@ type StoreSchema = {
244250
* - true:不再触发引导
245251
*/
246252
hasOnboarded?: boolean;
253+
/** 应用设置:语言等 */
254+
settings?: {
255+
/** 当前语言 */ language?: 'en' | 'zh-CN';
256+
};
247257
};
248258

249259
// 指定配置文件名称,默认存储在 app.getPath('userData') 下
250260
const store = new Store<StoreSchema>({ name: 'parallelchat' });
251261

262+
// —— i18n 初始化(主进程)——
263+
type Language = 'en' | 'zh-CN';
264+
const SUPPORTED_LANGS: Language[] = ['en', 'zh-CN'];
265+
const resources = {
266+
en: { common: enCommon, menu: enMenu },
267+
'zh-CN': { common: zhCommon, menu: zhMenu },
268+
};
269+
270+
function pickSupported(input: string): Language {
271+
const lower = (input || '').toLowerCase();
272+
if (lower.startsWith('zh')) return 'zh-CN';
273+
return 'en';
274+
}
275+
276+
function getInitialLanguage(): Language {
277+
try {
278+
const s = (store.get('settings') as { language?: Language } | undefined) || {};
279+
if (s.language && SUPPORTED_LANGS.includes(s.language)) return s.language;
280+
} catch {}
281+
const sys = app.getLocale();
282+
const init = pickSupported(sys);
283+
try {
284+
const existing = (store.get('settings') as any) || {};
285+
store.set('settings', { ...existing, language: init });
286+
} catch {}
287+
return init;
288+
}
289+
290+
function initI18n() {
291+
const initial = getInitialLanguage();
292+
i18next.init({
293+
lng: initial,
294+
fallbackLng: 'en',
295+
resources,
296+
ns: ['common', 'menu'],
297+
defaultNS: 'common',
298+
interpolation: { escapeValue: false },
299+
});
300+
}
301+
302+
function setLanguage(lang: Language) {
303+
if (!SUPPORTED_LANGS.includes(lang)) return;
304+
i18next.changeLanguage(lang);
305+
try {
306+
const existing = (store.get('settings') as any) || {};
307+
store.set('settings', { ...existing, language: lang });
308+
} catch {}
309+
try {
310+
mainWindow?.webContents.send('parallelchat/i18n/changed', lang);
311+
} catch {}
312+
}
313+
252314
// 允许读写的键集合(白名单)
253315
const allowedKeys: Array<keyof StoreSchema> = [
254316
'sessions',
255317
'activeSessionId',
256318
'aiProviders',
257319
'layout',
258320
'hasOnboarded',
321+
'settings',
259322
];
260323

261324
// 读取:验证键合法后返回存储值
@@ -277,6 +340,17 @@ ipcMain.handle(
277340
},
278341
);
279342

343+
// —— i18n IPC 通道 ——
344+
ipcMain.handle('parallelchat/i18n/get', () => {
345+
const language = (i18next.language as Language) || getInitialLanguage();
346+
return { language, supported: SUPPORTED_LANGS };
347+
});
348+
349+
ipcMain.handle('parallelchat/i18n/set', (_e, lang: Language) => {
350+
setLanguage(lang);
351+
return lang;
352+
});
353+
280354
// —— 缓存清理:支持单个 AI 与全部 ——
281355
ipcMain.on('parallelchat/cache/clear', (_e, id: string) => {
282356
try {

src/main/preload.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,17 @@ export type Channels =
2929
| 'parallelchat/view/visible'
3030
| 'parallelchat/onboarding/start'
3131
| 'parallelchat/onboarding/complete'
32-
// —— 会话管理 ——
3332
| 'parallelchat/session/new'
3433
| 'parallelchat/session/snapshot'
3534
| 'parallelchat/session/create'
3635
| 'parallelchat/session/load'
3736
| 'parallelchat/session/update-title'
3837
| 'parallelchat/session/delete'
39-
| 'parallelchat/session/changed';
38+
| 'parallelchat/session/changed'
39+
// —— i18n ——
40+
| 'parallelchat/i18n/get'
41+
| 'parallelchat/i18n/set'
42+
| 'parallelchat/i18n/changed';
4043

4144
// 兼容 ERB 原有的 electronHandler,用于示例交互(不建议在业务中扩展它)。
4245
const electronHandler = {

src/renderer/components/AddAiDialog.tsx

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import { useEffect, useMemo, useState } from 'react';
22
import { Button } from './ui/button';
3-
import {
4-
Dialog,
5-
DialogContent,
6-
DialogFooter,
7-
DialogHeader,
8-
DialogTitle,
9-
} from './ui/dialog';
3+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog';
4+
import { useTranslation } from 'react-i18next';
105

116
type AiProvider = { id: string; name: string; url: string; handler?: string };
127

@@ -74,6 +69,7 @@ export default function AddAiDialog({
7469
open: boolean;
7570
onClose: () => void;
7671
}) {
72+
const { t } = useTranslation();
7773
const [existing, setExisting] = useState<AiProvider[]>([]);
7874
const [selected, setSelected] = useState<Record<string, boolean>>({});
7975

@@ -131,7 +127,7 @@ export default function AddAiDialog({
131127
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
132128
<DialogContent className="w-[720px] max-w-[90vw]">
133129
<DialogHeader>
134-
<DialogTitle>添加AI助手</DialogTitle>
130+
<DialogTitle>{t('addAi.title')}</DialogTitle>
135131
</DialogHeader>
136132
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
137133
{PRESET_AI.map((p) => {
@@ -151,7 +147,7 @@ export default function AddAiDialog({
151147
}`}
152148
onClick={() => toggle(p.id)}
153149
disabled={isExisting}
154-
title={isExisting ? '已添加' : '点击选择'}
150+
title={isExisting ? t('addAi.added') : t('addAi.clickToSelect')}
155151
>
156152
<div className="font-medium">{p.name}</div>
157153
<div className="text-xs text-muted-foreground mt-1">{p.url}</div>
@@ -160,12 +156,8 @@ export default function AddAiDialog({
160156
})}
161157
</div>
162158
<DialogFooter>
163-
<Button variant="ghost" onClick={onClose}>
164-
取消
165-
</Button>
166-
<Button onClick={addSelected} disabled={!hasSelection}>
167-
添加
168-
</Button>
159+
<Button variant="ghost" onClick={onClose}>{t('actions.cancel')}</Button>
160+
<Button onClick={addSelected} disabled={!hasSelection}>{t('actions.add')}</Button>
169161
</DialogFooter>
170162
</DialogContent>
171163
</Dialog>

0 commit comments

Comments
 (0)