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
@@ -23,6 +23,7 @@ import {
import { cn } from '@/lib/utils';
import { ArrowsMerge } from '@/components/icons/ArrowsMerge';
import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore';
import { useI18n } from '@/lib/i18n';
type Props = {
hideDirectoryControls: boolean;
@@ -50,6 +51,7 @@ type Props = {
};
export function SidebarHeader(props: Props): React.ReactNode {
const { t } = useI18n();
const {
hideDirectoryControls,
handleOpenDirectoryDialog,
@@ -105,12 +107,12 @@ export function SidebarHeader(props: Props): React.ReactNode {
type="button"
onClick={onToggleSidebar}
className="inline-flex h-8 w-8 items-center justify-center rounded-md typography-ui-label font-medium text-foreground transition-colors hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50"
aria-label="Close sessions"
aria-label={t('sessions.sidebar.header.actions.closeSessions')}
>
<RiLayoutLeftLine className="h-[18px] w-[18px]" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>Close sessions</p></TooltipContent>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.closeSessions')}</p></TooltipContent>
</Tooltip>
) : null}
<Tooltip>
@@ -119,12 +121,12 @@ export function SidebarHeader(props: Props): React.ReactNode {
type="button"
onClick={handleOpenDirectoryDialog}
className={headerActionButtonClass}
aria-label="Add project"
aria-label={t('sessions.sidebar.header.actions.addProject')}
>
<RiFolderAddLine className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>Add project</p></TooltipContent>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.addProject')}</p></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
@@ -132,12 +134,12 @@ export function SidebarHeader(props: Props): React.ReactNode {
type="button"
onClick={handleNewSession}
className={headerActionButtonClass}
aria-label="New session"
aria-label={t('sessions.sidebar.header.actions.newSession')}
>
<RiChatNewLine className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>New session</p></TooltipContent>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.newSession')}</p></TooltipContent>
</Tooltip>
<Tooltip>
@@ -146,13 +148,13 @@ export function SidebarHeader(props: Props): React.ReactNode {
type="button"
onClick={openMultiRunLauncher}
className={headerActionButtonClass}
aria-label="New multi-run"
aria-label={t('sessions.sidebar.header.actions.newMultiRun')}
disabled={!canOpenMultiRun}
>
<ArrowsMerge className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>New multi-run</p></TooltipContent>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.newMultiRun')}</p></TooltipContent>
</Tooltip>
</div>
@@ -163,12 +165,12 @@ export function SidebarHeader(props: Props): React.ReactNode {
type="button"
onClick={openScheduledTasksDialog}
className={headerActionButtonClass}
aria-label="Scheduled tasks"
aria-label={t('sessions.sidebar.header.actions.scheduledTasks')}
>
<RiCalendarScheduleLine className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>Scheduled tasks</p></TooltipContent>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.scheduledTasks')}</p></TooltipContent>
</Tooltip>
<Tooltip>
@@ -177,13 +179,13 @@ export function SidebarHeader(props: Props): React.ReactNode {
type="button"
onClick={() => setIsSessionSearchOpen((prev) => !prev)}
className={headerActionButtonClass}
aria-label="Search sessions"
aria-label={t('sessions.sidebar.header.actions.searchSessions')}
aria-expanded={isSessionSearchOpen}
>
<RiSearchLine className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>Search sessions</p></TooltipContent>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.actions.searchSessions')}</p></TooltipContent>
</Tooltip>
<Tooltip>
@@ -192,14 +194,18 @@ export function SidebarHeader(props: Props): React.ReactNode {
type="button"
onClick={onToggleSelectionMode}
className={cn(headerActionButtonClass, selectionModeEnabled && 'bg-interactive-hover text-primary')}
aria-label={selectionModeEnabled ? 'Exit selection' : 'Select sessions'}
aria-label={selectionModeEnabled
? t('sessions.sidebar.header.actions.exitSelection')
: t('sessions.sidebar.header.actions.selectSessions')}
aria-pressed={selectionModeEnabled}
>
<RiCheckboxMultipleLine className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
<p>{selectionModeEnabled ? 'Exit selection' : 'Select sessions'}</p>
<p>{selectionModeEnabled
? t('sessions.sidebar.header.actions.exitSelection')
: t('sessions.sidebar.header.actions.selectSessions')}</p>
</TooltipContent>
</Tooltip>
@@ -210,37 +216,37 @@ export function SidebarHeader(props: Props): React.ReactNode {
<button
type="button"
className={headerActionButtonClass}
aria-label="Session display mode"
aria-label={t('sessions.sidebar.header.actions.sessionDisplayMode')}
>
<RiEqualizer2Line className={headerActionIconClass} />
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>Display mode</p></TooltipContent>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.header.displayMode.label')}</p></TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" className="min-w-[160px]">
<DropdownMenuItem
onClick={() => setDisplayMode('default')}
className="flex items-center justify-between"
>
<span>Default</span>
<span>{t('sessions.sidebar.header.displayMode.default')}</span>
{displayMode === 'default' ? <RiCheckLine className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setDisplayMode('minimal')}
className="flex items-center justify-between"
>
<span>Minimal</span>
<span>{t('sessions.sidebar.header.displayMode.minimal')}</span>
{displayMode === 'minimal' ? <RiCheckLine className="h-4 w-4 text-primary" /> : null}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={collapseAllProjects} className="flex items-center gap-2">
<RiContractUpDownLine className="h-4 w-4" />
<span>Collapse all</span>
<span>{t('sessions.sidebar.header.displayMode.collapseAll')}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={expandAllProjects} className="flex items-center gap-2">
<RiExpandUpDownLine className="h-4 w-4" />
<span>Expand all</span>
<span>{t('sessions.sidebar.header.displayMode.expandAll')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -251,9 +257,11 @@ export function SidebarHeader(props: Props): React.ReactNode {
<div className="pb-1">
<div className="mb-1 flex items-center justify-between px-0.5 typography-micro text-muted-foreground/80">
{hasSessionSearchQuery ? (
<span>{searchMatchCount} {searchMatchCount === 1 ? 'match' : 'matches'}</span>
<span>{searchMatchCount === 1
? t('sessions.sidebar.header.search.matchCountSingle', { count: searchMatchCount })
: t('sessions.sidebar.header.search.matchCountPlural', { count: searchMatchCount })}</span>
) : <span />}
<span>Esc to clear</span>
<span>{t('sessions.sidebar.header.search.escapeHint')}</span>
</div>
<div className="relative">
<RiSearchLine className="pointer-events-none absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@@ -261,7 +269,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
ref={sessionSearchInputRef}
value={sessionSearchQuery}
onChange={(event) => setSessionSearchQuery(event.target.value)}
placeholder="Search sessions..."
placeholder={t('sessions.sidebar.header.search.placeholder')}
className="h-8 w-full rounded-md border border-border bg-transparent pl-8 pr-8 typography-ui-label text-foreground outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
onKeyDown={(event) => {
if (event.key === 'Escape') {
@@ -279,7 +287,7 @@ export function SidebarHeader(props: Props): React.ReactNode {
type="button"
onClick={() => setSessionSearchQuery('')}
className="absolute right-1 top-1/2 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground hover:bg-interactive-hover/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label="Clear search"
aria-label={t('sessions.sidebar.header.search.clear')}
>
<RiCloseLine className="h-3.5 w-3.5" />
</button>