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
@@ -18,6 +18,7 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useI18n } from '@/lib/i18n';
import { ProviderLogo } from '@/components/ui/ProviderLogo';
import { UsageProgressBar } from '@/components/sections/usage/UsageProgressBar';
import { PaceIndicator } from '@/components/sections/usage/PaceIndicator';
@@ -54,6 +55,7 @@ const SESSIONS_SIDEBAR_MAX_WIDTH = 520;
type VSCodeView = 'sessions' | 'chat' | 'settings';
export const VSCodeLayout: React.FC = () => {
const { t } = useI18n();
const runtimeApis = useRuntimeAPIs();
const viewMode = React.useMemo<'sidebar' | 'editor'>(() => {
@@ -100,8 +102,8 @@ export const VSCodeLayout: React.FC = () => {
if (!currentSessionId) {
return null;
}
return sessions.find((session) => session.id === currentSessionId)?.title || 'Session';
}, [currentSessionId, sessions]);
return sessions.find((session) => session.id === currentSessionId)?.title || t('vscodeLayout.title.sessionFallback');
}, [currentSessionId, sessions, t]);
const newSessionDraftOpen = useSessionUIStore((state) => Boolean(state.newSessionDraft?.open));
const isSyncingMessages = useViewportStore((state) => state.isSyncing);
const hasActiveSessionWork = useDirectorySync((state) => {
@@ -379,7 +381,7 @@ export const VSCodeLayout: React.FC = () => {
// Editor mode: just chat, no sidebar
<div className="flex flex-col h-full">
<VSCodeHeader
title={sessions.find((session) => session.id === currentSessionId)?.title || 'Chat'}
title={sessions.find((session) => session.id === currentSessionId)?.title || t('vscodeLayout.title.chat')}
showMcp
showContextUsage
/>
@@ -422,15 +424,15 @@ export const VSCodeLayout: React.FC = () => {
onPointerCancel={handleExpandedSidebarResizeEnd}
role="separator"
aria-orientation="vertical"
aria-label="Resize sessions sidebar"
aria-label={t('vscodeLayout.actions.resizeSessionsSidebarAria')}
/>
</div>
{/* Chat content */}
<div className="flex-1 flex flex-col min-w-0">
<VSCodeHeader
title={newSessionDraftOpen && !currentSessionId
? 'New session'
: sessions.find((session) => session.id === currentSessionId)?.title || 'Chat'}
? t('vscodeLayout.title.newSession')
: sessions.find((session) => session.id === currentSessionId)?.title || t('vscodeLayout.title.chat')}
showMcp
showContextUsage
/>
@@ -447,7 +449,7 @@ export const VSCodeLayout: React.FC = () => {
{/* Sessions list view */}
<div className={cn('flex flex-col h-full', currentView !== 'sessions' && 'hidden')}>
<VSCodeHeader
title="Sessions"
title={t('vscodeLayout.title.sessions')}
/>
<div className="flex-1 overflow-hidden">
<SessionSidebar
@@ -463,8 +465,8 @@ export const VSCodeLayout: React.FC = () => {
<div className={cn('flex flex-col h-full', currentView !== 'chat' && 'hidden')}>
<VSCodeHeader
title={newSessionDraftOpen && !currentSessionId
? 'New session'
: sessions.find((session) => session.id === currentSessionId)?.title || 'Chat'}
? t('vscodeLayout.title.newSession')
: sessions.find((session) => session.id === currentSessionId)?.title || t('vscodeLayout.title.chat')}
showBack
onBack={handleBackToSessions}
showMcp
@@ -496,6 +498,7 @@ interface VSCodeHeaderProps {
}
const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, onNewSession, onSettings, onAgentManager, showMcp, showContextUsage, showRateLimits }) => {
const { t } = useI18n();
const getCurrentModel = useConfigStore((s) => s.getCurrentModel);
const getContextUsage = useSessionUIStore((state) => state.getContextUsage);
const quotaResults = useQuotaStore((state) => state.results);
@@ -561,7 +564,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
<button
onClick={onBack}
className="inline-flex h-7 w-7 items-center justify-center text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
aria-label="Back to sessions"
aria-label={t('vscodeLayout.actions.backToSessionsAria')}
>
<RiArrowLeftLine className="h-5 w-5" />
</button>
@@ -571,7 +574,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
<button
onClick={onNewSession}
className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
aria-label="New session"
aria-label={t('vscodeLayout.actions.newSessionAria')}
>
<RiAddLine className="h-5 w-5" />
</button>
@@ -580,7 +583,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
<button
onClick={onAgentManager}
className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
aria-label="Open Agent Manager"
aria-label={t('vscodeLayout.actions.openAgentManagerAria')}
>
<RiRobot2Line className="h-5 w-5" />
</button>
@@ -601,7 +604,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label="Rate limits"
aria-label={t('vscodeLayout.quota.actions.rateLimitsAria')}
className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
disabled={isQuotaLoading}
>
@@ -614,7 +617,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
>
<div className="sticky top-0 z-20 bg-[var(--surface-elevated)]">
<DropdownMenuLabel className="flex items-center justify-between gap-3 typography-ui-header font-semibold text-foreground">
<span>Rate limits</span>
<span>{t('vscodeLayout.quota.title')}</span>
<div className="flex items-center gap-1">
<div className="flex items-center rounded-md border border-[var(--interactive-border)] p-0.5">
<button
@@ -627,9 +630,9 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
}`
}
onClick={() => void handleDisplayModeChange('usage')}
aria-label="Show used quota"
aria-label={t('vscodeLayout.quota.actions.showUsedAria')}
>
Used
{t('vscodeLayout.quota.mode.used')}
</button>
<button
type="button"
@@ -641,9 +644,9 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
}`
}
onClick={() => void handleDisplayModeChange('remaining')}
aria-label="Show remaining quota"
aria-label={t('vscodeLayout.quota.actions.showRemainingAria')}
>
Remaining
{t('vscodeLayout.quota.mode.remaining')}
</button>
</div>
<button
@@ -651,7 +654,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
onClick={() => fetchAllQuotas()}
disabled={isQuotaLoading}
aria-label="Refresh rate limits"
aria-label={t('vscodeLayout.quota.actions.refreshAria')}
>
<RiRefreshLine className="h-4 w-4" />
</button>
@@ -659,11 +662,11 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
</DropdownMenuLabel>
</div>
<div className="border-b border-[var(--interactive-border)] px-2 pb-2 typography-micro text-muted-foreground text-[10px]">
Last updated {formatTime(quotaLastUpdated)}
{t('vscodeLayout.quota.lastUpdated', { time: formatTime(quotaLastUpdated) })}
</div>
{!hasRateLimits && (
<DropdownMenuItem className="cursor-default" closeOnClick={false}>
<span className="typography-ui-label text-muted-foreground">No rate limits available.</span>
<span className="typography-ui-label text-muted-foreground">{t('vscodeLayout.quota.noRateLimitsAvailable')}</span>
</DropdownMenuItem>
)}
{rateLimitGroups.map((group, index) => (
@@ -679,7 +682,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
closeOnClick={false}
>
<span className="typography-ui-label text-muted-foreground">
{group.error ?? 'No rate limits reported.'}
{group.error ?? t('vscodeLayout.quota.noRateLimitsReported')}
</span>
</DropdownMenuItem>
) : (
@@ -735,7 +738,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
<button
onClick={onSettings}
className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
aria-label="Settings"
aria-label={t('vscodeLayout.actions.settingsAria')}
>
<RiSettings3Line className="h-5 w-5" />
</button>