Add i18n foundation and translations (#1027)
* feat: add i18n foundation * feat: localize sessions sidebar * Localize multirun/scheduled tasks and fix dialog dropdown interactions * localize git sidebar surface and add zh-CN keys * feat(ui): localize context panel, diff/plan views, and context sidebar content * fix(config): resolve user config home via fs/home before embedded home * localize header/chat UI and complete model/worktree panel strings * localize worktree + github issue/pr dialog flows * localize settings sections and split settings i18n dictionaries * localize additional settings sections and sidebars * localize more settings pages and dialogs * fix settings select trigger localization * localize tunnel settings ui surface * localize additional settings sections * localize keyboard shortcuts labels in settings * localize terminal and utility dialogs surfaces * feat(i18n): localize remaining UI strings * Add Ukrainian locale * Add Spanish locale * Add Brazilian Portuguese locale * Polish locale translations
This commit is contained in:
committed by
GitHub
parent
87db2ea210
commit
7d7285655d
@@ -57,6 +57,7 @@ import { OpenChamberPage } from '@/components/sections/openchamber/OpenChamberPa
|
||||
import { McpIcon } from '@/components/icons/McpIcon';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
|
||||
import {
|
||||
SETTINGS_PAGE_METADATA,
|
||||
@@ -167,12 +168,13 @@ export function getSettingsNavIcon(slug: SettingsPageSlug): React.ComponentType<
|
||||
}
|
||||
|
||||
const SettingsHome: React.FC<{ onOpen: (slug: SettingsPageSlug) => void }> = ({ onOpen }) => {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="mx-auto w-full max-w-3xl px-6 py-6 space-y-6">
|
||||
<div className="space-y-1">
|
||||
<h1 className="typography-ui-header font-semibold text-foreground">Settings</h1>
|
||||
<p className="typography-ui text-muted-foreground">Jump to common pages.</p>
|
||||
<h1 className="typography-ui-header font-semibold text-foreground">{t('settings.view.home.title')}</h1>
|
||||
<p className="typography-ui text-muted-foreground">{t('settings.view.home.description')}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
@@ -184,8 +186,8 @@ const SettingsHome: React.FC<{ onOpen: (slug: SettingsPageSlug) => void }> = ({
|
||||
'hover:bg-[var(--interactive-hover)] transition-colors'
|
||||
)}
|
||||
>
|
||||
<div className="typography-ui-label text-foreground">Providers</div>
|
||||
<div className="typography-micro text-muted-foreground/70">Connect models + credentials</div>
|
||||
<div className="typography-ui-label text-foreground">{t('settings.view.home.cards.providers.title')}</div>
|
||||
<div className="typography-micro text-muted-foreground/70">{t('settings.view.home.cards.providers.description')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -196,8 +198,8 @@ const SettingsHome: React.FC<{ onOpen: (slug: SettingsPageSlug) => void }> = ({
|
||||
'hover:bg-[var(--interactive-hover)] transition-colors'
|
||||
)}
|
||||
>
|
||||
<div className="typography-ui-label text-foreground">Agents</div>
|
||||
<div className="typography-micro text-muted-foreground/70">Prompts, tools, permissions</div>
|
||||
<div className="typography-ui-label text-foreground">{t('settings.view.home.cards.agents.title')}</div>
|
||||
<div className="typography-micro text-muted-foreground/70">{t('settings.view.home.cards.agents.description')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -208,8 +210,8 @@ const SettingsHome: React.FC<{ onOpen: (slug: SettingsPageSlug) => void }> = ({
|
||||
'hover:bg-[var(--interactive-hover)] transition-colors'
|
||||
)}
|
||||
>
|
||||
<div className="typography-ui-label text-foreground">Skills Catalog</div>
|
||||
<div className="typography-micro text-muted-foreground/70">Install skills from catalogs</div>
|
||||
<div className="typography-ui-label text-foreground">{t('settings.view.home.cards.skillsCatalog.title')}</div>
|
||||
<div className="typography-micro text-muted-foreground/70">{t('settings.view.home.cards.skillsCatalog.description')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -220,8 +222,8 @@ const SettingsHome: React.FC<{ onOpen: (slug: SettingsPageSlug) => void }> = ({
|
||||
'hover:bg-[var(--interactive-hover)] transition-colors'
|
||||
)}
|
||||
>
|
||||
<div className="typography-ui-label text-foreground">MCP</div>
|
||||
<div className="typography-micro text-muted-foreground/70">Configure MCP servers + connections</div>
|
||||
<div className="typography-ui-label text-foreground">{t('settings.view.home.cards.mcp.title')}</div>
|
||||
<div className="typography-micro text-muted-foreground/70">{t('settings.view.home.cards.mcp.description')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -232,8 +234,8 @@ const SettingsHome: React.FC<{ onOpen: (slug: SettingsPageSlug) => void }> = ({
|
||||
'hover:bg-[var(--interactive-hover)] transition-colors'
|
||||
)}
|
||||
>
|
||||
<div className="typography-ui-label text-foreground">Usage</div>
|
||||
<div className="typography-micro text-muted-foreground/70">Quota + spend visibility</div>
|
||||
<div className="typography-ui-label text-foreground">{t('settings.view.home.cards.usage.title')}</div>
|
||||
<div className="typography-micro text-muted-foreground/70">{t('settings.view.home.cards.usage.description')}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,6 +244,7 @@ const SettingsHome: React.FC<{ onOpen: (slug: SettingsPageSlug) => void }> = ({
|
||||
};
|
||||
|
||||
export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile, isWindowed }) => {
|
||||
const { t } = useI18n();
|
||||
const deviceInfo = useDeviceInfo();
|
||||
const isMobile = forceMobile ?? deviceInfo.isMobile;
|
||||
|
||||
@@ -382,16 +385,60 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
tunnel: 'tunnel',
|
||||
}), []);
|
||||
|
||||
const getPageTitle = React.useCallback((slug: SettingsPageSlug): string => {
|
||||
switch (slug) {
|
||||
case 'projects':
|
||||
return t('settings.page.projects.title');
|
||||
case 'remote-instances':
|
||||
return t('settings.page.remoteInstances.title');
|
||||
case 'providers':
|
||||
return t('settings.page.providers.title');
|
||||
case 'usage':
|
||||
return t('settings.page.usage.title');
|
||||
case 'agents':
|
||||
return t('settings.page.agents.title');
|
||||
case 'commands':
|
||||
return t('settings.page.commands.title');
|
||||
case 'mcp':
|
||||
return t('settings.page.mcp.title');
|
||||
case 'skills.installed':
|
||||
return t('settings.page.skills.title');
|
||||
case 'skills.catalog':
|
||||
return t('settings.page.skillsCatalog.title');
|
||||
case 'git':
|
||||
return t('settings.page.git.title');
|
||||
case 'appearance':
|
||||
return t('settings.page.appearance.title');
|
||||
case 'chat':
|
||||
return t('settings.page.chat.title');
|
||||
case 'shortcuts':
|
||||
return t('settings.page.shortcuts.title');
|
||||
case 'sessions':
|
||||
return t('settings.page.sessions.title');
|
||||
case 'magic-prompts':
|
||||
return t('settings.page.magicPrompts.title');
|
||||
case 'notifications':
|
||||
return t('settings.page.notifications.title');
|
||||
case 'voice':
|
||||
return t('settings.page.voice.title');
|
||||
case 'tunnel':
|
||||
return t('settings.page.tunnel.title');
|
||||
case 'home':
|
||||
default:
|
||||
return t('settings.view.home.title');
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const renderUnavailable = React.useCallback(() => {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center px-6">
|
||||
<div className="max-w-md text-center">
|
||||
<div className="typography-ui-header font-semibold text-foreground">Not available</div>
|
||||
<p className="typography-ui text-muted-foreground mt-1">This settings page is not available in this runtime.</p>
|
||||
<div className="typography-ui-header font-semibold text-foreground">{t('settings.view.unavailable.title')}</div>
|
||||
<p className="typography-ui text-muted-foreground mt-1">{t('settings.view.unavailable.description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const renderPageSidebar = React.useCallback((slug: SettingsPageSlug, opts: { onItemSelect?: () => void }) => {
|
||||
switch (slug) {
|
||||
@@ -528,10 +575,10 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
collapsed ? 'opacity-0' : 'opacity-100'
|
||||
)}
|
||||
>
|
||||
<span className="typography-ui-label font-normal truncate">{page.title}</span>
|
||||
<span className="typography-ui-label font-normal truncate">{getPageTitle(page.slug)}</span>
|
||||
{(page.slug === 'voice' || page.slug === 'tunnel') && (
|
||||
<span className="shrink-0 typography-micro px-1 rounded leading-none pb-px text-[var(--status-warning)] bg-[var(--status-warning)]/10">
|
||||
beta
|
||||
{t('settings.view.badge.beta')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
@@ -539,7 +586,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
</TooltipTrigger>
|
||||
{collapsed && (
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
{page.title}
|
||||
{getPageTitle(page.slug)}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
@@ -569,11 +616,11 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
onClick={() => void reloadOpenCodeConfiguration({ message: 'Restarting OpenCode…', mode: 'projects', scopes: ['all'] })}
|
||||
>
|
||||
<RiRestartLine className="h-4 w-4 shrink-0" />
|
||||
<span>Reload OpenCode</span>
|
||||
<span>{t('settings.view.actions.reloadOpenCode')}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Restart OpenCode and reload its configuration.
|
||||
{t('settings.view.actions.reloadOpenCodeTooltip')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -666,7 +713,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
<button
|
||||
type="button"
|
||||
onClick={showBackButton ? handleBack : onClose}
|
||||
aria-label={showBackButton ? 'Back to Settings' : 'Close settings'}
|
||||
aria-label={showBackButton ? t('settings.view.actions.backToSettings') : t('settings.view.actions.closeSettings')}
|
||||
className="inline-flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<RiArrowLeftSLine className="h-5 w-5" />
|
||||
@@ -674,15 +721,15 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
|
||||
<div className="min-w-0 flex-1 typography-ui-label font-medium text-foreground truncate">
|
||||
{mobileStage === 'nav'
|
||||
? 'Settings'
|
||||
: (activePageMeta?.title ?? 'Settings')}
|
||||
? t('settings.view.home.title')
|
||||
: (activePageMeta ? getPageTitle(activePageMeta.slug) : t('settings.view.home.title'))}
|
||||
</div>
|
||||
|
||||
{mobileStage === 'page-content' && activePageMeta?.kind === 'split' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenPageSidebar}
|
||||
aria-label="Open section list"
|
||||
aria-label={t('settings.view.actions.openSectionList')}
|
||||
className="inline-flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<RiListUnordered className="h-5 w-5" />
|
||||
@@ -693,8 +740,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Close settings"
|
||||
title={`Close Settings (${shortcutKey}+,)`}
|
||||
aria-label={t('settings.view.actions.closeSettings')}
|
||||
title={t('settings.view.actions.closeSettingsWithShortcut', { shortcut: shortcutKey })}
|
||||
className="inline-flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<RiCloseLine className="h-5 w-5" />
|
||||
@@ -708,7 +755,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBack}
|
||||
aria-label="Back"
|
||||
aria-label={t('settings.view.actions.back')}
|
||||
className="inline-flex h-9 w-9 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<RiArrowLeftSLine className="h-5 w-5" />
|
||||
@@ -721,8 +768,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Close settings"
|
||||
title={`Close Settings (${shortcutKey}+,)`}
|
||||
aria-label={t('settings.view.actions.closeSettings')}
|
||||
title={t('settings.view.actions.closeSettingsWithShortcut', { shortcut: shortcutKey })}
|
||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md p-0.5 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<RiCloseLine className="h-5 w-5" />
|
||||
@@ -762,7 +809,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
onPointerDown={handlePointerDown}
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label="Resize settings navigation"
|
||||
aria-label={t('settings.view.actions.resizeNavigation')}
|
||||
/>
|
||||
)}
|
||||
<ErrorBoundary>
|
||||
|
||||
Reference in New Issue
Block a user