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>
|
||||
|
||||
@@ -11,8 +11,10 @@ import { RiCloseLine } from '@remixicon/react';
|
||||
import { WorktreeSectionContent } from '@/components/sections/openchamber/WorktreeSectionContent';
|
||||
import { ProjectActionsSection } from '@/components/sections/projects/ProjectActionsSection';
|
||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
export const ProjectsPage: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const projects = useProjectsStore((state) => state.projects);
|
||||
const updateProjectMeta = useProjectsStore((state) => state.updateProjectMeta);
|
||||
const uploadProjectIcon = useProjectsStore((state) => state.uploadProjectIcon);
|
||||
@@ -108,10 +110,10 @@ export const ProjectsPage: React.FC = () => {
|
||||
const uploadResult = await uploadProjectIcon(selectedProject.id, pendingUploadIconFile);
|
||||
setIsUploadingIcon(false);
|
||||
if (!uploadResult.ok) {
|
||||
toast.error(uploadResult.error || 'Failed to upload project icon');
|
||||
toast.error(uploadResult.error || t('settings.projects.page.toast.uploadIconFailed'));
|
||||
return;
|
||||
}
|
||||
toast.success('Project icon updated');
|
||||
toast.success(t('settings.projects.page.toast.iconUpdated'));
|
||||
clearPendingUploadIcon();
|
||||
setPendingRemoveImageIcon(false);
|
||||
}
|
||||
@@ -123,10 +125,10 @@ export const ProjectsPage: React.FC = () => {
|
||||
const removeResult = await removeProjectIcon(selectedProject.id);
|
||||
setIsRemovingCustomIcon(false);
|
||||
if (!removeResult.ok) {
|
||||
toast.error(removeResult.error || 'Failed to remove project icon');
|
||||
toast.error(removeResult.error || t('settings.projects.page.toast.removeIconFailed'));
|
||||
return;
|
||||
}
|
||||
toast.success('Project icon removed');
|
||||
toast.success(t('settings.projects.page.toast.iconRemoved'));
|
||||
setPendingRemoveImageIcon(false);
|
||||
setIconBackground(null);
|
||||
}
|
||||
@@ -220,25 +222,25 @@ export const ProjectsPage: React.FC = () => {
|
||||
void discoverProjectIcon(selectedProject.id)
|
||||
.then((result) => {
|
||||
if (!result.ok) {
|
||||
toast.error(result.error || 'Failed to discover project icon');
|
||||
toast.error(result.error || t('settings.projects.page.toast.discoverIconFailed'));
|
||||
return;
|
||||
}
|
||||
if (result.skipped) {
|
||||
toast.success('Custom icon already set for this project');
|
||||
toast.success(t('settings.projects.page.toast.customIconAlreadySet'));
|
||||
return;
|
||||
}
|
||||
toast.success('Project icon discovered');
|
||||
toast.success(t('settings.projects.page.toast.iconDiscovered'));
|
||||
})
|
||||
.finally(() => {
|
||||
setIsDiscoveringIcon(false);
|
||||
});
|
||||
}, [clearPendingUploadIcon, discoverProjectIcon, isDiscoveringIcon, selectedProject]);
|
||||
}, [clearPendingUploadIcon, discoverProjectIcon, isDiscoveringIcon, selectedProject, t]);
|
||||
|
||||
if (!selectedProject) {
|
||||
return (
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||
<div className="mx-auto w-full max-w-4xl p-3 sm:p-6 sm:pt-8">
|
||||
<p className="typography-meta text-muted-foreground">No projects available.</p>
|
||||
<p className="typography-meta text-muted-foreground">{t('settings.projects.page.empty.noProjects')}</p>
|
||||
</div>
|
||||
</ScrollableOverlay>
|
||||
);
|
||||
@@ -251,7 +253,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
<div className="mb-4 flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<h2 className="typography-ui-header font-semibold text-foreground truncate">
|
||||
{selectedProject.label ?? 'Project Settings'}
|
||||
{selectedProject.label ?? t('settings.projects.page.title.default')}
|
||||
</h2>
|
||||
<p className="typography-meta text-muted-foreground truncate" title={selectedProject.path}>
|
||||
{selectedProject.path}
|
||||
@@ -266,13 +268,13 @@ export const ProjectsPage: React.FC = () => {
|
||||
{/* Name */}
|
||||
<div className="py-1.5">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="typography-ui-label text-foreground">Project Name</span>
|
||||
<span className="typography-ui-label text-foreground">{t('settings.projects.page.field.projectName')}</span>
|
||||
</div>
|
||||
<div className="mt-1.5 flex min-w-0 items-center gap-2">
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Project name"
|
||||
placeholder={t('settings.projects.page.field.projectNamePlaceholder')}
|
||||
className="h-7 min-w-0 w-full sm:max-w-[19rem]"
|
||||
/>
|
||||
</div>
|
||||
@@ -281,7 +283,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
{/* Color */}
|
||||
<div className="py-1.5">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="typography-ui-label text-foreground">Accent Color</span>
|
||||
<span className="typography-ui-label text-foreground">{t('settings.projects.page.field.accentColor')}</span>
|
||||
</div>
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
@@ -293,7 +295,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
? 'border-2 border-foreground bg-[var(--primary-base)]/10'
|
||||
: 'border-border/40 hover:border-border hover:bg-[var(--surface-muted)]'
|
||||
)}
|
||||
title="None"
|
||||
title={t('settings.projects.page.field.none')}
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
@@ -318,7 +320,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
{/* Icon */}
|
||||
<div className="py-1.5">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="typography-ui-label text-foreground">Project Icon</span>
|
||||
<span className="typography-ui-label text-foreground">{t('settings.projects.page.field.projectIcon')}</span>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
@@ -341,7 +343,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
? 'border-2 border-foreground bg-[var(--primary-base)]/10'
|
||||
: 'border-border/40 hover:border-border hover:bg-[var(--surface-muted)]'
|
||||
)}
|
||||
title="None"
|
||||
title={t('settings.projects.page.field.none')}
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
@@ -367,7 +369,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
</div>
|
||||
{effectiveHasImageIcon && iconPreviewUrl && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="typography-meta text-muted-foreground">Preview</span>
|
||||
<span className="typography-meta text-muted-foreground">{t('settings.projects.page.field.preview')}</span>
|
||||
<span className="inline-flex h-7 w-7 items-center justify-center rounded-md border border-border/60 bg-[var(--surface-elevated)] p-1">
|
||||
<span
|
||||
className="inline-flex h-4 w-4 items-center justify-center overflow-hidden rounded-[2px]"
|
||||
@@ -391,7 +393,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
value={iconBackground ?? '#000000'}
|
||||
onChange={(event) => setIconBackground(event.target.value)}
|
||||
className="h-7 w-9 cursor-pointer rounded border border-border bg-transparent p-1"
|
||||
aria-label="Project icon background color"
|
||||
aria-label={t('settings.projects.page.field.projectIconBackgroundAria')}
|
||||
/>
|
||||
<Input
|
||||
value={iconBackground ?? ''}
|
||||
@@ -405,8 +407,8 @@ export const ProjectsPage: React.FC = () => {
|
||||
variant="outline"
|
||||
onClick={() => setIconBackground(null)}
|
||||
className="h-7 w-7 p-0"
|
||||
aria-label="Clear icon background"
|
||||
title="Clear background"
|
||||
aria-label={t('settings.projects.page.field.clearIconBackgroundAria')}
|
||||
title={t('settings.projects.page.field.clearBackground')}
|
||||
disabled={!iconBackground}
|
||||
>
|
||||
<RiCloseLine className="h-3.5 w-3.5" />
|
||||
@@ -422,7 +424,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isUploadingIcon}
|
||||
>
|
||||
{isUploadingIcon ? 'Uploading...' : 'Upload Icon'}
|
||||
{isUploadingIcon ? t('settings.projects.page.actions.uploading') : t('settings.projects.page.actions.uploadIcon')}
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -431,7 +433,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
onClick={() => void handleDiscoverIcon()}
|
||||
disabled={isDiscoveringIcon}
|
||||
>
|
||||
{isDiscoveringIcon ? 'Discovering...' : 'Discover Favicon'}
|
||||
{isDiscoveringIcon ? t('settings.projects.page.actions.discovering') : t('settings.projects.page.actions.discoverFavicon')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -443,7 +445,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
onClick={() => void handleRemoveImageIcon()}
|
||||
disabled={isRemovingCustomIcon}
|
||||
>
|
||||
{isRemovingCustomIcon ? 'Removing...' : 'Remove Project Icon'}
|
||||
{isRemovingCustomIcon ? t('settings.projects.page.actions.removing') : t('settings.projects.page.actions.removeProjectIcon')}
|
||||
</Button>
|
||||
)}
|
||||
{pendingRemoveImageIcon && (
|
||||
@@ -454,7 +456,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
onClick={() => setPendingRemoveImageIcon(false)}
|
||||
disabled={isRemovingCustomIcon}
|
||||
>
|
||||
Undo Remove
|
||||
{t('settings.projects.page.actions.undoRemove')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -469,7 +471,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
size="xs"
|
||||
className="!font-normal"
|
||||
>
|
||||
Save Changes
|
||||
{t('settings.common.actions.saveChanges')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -485,7 +487,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
<div className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
Worktree
|
||||
{t('settings.projects.page.section.worktree')}
|
||||
</h3>
|
||||
</div>
|
||||
<section className="px-2 pb-2 pt-0">
|
||||
|
||||
@@ -11,8 +11,10 @@ import { isDesktopLocalOriginActive, isTauriShell, isVSCodeRuntime, requestDirec
|
||||
import { sessionEvents } from '@/lib/sessionEvents';
|
||||
import { toast } from '@/components/ui';
|
||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
export const ProjectsSidebar: React.FC<{ onItemSelect?: () => void }> = ({ onItemSelect }) => {
|
||||
const { t } = useI18n();
|
||||
const projects = useProjectsStore((state) => state.projects);
|
||||
const addProject = useProjectsStore((state) => state.addProject);
|
||||
const selectedId = useUIStore((state) => state.settingsProjectsSelectedId);
|
||||
@@ -34,23 +36,23 @@ export const ProjectsSidebar: React.FC<{ onItemSelect?: () => void }> = ({ onIte
|
||||
if (result.success && result.path) {
|
||||
const added = addProject(result.path, { id: result.projectId });
|
||||
if (!added) {
|
||||
toast.error('Failed to add project', {
|
||||
description: 'Please select a valid directory.',
|
||||
toast.error(t('sessions.sidebar.directory.errorAddProjectTitle'), {
|
||||
description: t('sessions.sidebar.directory.errorAddProjectDescription'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSelectedId(added.id);
|
||||
} else if (result.error && result.error !== 'Directory selection cancelled') {
|
||||
toast.error('Failed to select directory', {
|
||||
toast.error(t('sessions.sidebar.directory.errorSelectDirectoryTitle'), {
|
||||
description: result.error,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to select directory:', error);
|
||||
toast.error('Failed to select directory');
|
||||
toast.error(t('sessions.sidebar.directory.errorSelectDirectoryTitle'));
|
||||
});
|
||||
}, [addProject, setSelectedId, tauriIpcAvailable]);
|
||||
}, [addProject, setSelectedId, tauriIpcAvailable, t]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (projects.length === 0) {
|
||||
@@ -70,9 +72,9 @@ export const ProjectsSidebar: React.FC<{ onItemSelect?: () => void }> = ({ onIte
|
||||
variant="background"
|
||||
header={
|
||||
<div className={cn('border-b px-3', 'pt-4 pb-3')}>
|
||||
<h2 className="text-base font-semibold text-foreground mb-3">Projects</h2>
|
||||
<h2 className="text-base font-semibold text-foreground mb-3">{t('settings.page.projects.title')}</h2>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="typography-meta text-muted-foreground">Total {projects.length}</span>
|
||||
<span className="typography-meta text-muted-foreground">{t('settings.projects.sidebar.total', { count: projects.length })}</span>
|
||||
{!isVSCode && (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -80,7 +82,7 @@ export const ProjectsSidebar: React.FC<{ onItemSelect?: () => void }> = ({ onIte
|
||||
size="icon"
|
||||
className="h-7 w-7 -my-1 text-muted-foreground"
|
||||
onClick={handleAddProject}
|
||||
aria-label="Add project"
|
||||
aria-label={t('settings.projects.sidebar.actions.addProject')}
|
||||
>
|
||||
<RiAddLine className="size-4" />
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user