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
@@ -21,6 +21,7 @@ import { compareSessionsByPinnedAndTime, isBranchDifferentFromLabel, normalizePa
import type { SessionFolder } from '@/stores/useSessionFoldersStore';
import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore';
import { openExternalUrl } from '@/lib/url';
import { useI18n } from '@/lib/i18n';
type DeleteFolderConfirm = {
scopeKey: string;
@@ -96,6 +97,7 @@ type Props = {
};
export function SessionGroupSection(props: Props): React.ReactNode {
const { t } = useI18n();
const {
group,
groupKey,
@@ -277,21 +279,28 @@ export function SessionGroupSection(props: Props): React.ReactNode {
const showBranchSubtitle = !prIndicator && !group.isMain && Boolean(group.branch);
const prVisualState = prIndicator?.visualState ?? null;
const checksSummary = prIndicator && prIndicator.state === 'open' && prIndicator.checks
? `${prIndicator.checks.success}/${prIndicator.checks.total} checks passed`
? t('sessions.sidebar.group.pr.checksPassed', {
success: prIndicator.checks.success,
total: prIndicator.checks.total,
})
: null;
const checksTail = prIndicator && prIndicator.state === 'open' && prIndicator.checks
? [
prIndicator.checks.failure > 0 ? `${prIndicator.checks.failure} failing` : null,
prIndicator.checks.pending > 0 ? `${prIndicator.checks.pending} pending` : null,
prIndicator.checks.failure > 0
? t('sessions.sidebar.group.pr.failingCount', { count: prIndicator.checks.failure })
: null,
prIndicator.checks.pending > 0
? t('sessions.sidebar.group.pr.pendingCount', { count: prIndicator.checks.pending })
: null,
].filter((item): item is string => Boolean(item)).join(', ')
: null;
const mergeabilityLabel = prIndicator && prIndicator.state === 'open'
? (prIndicator.mergeableState === 'blocked' || prIndicator.mergeableState === 'dirty'
? 'Conflicts or blocked'
: (prIndicator.mergeableState === 'clean' || prIndicator.canMerge === true ? 'Mergeable' : null))
? t('sessions.sidebar.group.pr.conflictsOrBlocked')
: (prIndicator.mergeableState === 'clean' || prIndicator.canMerge === true ? t('sessions.sidebar.group.pr.mergeable') : null))
: null;
const mergeStateLabel = prIndicator && prIndicator.state === 'open' && prIndicator.mergeableState
? `Merge state: ${prIndicator.mergeableState}`
? t('sessions.sidebar.group.pr.mergeState', { state: prIndicator.mergeableState })
: null;
const baseBranchLabel = prIndicator?.base ?? null;
const headBranchLabel = prIndicator?.head ?? null;
@@ -303,20 +312,22 @@ export function SessionGroupSection(props: Props): React.ReactNode {
}
switch (prIndicator.visualState) {
case 'merged':
return { label: 'Merged', color: 'var(--pr-merged)' };
return { label: t('sessions.sidebar.group.pr.status.merged'), color: 'var(--pr-merged)' };
case 'open':
return (prIndicator.canMerge === true || prIndicator.mergeableState === 'clean' || prIndicator.checks?.state === 'success')
? { label: 'Ready to merge', color: 'var(--pr-open)' }
: { label: 'PR open', color: 'var(--pr-open)' };
? { label: t('sessions.sidebar.group.pr.status.readyToMerge'), color: 'var(--pr-open)' }
: { label: t('sessions.sidebar.group.pr.status.open'), color: 'var(--pr-open)' };
case 'blocked':
return {
label: prIndicator.mergeableState === 'dirty' ? 'Merge conflicts' : 'Merge blocked',
label: prIndicator.mergeableState === 'dirty'
? t('sessions.sidebar.group.pr.status.mergeConflicts')
: t('sessions.sidebar.group.pr.status.mergeBlocked'),
color: 'var(--pr-blocked)',
};
case 'draft':
return { label: 'Draft PR', color: 'var(--pr-draft)' };
return { label: t('sessions.sidebar.group.pr.status.draft'), color: 'var(--pr-draft)' };
case 'closed':
return { label: 'Closed', color: 'var(--pr-closed)' };
return { label: t('sessions.sidebar.group.pr.status.closed'), color: 'var(--pr-closed)' };
default:
return null;
}
@@ -448,7 +459,9 @@ export function SessionGroupSection(props: Props): React.ReactNode {
{visibleSessions.map((node) => renderSessionNode(node, 0, group.directory, projectId, group.isArchivedBucket === true))}
{totalSessions === 0 && allFoldersForGroup.length === 0 ? (
<div className="py-1 text-left typography-micro text-muted-foreground">
{group.isArchivedBucket ? 'No archived sessions yet.' : 'No sessions in this workspace yet.'}
{group.isArchivedBucket
? t('sessions.sidebar.group.empty.noArchivedSessions')
: t('sessions.sidebar.group.empty.noSessionsInWorkspace')}
</div>
) : null}
{remainingCount > 0 && !isExpanded ? (
@@ -457,7 +470,9 @@ export function SessionGroupSection(props: Props): React.ReactNode {
onClick={() => toggleGroupSessionLimit(groupKey)}
className="mt-0.5 flex items-center justify-start rounded-md px-1.5 py-0.5 text-left text-xs text-muted-foreground/70 leading-tight hover:text-foreground hover:underline"
>
Show {remainingCount} more {remainingCount === 1 ? 'session' : 'sessions'}
{remainingCount === 1
? t('sessions.sidebar.group.showMoreSingle', { count: remainingCount })
: t('sessions.sidebar.group.showMorePlural', { count: remainingCount })}
</button>
) : null}
{isExpanded && totalSessions > maxVisible ? (
@@ -466,7 +481,7 @@ export function SessionGroupSection(props: Props): React.ReactNode {
onClick={() => toggleGroupSessionLimit(groupKey)}
className="mt-0.5 flex items-center justify-start rounded-md px-1.5 py-0.5 text-left text-xs text-muted-foreground/70 leading-tight hover:text-foreground hover:underline"
>
Show fewer sessions
{t('sessions.sidebar.group.showFewer')}
</button>
) : null}
</SessionFolderDndScope>
@@ -491,7 +506,9 @@ export function SessionGroupSection(props: Props): React.ReactNode {
onToggleCollapsedGroup(groupKey);
}
}}
aria-label={isCollapsed ? `Expand ${group.label}` : `Collapse ${group.label}`}
aria-label={isCollapsed
? t('sessions.sidebar.group.expandAria', { label: group.label })
: t('sessions.sidebar.group.collapseAria', { label: group.label })}
aria-expanded={!isCollapsed}
>
<div
@@ -661,12 +678,12 @@ export function SessionGroupSection(props: Props): React.ReactNode {
});
}}
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={`Delete archived sessions in ${group.label}`}
aria-label={t('sessions.sidebar.group.actions.deleteArchivedInGroupAria', { label: group.label })}
>
<RiDeleteBinLine className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>Delete archived sessions</p></TooltipContent>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.group.actions.deleteArchivedSessions')}</p></TooltipContent>
</Tooltip>
</div>
) : null}
@@ -685,12 +702,12 @@ export function SessionGroupSection(props: Props): React.ReactNode {
});
}}
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={`Delete ${group.label}`}
aria-label={t('sessions.sidebar.group.actions.deleteGroupAria', { label: group.label })}
>
<RiDeleteBinLine className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>Delete worktree</p></TooltipContent>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.group.actions.deleteWorktree')}</p></TooltipContent>
</Tooltip>
</div>
) : null}
@@ -708,12 +725,12 @@ export function SessionGroupSection(props: Props): React.ReactNode {
openNewSessionDraft({ directoryOverride: group.directory });
}}
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={`New draft session in ${group.label}`}
aria-label={t('sessions.sidebar.group.actions.newDraftInGroupAria', { label: group.label })}
>
<RiAddLine className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>New draft session</p></TooltipContent>
<TooltipContent side="bottom" sideOffset={4}><p>{t('sessions.sidebar.project.actions.newDraftSession')}</p></TooltipContent>
</Tooltip>
</div>
) : null}