From 08960d700fd99390d32d77a236190b52716c8f27 Mon Sep 17 00:00:00 2001 From: Jack_lv Date: Sun, 15 Mar 2026 20:36:32 +0800 Subject: [PATCH] fix: default setup language to system locale (#500) --- README.ja-JP.md | 2 + README.md | 2 + README.zh-CN.md | 2 + electron/utils/store.ts | 80 ++++++++++++++++----------- shared/language.ts | 24 ++++++++ src/i18n/index.ts | 12 ++-- src/stores/settings.ts | 28 ++++++---- tests/unit/language-detection.test.ts | 20 +++++++ 8 files changed, 121 insertions(+), 49 deletions(-) create mode 100644 shared/language.ts create mode 100644 tests/unit/language-detection.test.ts diff --git a/README.ja-JP.md b/README.ja-JP.md index 966edcc6b..8578a42fc 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -164,6 +164,8 @@ ClawXを初めて起動すると、**セットアップウィザード**が以 3. **スキルバンドル** – 一般的なユースケース向けの事前設定スキルを選択 4. **検証** – メインインターフェースに入る前に設定をテスト +サポート対象のシステム言語がある場合、ウィザードはその言語を初期選択し、未対応の場合は英語にフォールバックします。 + ### プロキシ設定 ClawXには、Electron、OpenClaw Gateway、またはTelegramなどのチャネルがローカルプロキシクライアントを介してインターネットにアクセスする必要がある環境向けに、組み込みのプロキシ設定が含まれています。 diff --git a/README.md b/README.md index ef4ac9f20..a4bff5276 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,8 @@ When you launch ClawX for the first time, the **Setup Wizard** will guide you th 3. **Skill Bundles** – Select pre-configured skills for common use cases 4. **Verification** – Test your configuration before entering the main interface +The wizard preselects your system language when it is supported, and falls back to English otherwise. + > Note for Moonshot (Kimi): ClawX keeps Kimi web search enabled by default. > When Moonshot is configured, ClawX also syncs Kimi web search to the China endpoint (`https://api.moonshot.cn/v1`) in OpenClaw config. diff --git a/README.zh-CN.md b/README.zh-CN.md index 44fd26ad7..779890a2d 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -165,6 +165,8 @@ pnpm dev 3. **技能包** – 选择适用于常见场景的预配置技能 4. **验证** – 在进入主界面前测试你的配置 +如果系统语言在支持列表中,向导会默认选中该语言;否则回退到英文。 + > Moonshot(Kimi)说明:ClawX 默认保持开启 Kimi 的 web search。 > 当配置 Moonshot 后,ClawX 也会将 OpenClaw 配置中的 Kimi web search 同步到中国区端点(`https://api.moonshot.cn/v1`)。 diff --git a/electron/utils/store.ts b/electron/utils/store.ts index 474e84bea..4d8bb8a9b 100644 --- a/electron/utils/store.ts +++ b/electron/utils/store.ts @@ -4,6 +4,8 @@ */ import { randomBytes } from 'crypto'; +import { app } from 'electron'; +import { resolveSupportedLanguage } from '../../shared/language'; // Lazy-load electron-store (ESM module) // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -59,42 +61,54 @@ export interface AppSettings { /** * Default settings */ -const defaults: AppSettings = { - // General - theme: 'system', - language: 'en', - startMinimized: false, - launchAtStartup: false, - telemetryEnabled: true, - machineId: '', - hasReportedInstall: false, +function getSystemLocale(): string { + const preferredLanguages = typeof app.getPreferredSystemLanguages === 'function' + ? app.getPreferredSystemLanguages() + : []; + return preferredLanguages[0] + || (typeof app.getLocale === 'function' ? app.getLocale() : '') + || Intl.DateTimeFormat().resolvedOptions().locale + || 'en'; +} - // Gateway - gatewayAutoStart: true, - gatewayPort: 18789, - gatewayToken: generateToken(), - proxyEnabled: false, - proxyServer: '', - proxyHttpServer: '', - proxyHttpsServer: '', - proxyAllServer: '', - proxyBypassRules: ';localhost;127.0.0.1;::1', +function createDefaultSettings(): AppSettings { + return { + // General + theme: 'system', + language: resolveSupportedLanguage(getSystemLocale()), + startMinimized: false, + launchAtStartup: false, + telemetryEnabled: true, + machineId: '', + hasReportedInstall: false, - // Update - updateChannel: 'stable', - autoCheckUpdate: true, - autoDownloadUpdate: false, - skippedVersions: [], + // Gateway + gatewayAutoStart: true, + gatewayPort: 18789, + gatewayToken: generateToken(), + proxyEnabled: false, + proxyServer: '', + proxyHttpServer: '', + proxyHttpsServer: '', + proxyAllServer: '', + proxyBypassRules: ';localhost;127.0.0.1;::1', - // UI State - sidebarCollapsed: false, - devModeUnlocked: false, + // Update + updateChannel: 'stable', + autoCheckUpdate: true, + autoDownloadUpdate: false, + skippedVersions: [], - // Presets - selectedBundles: ['productivity', 'developer'], - enabledSkills: [], - disabledSkills: [], -}; + // UI State + sidebarCollapsed: false, + devModeUnlocked: false, + + // Presets + selectedBundles: ['productivity', 'developer'], + enabledSkills: [], + disabledSkills: [], + }; +} /** * Get the settings store instance (lazy initialization) @@ -104,7 +118,7 @@ async function getSettingsStore() { const Store = (await import('electron-store')).default; settingsStoreInstance = new Store({ name: 'settings', - defaults, + defaults: createDefaultSettings(), }); } return settingsStoreInstance; diff --git a/shared/language.ts b/shared/language.ts new file mode 100644 index 000000000..a5b3d2692 --- /dev/null +++ b/shared/language.ts @@ -0,0 +1,24 @@ +export const SUPPORTED_LANGUAGE_CODES = ['en', 'zh', 'ja'] as const; + +export type LanguageCode = (typeof SUPPORTED_LANGUAGE_CODES)[number]; + +const SUPPORTED_LANGUAGE_CODE_SET = new Set(SUPPORTED_LANGUAGE_CODES); + +function normalizeLocale(locale: string | null | undefined): string { + return locale?.trim().toLowerCase().replaceAll('_', '-') ?? ''; +} + +export function resolveSupportedLanguage( + locale: string | null | undefined, + fallback: LanguageCode = 'en', +): LanguageCode { + const normalizedLocale = normalizeLocale(locale); + if (!normalizedLocale) { + return fallback; + } + + const [baseLanguage] = normalizedLocale.split('-'); + return SUPPORTED_LANGUAGE_CODE_SET.has(baseLanguage) + ? (baseLanguage as LanguageCode) + : fallback; +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 5e0cc5253..58ac0f41b 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -1,5 +1,10 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; +import { + SUPPORTED_LANGUAGE_CODES, + resolveSupportedLanguage, + type LanguageCode, +} from '../../shared/language'; // EN import enCommon from './locales/en/common.json'; @@ -38,9 +43,7 @@ export const SUPPORTED_LANGUAGES = [ { code: 'en', label: 'English' }, { code: 'zh', label: '中文' }, { code: 'ja', label: '日本語' }, -] as const; - -export type LanguageCode = (typeof SUPPORTED_LANGUAGES)[number]['code']; +] as const satisfies ReadonlyArray<{ code: LanguageCode; label: string }>; const resources = { en: { @@ -82,8 +85,9 @@ i18n .use(initReactI18next) .init({ resources, - lng: 'en', // will be overridden by settings store + lng: resolveSupportedLanguage(typeof navigator !== 'undefined' ? navigator.language : undefined), fallbackLng: 'en', + supportedLngs: [...SUPPORTED_LANGUAGE_CODES], defaultNS: 'common', ns: ['common', 'settings', 'dashboard', 'chat', 'channels', 'agents', 'skills', 'cron', 'setup'], interpolation: { diff --git a/src/stores/settings.ts b/src/stores/settings.ts index 3a186d61f..bfe9abe1b 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -6,6 +6,7 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import i18n from '@/i18n'; import { hostApiFetch } from '@/lib/host-api'; +import { resolveSupportedLanguage } from '../../shared/language'; type Theme = 'light' | 'dark' | 'system'; type UpdateChannel = 'stable' | 'beta' | 'dev'; @@ -66,12 +67,7 @@ interface SettingsState { const defaultSettings = { theme: 'system' as Theme, - language: (() => { - const lang = navigator.language.toLowerCase(); - if (lang.startsWith('zh')) return 'zh'; - if (lang.startsWith('ja')) return 'ja'; - return 'en'; - })(), + language: resolveSupportedLanguage(typeof navigator !== 'undefined' ? navigator.language : undefined), startMinimized: false, launchAtStartup: false, telemetryEnabled: true, @@ -99,9 +95,16 @@ export const useSettingsStore = create()( init: async () => { try { const settings = await hostApiFetch>('/api/settings'); - set((state) => ({ ...state, ...settings })); - if (settings.language) { - i18n.changeLanguage(settings.language); + const resolvedLanguage = settings.language + ? resolveSupportedLanguage(settings.language) + : undefined; + set((state) => ({ + ...state, + ...settings, + ...(resolvedLanguage ? { language: resolvedLanguage } : {}), + })); + if (resolvedLanguage) { + i18n.changeLanguage(resolvedLanguage); } } catch { // Keep renderer-persisted settings as a fallback when the main @@ -111,11 +114,12 @@ export const useSettingsStore = create()( setTheme: (theme) => set({ theme }), setLanguage: (language) => { - i18n.changeLanguage(language); - set({ language }); + const resolvedLanguage = resolveSupportedLanguage(language); + i18n.changeLanguage(resolvedLanguage); + set({ language: resolvedLanguage }); void hostApiFetch('/api/settings/language', { method: 'PUT', - body: JSON.stringify({ value: language }), + body: JSON.stringify({ value: resolvedLanguage }), }).catch(() => { }); }, setStartMinimized: (startMinimized) => set({ startMinimized }), diff --git a/tests/unit/language-detection.test.ts b/tests/unit/language-detection.test.ts new file mode 100644 index 000000000..c4ef21a61 --- /dev/null +++ b/tests/unit/language-detection.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; +import { resolveSupportedLanguage } from '../../shared/language'; + +describe('resolveSupportedLanguage', () => { + it('uses the base language for supported regional locales', () => { + expect(resolveSupportedLanguage('zh-CN')).toBe('zh'); + expect(resolveSupportedLanguage('ja_JP')).toBe('ja'); + expect(resolveSupportedLanguage('en-US')).toBe('en'); + }); + + it('falls back to English for unsupported locales', () => { + expect(resolveSupportedLanguage('fr-FR')).toBe('en'); + expect(resolveSupportedLanguage('ko')).toBe('en'); + }); + + it('falls back to English when locale is missing', () => { + expect(resolveSupportedLanguage('')).toBe('en'); + expect(resolveSupportedLanguage(undefined)).toBe('en'); + }); +});