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