feat(i18n): language selection feature (#9405)
Signed-off-by: Dmitry Beskov <43372966+besdar@users.noreply.github.com>
This commit is contained in:
@@ -100,11 +100,15 @@ Compiled files go to `src/i18n/compiled/` (gitignored).
|
||||
|
||||
The locale is resolved at startup in the following order:
|
||||
|
||||
1. **`GOOSE_LOCALE`** — explicit override (set on the `window` object or via env)
|
||||
2. **`navigator.language`** — the browser/OS locale
|
||||
3. **`"en"`** — fallback default
|
||||
1. **Desktop language setting** — selected in Settings, unless set to System Default
|
||||
2. **`GOOSE_LOCALE`** — explicit override from env/app config when the desktop setting is
|
||||
System Default
|
||||
3. **`navigator.language`** — the browser/OS locale
|
||||
4. **`"en"`** — fallback default
|
||||
|
||||
The resolved locale is used for both text translations and all Intl formatting (dates, numbers, relative times).
|
||||
The resolved locale is used for both text translations and all Intl formatting (dates, numbers,
|
||||
relative times). Changing the desktop language setting reloads the renderer so startup-only locale
|
||||
helpers pick up the new value.
|
||||
|
||||
## Date and number formatting
|
||||
|
||||
@@ -134,7 +138,8 @@ This ensures date/number formatting uses the same locale as the rest of the UI.
|
||||
1. Copy `src/i18n/messages/en.json` to a new file, e.g., `src/i18n/messages/ja.json`.
|
||||
2. Translate the `defaultMessage` values. Keep ICU syntax intact (e.g., `{count, plural, ...}`).
|
||||
3. Add the locale code to `SUPPORTED_LOCALES` in `src/i18n/index.ts`.
|
||||
4. Optionally run `pnpm i18n:compile` to pre-compile.
|
||||
4. Add the locale to the language selector in `src/components/settings/app/AppSettingsSection.tsx`.
|
||||
5. Optionally run `pnpm i18n:compile` to pre-compile.
|
||||
|
||||
No other code changes are needed — `loadMessages()` dynamically imports the correct catalog at runtime.
|
||||
|
||||
|
||||
@@ -2,8 +2,15 @@ import { useState, useEffect, useRef } from 'react';
|
||||
import { defineMessages, useIntl } from '../../../i18n';
|
||||
import { Switch } from '../../ui/switch';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Settings } from 'lucide-react';
|
||||
import { ChevronDown, Settings } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '../../ui/dropdown-menu';
|
||||
import UpdateSection from './UpdateSection';
|
||||
|
||||
import { COST_TRACKING_ENABLED, UPDATES_ENABLED } from '../../../updates';
|
||||
@@ -13,6 +20,7 @@ import BlockLogoBlack from './icons/block-lockup_black.png';
|
||||
import BlockLogoWhite from './icons/block-lockup_white.png';
|
||||
import TelemetrySettings from './TelemetrySettings';
|
||||
import { trackSettingToggled } from '../../../utils/analytics';
|
||||
import type { LanguageSetting } from '../../../utils/settings';
|
||||
|
||||
const i18n = defineMessages({
|
||||
appearanceTitle: { id: 'settings.appearance.title', defaultMessage: 'Appearance' },
|
||||
@@ -58,6 +66,21 @@ const i18n = defineMessages({
|
||||
id: 'settings.theme.description',
|
||||
defaultMessage: 'Customize the look and feel of goose',
|
||||
},
|
||||
languageTitle: { id: 'settings.language.title', defaultMessage: 'Language' },
|
||||
languageDesc: {
|
||||
id: 'settings.language.description',
|
||||
defaultMessage: 'Choose the display language for goose',
|
||||
},
|
||||
languageSystem: { id: 'settings.language.systemDefault', defaultMessage: 'System Default' },
|
||||
languageEnglish: { id: 'settings.language.english', defaultMessage: 'English' },
|
||||
languageChineseSimplified: {
|
||||
id: 'settings.language.zhCN',
|
||||
defaultMessage: 'Chinese (Simplified)',
|
||||
},
|
||||
languageRussian: { id: 'settings.language.russian', defaultMessage: 'Russian' },
|
||||
languageTurkish: { id: 'settings.language.turkish', defaultMessage: 'Turkish' },
|
||||
languageHindi: { id: 'settings.language.hindi', defaultMessage: 'Hindi' },
|
||||
languageJapanese: { id: 'settings.language.japanese', defaultMessage: 'Japanese' },
|
||||
helpTitle: { id: 'settings.help.title', defaultMessage: 'Help & feedback' },
|
||||
helpDesc: {
|
||||
id: 'settings.help.description',
|
||||
@@ -118,6 +141,16 @@ const i18n = defineMessages({
|
||||
close: { id: 'settings.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
||||
const LANGUAGE_OPTIONS: Array<{ value: LanguageSetting; message: keyof typeof i18n }> = [
|
||||
{ value: 'system', message: 'languageSystem' },
|
||||
{ value: 'en', message: 'languageEnglish' },
|
||||
{ value: 'hi', message: 'languageHindi' },
|
||||
{ value: 'ja', message: 'languageJapanese' },
|
||||
{ value: 'ru', message: 'languageRussian' },
|
||||
{ value: 'tr', message: 'languageTurkish' },
|
||||
{ value: 'zh-CN', message: 'languageChineseSimplified' },
|
||||
];
|
||||
|
||||
interface AppSettingsSectionProps {
|
||||
scrollToSection?: string;
|
||||
}
|
||||
@@ -131,6 +164,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
|
||||
const [isDockSwitchDisabled, setIsDockSwitchDisabled] = useState(false);
|
||||
const [showNotificationModal, setShowNotificationModal] = useState(false);
|
||||
const [showPricing, setShowPricing] = useState(true);
|
||||
const [language, setLanguage] = useState<LanguageSetting>('system');
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
const updateSectionRef = useRef<HTMLDivElement>(null);
|
||||
const shouldShowUpdates = !window.appConfig.get('GOOSE_VERSION');
|
||||
@@ -157,6 +191,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.getSetting('showPricing').then(setShowPricing);
|
||||
window.electron.getSetting('language').then((value) => setLanguage(value ?? 'system'));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -252,7 +287,25 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
|
||||
window.dispatchEvent(new CustomEvent('showPricingChanged'));
|
||||
};
|
||||
|
||||
const handleLanguageChange = async (value: string) => {
|
||||
const nextLanguage = LANGUAGE_OPTIONS.find((option) => option.value === value)?.value;
|
||||
if (!nextLanguage || nextLanguage === language) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLanguage(nextLanguage);
|
||||
try {
|
||||
await window.electron.setSetting('language', nextLanguage);
|
||||
window.electron.reloadApp();
|
||||
} catch (error) {
|
||||
console.error('Failed to update language setting:', error);
|
||||
setLanguage(language);
|
||||
}
|
||||
};
|
||||
|
||||
const intl = useIntl();
|
||||
const selectedLanguage =
|
||||
LANGUAGE_OPTIONS.find((option) => option.value === language) ?? LANGUAGE_OPTIONS[0];
|
||||
|
||||
return (
|
||||
<div className="space-y-4 pr-4 pb-8 mt-1">
|
||||
@@ -400,6 +453,29 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="rounded-lg">
|
||||
<CardHeader className="pb-0">
|
||||
<CardTitle className="mb-1">{intl.formatMessage(i18n.languageTitle)}</CardTitle>
|
||||
<CardDescription>{intl.formatMessage(i18n.languageDesc)}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 px-4">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex w-full max-w-[260px] items-center justify-between gap-2 rounded-md border border-border-primary bg-background-primary px-3 py-2 text-sm text-text-primary transition-colors hover:border-border-primary">
|
||||
<span className="truncate">{intl.formatMessage(i18n[selectedLanguage.message])}</span>
|
||||
<ChevronDown className="h-4 w-4 shrink-0" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-[260px]">
|
||||
<DropdownMenuRadioGroup value={language} onValueChange={handleLanguageChange}>
|
||||
{LANGUAGE_OPTIONS.map((option) => (
|
||||
<DropdownMenuRadioItem key={option.value} value={option.value}>
|
||||
{intl.formatMessage(i18n[option.message])}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TelemetrySettings />
|
||||
|
||||
<Card className="rounded-lg">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Locale detection and message loading for the i18n system.
|
||||
*
|
||||
* Locale resolution order:
|
||||
* 1. GOOSE_LOCALE config value (set via environment variable, passed through appConfig)
|
||||
* 1. GOOSE_LOCALE config value (manual setting or environment variable, passed through appConfig)
|
||||
* 2. navigator.languages (full accept-language list from OS/browser)
|
||||
* 3. "en" (fallback)
|
||||
*
|
||||
@@ -15,7 +15,9 @@
|
||||
export { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
/** The set of locales that have translation catalogs. */
|
||||
const SUPPORTED_LOCALES = new Set(['en', 'hi', 'ru', 'tr', 'zh-CN', 'ja']);
|
||||
export const SUPPORTED_LOCALES = ['en', 'hi', 'ja', 'ru', 'tr', 'zh-CN'] as const;
|
||||
export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
|
||||
const SUPPORTED_LOCALE_SET = new Set<string>(SUPPORTED_LOCALES);
|
||||
|
||||
/**
|
||||
* Map Simplified Chinese aliases (zh, zh-Hans*, zh-SG, zh-MY) to "zh-CN".
|
||||
@@ -61,12 +63,12 @@ export function getLocale(): { locale: string; messageLocale: string } {
|
||||
const tag = resolveChineseAlias(normalized);
|
||||
|
||||
// Exact match first
|
||||
if (SUPPORTED_LOCALES.has(tag)) return { locale: tag, messageLocale: tag };
|
||||
if (SUPPORTED_LOCALE_SET.has(tag)) return { locale: tag, messageLocale: tag };
|
||||
|
||||
// Try base language (e.g. "pt-BR" → "pt") for the catalog, but keep the
|
||||
// full regional tag for formatting so date/number output respects the region.
|
||||
const base = tag.split('-')[0];
|
||||
if (SUPPORTED_LOCALES.has(base)) {
|
||||
if (SUPPORTED_LOCALE_SET.has(base)) {
|
||||
// Validate the full tag is a well-formed BCP 47 locale before using it
|
||||
// for formatting. Invalid tags (e.g. "en-") would cause RangeError in
|
||||
// Intl APIs, so fall back to the base language in that case.
|
||||
|
||||
@@ -4046,6 +4046,33 @@
|
||||
"settings.help.title": {
|
||||
"defaultMessage": "Help & feedback"
|
||||
},
|
||||
"settings.language.description": {
|
||||
"defaultMessage": "Choose the display language for goose"
|
||||
},
|
||||
"settings.language.english": {
|
||||
"defaultMessage": "English"
|
||||
},
|
||||
"settings.language.hindi": {
|
||||
"defaultMessage": "Hindi"
|
||||
},
|
||||
"settings.language.japanese": {
|
||||
"defaultMessage": "Japanese"
|
||||
},
|
||||
"settings.language.russian": {
|
||||
"defaultMessage": "Russian"
|
||||
},
|
||||
"settings.language.systemDefault": {
|
||||
"defaultMessage": "System Default"
|
||||
},
|
||||
"settings.language.title": {
|
||||
"defaultMessage": "Language"
|
||||
},
|
||||
"settings.language.turkish": {
|
||||
"defaultMessage": "Turkish"
|
||||
},
|
||||
"settings.language.zhCN": {
|
||||
"defaultMessage": "Chinese (Simplified)"
|
||||
},
|
||||
"settings.menuBarIcon.description": {
|
||||
"defaultMessage": "Show goose in the menu bar"
|
||||
},
|
||||
|
||||
@@ -4046,6 +4046,33 @@
|
||||
"settings.help.title": {
|
||||
"defaultMessage": "सहायता एवं प्रतिक्रिया"
|
||||
},
|
||||
"settings.language.description": {
|
||||
"defaultMessage": "goose के लिए प्रदर्शन भाषा चुनें"
|
||||
},
|
||||
"settings.language.english": {
|
||||
"defaultMessage": "अंग्रेज़ी"
|
||||
},
|
||||
"settings.language.hindi": {
|
||||
"defaultMessage": "हिन्दी"
|
||||
},
|
||||
"settings.language.japanese": {
|
||||
"defaultMessage": "जापानी"
|
||||
},
|
||||
"settings.language.russian": {
|
||||
"defaultMessage": "रूसी"
|
||||
},
|
||||
"settings.language.systemDefault": {
|
||||
"defaultMessage": "सिस्टम डिफ़ॉल्ट"
|
||||
},
|
||||
"settings.language.title": {
|
||||
"defaultMessage": "भाषा"
|
||||
},
|
||||
"settings.language.turkish": {
|
||||
"defaultMessage": "तुर्की"
|
||||
},
|
||||
"settings.language.zhCN": {
|
||||
"defaultMessage": "चीनी (सरलीकृत)"
|
||||
},
|
||||
"settings.menuBarIcon.description": {
|
||||
"defaultMessage": "मेनू बार में goose दिखाएँ"
|
||||
},
|
||||
|
||||
@@ -4046,6 +4046,33 @@
|
||||
"settings.help.title": {
|
||||
"defaultMessage": "ヘルプとフィードバック"
|
||||
},
|
||||
"settings.language.description": {
|
||||
"defaultMessage": "goose の表示言語を選択"
|
||||
},
|
||||
"settings.language.english": {
|
||||
"defaultMessage": "英語"
|
||||
},
|
||||
"settings.language.hindi": {
|
||||
"defaultMessage": "ヒンディー語"
|
||||
},
|
||||
"settings.language.japanese": {
|
||||
"defaultMessage": "日本語"
|
||||
},
|
||||
"settings.language.russian": {
|
||||
"defaultMessage": "ロシア語"
|
||||
},
|
||||
"settings.language.systemDefault": {
|
||||
"defaultMessage": "システムデフォルト"
|
||||
},
|
||||
"settings.language.title": {
|
||||
"defaultMessage": "言語"
|
||||
},
|
||||
"settings.language.turkish": {
|
||||
"defaultMessage": "トルコ語"
|
||||
},
|
||||
"settings.language.zhCN": {
|
||||
"defaultMessage": "中国語(簡体字)"
|
||||
},
|
||||
"settings.menuBarIcon.description": {
|
||||
"defaultMessage": "メニューバーにgooseを表示します"
|
||||
},
|
||||
|
||||
@@ -4046,6 +4046,33 @@
|
||||
"settings.help.title": {
|
||||
"defaultMessage": "Помощь и отзывы"
|
||||
},
|
||||
"settings.language.description": {
|
||||
"defaultMessage": "Выберите язык интерфейса goose"
|
||||
},
|
||||
"settings.language.english": {
|
||||
"defaultMessage": "Английский"
|
||||
},
|
||||
"settings.language.hindi": {
|
||||
"defaultMessage": "Хинди"
|
||||
},
|
||||
"settings.language.japanese": {
|
||||
"defaultMessage": "Японский"
|
||||
},
|
||||
"settings.language.russian": {
|
||||
"defaultMessage": "Русский"
|
||||
},
|
||||
"settings.language.systemDefault": {
|
||||
"defaultMessage": "Системный язык"
|
||||
},
|
||||
"settings.language.title": {
|
||||
"defaultMessage": "Язык"
|
||||
},
|
||||
"settings.language.turkish": {
|
||||
"defaultMessage": "Турецкий"
|
||||
},
|
||||
"settings.language.zhCN": {
|
||||
"defaultMessage": "Китайский (упрощенный)"
|
||||
},
|
||||
"settings.menuBarIcon.description": {
|
||||
"defaultMessage": "Показывать goose в строке меню"
|
||||
},
|
||||
|
||||
@@ -4046,6 +4046,33 @@
|
||||
"settings.help.title": {
|
||||
"defaultMessage": "Yardım ve geri bildirim"
|
||||
},
|
||||
"settings.language.description": {
|
||||
"defaultMessage": "goose için görüntüleme dilini seçin"
|
||||
},
|
||||
"settings.language.english": {
|
||||
"defaultMessage": "İngilizce"
|
||||
},
|
||||
"settings.language.hindi": {
|
||||
"defaultMessage": "Hintçe"
|
||||
},
|
||||
"settings.language.japanese": {
|
||||
"defaultMessage": "Japonca"
|
||||
},
|
||||
"settings.language.russian": {
|
||||
"defaultMessage": "Rusça"
|
||||
},
|
||||
"settings.language.systemDefault": {
|
||||
"defaultMessage": "Sistem varsayılanı"
|
||||
},
|
||||
"settings.language.title": {
|
||||
"defaultMessage": "Dil"
|
||||
},
|
||||
"settings.language.turkish": {
|
||||
"defaultMessage": "Türkçe"
|
||||
},
|
||||
"settings.language.zhCN": {
|
||||
"defaultMessage": "Çince (Basitleştirilmiş)"
|
||||
},
|
||||
"settings.menuBarIcon.description": {
|
||||
"defaultMessage": "Menü çubuğunda goose'yi göster"
|
||||
},
|
||||
|
||||
@@ -4046,6 +4046,33 @@
|
||||
"settings.help.title": {
|
||||
"defaultMessage": "帮助与反馈"
|
||||
},
|
||||
"settings.language.description": {
|
||||
"defaultMessage": "选择 goose 的显示语言"
|
||||
},
|
||||
"settings.language.english": {
|
||||
"defaultMessage": "英语"
|
||||
},
|
||||
"settings.language.hindi": {
|
||||
"defaultMessage": "印地语"
|
||||
},
|
||||
"settings.language.japanese": {
|
||||
"defaultMessage": "日语"
|
||||
},
|
||||
"settings.language.russian": {
|
||||
"defaultMessage": "俄语"
|
||||
},
|
||||
"settings.language.systemDefault": {
|
||||
"defaultMessage": "系统默认"
|
||||
},
|
||||
"settings.language.title": {
|
||||
"defaultMessage": "语言"
|
||||
},
|
||||
"settings.language.turkish": {
|
||||
"defaultMessage": "土耳其语"
|
||||
},
|
||||
"settings.language.zhCN": {
|
||||
"defaultMessage": "简体中文"
|
||||
},
|
||||
"settings.menuBarIcon.description": {
|
||||
"defaultMessage": "在菜单栏显示 goose"
|
||||
},
|
||||
|
||||
+53
-21
@@ -130,13 +130,7 @@ const MENU_TRANSLATIONS_ZH_CN: Record<string, string> = {
|
||||
};
|
||||
|
||||
function detectMenuLocale(): string {
|
||||
const explicit = process.env.GOOSE_LOCALE;
|
||||
if (explicit) return explicit;
|
||||
try {
|
||||
return app.getSystemLocale() || 'en';
|
||||
} catch {
|
||||
return 'en';
|
||||
}
|
||||
return getConfiguredGooseLocale() ?? 'en';
|
||||
}
|
||||
|
||||
function menuT(label: string): string {
|
||||
@@ -174,6 +168,19 @@ function translateMenuLabels(items: MenuItem[]): void {
|
||||
// Settings management
|
||||
const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json');
|
||||
const STARTUP_LOGS_DIR = path.join(app.getPath('userData'), 'logs', 'startup');
|
||||
const validLanguageSettings = new Set<Settings['language']>([
|
||||
'system',
|
||||
'en',
|
||||
'hi',
|
||||
'ja',
|
||||
'ru',
|
||||
'tr',
|
||||
'zh-CN',
|
||||
]);
|
||||
|
||||
function isValidLanguageSetting(value: unknown): value is Settings['language'] {
|
||||
return typeof value === 'string' && validLanguageSettings.has(value as Settings['language']);
|
||||
}
|
||||
|
||||
function getSettings(): Settings {
|
||||
if (fsSync.existsSync(SETTINGS_FILE)) {
|
||||
@@ -211,6 +218,23 @@ function updateSettings(modifier: (settings: Settings) => void): void {
|
||||
fsSync.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
||||
}
|
||||
|
||||
function getConfiguredGooseLocale(): string | undefined {
|
||||
const language = getSettings().language;
|
||||
if (isValidLanguageSetting(language) && language !== 'system') {
|
||||
return language;
|
||||
}
|
||||
|
||||
if (process.env.GOOSE_LOCALE) {
|
||||
return process.env.GOOSE_LOCALE;
|
||||
}
|
||||
|
||||
try {
|
||||
return app.isReady() ? app.getSystemLocale() || undefined : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function listGitWorktreeDirs(dir: string): Promise<string[]> {
|
||||
return new Promise((resolve) => {
|
||||
if (!dir?.trim()) {
|
||||
@@ -314,20 +338,8 @@ app.on('certificate-error', (event, _webContents, url, _error, certificate, call
|
||||
}
|
||||
});
|
||||
|
||||
// Fill in GOOSE_LOCALE from the OS region locale once Electron is ready.
|
||||
// Kept separate from the initial appConfig assignment above because
|
||||
// app.getSystemLocale() is only available after the app.ready event fires.
|
||||
app.whenReady().then(() => {
|
||||
if (!appConfig.GOOSE_LOCALE) {
|
||||
try {
|
||||
const sysLocale = app.getSystemLocale();
|
||||
if (sysLocale) {
|
||||
appConfig.GOOSE_LOCALE = sysLocale;
|
||||
}
|
||||
} catch {
|
||||
// Locale detection is best-effort; renderer will fall back to navigator.language.
|
||||
}
|
||||
}
|
||||
appConfig.GOOSE_LOCALE = getConfiguredGooseLocale();
|
||||
});
|
||||
|
||||
// Main-process net.fetch: pin to the exact cert goosed generated.
|
||||
@@ -905,6 +917,7 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => {
|
||||
additionalArguments: [
|
||||
JSON.stringify({
|
||||
...appConfig,
|
||||
GOOSE_LOCALE: getConfiguredGooseLocale(),
|
||||
GOOSE_API_HOST: baseUrl,
|
||||
GOOSE_WORKING_DIR: workingDir,
|
||||
REQUEST_DIR: dir,
|
||||
@@ -1245,7 +1258,12 @@ const createLauncher = () => {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
additionalArguments: [JSON.stringify(appConfig)],
|
||||
additionalArguments: [
|
||||
JSON.stringify({
|
||||
...appConfig,
|
||||
GOOSE_LOCALE: getConfiguredGooseLocale(),
|
||||
}),
|
||||
],
|
||||
partition: 'persist:goose',
|
||||
},
|
||||
skipTaskbar: true,
|
||||
@@ -1635,6 +1653,7 @@ const validSettingKeys: Set<string> = new Set([
|
||||
'keyboardShortcuts',
|
||||
'theme',
|
||||
'useSystemTheme',
|
||||
'language',
|
||||
'responseStyle',
|
||||
'showPricing',
|
||||
'sessionSharing',
|
||||
@@ -1648,11 +1667,20 @@ ipcMain.handle('set-setting', (_event, key: SettingKey, value: unknown) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'language' && !isValidLanguageSetting(value)) {
|
||||
console.error(`Invalid language setting rejected: ${String(value)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = getSettings();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(settings as any)[key] = value;
|
||||
fsSync.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
||||
|
||||
if (key === 'language') {
|
||||
appConfig.GOOSE_LOCALE = getConfiguredGooseLocale();
|
||||
}
|
||||
|
||||
// Re-register shortcuts if keyboard shortcuts changed
|
||||
if (key === 'keyboardShortcuts') {
|
||||
registerGlobalShortcuts();
|
||||
@@ -2661,6 +2689,10 @@ async function appMain() {
|
||||
event.returnValue = app.getVersion();
|
||||
});
|
||||
|
||||
ipcMain.on('get-app-locale', (event) => {
|
||||
event.returnValue = getConfiguredGooseLocale();
|
||||
});
|
||||
|
||||
ipcMain.handle('open-directory-in-explorer', async (_event, path: string) => {
|
||||
try {
|
||||
return !!(await shell.openPath(path));
|
||||
|
||||
@@ -352,9 +352,17 @@ const electronAPI: ElectronAPI = {
|
||||
listGitWorktreeDirs: (dir: string) => ipcRenderer.invoke('list-git-worktree-dirs', dir),
|
||||
};
|
||||
|
||||
function getAppLocale(): unknown {
|
||||
try {
|
||||
return ipcRenderer.sendSync('get-app-locale') ?? config.GOOSE_LOCALE;
|
||||
} catch {
|
||||
return config.GOOSE_LOCALE;
|
||||
}
|
||||
}
|
||||
|
||||
const appConfigAPI: AppConfigAPI = {
|
||||
get: (key: string) => config[key],
|
||||
getAll: () => config,
|
||||
get: (key: string) => (key === 'GOOSE_LOCALE' ? getAppLocale() : config[key]),
|
||||
getAll: () => ({ ...config, GOOSE_LOCALE: getAppLocale() }),
|
||||
};
|
||||
|
||||
// Expose the APIs
|
||||
|
||||
@@ -67,6 +67,7 @@ const mockSettings: Record<string, unknown> = {
|
||||
},
|
||||
theme: 'light',
|
||||
useSystemTheme: true,
|
||||
language: 'system',
|
||||
responseStyle: 'concise',
|
||||
showPricing: true,
|
||||
sessionSharing: {
|
||||
@@ -86,6 +87,7 @@ Object.defineProperty(window, 'electron', {
|
||||
mockSettings[key] = value;
|
||||
return Promise.resolve();
|
||||
}),
|
||||
reloadApp: vi.fn(),
|
||||
showMessageBox: vi.fn(() => Promise.resolve({ response: 0 })),
|
||||
getIsFullScreen: vi.fn(() => Promise.resolve(false)),
|
||||
on: vi.fn(),
|
||||
|
||||
@@ -28,6 +28,8 @@ export interface SessionSharingConfig {
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
export type LanguageSetting = 'system' | 'en' | 'hi' | 'ja' | 'ru' | 'tr' | 'zh-CN';
|
||||
|
||||
export interface Settings {
|
||||
// Desktop app settings
|
||||
showMenuBarIcon: boolean;
|
||||
@@ -42,6 +44,7 @@ export interface Settings {
|
||||
// UI preferences (migrated from localStorage)
|
||||
theme: 'dark' | 'light';
|
||||
useSystemTheme: boolean;
|
||||
language: LanguageSetting;
|
||||
responseStyle: string;
|
||||
showPricing: boolean;
|
||||
sessionSharing: SessionSharingConfig;
|
||||
@@ -81,6 +84,7 @@ export const defaultSettings: Settings = {
|
||||
// UI preferences
|
||||
theme: 'light',
|
||||
useSystemTheme: true,
|
||||
language: 'system',
|
||||
responseStyle: 'concise',
|
||||
showPricing: true,
|
||||
sessionSharing: {
|
||||
|
||||
Reference in New Issue
Block a user