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
@@ -44,6 +44,7 @@ import {
PROJECT_ACTION_ICONS,
PROJECT_ACTIONS_UPDATED_EVENT,
} from '@/lib/projectActions';
import { useI18n } from '@/lib/i18n';
import { cn } from '@/lib/utils';
type EditableProjectAction = OpenChamberProjectAction;
@@ -67,6 +68,7 @@ interface ProjectActionsSectionProps {
}
export const ProjectActionsSection: React.FC<ProjectActionsSectionProps> = ({ projectRef }) => {
const { t } = useI18n();
const isDesktopShellApp = React.useMemo(() => isDesktopShell(), []);
const desktopSshInstances = useDesktopSshStore((state) => state.instances);
const loadDesktopSsh = useDesktopSshStore((state) => state.load);
@@ -126,10 +128,10 @@ export const ProjectActionsSection: React.FC<ProjectActionsSectionProps> = ({ pr
return entry.name.trim().length === 0 || entry.command.trim().length === 0;
});
if (hasIncomplete) {
return 'Fill action name and command before saving.';
return t('settings.projects.actions.validation.fillNameAndCommand');
}
return null;
}, [actions]);
}, [actions, t]);
const hasChanges = React.useMemo(() => {
if (initialSnapshot === null) {
@@ -172,7 +174,7 @@ export const ProjectActionsSection: React.FC<ProjectActionsSectionProps> = ({ pr
primaryActionId: null,
});
if (!ok) {
toast.error('Failed to save actions');
toast.error(t('settings.projects.actions.toast.saveFailed'));
return;
}
setInitialSnapshot(JSON.stringify({ actions }));
@@ -181,13 +183,13 @@ export const ProjectActionsSection: React.FC<ProjectActionsSectionProps> = ({ pr
detail: { projectId: projectRef.id },
}));
}
toast.success('Project actions saved');
toast.success(t('settings.projects.actions.toast.saved'));
} catch {
toast.error('Failed to save actions');
toast.error(t('settings.projects.actions.toast.saveFailed'));
} finally {
setIsSaving(false);
}
}, [actions, projectRef, validationError]);
}, [actions, projectRef, t, validationError]);
const canSave = !isSaving && !isLoading && hasChanges && !validationError;
@@ -195,21 +197,21 @@ export const ProjectActionsSection: React.FC<ProjectActionsSectionProps> = ({ pr
<div className="mb-8">
<div className="mb-1 flex items-start justify-between gap-2">
<div>
<h3 className="typography-ui-header font-medium text-foreground">Actions</h3>
<p className="typography-meta text-muted-foreground">Per-project commands shown in header next to project name.</p>
<h3 className="typography-ui-header font-medium text-foreground">{t('settings.projects.actions.title')}</h3>
<p className="typography-meta text-muted-foreground">{t('settings.projects.actions.description')}</p>
</div>
<Button type="button" variant="outline" size="xs" className="!font-normal" onClick={handleAddAction}>
<RiAddLine className="h-3.5 w-3.5" />
Add action
{t('settings.projects.actions.actions.add')}
</Button>
</div>
<section className="pb-2 pt-0 space-y-2">
{isLoading ? (
<p className="typography-meta text-muted-foreground">Loading...</p>
<p className="typography-meta text-muted-foreground">{t('settings.projects.actions.state.loading')}</p>
) : actions.length === 0 ? (
<div className="py-2">
<p className="typography-meta text-muted-foreground">No actions configured yet.</p>
<p className="typography-meta text-muted-foreground">{t('settings.projects.actions.state.empty')}</p>
</div>
) : (
<div className="space-y-0 max-w-[30rem]">
@@ -217,7 +219,7 @@ export const ProjectActionsSection: React.FC<ProjectActionsSectionProps> = ({ pr
const selectedIconKey = (action.icon as keyof typeof PROJECT_ACTION_ICON_MAP) || 'play';
const SelectedIcon = PROJECT_ACTION_ICON_MAP[selectedIconKey] || RiPlayLine;
const isOpen = expandedActions[action.id] ?? false;
const title = action.name.trim() || 'Untitled action';
const title = action.name.trim() || t('settings.projects.actions.state.untitled');
return (
<Collapsible
@@ -267,7 +269,7 @@ export const ProjectActionsSection: React.FC<ProjectActionsSectionProps> = ({ pr
<button
type="button"
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-[var(--interactive-border)] text-foreground hover:bg-[var(--interactive-hover)]"
aria-label="Select icon"
aria-label={t('settings.projects.actions.field.selectIconAria')}
>
<SelectedIcon className="h-4 w-4" />
</button>
@@ -286,7 +288,7 @@ export const ProjectActionsSection: React.FC<ProjectActionsSectionProps> = ({ pr
'inline-flex h-8 w-8 items-center justify-center rounded-md border border-transparent text-foreground hover:bg-[var(--interactive-hover)]',
selected && 'border-[var(--primary-base)] bg-[var(--primary-base)]/10 text-[var(--primary-base)]'
)}
aria-label={`Icon ${entry.label}`}
aria-label={t('settings.projects.actions.field.iconAria', { icon: entry.label })}
>
<Icon className="h-4 w-4" />
</button>
@@ -299,24 +301,24 @@ export const ProjectActionsSection: React.FC<ProjectActionsSectionProps> = ({ pr
<Input
value={action.name}
onChange={(event) => updateAction(action.id, (current) => ({ ...current, name: event.target.value }))}
placeholder="Action name"
placeholder={t('settings.projects.actions.field.actionNamePlaceholder')}
className="h-7 max-w-[14rem]"
/>
</div>
<div className="py-1">
<p className="typography-meta mb-0.5 text-muted-foreground">Command</p>
<p className="typography-meta mb-0.5 text-muted-foreground">{t('settings.projects.actions.field.command')}</p>
<Textarea
value={action.command}
onChange={(event) => updateAction(action.id, (current) => ({ ...current, command: event.target.value }))}
placeholder="e.g. bun run lint"
placeholder={t('settings.projects.actions.field.commandPlaceholder')}
className="min-h-[88px] max-w-[30rem] font-mono text-xs"
/>
</div>
<div className="py-1">
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
<span className="typography-ui-label text-foreground">Auto-open URL</span>
<span className="typography-ui-label text-foreground">{t('settings.projects.actions.field.autoOpenUrl')}</span>
<div
className="group flex cursor-pointer items-center gap-2"
role="button"
@@ -342,9 +344,9 @@ export const ProjectActionsSection: React.FC<ProjectActionsSectionProps> = ({ pr
...current,
...(checked ? { autoOpenUrl: true } : { autoOpenUrl: undefined }),
}))}
ariaLabel={`Auto-open URL for ${title}`}
ariaLabel={t('settings.projects.actions.field.autoOpenUrlForAria', { title })}
/>
<span className="typography-ui-label font-normal text-foreground/80">Open URL from output or custom URL below</span>
<span className="typography-ui-label font-normal text-foreground/80">{t('settings.projects.actions.field.autoOpenUrlDescription')}</span>
</div>
</div>
@@ -357,7 +359,7 @@ export const ProjectActionsSection: React.FC<ProjectActionsSectionProps> = ({ pr
...current,
openUrl: event.target.value,
}))}
placeholder="Override URL (optional)"
placeholder={t('settings.projects.actions.field.overrideUrlPlaceholder')}
className="h-7 w-full max-w-[24rem]"
/>
<Tooltip delayDuration={1000}>
@@ -365,14 +367,14 @@ export const ProjectActionsSection: React.FC<ProjectActionsSectionProps> = ({ pr
<RiInformationLine className="h-3.5 w-3.5 shrink-0 text-muted-foreground/60 cursor-help" />
</TooltipTrigger>
<TooltipContent sideOffset={8} className="max-w-xs">
If this field is filled, custom URL is used. If empty, app opens best URL from output.
{t('settings.projects.actions.field.overrideUrlTooltip')}
</TooltipContent>
</Tooltip>
</div>
{isDesktopShellApp ? (
<div className="mt-2">
<p className="typography-meta mb-0.5 text-muted-foreground">Desktop SSH forward</p>
<p className="typography-meta mb-0.5 text-muted-foreground">{t('settings.projects.actions.field.desktopSshForward')}</p>
{desktopForwardOptions.length > 0 ? (
<Select
value={
@@ -388,17 +390,17 @@ export const ProjectActionsSection: React.FC<ProjectActionsSectionProps> = ({ pr
}}
>
<SelectTrigger className="h-7 w-full max-w-[30rem]">
<SelectValue placeholder="Use output/manual URL" />
<SelectValue placeholder={t('settings.projects.actions.field.useOutputManualUrl')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">Use output/manual URL</SelectItem>
<SelectItem value="__none__">{t('settings.projects.actions.field.useOutputManualUrl')}</SelectItem>
{desktopForwardOptions.map((entry) => (
<SelectItem key={entry.id} value={entry.id}>{entry.label}</SelectItem>
))}
</SelectContent>
</Select>
) : (
<p className="typography-meta text-muted-foreground">No enabled local SSH forwards available.</p>
<p className="typography-meta text-muted-foreground">{t('settings.projects.actions.state.noDesktopSshForwards')}</p>
)}
</div>
) : null}
@@ -425,7 +427,7 @@ export const ProjectActionsSection: React.FC<ProjectActionsSectionProps> = ({ pr
onClick={handleSave}
disabled={!canSave}
>
{isSaving ? 'Saving...' : 'Save Actions'}
{isSaving ? t('settings.common.actions.saving') : t('settings.projects.actions.actions.save')}
</Button>
</div>
</section>