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
@@ -9,6 +9,7 @@ import {
} from '@/components/ui/select';
import { cn } from '@/lib/utils';
import { useConfigStore } from '@/stores/useConfigStore';
import { useI18n } from '@/lib/i18n';
export interface AgentSelectorProps {
/** Currently selected agent name (empty string for no agent) */
@@ -34,6 +35,7 @@ export const AgentSelector: React.FC<AgentSelectorProps> = ({
disabled,
id,
}) => {
const { t } = useI18n();
const getVisibleAgents = useConfigStore((state) => state.getVisibleAgents);
const loadAgents = useConfigStore((state) => state.loadAgents);
const defaultAgentName = useConfigStore((state) => state.currentAgentName);
@@ -91,7 +93,7 @@ export const AgentSelector: React.FC<AgentSelectorProps> = ({
className,
)}
>
<SelectValue placeholder="Select an agent" />
<SelectValue placeholder={t('multirun.agentSelector.placeholder')} />
</SelectTrigger>
<SelectContent fitContent>
{selectableAgents.length > 0 && (
@@ -12,6 +12,7 @@ import {
import { useGitStore, useGitBranches, useGitLoadingBranches } from '@/stores/useGitStore';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { getRootBranch } from '@/lib/worktrees/worktreeStatus';
import { useI18n } from '@/lib/i18n';
/** localStorage key matching NewWorktreeDialog */
const LAST_SOURCE_BRANCH_KEY = 'oc:lastWorktreeSourceBranch';
@@ -101,6 +102,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
disabled,
id,
}) => {
const { t } = useI18n();
const { localBranches, remoteBranches, isLoading, isGitRepository } = useBranchOptions(directory);
const allBranches = React.useMemo(
() => [...localBranches, ...remoteBranches.map(b => `remotes/${b}`)],
@@ -151,22 +153,22 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
size="lg"
className={className ?? 'w-fit typography-meta text-foreground'}
>
<SelectValue placeholder={isLoading ? 'Loading branches' : 'Select source branch...'} />
<SelectValue placeholder={isLoading ? t('multiRun.branchSelector.status.loadingBranches') : t('multiRun.branchSelector.placeholder.selectSourceBranch')} />
</SelectTrigger>
<SelectContent className="max-h-[280px] max-w-[320px]">
{isLoading ? (
<div className="px-2 py-4 text-center typography-meta text-muted-foreground">
Loading branches...
{t('multiRun.branchSelector.status.loadingBranches')}
</div>
) : localBranches.length === 0 && remoteBranches.length === 0 ? (
<div className="px-2 py-4 text-center typography-meta text-muted-foreground">
No branches found
{t('multiRun.branchSelector.status.noBranchesFound')}
</div>
) : (
<>
{localBranches.length > 0 && (
<SelectGroup>
<SelectLabel className="font-semibold text-foreground">Local branches</SelectLabel>
<SelectLabel className="font-semibold text-foreground">{t('multiRun.branchSelector.groups.localBranches')}</SelectLabel>
{localBranches.map((branch) => (
<SelectItem key={branch} value={branch} className="whitespace-normal break-all">
{branch}
@@ -179,7 +181,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
)}
{remoteBranches.length > 0 && (
<SelectGroup>
<SelectLabel className="font-semibold text-foreground">Remote branches</SelectLabel>
<SelectLabel className="font-semibold text-foreground">{t('multiRun.branchSelector.groups.remoteBranches')}</SelectLabel>
{remoteBranches.map((branch) => (
<SelectItem key={`remotes/${branch}`} value={`remotes/${branch}`} className="whitespace-normal break-all">
{branch}
@@ -193,7 +195,7 @@ export const BranchSelector: React.FC<BranchSelectorProps> = ({
</Select>
{isGitRepository === false && (
<p className="typography-micro text-muted-foreground/70 mt-2">Not in a git repository.</p>
<p className="typography-micro text-muted-foreground/70 mt-2">{t('multiRun.branchSelector.status.notInGitRepository')}</p>
)}
</div>
);
@@ -10,6 +10,7 @@ import { isIMECompositionEvent } from '@/lib/ime';
import { useConfigStore } from '@/stores/useConfigStore';
import { useModelLists } from '@/hooks/useModelLists';
import type { ModelMetadata } from '@/types';
import { useI18n } from '@/lib/i18n';
/** Chip height class - shared between chips and add button */
const CHIP_HEIGHT_CLASS = 'h-7';
@@ -110,11 +111,12 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
onRemove,
onUpdate,
minModels,
addButtonLabel = 'Add model',
addButtonLabel,
showChips = true,
maxModels,
addButtonClassName,
}) => {
const { t } = useI18n();
const providers = useConfigStore((state) => state.providers);
const modelsMetadata = useConfigStore((state) => state.modelsMetadata);
const { favoriteModelsList, recentModelsList } = useModelLists();
@@ -334,7 +336,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
onClick={() => setIsOpen(!isOpen)}
>
<RiAddLine className="h-3.5 w-3.5 mr-1" />
{addButtonLabel}
{addButtonLabel ?? t('multirun.modelMultiSelect.actions.addModel')}
</Button>
{isOpen && (() => {
@@ -414,7 +416,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
<Input
ref={searchInputRef}
type="text"
placeholder="Search models"
placeholder={t('multirun.modelMultiSelect.search.placeholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleKeyDown}
@@ -431,7 +433,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
<div className="p-1">
{!hasResults && (
<div className="px-2 py-4 text-center typography-meta text-muted-foreground">
No models found
{t('multirun.modelMultiSelect.search.noResults')}
</div>
)}
@@ -440,7 +442,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
<>
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider sticky top-0 z-10 -mx-1 flex items-center gap-2 border-b border-border/30 px-3 py-1.5 [background:linear-gradient(var(--surface-elevated),var(--surface-elevated)),linear-gradient(var(--surface-background),var(--surface-background))]">
<RiStarFill className="h-4 w-4 text-primary" />
Favorites
{t('multirun.modelMultiSelect.sections.favorites')}
</div>
{filteredFavorites.map(({ model, providerID, modelID }) => {
const idx = currentFlatIndex++;
@@ -455,7 +457,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
{filteredFavorites.length > 0 && <div className="h-px bg-border/40 my-1" />}
<div className="typography-micro font-semibold text-muted-foreground uppercase tracking-wider sticky top-0 z-10 -mx-1 flex items-center gap-2 border-b border-border/30 px-3 py-1.5 [background:linear-gradient(var(--surface-elevated),var(--surface-elevated)),linear-gradient(var(--surface-background),var(--surface-background))]">
<RiTimeLine className="h-4 w-4" />
Recent
{t('multirun.modelMultiSelect.sections.recent')}
</div>
{filteredRecents.map(({ model, providerID, modelID }) => {
const idx = currentFlatIndex++;
@@ -491,7 +493,7 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
{/* Keyboard hints footer */}
<div className="px-3 pt-1 pb-1.5 border-t border-border/40 typography-micro text-muted-foreground">
navigate Enter select Esc close
{t('multirun.modelMultiSelect.keyboard.hint')}
</div>
</div>
);
@@ -544,11 +546,11 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
variantValue === DEFAULT_VARIANT_VALUE ? 'text-muted-foreground' : 'text-[color:var(--status-info)]'
)}
/>
<SelectValue placeholder="Thinking" />
<SelectValue placeholder={t('multirun.modelMultiSelect.variant.placeholder')} />
</SelectTrigger>
<SelectContent fitContent>
<SelectItem value={DEFAULT_VARIANT_VALUE} className="pr-2 [&>span:first-child]:hidden">
Default
{t('multirun.modelMultiSelect.variant.default')}
</SelectItem>
{variantKeys.map((variant) => (
<SelectItem key={variant} value={variant} className="pr-2 [&>span:first-child]:hidden">
@@ -568,7 +570,9 @@ export const ModelMultiSelect: React.FC<ModelMultiSelectProps> = ({
{/* Validation hint */}
{minModels !== undefined && selectedModels.length < minModels && (
<p className="typography-micro text-muted-foreground">
Select from {minModels} {maxModels !== undefined ? `to ${maxModels} models` : ''}.
{maxModels !== undefined
? t('multirun.modelMultiSelect.validation.minToMax', { min: minModels, max: maxModels })
: t('multirun.modelMultiSelect.validation.minOnly', { min: minModels })}
</p>
)}
</div>
@@ -26,6 +26,7 @@ import { useThemeSystem } from '@/contexts/useThemeSystem';
import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP, getProjectIconImageUrl } from '@/lib/projectMeta';
import type { ProjectEntry } from '@/lib/api/types';
import { startDesktopWindowDrag } from '@/lib/desktopNative';
import { useI18n } from '@/lib/i18n';
/** Max file size in bytes (10MB) */
const MAX_FILE_SIZE = 10 * 1024 * 1024;
@@ -93,6 +94,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
onCancel,
isWindowed = false,
}) => {
const { t } = useI18n();
const [name, setName] = React.useState('');
const [prompt, setPrompt] = React.useState(() => initialPrompt ?? '');
const [selectedModels, setSelectedModels] = React.useState<ModelSelectionWithId[]>([]);
@@ -358,7 +360,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.size > MAX_FILE_SIZE) {
toast.error(`File "${file.name}" is too large (max 10MB)`);
toast.error(t('multirun.launcher.toast.fileTooLarge', { fileName: file.name }));
continue;
}
@@ -382,12 +384,16 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
attachedCount++;
} catch (error) {
console.error('File attach failed', error);
toast.error(`Failed to attach "${file.name}"`);
toast.error(t('multirun.launcher.toast.attachFailed', { fileName: file.name }));
}
}
if (attachedCount > 0) {
toast.success(`Attached ${attachedCount} file${attachedCount > 1 ? 's' : ''}`);
toast.success(
attachedCount === 1
? t('multirun.launcher.toast.attachedSingle', { count: attachedCount })
: t('multirun.launcher.toast.attachedPlural', { count: attachedCount })
);
}
if (fileInputRef.current) {
@@ -596,7 +602,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
)}
style={{ borderColor: 'var(--interactive-border)' }}
>
<h1 className="typography-ui-label font-medium">New Multi-Run</h1>
<h1 className="typography-ui-label font-medium">{t('multirun.launcher.title')}</h1>
{onCancel && (
<div className="absolute right-0 flex items-center pr-3">
<Tooltip delayDuration={500}>
@@ -604,14 +610,14 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
<button
type="button"
onClick={onCancel}
aria-label="Close (Esc)"
aria-label={t('multirun.launcher.actions.closeEsc')}
className="inline-flex h-9 w-9 items-center justify-center p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary app-region-no-drag"
>
<RiCloseLine className="h-5 w-5" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Close (Esc)</p>
<p>{t('multirun.launcher.actions.closeEsc')}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -628,7 +634,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-3">
{/* Project */}
<div className="flex flex-col gap-1">
<FieldLabel htmlFor="multirun-project" required>Project</FieldLabel>
<FieldLabel htmlFor="multirun-project" required>{t('multirun.launcher.project.label')}</FieldLabel>
{projects.length > 0 ? (
<Select
value={selectedProjectId ?? undefined}
@@ -638,7 +644,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
{selectedProject ? (
<SelectValue>{renderProjectLabel(selectedProject)}</SelectValue>
) : (
<SelectValue placeholder="Select project" />
<SelectValue placeholder={t('multirun.launcher.project.placeholder')} />
)}
</SelectTrigger>
<SelectContent fitContent>
@@ -650,7 +656,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
</SelectContent>
</Select>
) : (
<p className="typography-micro text-muted-foreground py-2">Add a project first.</p>
<p className="typography-micro text-muted-foreground py-2">{t('multirun.launcher.project.empty')}</p>
)}
</div>
@@ -659,15 +665,15 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
<FieldLabel
htmlFor="group-name"
required
info={<InfoTip>Used for worktree directory and branch names</InfoTip>}
info={<InfoTip>{t('multirun.launcher.groupName.info')}</InfoTip>}
>
Group name
{t('multirun.launcher.groupName.label')}
</FieldLabel>
<Input
id="group-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="feature-auth, bugfix-login"
placeholder={t('multirun.launcher.groupName.placeholder')}
className="typography-meta w-full"
required
/>
@@ -677,9 +683,9 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
<div className="flex flex-col gap-1">
<FieldLabel
htmlFor="multirun-worktree-base-branch"
info={<InfoTip>New branch created from this base per model</InfoTip>}
info={<InfoTip>{t('multirun.launcher.baseBranch.info')}</InfoTip>}
>
Base branch
{t('multirun.launcher.baseBranch.label')}
</FieldLabel>
<BranchSelector
directory={selectedProjectDirectory}
@@ -693,9 +699,9 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
<div className="flex flex-col gap-1">
<FieldLabel
htmlFor="multirun-agent"
info={<InfoTip>Agent used for all runs. Defaults to your configured agent.</InfoTip>}
info={<InfoTip>{t('multirun.launcher.agent.info')}</InfoTip>}
>
Agent
{t('multirun.launcher.agent.label')}
</FieldLabel>
<AgentSelector
value={selectedAgent}
@@ -710,7 +716,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
<CollapsibleTrigger className="w-full flex items-center gap-2 py-1.5 px-2 -mx-2 rounded-lg hover:bg-[var(--interactive-hover)]/50 transition-colors group">
<RiTerminalLine className="h-3.5 w-3.5 text-muted-foreground/70" />
<span className="typography-meta font-medium text-muted-foreground group-hover:text-foreground transition-colors">
Setup commands
{t('multirun.launcher.setupCommands.label')}
</span>
{configuredSetupCount > 0 && (
<span
@@ -733,7 +739,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
<CollapsibleContent>
<div className="pt-2 space-y-1.5">
{isLoadingSetupCommands ? (
<p className="typography-meta text-muted-foreground/70 px-2">Loading...</p>
<p className="typography-meta text-muted-foreground/70 px-2">{t('multirun.launcher.setupCommands.loading')}</p>
) : (
<>
{setupCommands.map((command, index) => (
@@ -745,7 +751,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
newCommands[index] = e.target.value;
setSetupCommands(newCommands);
}}
placeholder="bun install"
placeholder={t('multirun.launcher.setupCommands.commandPlaceholder')}
className="h-8 flex-1 font-mono text-xs"
/>
<button
@@ -755,7 +761,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
setSetupCommands(newCommands);
}}
className="flex-shrink-0 flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label="Remove command"
aria-label={t('multirun.launcher.setupCommands.removeCommandAria')}
>
<RiCloseLine className="h-3.5 w-3.5" />
</button>
@@ -767,7 +773,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
className="flex items-center gap-1 typography-meta text-muted-foreground hover:text-foreground transition-colors px-1"
>
<RiAddLine className="h-3 w-3" />
Add command
{t('multirun.launcher.setupCommands.addCommand')}
</button>
</>
)}
@@ -777,7 +783,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
{/* ── Prompt ── */}
<div className="flex flex-col gap-1.5">
<FieldLabel htmlFor="prompt" required>Prompt</FieldLabel>
<FieldLabel htmlFor="prompt" required>{t('multirun.launcher.prompt.label')}</FieldLabel>
<div className="relative">
<Textarea
id="prompt"
@@ -790,7 +796,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
updateAutocompleteState(nextPrompt, cursorPosition);
}}
onKeyDown={handlePromptKeyDown}
placeholder="Enter the prompt to send to all models..."
placeholder={t('multirun.launcher.prompt.placeholder')}
className="typography-meta min-h-[100px] max-h-[300px] resize-none overflow-y-auto field-sizing-content"
required
/>
@@ -847,10 +853,10 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
className="inline-flex items-center gap-1 h-6 px-2 rounded-md typography-micro text-muted-foreground hover:text-foreground hover:bg-[var(--interactive-hover)]/50 transition-colors"
>
<RiAttachment2 className="h-3 w-3" />
Attach
{t('multirun.launcher.attachments.attach')}
</button>
</TooltipTrigger>
<TooltipContent>Same files sent to all runs</TooltipContent>
<TooltipContent>{t('multirun.launcher.attachments.tooltip')}</TooltipContent>
</Tooltip>
{attachedFiles.map((file) => (
@@ -886,9 +892,9 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
<div className="flex flex-col gap-1.5">
<FieldLabel
required
info={<InfoTip>Select 2{MAX_MODELS} models. Same model can be added multiple times.</InfoTip>}
info={<InfoTip>{t('multirun.launcher.models.info', { max: MAX_MODELS })}</InfoTip>}
>
Models
{t('multirun.launcher.models.label')}
</FieldLabel>
<ModelMultiSelect
selectedModels={selectedModels}
@@ -927,7 +933,7 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
size="sm"
onClick={onCancel}
>
Cancel
{t('multirun.launcher.actions.cancel')}
</Button>
<Button
type="submit"
@@ -935,9 +941,9 @@ export const MultiRunLauncher: React.FC<MultiRunLauncherProps> = ({
disabled={!isValid || isSubmitting}
>
{isSubmitting ? (
'Creating...'
t('multirun.launcher.actions.creating')
) : (
<>Start ({selectedModels.length} models)</>
<>{t('multirun.launcher.actions.startWithModelCount', { count: selectedModels.length })}</>
)}
</Button>
</div>