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:
Bohdan Triapitsyn
2026-04-26 14:03:39 +03:00
committed by GitHub
parent 87db2ea210
commit 7d7285655d
198 changed files with 24173 additions and 4365 deletions
@@ -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>