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
@@ -37,6 +37,7 @@ import { cn } from '@/lib/utils';
|
||||
import { toast } from '@/components/ui';
|
||||
import { isTauriShell, isDesktopShell } from '@/lib/desktop';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import {
|
||||
desktopHostProbe,
|
||||
desktopHostsGet,
|
||||
@@ -102,12 +103,17 @@ const statusDotClass = (status: HostProbeResult['status'] | null): string => {
|
||||
return 'bg-muted-foreground/40';
|
||||
};
|
||||
|
||||
const statusLabel = (status: HostProbeResult['status'] | null): string => {
|
||||
if (status === 'ok') return 'Connected';
|
||||
if (status === 'auth') return 'Auth required';
|
||||
if (status === 'wrong-service') return 'Wrong service';
|
||||
if (status === 'unreachable') return 'Unreachable';
|
||||
return 'Unknown';
|
||||
const statusLabelKey = (status: HostProbeResult['status'] | null):
|
||||
| 'desktopHostSwitcher.status.connected'
|
||||
| 'desktopHostSwitcher.status.authRequired'
|
||||
| 'desktopHostSwitcher.status.wrongService'
|
||||
| 'desktopHostSwitcher.status.unreachable'
|
||||
| 'desktopHostSwitcher.status.unknown' => {
|
||||
if (status === 'ok') return 'desktopHostSwitcher.status.connected';
|
||||
if (status === 'auth') return 'desktopHostSwitcher.status.authRequired';
|
||||
if (status === 'wrong-service') return 'desktopHostSwitcher.status.wrongService';
|
||||
if (status === 'unreachable') return 'desktopHostSwitcher.status.unreachable';
|
||||
return 'desktopHostSwitcher.status.unknown';
|
||||
};
|
||||
|
||||
const statusIcon = (status: HostProbeResult['status'] | null) => {
|
||||
@@ -120,34 +126,47 @@ const statusIcon = (status: HostProbeResult['status'] | null) => {
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms));
|
||||
|
||||
const sshPhaseLabel = (phase: DesktopSshInstanceStatus['phase'] | undefined): string => {
|
||||
const sshPhaseLabelKey = (phase: DesktopSshInstanceStatus['phase'] | undefined):
|
||||
| 'desktopHostSwitcher.sshPhase.ready'
|
||||
| 'desktopHostSwitcher.sshPhase.error'
|
||||
| 'desktopHostSwitcher.sshPhase.reconnecting'
|
||||
| 'desktopHostSwitcher.sshPhase.resolvingConfig'
|
||||
| 'desktopHostSwitcher.sshPhase.checkingAuth'
|
||||
| 'desktopHostSwitcher.sshPhase.connectingSsh'
|
||||
| 'desktopHostSwitcher.sshPhase.probingRemote'
|
||||
| 'desktopHostSwitcher.sshPhase.installing'
|
||||
| 'desktopHostSwitcher.sshPhase.updating'
|
||||
| 'desktopHostSwitcher.sshPhase.detectingServer'
|
||||
| 'desktopHostSwitcher.sshPhase.startingServer'
|
||||
| 'desktopHostSwitcher.sshPhase.forwardingPorts'
|
||||
| 'desktopHostSwitcher.sshPhase.idle' => {
|
||||
switch (phase) {
|
||||
case 'ready':
|
||||
return 'Ready';
|
||||
return 'desktopHostSwitcher.sshPhase.ready';
|
||||
case 'error':
|
||||
return 'Error';
|
||||
return 'desktopHostSwitcher.sshPhase.error';
|
||||
case 'degraded':
|
||||
return 'Reconnecting';
|
||||
return 'desktopHostSwitcher.sshPhase.reconnecting';
|
||||
case 'config_resolved':
|
||||
return 'Resolving config';
|
||||
return 'desktopHostSwitcher.sshPhase.resolvingConfig';
|
||||
case 'auth_check':
|
||||
return 'Checking auth';
|
||||
return 'desktopHostSwitcher.sshPhase.checkingAuth';
|
||||
case 'master_connecting':
|
||||
return 'Connecting SSH';
|
||||
return 'desktopHostSwitcher.sshPhase.connectingSsh';
|
||||
case 'remote_probe':
|
||||
return 'Probing remote';
|
||||
return 'desktopHostSwitcher.sshPhase.probingRemote';
|
||||
case 'installing':
|
||||
return 'Installing';
|
||||
return 'desktopHostSwitcher.sshPhase.installing';
|
||||
case 'updating':
|
||||
return 'Updating';
|
||||
return 'desktopHostSwitcher.sshPhase.updating';
|
||||
case 'server_detecting':
|
||||
return 'Detecting server';
|
||||
return 'desktopHostSwitcher.sshPhase.detectingServer';
|
||||
case 'server_starting':
|
||||
return 'Starting server';
|
||||
return 'desktopHostSwitcher.sshPhase.startingServer';
|
||||
case 'forwarding':
|
||||
return 'Forwarding ports';
|
||||
return 'desktopHostSwitcher.sshPhase.forwardingPorts';
|
||||
default:
|
||||
return 'Idle';
|
||||
return 'desktopHostSwitcher.sshPhase.idle';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -246,6 +265,7 @@ export function DesktopHostSwitcherDialog({
|
||||
embedded = false,
|
||||
onHostSwitched,
|
||||
}: DesktopHostSwitcherDialogProps) {
|
||||
const { t } = useI18n();
|
||||
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
|
||||
const setSettingsPage = useUIStore((state) => state.setSettingsPage);
|
||||
|
||||
@@ -296,8 +316,8 @@ export function DesktopHostSwitcherDialog({
|
||||
const current = React.useMemo(() => resolveCurrentHost(allHosts), [allHosts]);
|
||||
const currentDefaultLabel = React.useMemo(() => {
|
||||
const id = defaultHostId || LOCAL_HOST_ID;
|
||||
return allHosts.find((h) => h.id === id)?.label || 'Local';
|
||||
}, [allHosts, defaultHostId]);
|
||||
return allHosts.find((h) => h.id === id)?.label || t('desktopHostSwitcher.instance.local');
|
||||
}, [allHosts, defaultHostId, t]);
|
||||
|
||||
const persist = React.useCallback(async (nextHosts: DesktopHost[], nextDefaultHostId: string | null) => {
|
||||
if (!isTauriShell()) return;
|
||||
@@ -309,11 +329,11 @@ export function DesktopHostSwitcherDialog({
|
||||
setConfigHosts(remote);
|
||||
setDefaultHostId(nextDefaultHostId);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to save');
|
||||
setError(err instanceof Error ? err.message : t('desktopHostSwitcher.error.failedToSave'));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const openRemoteInstancesSettings = React.useCallback(() => {
|
||||
setSettingsPage('remote-instances');
|
||||
@@ -340,7 +360,7 @@ export function DesktopHostSwitcherDialog({
|
||||
setSshHostIds(nextSshHostIds);
|
||||
setSshStatusesById(sshStatusMap);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load');
|
||||
setError(err instanceof Error ? err.message : t('desktopHostSwitcher.error.failedToLoad'));
|
||||
setConfigHosts([]);
|
||||
setDefaultHostId(null);
|
||||
setSshHostIds({});
|
||||
@@ -348,7 +368,7 @@ export function DesktopHostSwitcherDialog({
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const probeAll = React.useCallback(async (hosts: DesktopHost[]) => {
|
||||
if (!isTauriShell()) return;
|
||||
@@ -500,7 +520,7 @@ export function DesktopHostSwitcherDialog({
|
||||
...prev,
|
||||
error: message,
|
||||
}));
|
||||
toast.error(`SSH instance "${redactSensitiveUrl(host.label)}" failed to connect`, {
|
||||
toast.error(t('desktopHostSwitcher.toast.sshFailedToConnect', { host: redactSensitiveUrl(host.label) }), {
|
||||
description: message,
|
||||
});
|
||||
return;
|
||||
@@ -520,7 +540,7 @@ export function DesktopHostSwitcherDialog({
|
||||
}));
|
||||
|
||||
if (probe.status === 'unreachable' || probe.status === 'wrong-service') {
|
||||
toast.error(`Instance "${redactSensitiveUrl(host.label)}" is unreachable`);
|
||||
toast.error(t('desktopHostSwitcher.toast.instanceUnreachable', { host: redactSensitiveUrl(host.label) }));
|
||||
setSwitchingHostId(null);
|
||||
return;
|
||||
}
|
||||
@@ -534,7 +554,7 @@ export function DesktopHostSwitcherDialog({
|
||||
} catch {
|
||||
window.location.href = target;
|
||||
}
|
||||
}, [onHostSwitched, sshHostIds, sshStatusesById]);
|
||||
}, [onHostSwitched, sshHostIds, sshStatusesById, t]);
|
||||
|
||||
const beginEdit = React.useCallback((host: DesktopHost) => {
|
||||
setEditingId(host.id);
|
||||
@@ -562,7 +582,7 @@ export function DesktopHostSwitcherDialog({
|
||||
|
||||
const url = normalizeHostUrl(editUrl);
|
||||
if (!url) {
|
||||
setError('Invalid URL (must be http/https)');
|
||||
setError(t('desktopHostSwitcher.error.invalidUrl'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -570,12 +590,12 @@ export function DesktopHostSwitcherDialog({
|
||||
const nextHosts = configHosts.map((h) => (h.id === editingId ? { ...h, label, url } : h));
|
||||
await persist(nextHosts, defaultHostId);
|
||||
cancelEdit();
|
||||
}, [cancelEdit, configHosts, defaultHostId, editLabel, editUrl, editingId, persist]);
|
||||
}, [cancelEdit, configHosts, defaultHostId, editLabel, editUrl, editingId, persist, t]);
|
||||
|
||||
const addHost = React.useCallback(async () => {
|
||||
const url = normalizeHostUrl(newUrl);
|
||||
if (!url) {
|
||||
setError('Invalid URL (must be http/https)');
|
||||
setError(t('desktopHostSwitcher.error.invalidUrl'));
|
||||
return;
|
||||
}
|
||||
const label = (newLabel || redactSensitiveUrl(url)).trim();
|
||||
@@ -588,7 +608,7 @@ export function DesktopHostSwitcherDialog({
|
||||
if (embedded) {
|
||||
setIsAddFormOpen(false);
|
||||
}
|
||||
}, [configHosts, defaultHostId, embedded, newLabel, newUrl, persist]);
|
||||
}, [configHosts, defaultHostId, embedded, newLabel, newUrl, persist, t]);
|
||||
|
||||
const deleteHost = React.useCallback(async (id: string) => {
|
||||
if (id === LOCAL_HOST_ID) return;
|
||||
@@ -607,11 +627,11 @@ export function DesktopHostSwitcherDialog({
|
||||
if (!origin) return;
|
||||
const target = toNavigationUrl(origin);
|
||||
desktopOpenNewWindowAtUrl(target).catch((err: unknown) => {
|
||||
toast.error('Failed to open new window', {
|
||||
toast.error(t('desktopHostSwitcher.error.failedToOpenNewWindow'), {
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
const switchToLocal = React.useCallback(() => {
|
||||
sshSwitchTokenRef.current += 1;
|
||||
@@ -669,19 +689,19 @@ export function DesktopHostSwitcherDialog({
|
||||
}));
|
||||
});
|
||||
if (readyStatus.phase === 'ready') {
|
||||
toast.success(`SSH instance "${redactSensitiveUrl(host.label)}" connected`);
|
||||
toast.success(t('desktopHostSwitcher.toast.sshConnected', { host: redactSensitiveUrl(host.label) }));
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
if (message !== SSH_CONNECT_CANCELLED_ERROR) {
|
||||
toast.error(`SSH instance "${redactSensitiveUrl(host.label)}" failed to connect`, {
|
||||
toast.error(t('desktopHostSwitcher.toast.sshFailedToConnect', { host: redactSensitiveUrl(host.label) }), {
|
||||
description: message,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setSwitchingHostId(null);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
if (!isDesktopShell()) {
|
||||
return null;
|
||||
@@ -695,10 +715,10 @@ export function DesktopHostSwitcherDialog({
|
||||
<div className="flex-shrink-0 border-b border-[var(--interactive-border)] px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex items-baseline gap-1.5 typography-ui-label">
|
||||
<span className="font-medium text-foreground">Current</span>
|
||||
<span className="font-medium text-foreground">{t('desktopHostSwitcher.header.current')}</span>
|
||||
<span className="max-w-[9rem] truncate text-muted-foreground">{redactSensitiveUrl(current.label)}</span>
|
||||
<span className="text-muted-foreground/50">•</span>
|
||||
<span className="font-medium text-foreground">Default</span>
|
||||
<span className="font-medium text-foreground">{t('desktopHostSwitcher.header.default')}</span>
|
||||
<span className="max-w-[9rem] truncate text-muted-foreground">{redactSensitiveUrl(currentDefaultLabel)}</span>
|
||||
</div>
|
||||
<button
|
||||
@@ -710,7 +730,7 @@ export function DesktopHostSwitcherDialog({
|
||||
)}
|
||||
onClick={() => void probeAll(allHosts)}
|
||||
disabled={!tauriAvailable || isLoading || isProbing}
|
||||
aria-label="Refresh instances"
|
||||
aria-label={t('desktopHostSwitcher.actions.refreshInstancesAria')}
|
||||
>
|
||||
<RiRefreshLine className={cn('h-4 w-4', isProbing && 'animate-spin')} />
|
||||
</button>
|
||||
@@ -720,10 +740,10 @@ export function DesktopHostSwitcherDialog({
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<RiServerLine className="h-5 w-5" />
|
||||
Instance
|
||||
{t('desktopHostSwitcher.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Switch between Local and remote OpenChamber servers
|
||||
{t('desktopHostSwitcher.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
)}
|
||||
@@ -731,9 +751,9 @@ export function DesktopHostSwitcherDialog({
|
||||
{!embedded && (
|
||||
<div className="flex items-center justify-between gap-2 flex-shrink-0">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="typography-meta text-muted-foreground">Current:</span>
|
||||
<span className="typography-meta text-muted-foreground">{t('desktopHostSwitcher.header.currentColon')}</span>
|
||||
<span className="typography-ui-label text-foreground truncate">{redactSensitiveUrl(current.label)}</span>
|
||||
<span className="typography-meta text-muted-foreground">Current default:</span>
|
||||
<span className="typography-meta text-muted-foreground">{t('desktopHostSwitcher.header.currentDefaultColon')}</span>
|
||||
<span className="typography-ui-label text-foreground truncate">{redactSensitiveUrl(currentDefaultLabel)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -745,7 +765,7 @@ export function DesktopHostSwitcherDialog({
|
||||
disabled={!tauriAvailable || isLoading || isProbing}
|
||||
>
|
||||
<RiRefreshLine className={cn('h-4 w-4', isProbing && 'animate-spin')} />
|
||||
Refresh
|
||||
{t('desktopHostSwitcher.actions.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -753,10 +773,10 @@ export function DesktopHostSwitcherDialog({
|
||||
|
||||
{tauriAvailable && (
|
||||
<div className="flex-shrink-0 flex items-center justify-between gap-2 px-2.5 py-1.5">
|
||||
<span className="typography-micro text-muted-foreground">Need SSH instances?<br />Manage them in Settings.</span>
|
||||
<span className="typography-micro text-muted-foreground">{t('desktopHostSwitcher.ssh.needInstancesHint')}</span>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={openRemoteInstancesSettings}>
|
||||
<RiSettings3Line className="h-4 w-4" />
|
||||
Remote SSH
|
||||
{t('desktopHostSwitcher.actions.remoteSsh')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -764,7 +784,7 @@ export function DesktopHostSwitcherDialog({
|
||||
{!tauriAvailable && (
|
||||
<div className="flex-shrink-0 rounded-lg border border-border/50 bg-muted/20 p-3">
|
||||
<div className="typography-meta text-muted-foreground">
|
||||
Instance switcher is limited on this page. Use Local to recover.
|
||||
{t('desktopHostSwitcher.state.limitedOnPage')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -772,7 +792,7 @@ export function DesktopHostSwitcherDialog({
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
<div className="space-y-1">
|
||||
{isLoading ? (
|
||||
<div className="px-2 py-2 text-muted-foreground text-sm">Loading…</div>
|
||||
<div className="px-2 py-2 text-muted-foreground text-sm">{t('desktopHostSwitcher.state.loading')}</div>
|
||||
) : (
|
||||
allHosts.map((host) => {
|
||||
const isLocal = host.id === LOCAL_HOST_ID;
|
||||
@@ -784,7 +804,9 @@ export function DesktopHostSwitcherDialog({
|
||||
const statusKind = isSsh ? sshPhaseToHostStatus(sshStatus?.phase) : (status?.status ?? null);
|
||||
const isEditing = editingId === host.id;
|
||||
const effectiveUrl = isLocal ? getLocalOrigin() : (normalizeHostUrl(host.url) || host.url);
|
||||
const displayLabel = redactSensitiveUrl(host.label);
|
||||
const displayLabel = host.id === LOCAL_HOST_ID
|
||||
? t('desktopHostSwitcher.instance.local')
|
||||
: redactSensitiveUrl(host.label);
|
||||
const displayUrl = redactSensitiveUrl(effectiveUrl);
|
||||
|
||||
return (
|
||||
@@ -803,7 +825,7 @@ export function DesktopHostSwitcherDialog({
|
||||
)}
|
||||
onClick={() => void handleSwitch(host)}
|
||||
disabled={switchingHostId === host.id}
|
||||
aria-label={`Switch to ${displayLabel}`}
|
||||
aria-label={t('desktopHostSwitcher.actions.switchToAria', { instance: displayLabel })}
|
||||
>
|
||||
<span className={cn('h-2 w-2 rounded-full flex-shrink-0', statusDotClass(statusKind))} />
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -817,13 +839,15 @@ export function DesktopHostSwitcherDialog({
|
||||
</span>
|
||||
)}
|
||||
{isActive && (
|
||||
<span className="typography-micro text-muted-foreground">Current</span>
|
||||
<span className="typography-micro text-muted-foreground">{t('desktopHostSwitcher.header.current')}</span>
|
||||
)}
|
||||
<span className="inline-flex items-center gap-1 typography-micro text-muted-foreground">
|
||||
{statusIcon(statusKind)}
|
||||
<span>
|
||||
{isSsh ? sshPhaseLabel(sshStatus?.phase) : statusLabel(status?.status ?? null)}
|
||||
{!isSsh && status?.status === 'ok' && typeof status.latencyMs === 'number' ? ` · ${Math.max(0, Math.round(status.latencyMs))}ms ping` : ''}
|
||||
{isSsh ? t(sshPhaseLabelKey(sshStatus?.phase)) : t(statusLabelKey(status?.status ?? null))}
|
||||
{!isSsh && status?.status === 'ok' && typeof status.latencyMs === 'number'
|
||||
? t('desktopHostSwitcher.status.ping', { ms: Math.max(0, Math.round(status.latencyMs)) })
|
||||
: ''}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -840,7 +864,7 @@ export function DesktopHostSwitcherDialog({
|
||||
<button
|
||||
type="button"
|
||||
className="h-8 w-8 rounded-md inline-flex items-center justify-center text-muted-foreground hover:text-foreground hover:bg-interactive-hover transition-colors"
|
||||
aria-label="Instance actions"
|
||||
aria-label={t('desktopHostSwitcher.actions.instanceActionsAria')}
|
||||
disabled={isSaving}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@@ -856,7 +880,7 @@ export function DesktopHostSwitcherDialog({
|
||||
disabled={isSaving}
|
||||
>
|
||||
<RiPencilLine className="h-4 w-4 mr-1" />
|
||||
Edit
|
||||
{t('desktopHostSwitcher.actions.edit')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
@@ -867,7 +891,7 @@ export function DesktopHostSwitcherDialog({
|
||||
disabled={isSaving}
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4 mr-1" />
|
||||
Delete
|
||||
{t('desktopHostSwitcher.actions.delete')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -894,7 +918,7 @@ export function DesktopHostSwitcherDialog({
|
||||
}}
|
||||
>
|
||||
{switchingHostId === host.id ? <RiLoader4Line className="h-3.5 w-3.5 animate-spin" /> : <RiPlug2Line className="h-3.5 w-3.5" />}
|
||||
Connect
|
||||
{t('desktopHostSwitcher.actions.connect')}
|
||||
</Button>
|
||||
) : (
|
||||
<div
|
||||
@@ -915,14 +939,14 @@ export function DesktopHostSwitcherDialog({
|
||||
: 'text-muted-foreground/60 hover:text-primary/80',
|
||||
)}
|
||||
onClick={() => void setDefault(host.id)}
|
||||
aria-label={isDefault ? 'Default instance' : 'Set as default'}
|
||||
aria-label={isDefault ? t('desktopHostSwitcher.actions.defaultInstanceAria') : t('desktopHostSwitcher.actions.setAsDefaultAria')}
|
||||
disabled={isSaving || (!isDefault && (statusKind === 'unreachable' || statusKind === 'wrong-service'))}
|
||||
>
|
||||
{isDefault ? <RiStarFill className="h-4 w-4" /> : <RiStarLine className="h-4 w-4" />}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={6}>
|
||||
{isDefault ? 'Default' : 'Set as default'}
|
||||
{isDefault ? t('desktopHostSwitcher.header.default') : t('desktopHostSwitcher.actions.setAsDefault')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -941,13 +965,15 @@ export function DesktopHostSwitcherDialog({
|
||||
openInNewWindow(host);
|
||||
}}
|
||||
disabled={statusKind === 'unreachable' || statusKind === 'wrong-service'}
|
||||
aria-label="Open in new window"
|
||||
aria-label={t('desktopHostSwitcher.actions.openInNewWindowAria')}
|
||||
>
|
||||
<RiWindowLine className="h-4 w-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={6}>
|
||||
{(statusKind === 'unreachable' || statusKind === 'wrong-service') ? 'Instance unreachable' : 'Open in new window'}
|
||||
{(statusKind === 'unreachable' || statusKind === 'wrong-service')
|
||||
? t('desktopHostSwitcher.state.instanceUnreachable')
|
||||
: t('desktopHostSwitcher.actions.openInNewWindow')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -961,14 +987,14 @@ export function DesktopHostSwitcherDialog({
|
||||
{tauriAvailable && editingId && editingId !== LOCAL_HOST_ID && (
|
||||
<div className="flex-shrink-0 rounded-lg border border-border/50 bg-muted/20 p-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="typography-ui-label font-medium text-foreground">Edit instance</div>
|
||||
<div className="typography-ui-label font-medium text-foreground">{t('desktopHostSwitcher.edit.title')}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="button" variant="outline" size="sm" onClick={cancelEdit} disabled={isSaving}>
|
||||
Cancel
|
||||
{t('desktopHostSwitcher.actions.cancel')}
|
||||
</Button>
|
||||
<Button type="button" size="sm" onClick={() => void commitEdit()} disabled={isSaving}>
|
||||
{isSaving ? <RiLoader4Line className="h-4 w-4 animate-spin" /> : null}
|
||||
Save
|
||||
{t('desktopHostSwitcher.actions.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -977,14 +1003,14 @@ export function DesktopHostSwitcherDialog({
|
||||
value={editLabel}
|
||||
onChange={(e) => setEditLabel(e.target.value)}
|
||||
onKeyDown={stopDropdownTypeahead}
|
||||
placeholder="Label"
|
||||
placeholder={t('desktopHostSwitcher.field.labelPlaceholder')}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<Input
|
||||
value={editUrl}
|
||||
onChange={(e) => setEditUrl(e.target.value)}
|
||||
onKeyDown={stopDropdownTypeahead}
|
||||
placeholder="https://host:port"
|
||||
placeholder={t('desktopHostSwitcher.field.urlPlaceholder')}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
@@ -1000,7 +1026,7 @@ export function DesktopHostSwitcherDialog({
|
||||
disabled={!tauriAvailable || isSaving}
|
||||
>
|
||||
<RiAddLine className="h-4 w-4" />
|
||||
<span className="typography-ui-label">Add instance</span>
|
||||
<span className="typography-ui-label">{t('desktopHostSwitcher.actions.addInstance')}</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -1011,7 +1037,7 @@ export function DesktopHostSwitcherDialog({
|
||||
: 'rounded-md border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-2.5'
|
||||
)}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="typography-ui-label font-medium text-foreground">Add instance</div>
|
||||
<div className="typography-ui-label font-medium text-foreground">{t('desktopHostSwitcher.add.title')}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{embedded && (
|
||||
<Button
|
||||
@@ -1021,7 +1047,7 @@ export function DesktopHostSwitcherDialog({
|
||||
onClick={() => setIsAddFormOpen(false)}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Cancel
|
||||
{t('desktopHostSwitcher.actions.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
@@ -1031,7 +1057,7 @@ export function DesktopHostSwitcherDialog({
|
||||
disabled={!tauriAvailable || isSaving || !newUrl.trim()}
|
||||
>
|
||||
{isSaving ? <RiLoader4Line className="h-4 w-4 animate-spin" /> : null}
|
||||
Add
|
||||
{t('desktopHostSwitcher.actions.add')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1040,14 +1066,14 @@ export function DesktopHostSwitcherDialog({
|
||||
value={newLabel}
|
||||
onChange={(e) => setNewLabel(e.target.value)}
|
||||
onKeyDown={stopDropdownTypeahead}
|
||||
placeholder="Label (optional)"
|
||||
placeholder={t('desktopHostSwitcher.field.labelOptionalPlaceholder')}
|
||||
disabled={!tauriAvailable || isSaving}
|
||||
/>
|
||||
<Input
|
||||
value={newUrl}
|
||||
onChange={(e) => setNewUrl(e.target.value)}
|
||||
onKeyDown={stopDropdownTypeahead}
|
||||
placeholder="https://host:port"
|
||||
placeholder={t('desktopHostSwitcher.field.urlPlaceholder')}
|
||||
disabled={!tauriAvailable || isSaving}
|
||||
/>
|
||||
</div>
|
||||
@@ -1079,12 +1105,12 @@ export function DesktopHostSwitcherDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<RiLoader4Line className={cn('h-4 w-4', !sshSwitchModal.error && 'animate-spin')} />
|
||||
Connecting to {sshSwitchModal.hostLabel || 'SSH instance'}
|
||||
{t('desktopHostSwitcher.ssh.connectingTo', { host: sshSwitchModal.hostLabel || t('desktopHostSwitcher.ssh.instanceFallback') })}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{sshSwitchModal.error
|
||||
? sshSwitchModal.error
|
||||
: sshSwitchModal.detail || sshPhaseLabel(sshSwitchModal.phase)}
|
||||
: sshSwitchModal.detail || t(sshPhaseLabelKey(sshSwitchModal.phase))}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{sshSwitchModal.error ? (
|
||||
@@ -1095,7 +1121,7 @@ export function DesktopHostSwitcherDialog({
|
||||
variant="outline"
|
||||
onClick={switchToLocal}
|
||||
>
|
||||
Switch to Local
|
||||
{t('desktopHostSwitcher.actions.switchToLocal')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -1103,7 +1129,7 @@ export function DesktopHostSwitcherDialog({
|
||||
onClick={retrySshSwitch}
|
||||
disabled={!sshSwitchModal.hostId}
|
||||
>
|
||||
Retry
|
||||
{t('desktopHostSwitcher.actions.retry')}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -1139,6 +1165,7 @@ type DesktopHostSwitcherButtonProps = {
|
||||
};
|
||||
|
||||
export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHostSwitcherButtonProps) {
|
||||
const { t } = useI18n();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [label, setLabel] = React.useState('Local');
|
||||
const [status, setStatus] = React.useState<HostProbeResult['status'] | null>(null);
|
||||
@@ -1251,7 +1278,7 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost
|
||||
}
|
||||
|
||||
if (cancelled) return;
|
||||
setLabel(redactSensitiveUrl(current.label || 'Instance'));
|
||||
setLabel(redactSensitiveUrl(current.label || t('desktopHostSwitcher.instance.fallback')));
|
||||
const normalized = normalizeHostUrl(current.url);
|
||||
if (!normalized) {
|
||||
setStatus(null);
|
||||
@@ -1262,7 +1289,7 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost
|
||||
setStatus(res.status);
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setLabel('Instance');
|
||||
setLabel(t('desktopHostSwitcher.instance.fallback'));
|
||||
setStatus(null);
|
||||
}
|
||||
}
|
||||
@@ -1290,13 +1317,13 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost
|
||||
|
||||
const fallbackLabel = typeof window !== 'undefined' && window.location.hostname
|
||||
? window.location.hostname
|
||||
: 'Instance';
|
||||
: t('desktopHostSwitcher.instance.fallback');
|
||||
|
||||
const effectiveLabel = isCurrentlyLocal
|
||||
? 'Local'
|
||||
: label === 'Local'
|
||||
? fallbackLabel
|
||||
: label;
|
||||
? t('desktopHostSwitcher.instance.local')
|
||||
: label === 'Local'
|
||||
? fallbackLabel
|
||||
: label;
|
||||
const safeEffectiveLabel = redactSensitiveUrl(effectiveLabel);
|
||||
|
||||
return (
|
||||
@@ -1306,7 +1333,7 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(true)}
|
||||
aria-label="Switch instance"
|
||||
aria-label={t('desktopHostSwitcher.actions.switchInstanceAria')}
|
||||
data-oc-host-switcher
|
||||
className={cn(headerIconButtonClass, 'relative w-auto px-3')}
|
||||
>
|
||||
@@ -1319,12 +1346,12 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost
|
||||
'pointer-events-none absolute top-1.5 right-1.5 h-1.5 w-1.5 rounded-full',
|
||||
statusDotClass(status)
|
||||
)}
|
||||
aria-label="Instance status"
|
||||
aria-label={t('desktopHostSwitcher.statusAria')}
|
||||
/>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Instance</p>
|
||||
<p>{t('desktopHostSwitcher.title')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DesktopHostSwitcherDialog open={open} onOpenChange={setOpen} />
|
||||
@@ -1347,11 +1374,11 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost
|
||||
>
|
||||
<DialogContent className="w-[min(30rem,calc(100vw-2rem))] max-w-none">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Default SSH instance unavailable</DialogTitle>
|
||||
<DialogTitle>{t('desktopHostSwitcher.startup.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{startupSshModal.connecting
|
||||
? `Connecting to ${startupSshModal.hostLabel || 'SSH instance'}...`
|
||||
: startupSshModal.error || 'Failed to connect the default SSH instance.'}
|
||||
? t('desktopHostSwitcher.startup.connectingTo', { host: startupSshModal.hostLabel || t('desktopHostSwitcher.ssh.instanceFallback') })
|
||||
: startupSshModal.error || t('desktopHostSwitcher.startup.failed')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-end gap-2">
|
||||
@@ -1362,7 +1389,7 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost
|
||||
onClick={() => void switchStartupToLocal()}
|
||||
disabled={startupSshModal.connecting}
|
||||
>
|
||||
Switch to Local
|
||||
{t('desktopHostSwitcher.actions.switchToLocal')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -1371,7 +1398,7 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost
|
||||
disabled={startupSshModal.connecting || !startupSshModal.hostId}
|
||||
>
|
||||
{startupSshModal.connecting ? <RiLoader4Line className="h-4 w-4 animate-spin" /> : null}
|
||||
Retry
|
||||
{t('desktopHostSwitcher.actions.retry')}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
@@ -1382,6 +1409,7 @@ export function DesktopHostSwitcherButton({ headerIconButtonClass }: DesktopHost
|
||||
|
||||
export function DesktopHostSwitcherInline() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const { t } = useI18n();
|
||||
|
||||
if (!isDesktopShell()) {
|
||||
return null;
|
||||
@@ -1398,7 +1426,7 @@ export function DesktopHostSwitcherInline() {
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<RiServerLine className="h-4 w-4" />
|
||||
Switch instance
|
||||
{t('desktopHostSwitcher.actions.switchInstance')}
|
||||
</Button>
|
||||
<DesktopHostSwitcherDialog open={open} onOpenChange={setOpen} />
|
||||
</>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { isDesktopLocalOriginActive, isTauriShell, openDesktopPath, openDesktopP
|
||||
import { DEFAULT_OPEN_IN_APP_ID, OPEN_IN_APPS } from '@/lib/openInApps';
|
||||
import { useOpenInAppsStore, type OpenInAppOption } from '@/stores/useOpenInAppsStore';
|
||||
import { RiArrowDownSLine, RiCheckLine, RiFileCopyLine, RiRefreshLine } from '@remixicon/react';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
const FINDER_DEFAULT_ICON_DATA_URL = 'data:image/png;base64,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';
|
||||
const TERMINAL_DEFAULT_ICON_DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAeGVYSWZNTQAqAAAACAAEARoABQAAAAEAAAA+ARsABQAAAAEAAABGASgAAwAAAAEAAgAAh2kABAAAAAEAAABOAAAAAAAAAJAAAAABAAAAkAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAAB+C9pSAAAACXBIWXMAABYlAAAWJQFJUiTwAAABnWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyI+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4yNTY8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+MjU2PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cl6wHhsAAAQzSURBVFgJ7VZNbBNHFH67Xv9RxwnBDqlUoQglcZK6qSIEJIQWAYJQoVY9IE5RTzn20FMvqdpDesq9B24+NdwthAJCkZChJg1JSOXYQIwQKQIaBdtENbs73t2+N8miGWOcpFHUHniyd97OvJ9v3nv7ZgDe038cAeVd/jOZjC94sKdfU+Bj24G9igpexwYPyiu2bauKqqqirkOTqmrjnIOyFsoyUKDocSCj/7mU7ujoMER5l68JYOFZ4YSiwPjd9O0jjx7ch1KhAJZVAcdx0LxDv3XetYKjggr4I4bzHo8G4aYmONjZBYf6+2dUzfd9PNowJajUZmef/PX5zcWl0rmvvnbQHrra+f/M+S+dqYXs2t3Hz09Ve5UicCmZ3NPb1Zv66btv+65dSULA64WGxkbw+Xx8V9XK9d4pWowxeFUqgW6acHroC/j5l0sLD/PZY98MDf3t6mouQ+On3X1H7/2e7rtOztHpgbY2+CAUgperq+D3+7cNgtLSEA7D0+VluDF5FS7cSff2HT56DF1dd/3KhQTWJ/lclsc8jIrk9IfRURgZGQEvRqNSWa8D2t1W/liXXK8Ro0i0lF0ExaPEXec0SgAqhrm3VCzwdS9GQNd1GBsbg0AgAIlEAlpbW7EYLVF/U56AagieiGwbuhERlSQApmEE8c/XKXxU0fF4HNowFfPz81Aul7edBjLGbeHITANsZga4g42HVAM2Y74KM/kSIQ/izgcHB2FiYgJmZmZ4MZpYULRG5PF4+Bx/2cLDxuhhYUqFLwGoWCaQEBGhNjAa4+Pj/J3SQA6pHpqbm/kcNitIJpOgaZIZvlbrQbZNJvcjSZOZDKhwRKLic4l2Pjc3B8FgkE+trKxAVUN0RWuOZNtCHyJJACj/bgREIZcnA9PT029SQM63unuywSOwUWOuTQmAhfmnlluPxIjUk6u1RrbJh0jyV0Ap2OZnJhrbjOcRqEqBBMDCAtltAORDJAkAVj2mWS5CUXinPDUx+oxFkgBYjO0qANu2wKoqQgkAfgW7C4AiYMmfoQSgwpjj7GYRUh/Q66SAmdisNxql227FfP1bXrRlVExdtCNHwDRLdPkgwmi8OUREhe3y1NLJFpEfbWMNvBRtSI2o+KqYi+zbx4NQwptMCO8E1HjEHYjKm/HknG5FZIsCG4lEoLS2lhP1JAB3bt1KH//s+GJPd3dPJpvlN5kwXiYIhHukisr1eAItXsm6YzGItrTcn5+dvS3qSQBSqVQhFouNnj039CsaCC7mcqDjgbNT6op1AtrU8Wo3Ojk5KaVAOptdR8PDwxf3t7SMvXjxvJNOPP31a35Krt8CXKl3j2SUDip/IAjRaBRaP9z/cHW18GMikbhcrVUTAAm1t7d/NDAwcDIUCvVqmtqkyLe3ajtvvTtg4x3SLpbLa3+kUr9N5fP55beE3k/8HyLwDx2/HIx7q3WfAAAAAElFTkSuQmCC';
|
||||
@@ -73,6 +74,7 @@ type OpenInAppButtonProps = {
|
||||
};
|
||||
|
||||
export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps) => {
|
||||
const { t } = useI18n();
|
||||
const selectedAppId = useOpenInAppsStore((state) => state.selectedAppId);
|
||||
const availableApps = useOpenInAppsStore((state) => state.availableApps);
|
||||
const isCacheStale = useOpenInAppsStore((state) => state.isCacheStale);
|
||||
@@ -124,7 +126,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
toast.success('Path copied to clipboard');
|
||||
toast.success(t('openInApp.toast.pathCopied'));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -143,7 +145,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
|
||||
'inline-flex h-full items-center gap-2 px-3 typography-ui-label font-medium',
|
||||
'text-foreground hover:bg-interactive-hover transition-colors'
|
||||
)}
|
||||
aria-label={`Open in ${selectedApp.label}`}
|
||||
aria-label={t('openInApp.actions.openInAria', { app: selectedApp.label })}
|
||||
>
|
||||
<AppIcon
|
||||
label={selectedApp.label}
|
||||
@@ -151,7 +153,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
|
||||
fallbackIconDataUrl={selectedApp.fallbackIconDataUrl}
|
||||
/>
|
||||
<span className={cn('header-open-label', isScanning ? 'animate-pulse text-muted-foreground' : undefined)}>
|
||||
Open
|
||||
{t('openInApp.actions.open')}
|
||||
</span>
|
||||
</button>
|
||||
<DropdownMenu>
|
||||
@@ -163,7 +165,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
|
||||
'border-l border-[var(--interactive-border)] text-muted-foreground',
|
||||
'hover:bg-interactive-hover hover:text-foreground transition-colors'
|
||||
)}
|
||||
aria-label="Choose app to open"
|
||||
aria-label={t('openInApp.actions.chooseAppAria')}
|
||||
>
|
||||
<RiArrowDownSLine className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -175,7 +177,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
|
||||
>
|
||||
<DropdownMenuItem className="flex items-center gap-2" onClick={() => void handleCopyPath()}>
|
||||
<RiFileCopyLine className="h-4 w-4" />
|
||||
<span className="typography-ui-label text-foreground">Copy Path</span>
|
||||
<span className="typography-ui-label text-foreground">{t('openInApp.actions.copyPath')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{availableApps.map((app) => {
|
||||
@@ -204,7 +206,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
|
||||
onClick={() => void loadInstalledApps(true)}
|
||||
>
|
||||
<RiRefreshLine className="h-4 w-4" />
|
||||
<span className="typography-ui-label text-foreground">Refresh Apps</span>
|
||||
<span className="typography-ui-label text-foreground">{t('openInApp.actions.refreshApps')}</span>
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
</DropdownMenuContent>
|
||||
|
||||
Reference in New Issue
Block a user