mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
feat(i18n): add Simplified Chinese (zh-CN) translation (#8765)
Signed-off-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: antai <antai12232931@anaiguo.com> Co-authored-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Lifei Zhou <lifei@squareup.com>
This commit is contained in:
@@ -1381,7 +1381,7 @@ export default function ChatInput({
|
||||
data-testid="chat-input"
|
||||
autoFocus
|
||||
id="dynamic-textarea"
|
||||
placeholder={isRecording ? '' : getNavigationShortcutText()}
|
||||
placeholder={isRecording ? '' : getNavigationShortcutText(intl)}
|
||||
value={displayValue}
|
||||
onChange={handleChange}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
@@ -1514,7 +1514,7 @@ export default function ChatInput({
|
||||
}`}
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
<span className="text-sm">Send</span>
|
||||
<span className="text-sm">{intl.formatMessage(i18n.send)}</span>
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { DropdownMenu, DropdownMenuTrigger } from '../ui/dropdown-menu';
|
||||
import { ChatSessionsDropdown, SessionsList } from './navigation';
|
||||
import { ChatHistorySearch } from '../conversation/ChatHistorySearch';
|
||||
import type { NavigationRendererProps } from './navigation/types';
|
||||
import { getNavItemLabel } from '../../hooks/useNavigationItems';
|
||||
|
||||
const i18n = defineMessages({
|
||||
newChat: {
|
||||
@@ -176,7 +177,7 @@ export const CondensedRenderer: React.FC<NavigationRendererProps> = ({
|
||||
</div>
|
||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-left flex-1">
|
||||
{item.label}
|
||||
{getNavItemLabel(item, intl)}
|
||||
</span>
|
||||
<div className="flex-shrink-0">
|
||||
{isChatExpanded ? (
|
||||
@@ -230,7 +231,7 @@ export const CondensedRenderer: React.FC<NavigationRendererProps> = ({
|
||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||
{!isCondensedIconOnly && (
|
||||
<span className="text-sm font-medium text-left flex-1">
|
||||
{item.label}
|
||||
{getNavItemLabel(item, intl)}
|
||||
</span>
|
||||
)}
|
||||
{!isCondensedIconOnly && item.getTag && (
|
||||
@@ -320,7 +321,7 @@ export const CondensedRenderer: React.FC<NavigationRendererProps> = ({
|
||||
>
|
||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-left hidden min-[1200px]:block">
|
||||
{item.label}
|
||||
{getNavItemLabel(item, intl)}
|
||||
</span>
|
||||
</motion.button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -350,7 +351,7 @@ export const CondensedRenderer: React.FC<NavigationRendererProps> = ({
|
||||
>
|
||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-left hidden min-[1200px]:block">
|
||||
{item.label}
|
||||
{getNavItemLabel(item, intl)}
|
||||
</span>
|
||||
</motion.button>
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,8 @@ import { DropdownMenu, DropdownMenuTrigger } from '../ui/dropdown-menu';
|
||||
import { ChatSessionsDropdown } from './navigation';
|
||||
import { ChatHistorySearch } from '../conversation/ChatHistorySearch';
|
||||
import type { NavigationRendererProps } from './navigation/types';
|
||||
import { useIntl } from '../../i18n';
|
||||
import { getNavItemLabel } from '../../hooks/useNavigationItems';
|
||||
|
||||
export const ExpandedRenderer: React.FC<NavigationRendererProps> = ({
|
||||
isNavExpanded,
|
||||
@@ -26,6 +28,7 @@ export const ExpandedRenderer: React.FC<NavigationRendererProps> = ({
|
||||
drag,
|
||||
navFocusRef,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [chatDropdownOpen, setChatDropdownOpen] = useState(false);
|
||||
const [gridColumns, setGridColumns] = useState(2);
|
||||
const [gridMeasured, setGridMeasured] = useState(false);
|
||||
@@ -213,7 +216,7 @@ export const ExpandedRenderer: React.FC<NavigationRendererProps> = ({
|
||||
)}
|
||||
<div className="mt-auto w-full">
|
||||
<Icon className="w-6 h-6 mb-2" />
|
||||
<h2 className="font-light text-left text-xl">{item.label}</h2>
|
||||
<h2 className="font-light text-left text-xl">{getNavItemLabel(item, intl)}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -282,7 +285,7 @@ export const ExpandedRenderer: React.FC<NavigationRendererProps> = ({
|
||||
)}
|
||||
<div className="mt-auto w-full">
|
||||
<Icon className="w-6 h-6 mb-2" />
|
||||
<h2 className="font-light text-left text-xl">{item.label}</h2>
|
||||
<h2 className="font-light text-left text-xl">{getNavItemLabel(item, intl)}</h2>
|
||||
</div>
|
||||
</button>
|
||||
</motion.div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Zap,
|
||||
} from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { defineMessages, type IntlShape, type MessageDescriptor } from 'react-intl';
|
||||
|
||||
export interface NavItem {
|
||||
id: string;
|
||||
@@ -31,6 +32,52 @@ export const NAV_ITEMS: NavItem[] = [
|
||||
{ id: 'settings', path: '/settings', label: 'Settings', icon: Settings },
|
||||
];
|
||||
|
||||
// Translation descriptors for nav labels. Kept here next to NAV_ITEMS so the two
|
||||
// stay in sync. Reuses the existing `navigationCustomization.item*` ids that are
|
||||
// also used by the "Customize Navigation" settings screen.
|
||||
const navItemMessages = defineMessages({
|
||||
home: {
|
||||
id: 'navigationCustomization.itemHome',
|
||||
defaultMessage: 'Home',
|
||||
},
|
||||
chat: {
|
||||
id: 'navigationCustomization.itemChat',
|
||||
defaultMessage: 'Chat',
|
||||
},
|
||||
recipes: {
|
||||
id: 'navigationCustomization.itemRecipes',
|
||||
defaultMessage: 'Recipes',
|
||||
},
|
||||
skills: {
|
||||
id: 'navigationCustomization.itemSkills',
|
||||
defaultMessage: 'Skills',
|
||||
},
|
||||
apps: {
|
||||
id: 'navigationCustomization.itemApps',
|
||||
defaultMessage: 'Apps',
|
||||
},
|
||||
scheduler: {
|
||||
id: 'navigationCustomization.itemScheduler',
|
||||
defaultMessage: 'Scheduler',
|
||||
},
|
||||
extensions: {
|
||||
id: 'navigationCustomization.itemExtensions',
|
||||
defaultMessage: 'Extensions',
|
||||
},
|
||||
settings: {
|
||||
id: 'navigationCustomization.itemSettings',
|
||||
defaultMessage: 'Settings',
|
||||
},
|
||||
});
|
||||
|
||||
const NAV_ITEM_MESSAGES: Record<string, MessageDescriptor> = navItemMessages;
|
||||
|
||||
/** Format a NavItem's label using the provided intl instance, falling back to `item.label`. */
|
||||
export function getNavItemLabel(item: NavItem, intl: IntlShape): string {
|
||||
const descriptor = NAV_ITEM_MESSAGES[item.id];
|
||||
return descriptor ? intl.formatMessage(descriptor) : item.label;
|
||||
}
|
||||
|
||||
export function getNavItemById(id: string): NavItem | undefined {
|
||||
return NAV_ITEMS.find((item) => item.id === id);
|
||||
}
|
||||
|
||||
@@ -19,43 +19,43 @@ describe('getLocale', () => {
|
||||
});
|
||||
|
||||
it('returns "en" as the default fallback', () => {
|
||||
// navigator.language returns something unsupported
|
||||
vi.stubGlobal('navigator', { language: 'xx-XX' });
|
||||
// navigator.languages contains only unsupported tags
|
||||
vi.stubGlobal('navigator', { languages: ['xx-XX'] });
|
||||
expect(getLocale()).toEqual({ locale: 'en', messageLocale: 'en' });
|
||||
});
|
||||
|
||||
it('preserves regional tag for formatting when base language is supported', () => {
|
||||
vi.stubGlobal('navigator', { language: 'en-US' });
|
||||
vi.stubGlobal('navigator', { languages: ['en-US'] });
|
||||
expect(getLocale()).toEqual({ locale: 'en-US', messageLocale: 'en' });
|
||||
});
|
||||
|
||||
it('returns exact match when navigator.language matches a supported locale', () => {
|
||||
vi.stubGlobal('navigator', { language: 'en' });
|
||||
it('returns exact match when navigator.languages contains a supported locale', () => {
|
||||
vi.stubGlobal('navigator', { languages: ['en'] });
|
||||
expect(getLocale()).toEqual({ locale: 'en', messageLocale: 'en' });
|
||||
});
|
||||
|
||||
it('respects GOOSE_LOCALE over navigator.language', () => {
|
||||
it('respects GOOSE_LOCALE over navigator.languages', () => {
|
||||
mockAppConfig({ GOOSE_LOCALE: 'en' });
|
||||
vi.stubGlobal('navigator', { language: 'xx-XX' });
|
||||
vi.stubGlobal('navigator', { languages: ['xx-XX'] });
|
||||
expect(getLocale()).toEqual({ locale: 'en', messageLocale: 'en' });
|
||||
});
|
||||
|
||||
it('preserves regional tag from GOOSE_LOCALE', () => {
|
||||
mockAppConfig({ GOOSE_LOCALE: 'en-GB' });
|
||||
vi.stubGlobal('navigator', { language: 'xx-XX' });
|
||||
vi.stubGlobal('navigator', { languages: ['xx-XX'] });
|
||||
expect(getLocale()).toEqual({ locale: 'en-GB', messageLocale: 'en' });
|
||||
});
|
||||
|
||||
it('falls back to base language tag for message catalog', () => {
|
||||
// "en-GB" should use "en" catalog but keep "en-GB" for formatting
|
||||
vi.stubGlobal('navigator', { language: 'en-GB' });
|
||||
vi.stubGlobal('navigator', { languages: ['en-GB'] });
|
||||
expect(getLocale()).toEqual({ locale: 'en-GB', messageLocale: 'en' });
|
||||
});
|
||||
|
||||
it('falls back to base language when locale tag is invalid BCP 47', () => {
|
||||
// "en-" is not a valid BCP 47 tag and would cause RangeError in Intl APIs
|
||||
mockAppConfig({ GOOSE_LOCALE: 'en-' });
|
||||
vi.stubGlobal('navigator', { language: 'xx-XX' });
|
||||
vi.stubGlobal('navigator', { languages: ['xx-XX'] });
|
||||
expect(getLocale()).toEqual({ locale: 'en', messageLocale: 'en' });
|
||||
});
|
||||
});
|
||||
@@ -72,9 +72,7 @@ describe('loadMessages', () => {
|
||||
const { loadMessages } = await import('./index');
|
||||
const messages = await loadMessages('xx');
|
||||
expect(messages).toEqual({});
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('No message catalog found')
|
||||
);
|
||||
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('No message catalog found'));
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,22 +3,37 @@
|
||||
*
|
||||
* Locale resolution order:
|
||||
* 1. GOOSE_LOCALE config value (set via environment variable, passed through appConfig)
|
||||
* 2. navigator.language (browser/OS locale)
|
||||
* 2. navigator.languages (full accept-language list from OS/browser)
|
||||
* 3. "en" (fallback)
|
||||
*
|
||||
* For Chinese: any Simplified Chinese tag (zh, zh-CN, zh-Hans, zh-Hans-CN, zh-SG, zh-MY)
|
||||
* maps to the "zh-CN" catalog. Traditional variants (zh-TW, zh-HK, zh-Hant) are not yet
|
||||
* translated and fall through to English.
|
||||
*/
|
||||
|
||||
// Re-export react-intl utilities that components use directly
|
||||
export { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
/** The set of locales that have translation catalogs. */
|
||||
const SUPPORTED_LOCALES = new Set(['en']);
|
||||
const SUPPORTED_LOCALES = new Set(['en', 'zh-CN']);
|
||||
|
||||
/**
|
||||
* Map Simplified Chinese aliases (zh, zh-Hans*, zh-SG, zh-MY) to "zh-CN".
|
||||
* Traditional variants (zh-Hant*, zh-TW, zh-HK, zh-MO) and non-Chinese tags pass through unchanged.
|
||||
*/
|
||||
function resolveChineseAlias(tag: string): string {
|
||||
const lower = tag.toLowerCase();
|
||||
if (/^zh-(hant|tw|hk|mo)(-|$)/.test(lower)) return tag;
|
||||
if (lower === 'zh' || lower.startsWith('zh-')) return 'zh-CN';
|
||||
return tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the user's preferred locale.
|
||||
*
|
||||
* Returns two values:
|
||||
* - `locale`: the full BCP 47 tag (e.g. "en-GB") for formatting (dates, numbers).
|
||||
* - `messageLocale`: the base language that has a translation catalog (e.g. "en").
|
||||
* - `messageLocale`: the locale key that has a translation catalog (e.g. "en", "zh-CN").
|
||||
*/
|
||||
export function getLocale(): { locale: string; messageLocale: string } {
|
||||
const explicit =
|
||||
@@ -32,13 +47,22 @@ export function getLocale(): { locale: string; messageLocale: string } {
|
||||
candidates.push(explicit);
|
||||
}
|
||||
|
||||
if (typeof navigator !== 'undefined' && navigator.language) {
|
||||
candidates.push(navigator.language);
|
||||
// Walk navigator.languages (full preference list) so a user whose primary UI
|
||||
// language isn't supported still gets a supported language from later in their list.
|
||||
if (typeof navigator !== 'undefined' && Array.isArray(navigator.languages)) {
|
||||
for (const tag of navigator.languages) {
|
||||
if (tag) candidates.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
for (const tag of candidates) {
|
||||
for (const rawTag of candidates) {
|
||||
// Normalize underscores to hyphens so POSIX-style tags like "zh_CN" work.
|
||||
const normalized = rawTag.replace(/_/g, '-');
|
||||
const tag = resolveChineseAlias(normalized);
|
||||
|
||||
// Exact match first
|
||||
if (SUPPORTED_LOCALES.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];
|
||||
@@ -48,7 +72,7 @@ export function getLocale(): { locale: string; messageLocale: string } {
|
||||
// Intl APIs, so fall back to the base language in that case.
|
||||
let locale = base;
|
||||
try {
|
||||
[locale] = Intl.getCanonicalLocales(tag);
|
||||
[locale] = Intl.getCanonicalLocales(normalized);
|
||||
} catch {
|
||||
// tag is not valid BCP 47 — use the base language instead
|
||||
}
|
||||
@@ -70,9 +94,7 @@ export const currentMessageLocale = resolvedLocale.messageLocale;
|
||||
* Load compiled messages for a given locale.
|
||||
* Returns an empty object for English (react-intl uses defaultMessage as fallback).
|
||||
*/
|
||||
export async function loadMessages(
|
||||
locale: string
|
||||
): Promise<Record<string, string>> {
|
||||
export async function loadMessages(locale: string): Promise<Record<string, string>> {
|
||||
if (locale === 'en') {
|
||||
// English strings live in source code as defaultMessage — no catalog needed.
|
||||
return {};
|
||||
@@ -83,7 +105,9 @@ export async function loadMessages(
|
||||
const mod = await import(`./compiled/${locale}.json`);
|
||||
return mod.default ?? mod;
|
||||
} catch {
|
||||
console.warn(`[i18n] No message catalog found for locale "${locale}", falling back to English.`);
|
||||
console.warn(
|
||||
`[i18n] No message catalog found for locale "${locale}", falling back to English.`
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,9 @@
|
||||
"chatInput.failedToReadImage": {
|
||||
"defaultMessage": "Failed to read image file"
|
||||
},
|
||||
"chatInput.navigationShortcut": {
|
||||
"defaultMessage": "{prefix}↑/{prefix}↓ to navigate messages"
|
||||
},
|
||||
"chatInput.processingDroppedFiles": {
|
||||
"defaultMessage": "Processing dropped files..."
|
||||
},
|
||||
@@ -2426,6 +2429,9 @@
|
||||
"navigationCustomization.itemSettings": {
|
||||
"defaultMessage": "Settings"
|
||||
},
|
||||
"navigationCustomization.itemSkills": {
|
||||
"defaultMessage": "Skills"
|
||||
},
|
||||
"navigationCustomization.resetToDefaults": {
|
||||
"defaultMessage": "Reset to defaults"
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+157
-22
@@ -59,6 +59,118 @@ function shouldSetupUpdater(): boolean {
|
||||
return UPDATES_ENABLED || process.env.ENABLE_DEV_UPDATES === 'true';
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// Native menu localization
|
||||
// -----------------------------------------------------------------------
|
||||
// Electron's main process can't use react-intl (which runs in the renderer),
|
||||
// so the native menu bar is translated here with a small hand-maintained
|
||||
// dictionary. Only Simplified Chinese is filled in right now; other locales
|
||||
// fall through to the original English labels. Keep the keys in sync with
|
||||
// the raw label strings used below.
|
||||
// =======================================================================
|
||||
|
||||
const MENU_TRANSLATIONS_ZH_CN: Record<string, string> = {
|
||||
// Top-level
|
||||
File: '文件',
|
||||
Edit: '编辑',
|
||||
View: '视图',
|
||||
Window: '窗口',
|
||||
Help: '帮助',
|
||||
// Context menu
|
||||
'Add to dictionary': '添加到词典',
|
||||
Cut: '剪切',
|
||||
Copy: '复制',
|
||||
Paste: '粘贴',
|
||||
// Goose-added items
|
||||
'New Window': '新建窗口',
|
||||
Settings: '设置',
|
||||
'Find…': '查找…',
|
||||
'Find Next': '查找下一个',
|
||||
'Find Previous': '查找上一个',
|
||||
'Use Selection for Find': '用所选内容查找',
|
||||
Find: '查找',
|
||||
'New Chat': '新建聊天',
|
||||
'New Chat Window': '新建聊天窗口',
|
||||
'Open Directory...': '打开目录…',
|
||||
'Recent Directories': '最近的目录',
|
||||
'Focus Goose Window': '聚焦 Goose 窗口',
|
||||
'Quick Launcher': '快速启动器',
|
||||
'Always on Top': '窗口置顶',
|
||||
'Toggle Navigation': '切换导航',
|
||||
'About Goose': '关于 Goose',
|
||||
// Electron's default role-based labels we want to translate as well.
|
||||
// (The menu role itself still provides the correct behaviour; only the
|
||||
// display string is overridden.)
|
||||
Undo: '撤销',
|
||||
Redo: '重做',
|
||||
'Select All': '全选',
|
||||
Delete: '删除',
|
||||
Speech: '语音',
|
||||
Reload: '重新加载',
|
||||
'Force Reload': '强制重新加载',
|
||||
'Toggle Developer Tools': '切换开发者工具',
|
||||
'Actual Size': '实际大小',
|
||||
'Reset Zoom': '重置缩放',
|
||||
'Zoom In': '放大',
|
||||
'Zoom Out': '缩小',
|
||||
'Toggle Full Screen': '切换全屏',
|
||||
'Toggle Fullscreen': '切换全屏',
|
||||
Minimize: '最小化',
|
||||
Close: '关闭',
|
||||
'Close Window': '关闭窗口',
|
||||
Quit: '退出',
|
||||
Exit: '退出',
|
||||
'Bring All to Front': '全部置于最前',
|
||||
'Emoji & Symbols': '表情符号',
|
||||
'Start Dictation…': '开始听写…',
|
||||
'Hide Goose': '隐藏 Goose',
|
||||
'Hide Others': '隐藏其他',
|
||||
'Show All': '全部显示',
|
||||
Services: '服务',
|
||||
};
|
||||
|
||||
function detectMenuLocale(): string {
|
||||
const explicit = process.env.GOOSE_LOCALE;
|
||||
if (explicit) return explicit;
|
||||
try {
|
||||
return app.getSystemLocale() || 'en';
|
||||
} catch {
|
||||
return 'en';
|
||||
}
|
||||
}
|
||||
|
||||
function menuT(label: string): string {
|
||||
// Normalize underscores to hyphens so POSIX-style tags like "zh_CN" work.
|
||||
const lower = detectMenuLocale().replace(/_/g, '-').toLowerCase();
|
||||
const isTraditional = /^zh-(hant|tw|hk|mo)\b/.test(lower);
|
||||
const isSimplifiedChinese = !isTraditional && (lower === 'zh' || lower.startsWith('zh-'));
|
||||
if (isSimplifiedChinese) {
|
||||
return MENU_TRANSLATIONS_ZH_CN[label] ?? label;
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively translate `label` on every item in the given menu, including nested submenus.
|
||||
* Electron's default application menu comes with English labels that are not otherwise
|
||||
* configurable, so we post-process them here before calling `Menu.setApplicationMenu`.
|
||||
*/
|
||||
function translateMenuLabels(items: MenuItem[]): void {
|
||||
for (const item of items) {
|
||||
if (item.label) {
|
||||
const translated = menuT(item.label);
|
||||
if (translated !== item.label) {
|
||||
// MenuItem.label is a writable property on the main-process side, even though
|
||||
// the TS type sometimes claims otherwise. Cast through unknown for safety.
|
||||
(item as unknown as { label: string }).label = translated;
|
||||
}
|
||||
}
|
||||
if (item.submenu && item.submenu.items) {
|
||||
translateMenuLabels(item.submenu.items);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Settings management
|
||||
const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json');
|
||||
const STARTUP_LOGS_DIR = path.join(app.getPath('userData'), 'logs', 'startup');
|
||||
@@ -202,6 +314,22 @@ 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.
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Main-process net.fetch: pin to the exact cert goosed generated.
|
||||
app.whenReady().then(() => {
|
||||
session.defaultSession.setCertificateVerifyProc((request, callback) => {
|
||||
@@ -580,6 +708,8 @@ let appConfig = {
|
||||
GOOSE_API_HOST: 'https://localhost',
|
||||
GOOSE_PATH_ROOT: resolveGoosePathRoot(),
|
||||
GOOSE_WORKING_DIR: '',
|
||||
// Start with the env-var override; the OS region locale is filled in after app.ready
|
||||
// (see updateLocaleFromSystem below) since getSystemLocale() cannot be called earlier.
|
||||
GOOSE_LOCALE: process.env.GOOSE_LOCALE || undefined,
|
||||
// If GOOSE_ALLOWLIST_WARNING env var is not set, defaults to false (strict blocking mode)
|
||||
GOOSE_ALLOWLIST_WARNING: process.env.GOOSE_ALLOWLIST_WARNING === 'true',
|
||||
@@ -833,7 +963,7 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => {
|
||||
if (params.misspelledWord) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: 'Add to dictionary',
|
||||
label: menuT('Add to dictionary'),
|
||||
click: () =>
|
||||
mainWindow.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord),
|
||||
})
|
||||
@@ -847,14 +977,14 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => {
|
||||
if (params.selectionText) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: 'Cut',
|
||||
label: menuT('Cut'),
|
||||
accelerator: 'CmdOrCtrl+X',
|
||||
role: 'cut',
|
||||
})
|
||||
);
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: 'Copy',
|
||||
label: menuT('Copy'),
|
||||
accelerator: 'CmdOrCtrl+C',
|
||||
role: 'copy',
|
||||
})
|
||||
@@ -865,7 +995,7 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => {
|
||||
if (params.isEditable) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: 'Paste',
|
||||
label: menuT('Paste'),
|
||||
accelerator: 'CmdOrCtrl+V',
|
||||
role: 'paste',
|
||||
})
|
||||
@@ -1956,7 +2086,7 @@ async function appMain() {
|
||||
if (process.platform === 'darwin') {
|
||||
const dockMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'New Window',
|
||||
label: menuT('New Window'),
|
||||
click: () => {
|
||||
createNewWindow(app);
|
||||
},
|
||||
@@ -1976,7 +2106,7 @@ async function appMain() {
|
||||
appMenu.submenu.insert(
|
||||
1,
|
||||
new MenuItem({
|
||||
label: 'Settings',
|
||||
label: menuT('Settings'),
|
||||
accelerator: shortcuts.settings,
|
||||
click() {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
@@ -1994,7 +2124,7 @@ async function appMain() {
|
||||
|
||||
const findSubmenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Find…',
|
||||
label: menuT('Find…'),
|
||||
accelerator: shortcuts.find || undefined,
|
||||
click() {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
@@ -2002,7 +2132,7 @@ async function appMain() {
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Find Next',
|
||||
label: menuT('Find Next'),
|
||||
accelerator: shortcuts.findNext || undefined,
|
||||
click() {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
@@ -2010,7 +2140,7 @@ async function appMain() {
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Find Previous',
|
||||
label: menuT('Find Previous'),
|
||||
accelerator: shortcuts.findPrevious || undefined,
|
||||
click() {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
@@ -2018,7 +2148,7 @@ async function appMain() {
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Use Selection for Find',
|
||||
label: menuT('Use Selection for Find'),
|
||||
accelerator: process.platform === 'darwin' ? 'Command+E' : undefined,
|
||||
click() {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
@@ -2031,7 +2161,7 @@ async function appMain() {
|
||||
editMenu.submenu.insert(
|
||||
selectAllIndex + 1,
|
||||
new MenuItem({
|
||||
label: 'Find',
|
||||
label: menuT('Find'),
|
||||
submenu: findSubmenu,
|
||||
})
|
||||
);
|
||||
@@ -2047,7 +2177,7 @@ async function appMain() {
|
||||
fileMenu.submenu.insert(
|
||||
menuIndex++,
|
||||
new MenuItem({
|
||||
label: 'New Chat',
|
||||
label: menuT('New Chat'),
|
||||
accelerator: shortcuts.newChat,
|
||||
click() {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
@@ -2061,7 +2191,7 @@ async function appMain() {
|
||||
fileMenu.submenu.insert(
|
||||
menuIndex++,
|
||||
new MenuItem({
|
||||
label: 'New Chat Window',
|
||||
label: menuT('New Chat Window'),
|
||||
accelerator: shortcuts.newChatWindow,
|
||||
click() {
|
||||
ipcMain.emit('create-chat-window');
|
||||
@@ -2074,7 +2204,7 @@ async function appMain() {
|
||||
fileMenu.submenu.insert(
|
||||
menuIndex++,
|
||||
new MenuItem({
|
||||
label: 'Open Directory...',
|
||||
label: menuT('Open Directory...'),
|
||||
accelerator: shortcuts.openDirectory,
|
||||
click: () => openDirectoryDialog(),
|
||||
})
|
||||
@@ -2086,7 +2216,7 @@ async function appMain() {
|
||||
fileMenu.submenu.insert(
|
||||
menuIndex++,
|
||||
new MenuItem({
|
||||
label: 'Recent Directories',
|
||||
label: menuT('Recent Directories'),
|
||||
submenu: recentFilesSubmenu,
|
||||
})
|
||||
);
|
||||
@@ -2097,7 +2227,7 @@ async function appMain() {
|
||||
if (shortcuts.focusWindow) {
|
||||
fileMenu.submenu.append(
|
||||
new MenuItem({
|
||||
label: 'Focus Goose Window',
|
||||
label: menuT('Focus Goose Window'),
|
||||
accelerator: shortcuts.focusWindow,
|
||||
click() {
|
||||
focusWindow();
|
||||
@@ -2109,7 +2239,7 @@ async function appMain() {
|
||||
if (shortcuts.quickLauncher) {
|
||||
fileMenu.submenu.append(
|
||||
new MenuItem({
|
||||
label: 'Quick Launcher',
|
||||
label: menuT('Quick Launcher'),
|
||||
accelerator: shortcuts.quickLauncher,
|
||||
click() {
|
||||
createLauncher();
|
||||
@@ -2124,7 +2254,7 @@ async function appMain() {
|
||||
|
||||
if (!windowMenu) {
|
||||
windowMenu = new MenuItem({
|
||||
label: 'Window',
|
||||
label: menuT('Window'),
|
||||
submenu: Menu.buildFromTemplate([]),
|
||||
});
|
||||
|
||||
@@ -2140,7 +2270,7 @@ async function appMain() {
|
||||
if (shortcuts.alwaysOnTop) {
|
||||
windowMenu.submenu.append(
|
||||
new MenuItem({
|
||||
label: 'Always on Top',
|
||||
label: menuT('Always on Top'),
|
||||
type: 'checkbox',
|
||||
accelerator: shortcuts.alwaysOnTop,
|
||||
click(menuItem) {
|
||||
@@ -2169,7 +2299,7 @@ async function appMain() {
|
||||
viewMenu.submenu.append(new MenuItem({ type: 'separator' }));
|
||||
viewMenu.submenu.append(
|
||||
new MenuItem({
|
||||
label: 'Toggle Navigation',
|
||||
label: menuT('Toggle Navigation'),
|
||||
accelerator: shortcuts.toggleNavigation,
|
||||
click() {
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
@@ -2189,7 +2319,7 @@ async function appMain() {
|
||||
// If Help menu doesn't exist, create it and add it to the menu
|
||||
if (!helpMenu) {
|
||||
helpMenu = new MenuItem({
|
||||
label: 'Help',
|
||||
label: menuT('Help'),
|
||||
submenu: Menu.buildFromTemplate([]), // Start with an empty submenu
|
||||
});
|
||||
// Find a reasonable place to insert the Help menu, usually near the end
|
||||
@@ -2206,7 +2336,7 @@ async function appMain() {
|
||||
|
||||
// Create the About Goose menu item with a submenu
|
||||
const aboutGooseMenuItem = new MenuItem({
|
||||
label: 'About Goose',
|
||||
label: menuT('About Goose'),
|
||||
submenu: Menu.buildFromTemplate([]), // Start with an empty submenu for About
|
||||
});
|
||||
|
||||
@@ -2225,6 +2355,11 @@ async function appMain() {
|
||||
}
|
||||
|
||||
if (menu) {
|
||||
// Translate labels (including Electron's default top-level entries
|
||||
// File/Edit/View/Window/Help and submenu items populated by roles) before
|
||||
// installing the menu. Called last so the lookups above that match on the
|
||||
// English labels still succeed.
|
||||
translateMenuLabels(menu.items);
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import type { IntlShape } from 'react-intl';
|
||||
|
||||
function isMac(): boolean {
|
||||
return window.electron?.platform === 'darwin';
|
||||
}
|
||||
|
||||
export function getNavigationShortcutText(): string {
|
||||
return isMac() ? '⌘↑/⌘↓ to navigate messages' : 'Ctrl+↑/Ctrl+↓ to navigate messages';
|
||||
/**
|
||||
* Localised message for the "navigate messages with arrow keys" chat input placeholder.
|
||||
* Returns the legacy English string if no intl instance is supplied, so call sites that
|
||||
* run before the intl provider is available still get a sensible default.
|
||||
*/
|
||||
export function getNavigationShortcutText(intl?: IntlShape): string {
|
||||
const prefix = isMac() ? '⌘' : 'Ctrl+';
|
||||
if (intl) {
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: 'chatInput.navigationShortcut',
|
||||
defaultMessage: '{prefix}↑/{prefix}↓ to navigate messages',
|
||||
},
|
||||
{ prefix }
|
||||
);
|
||||
}
|
||||
return `${prefix}↑/${prefix}↓ to navigate messages`;
|
||||
}
|
||||
|
||||
export function getSearchShortcutText(): string {
|
||||
|
||||
Reference in New Issue
Block a user