fix: default setup language to system locale (#500)

This commit is contained in:
Jack_lv
2026-03-15 20:36:32 +08:00
committed by GitHub
Unverified
parent 04aa94f907
commit 08960d700f
8 changed files with 121 additions and 49 deletions

View File

@@ -164,6 +164,8 @@ ClawXを初めて起動すると、**セットアップウィザード**が以
3. **スキルバンドル** 一般的なユースケース向けの事前設定スキルを選択 3. **スキルバンドル** 一般的なユースケース向けの事前設定スキルを選択
4. **検証** メインインターフェースに入る前に設定をテスト 4. **検証** メインインターフェースに入る前に設定をテスト
サポート対象のシステム言語がある場合、ウィザードはその言語を初期選択し、未対応の場合は英語にフォールバックします。
### プロキシ設定 ### プロキシ設定
ClawXには、Electron、OpenClaw Gateway、またはTelegramなどのチャネルがローカルプロキシクライアントを介してインターネットにアクセスする必要がある環境向けに、組み込みのプロキシ設定が含まれています。 ClawXには、Electron、OpenClaw Gateway、またはTelegramなどのチャネルがローカルプロキシクライアントを介してインターネットにアクセスする必要がある環境向けに、組み込みのプロキシ設定が含まれています。

View File

@@ -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 3. **Skill Bundles** Select pre-configured skills for common use cases
4. **Verification** Test your configuration before entering the main interface 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. > 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. > When Moonshot is configured, ClawX also syncs Kimi web search to the China endpoint (`https://api.moonshot.cn/v1`) in OpenClaw config.

View File

@@ -165,6 +165,8 @@ pnpm dev
3. **技能包** 选择适用于常见场景的预配置技能 3. **技能包** 选择适用于常见场景的预配置技能
4. **验证** 在进入主界面前测试你的配置 4. **验证** 在进入主界面前测试你的配置
如果系统语言在支持列表中,向导会默认选中该语言;否则回退到英文。
> MoonshotKimi说明ClawX 默认保持开启 Kimi 的 web search。 > MoonshotKimi说明ClawX 默认保持开启 Kimi 的 web search。
> 当配置 Moonshot 后ClawX 也会将 OpenClaw 配置中的 Kimi web search 同步到中国区端点(`https://api.moonshot.cn/v1`)。 > 当配置 Moonshot 后ClawX 也会将 OpenClaw 配置中的 Kimi web search 同步到中国区端点(`https://api.moonshot.cn/v1`)。

View File

@@ -4,6 +4,8 @@
*/ */
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import { app } from 'electron';
import { resolveSupportedLanguage } from '../../shared/language';
// Lazy-load electron-store (ESM module) // Lazy-load electron-store (ESM module)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -59,42 +61,54 @@ export interface AppSettings {
/** /**
* Default settings * Default settings
*/ */
const defaults: AppSettings = { function getSystemLocale(): string {
// General const preferredLanguages = typeof app.getPreferredSystemLanguages === 'function'
theme: 'system', ? app.getPreferredSystemLanguages()
language: 'en', : [];
startMinimized: false, return preferredLanguages[0]
launchAtStartup: false, || (typeof app.getLocale === 'function' ? app.getLocale() : '')
telemetryEnabled: true, || Intl.DateTimeFormat().resolvedOptions().locale
machineId: '', || 'en';
hasReportedInstall: false, }
// Gateway function createDefaultSettings(): AppSettings {
gatewayAutoStart: true, return {
gatewayPort: 18789, // General
gatewayToken: generateToken(), theme: 'system',
proxyEnabled: false, language: resolveSupportedLanguage(getSystemLocale()),
proxyServer: '', startMinimized: false,
proxyHttpServer: '', launchAtStartup: false,
proxyHttpsServer: '', telemetryEnabled: true,
proxyAllServer: '', machineId: '',
proxyBypassRules: '<local>;localhost;127.0.0.1;::1', hasReportedInstall: false,
// Update // Gateway
updateChannel: 'stable', gatewayAutoStart: true,
autoCheckUpdate: true, gatewayPort: 18789,
autoDownloadUpdate: false, gatewayToken: generateToken(),
skippedVersions: [], proxyEnabled: false,
proxyServer: '',
proxyHttpServer: '',
proxyHttpsServer: '',
proxyAllServer: '',
proxyBypassRules: '<local>;localhost;127.0.0.1;::1',
// UI State // Update
sidebarCollapsed: false, updateChannel: 'stable',
devModeUnlocked: false, autoCheckUpdate: true,
autoDownloadUpdate: false,
skippedVersions: [],
// Presets // UI State
selectedBundles: ['productivity', 'developer'], sidebarCollapsed: false,
enabledSkills: [], devModeUnlocked: false,
disabledSkills: [],
}; // Presets
selectedBundles: ['productivity', 'developer'],
enabledSkills: [],
disabledSkills: [],
};
}
/** /**
* Get the settings store instance (lazy initialization) * Get the settings store instance (lazy initialization)
@@ -104,7 +118,7 @@ async function getSettingsStore() {
const Store = (await import('electron-store')).default; const Store = (await import('electron-store')).default;
settingsStoreInstance = new Store<AppSettings>({ settingsStoreInstance = new Store<AppSettings>({
name: 'settings', name: 'settings',
defaults, defaults: createDefaultSettings(),
}); });
} }
return settingsStoreInstance; return settingsStoreInstance;

24
shared/language.ts Normal file
View File

@@ -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<string>(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;
}

View File

@@ -1,5 +1,10 @@
import i18n from 'i18next'; import i18n from 'i18next';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
import {
SUPPORTED_LANGUAGE_CODES,
resolveSupportedLanguage,
type LanguageCode,
} from '../../shared/language';
// EN // EN
import enCommon from './locales/en/common.json'; import enCommon from './locales/en/common.json';
@@ -38,9 +43,7 @@ export const SUPPORTED_LANGUAGES = [
{ code: 'en', label: 'English' }, { code: 'en', label: 'English' },
{ code: 'zh', label: '中文' }, { code: 'zh', label: '中文' },
{ code: 'ja', label: '日本語' }, { code: 'ja', label: '日本語' },
] as const; ] as const satisfies ReadonlyArray<{ code: LanguageCode; label: string }>;
export type LanguageCode = (typeof SUPPORTED_LANGUAGES)[number]['code'];
const resources = { const resources = {
en: { en: {
@@ -82,8 +85,9 @@ i18n
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
resources, resources,
lng: 'en', // will be overridden by settings store lng: resolveSupportedLanguage(typeof navigator !== 'undefined' ? navigator.language : undefined),
fallbackLng: 'en', fallbackLng: 'en',
supportedLngs: [...SUPPORTED_LANGUAGE_CODES],
defaultNS: 'common', defaultNS: 'common',
ns: ['common', 'settings', 'dashboard', 'chat', 'channels', 'agents', 'skills', 'cron', 'setup'], ns: ['common', 'settings', 'dashboard', 'chat', 'channels', 'agents', 'skills', 'cron', 'setup'],
interpolation: { interpolation: {

View File

@@ -6,6 +6,7 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import i18n from '@/i18n'; import i18n from '@/i18n';
import { hostApiFetch } from '@/lib/host-api'; import { hostApiFetch } from '@/lib/host-api';
import { resolveSupportedLanguage } from '../../shared/language';
type Theme = 'light' | 'dark' | 'system'; type Theme = 'light' | 'dark' | 'system';
type UpdateChannel = 'stable' | 'beta' | 'dev'; type UpdateChannel = 'stable' | 'beta' | 'dev';
@@ -66,12 +67,7 @@ interface SettingsState {
const defaultSettings = { const defaultSettings = {
theme: 'system' as Theme, theme: 'system' as Theme,
language: (() => { language: resolveSupportedLanguage(typeof navigator !== 'undefined' ? navigator.language : undefined),
const lang = navigator.language.toLowerCase();
if (lang.startsWith('zh')) return 'zh';
if (lang.startsWith('ja')) return 'ja';
return 'en';
})(),
startMinimized: false, startMinimized: false,
launchAtStartup: false, launchAtStartup: false,
telemetryEnabled: true, telemetryEnabled: true,
@@ -99,9 +95,16 @@ export const useSettingsStore = create<SettingsState>()(
init: async () => { init: async () => {
try { try {
const settings = await hostApiFetch<Partial<typeof defaultSettings>>('/api/settings'); const settings = await hostApiFetch<Partial<typeof defaultSettings>>('/api/settings');
set((state) => ({ ...state, ...settings })); const resolvedLanguage = settings.language
if (settings.language) { ? resolveSupportedLanguage(settings.language)
i18n.changeLanguage(settings.language); : undefined;
set((state) => ({
...state,
...settings,
...(resolvedLanguage ? { language: resolvedLanguage } : {}),
}));
if (resolvedLanguage) {
i18n.changeLanguage(resolvedLanguage);
} }
} catch { } catch {
// Keep renderer-persisted settings as a fallback when the main // Keep renderer-persisted settings as a fallback when the main
@@ -111,11 +114,12 @@ export const useSettingsStore = create<SettingsState>()(
setTheme: (theme) => set({ theme }), setTheme: (theme) => set({ theme }),
setLanguage: (language) => { setLanguage: (language) => {
i18n.changeLanguage(language); const resolvedLanguage = resolveSupportedLanguage(language);
set({ language }); i18n.changeLanguage(resolvedLanguage);
set({ language: resolvedLanguage });
void hostApiFetch('/api/settings/language', { void hostApiFetch('/api/settings/language', {
method: 'PUT', method: 'PUT',
body: JSON.stringify({ value: language }), body: JSON.stringify({ value: resolvedLanguage }),
}).catch(() => { }); }).catch(() => { });
}, },
setStartMinimized: (startMinimized) => set({ startMinimized }), setStartMinimized: (startMinimized) => set({ startMinimized }),

View File

@@ -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');
});
});