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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user