fix: default setup language to system locale (#500)
This commit is contained in:
committed by
GitHub
Unverified
parent
04aa94f907
commit
08960d700f
@@ -164,6 +164,8 @@ ClawXを初めて起動すると、**セットアップウィザード**が以
|
|||||||
3. **スキルバンドル** – 一般的なユースケース向けの事前設定スキルを選択
|
3. **スキルバンドル** – 一般的なユースケース向けの事前設定スキルを選択
|
||||||
4. **検証** – メインインターフェースに入る前に設定をテスト
|
4. **検証** – メインインターフェースに入る前に設定をテスト
|
||||||
|
|
||||||
|
サポート対象のシステム言語がある場合、ウィザードはその言語を初期選択し、未対応の場合は英語にフォールバックします。
|
||||||
|
|
||||||
### プロキシ設定
|
### プロキシ設定
|
||||||
|
|
||||||
ClawXには、Electron、OpenClaw Gateway、またはTelegramなどのチャネルがローカルプロキシクライアントを介してインターネットにアクセスする必要がある環境向けに、組み込みのプロキシ設定が含まれています。
|
ClawXには、Electron、OpenClaw Gateway、またはTelegramなどのチャネルがローカルプロキシクライアントを介してインターネットにアクセスする必要がある環境向けに、組み込みのプロキシ設定が含まれています。
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ pnpm dev
|
|||||||
3. **技能包** – 选择适用于常见场景的预配置技能
|
3. **技能包** – 选择适用于常见场景的预配置技能
|
||||||
4. **验证** – 在进入主界面前测试你的配置
|
4. **验证** – 在进入主界面前测试你的配置
|
||||||
|
|
||||||
|
如果系统语言在支持列表中,向导会默认选中该语言;否则回退到英文。
|
||||||
|
|
||||||
> Moonshot(Kimi)说明:ClawX 默认保持开启 Kimi 的 web search。
|
> Moonshot(Kimi)说明: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`)。
|
||||||
|
|
||||||
|
|||||||
@@ -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,10 +61,21 @@ export interface AppSettings {
|
|||||||
/**
|
/**
|
||||||
* Default settings
|
* Default settings
|
||||||
*/
|
*/
|
||||||
const defaults: AppSettings = {
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDefaultSettings(): AppSettings {
|
||||||
|
return {
|
||||||
// General
|
// General
|
||||||
theme: 'system',
|
theme: 'system',
|
||||||
language: 'en',
|
language: resolveSupportedLanguage(getSystemLocale()),
|
||||||
startMinimized: false,
|
startMinimized: false,
|
||||||
launchAtStartup: false,
|
launchAtStartup: false,
|
||||||
telemetryEnabled: true,
|
telemetryEnabled: true,
|
||||||
@@ -94,7 +107,8 @@ const defaults: AppSettings = {
|
|||||||
selectedBundles: ['productivity', 'developer'],
|
selectedBundles: ['productivity', 'developer'],
|
||||||
enabledSkills: [],
|
enabledSkills: [],
|
||||||
disabledSkills: [],
|
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
24
shared/language.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
20
tests/unit/language-detection.test.ts
Normal file
20
tests/unit/language-detection.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user