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
@@ -9,6 +9,7 @@ import { restartDesktopApp } from '@/lib/desktop';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { RemoteConnectionForm } from './RemoteConnectionForm';
|
||||
import { desktopHostsGet, desktopHostsSet } from '@/lib/desktopHosts';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
const INSTALL_COMMAND = 'curl -fsSL https://opencode.ai/install | bash';
|
||||
const DOCS_URL = 'https://opencode.ai/docs';
|
||||
@@ -21,7 +22,7 @@ type ChooserScreenProps = {
|
||||
onCliAvailable?: () => void;
|
||||
};
|
||||
|
||||
function BashCommand({ onCopy }: { onCopy: () => void }) {
|
||||
function BashCommand({ onCopy, copyTitle }: { onCopy: () => void; copyTitle: string }) {
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<code>
|
||||
@@ -34,7 +35,7 @@ function BashCommand({ onCopy }: { onCopy: () => void }) {
|
||||
<button
|
||||
onClick={onCopy}
|
||||
className="inline-flex items-center text-muted-foreground hover:text-foreground transition-colors"
|
||||
title="Copy to clipboard"
|
||||
title={copyTitle}
|
||||
>
|
||||
<RiFileCopyLine className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -45,6 +46,7 @@ function BashCommand({ onCopy }: { onCopy: () => void }) {
|
||||
const HINT_DELAY_MS = 30000;
|
||||
|
||||
export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
|
||||
const { t } = useI18n();
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const [showHint, setShowHint] = React.useState(false);
|
||||
const [isDesktopApp, setIsDesktopApp] = React.useState(false);
|
||||
@@ -149,7 +151,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
|
||||
|
||||
try {
|
||||
const selected = await tauri.dialog.open({
|
||||
title: 'Select opencode binary',
|
||||
title: t('onboarding.localSetup.dialog.selectOpencodeBinary'),
|
||||
multiple: false,
|
||||
directory: false,
|
||||
});
|
||||
@@ -159,7 +161,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, [isDesktopApp]);
|
||||
}, [isDesktopApp, t]);
|
||||
|
||||
// Persist the user's first choice (local or remote)
|
||||
const persistFirstChoice = React.useCallback(async (choice: 'local' | 'remote') => {
|
||||
@@ -226,14 +228,14 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
|
||||
}
|
||||
onCliAvailable?.();
|
||||
} else {
|
||||
setCheckError('OpenCode CLI is not ready yet. Please confirm installation is complete and try again.');
|
||||
setCheckError(t('onboarding.localSetup.errors.cliNotReady'));
|
||||
}
|
||||
} catch (err) {
|
||||
setCheckError(err instanceof Error ? err.message : 'Detection failed');
|
||||
setCheckError(err instanceof Error ? err.message : t('onboarding.localSetup.errors.detectionFailed'));
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
}, [checkCliAvailability, onCliAvailable, persistFirstChoice]);
|
||||
}, [checkCliAvailability, onCliAvailable, persistFirstChoice, t]);
|
||||
|
||||
const docsUrl = platform === 'windows' ? WINDOWS_WSL_DOCS_URL : DOCS_URL;
|
||||
const binaryPlaceholder =
|
||||
@@ -251,10 +253,10 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
|
||||
<div className="w-full space-y-4 text-center">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-3xl font-semibold tracking-tight text-foreground">
|
||||
Welcome to OpenChamber
|
||||
{t('onboarding.chooser.title')}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Choose how you want to connect to get started.
|
||||
{t('onboarding.chooser.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -270,7 +272,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
|
||||
)}
|
||||
onClick={() => setActiveTab('local')}
|
||||
>
|
||||
Local Install
|
||||
{t('onboarding.chooser.tabs.localInstall')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -282,7 +284,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
|
||||
)}
|
||||
onClick={handleChooseRemote}
|
||||
>
|
||||
Connect Remote
|
||||
{t('onboarding.chooser.tabs.connectRemote')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -299,11 +301,11 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
|
||||
<>
|
||||
{platform === 'windows' && (
|
||||
<div className="mx-auto max-w-2xl rounded-lg border border-border bg-background/50 p-4 text-left">
|
||||
<div className="text-sm text-foreground">Windows setup (WSL recommended)</div>
|
||||
<div className="text-sm text-foreground">{t('onboarding.localSetup.windows.title')}</div>
|
||||
<ol className="mt-2 list-decimal space-y-1 pl-5 text-sm text-muted-foreground">
|
||||
<li>Install WSL (if needed) with <code className="text-foreground/80">wsl --install</code> in PowerShell.</li>
|
||||
<li>Run the install command below inside your WSL terminal.</li>
|
||||
<li>If OpenChamber does not detect OpenCode automatically, set the binary path below.</li>
|
||||
<li>{t('onboarding.localSetup.windows.stepInstallWsl')} <code className="text-foreground/80">wsl --install</code> {t('onboarding.localSetup.windows.stepInstallWslSuffix')}</li>
|
||||
<li>{t('onboarding.localSetup.windows.stepRunInstallInWsl')}</li>
|
||||
<li>{t('onboarding.localSetup.windows.stepSetBinaryPath')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
)}
|
||||
@@ -313,10 +315,10 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
|
||||
{copied ? (
|
||||
<div className="flex items-center justify-center gap-2" style={{ color: 'var(--status-success)' }}>
|
||||
<RiCheckLine className="h-4 w-4" />
|
||||
Copied to clipboard
|
||||
{t('onboarding.common.status.copiedToClipboard')}
|
||||
</div>
|
||||
) : (
|
||||
<BashCommand onCopy={handleCopy} />
|
||||
<BashCommand onCopy={handleCopy} copyTitle={t('onboarding.common.copyToClipboard')} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -327,7 +329,7 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors inline-flex items-center gap-1 justify-center"
|
||||
>
|
||||
{platform === 'windows' ? 'View Windows + WSL documentation' : 'View documentation'}
|
||||
{platform === 'windows' ? t('onboarding.localSetup.docs.windows') : t('onboarding.localSetup.docs.default')}
|
||||
<RiExternalLinkLine className="h-3 w-3" />
|
||||
</a>
|
||||
|
||||
@@ -345,17 +347,17 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
|
||||
className="w-full max-w-xs"
|
||||
size="lg"
|
||||
>
|
||||
{isChecking ? 'Checking...' : "I've completed installation, check and continue"}
|
||||
{isChecking ? t('onboarding.localSetup.actions.checking') : t('onboarding.localSetup.actions.checkAndContinue')}
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Click to check if OpenCode CLI is available. If successful, you'll automatically enter the main screen.
|
||||
{t('onboarding.localSetup.helper.checkAndContinue')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto w-full max-w-xl pt-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-muted-foreground">Already installed? Set the OpenCode CLI path:</div>
|
||||
<div className="text-sm text-muted-foreground">{t('onboarding.localSetup.field.alreadyInstalled')}</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={opencodeBinary}
|
||||
@@ -370,17 +372,17 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
|
||||
onClick={handleBrowse}
|
||||
disabled={isRetrying || !isDesktopApp || !isTauriShell()}
|
||||
>
|
||||
Browse
|
||||
{t('onboarding.localSetup.actions.browse')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleApplyPath}
|
||||
disabled={isRetrying}
|
||||
>
|
||||
Apply
|
||||
{t('onboarding.localSetup.actions.apply')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground/70">Saves to OpenChamber settings and reloads OpenCode configuration.</div>
|
||||
<div className="text-xs text-muted-foreground/70">{t('onboarding.localSetup.helper.saveAndReload')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -394,22 +396,22 @@ export function ChooserScreen({ onCliAvailable }: ChooserScreenProps) {
|
||||
{platform === 'windows' ? (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground/70">
|
||||
On Windows, install and run OpenCode in WSL for best compatibility.
|
||||
{t('onboarding.localSetup.windows.hintInstallInWsl')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground/70">
|
||||
If detection fails, set a native path (<code className="text-foreground/70">opencode.cmd</code>/<code className="text-foreground/70">opencode.exe</code>), <code className="text-foreground/70">wsl.exe</code>, or <code className="text-foreground/70">wsl:/usr/local/bin/opencode</code>.
|
||||
{t('onboarding.localSetup.windows.hintDetectionFailed')}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground/70">
|
||||
Already installed? Make sure <code className="text-foreground/70">opencode</code> is in your PATH
|
||||
{t('onboarding.localSetup.hint.ensurePath')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground/70">
|
||||
or set <code className="text-foreground/70">OPENCODE_BINARY</code> environment variable.
|
||||
{t('onboarding.localSetup.hint.setEnv')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground/70">
|
||||
If you see <code className="text-foreground/70">env: node: No such file or directory</code> or <code className="text-foreground/70">env: bun: No such file or directory</code>, install that runtime or ensure it is on PATH.
|
||||
{t('onboarding.localSetup.hint.missingRuntime')}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { RiRefreshLine, RiServerLine, RiMacbookLine } from '@remixicon/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { redactSensitiveUrl } from '@/lib/desktopHosts';
|
||||
import {
|
||||
@@ -39,7 +40,21 @@ export function DesktopConnectionRecovery({
|
||||
onUseRemote,
|
||||
isRetrying = false,
|
||||
}: DesktopConnectionRecoveryProps) {
|
||||
const { t } = useI18n();
|
||||
const config = getDesktopRecoveryConfig(variant, hostLabel, hostUrl);
|
||||
const retryLabelKey = (config.retryLabelKey ?? 'onboarding.desktopRecovery.actions.retryConnection') as Parameters<typeof t>[0];
|
||||
const descriptionParams = React.useMemo(() => {
|
||||
if (config.descriptionParams?.host) {
|
||||
return config.descriptionParams;
|
||||
}
|
||||
if (variant === 'remote-unreachable') {
|
||||
return { host: t('onboarding.desktopRecovery.placeholders.remoteServer') };
|
||||
}
|
||||
if (variant === 'remote-wrong-service') {
|
||||
return { host: t('onboarding.desktopRecovery.placeholders.unknownServer') };
|
||||
}
|
||||
return undefined;
|
||||
}, [config.descriptionParams, t, variant]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full p-8">
|
||||
@@ -58,17 +73,20 @@ export function DesktopConnectionRecovery({
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="typography-ui-header text-xl font-semibold text-foreground">
|
||||
{config.title}
|
||||
{t(config.titleKey as Parameters<typeof t>[0])}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm max-w-sm">
|
||||
{config.description}
|
||||
{t(
|
||||
config.descriptionKey as Parameters<typeof t>[0],
|
||||
descriptionParams
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Host info if available */}
|
||||
{hostUrl && (variant === 'remote-unreachable' || variant === 'remote-wrong-service') && (
|
||||
<div className="rounded-lg border border-border bg-background/50 p-3">
|
||||
<div className="text-xs text-muted-foreground mb-1">Server Address</div>
|
||||
<div className="text-xs text-muted-foreground mb-1">{t('onboarding.remoteConnection.field.serverAddress')}</div>
|
||||
<div className="font-mono text-sm text-foreground truncate">{redactSensitiveUrl(hostUrl)}</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -82,7 +100,9 @@ export function DesktopConnectionRecovery({
|
||||
className="w-full"
|
||||
>
|
||||
<RiRefreshLine className={cn('h-4 w-4', isRetrying && 'animate-spin')} />
|
||||
{isRetrying ? 'Retrying…' : (config.retryLabel ?? 'Retry Connection')}
|
||||
{isRetrying
|
||||
? t('onboarding.desktopRecovery.actions.retrying')
|
||||
: t(retryLabelKey)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -95,7 +115,7 @@ export function DesktopConnectionRecovery({
|
||||
className="flex-1"
|
||||
>
|
||||
<RiMacbookLine className="h-4 w-4" />
|
||||
{config.useLocalLabel}
|
||||
{t(config.useLocalLabelKey as Parameters<typeof t>[0])}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -107,7 +127,7 @@ export function DesktopConnectionRecovery({
|
||||
className="flex-1"
|
||||
>
|
||||
<RiServerLine className="h-4 w-4" />
|
||||
{config.useRemoteLabel}
|
||||
{t(config.useRemoteLabelKey as Parameters<typeof t>[0])}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import { restartDesktopApp } from '@/lib/desktop';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
const INSTALL_COMMAND = 'curl -fsSL https://opencode.ai/install | bash';
|
||||
const DOCS_URL = 'https://opencode.ai/docs';
|
||||
@@ -24,7 +25,7 @@ type LocalSetupScreenProps = {
|
||||
onSwitchToRemote?: () => void;
|
||||
};
|
||||
|
||||
function BashCommand({ onCopy }: { onCopy: () => void }) {
|
||||
function BashCommand({ onCopy, copyTitle }: { onCopy: () => void; copyTitle: string }) {
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<code>
|
||||
@@ -37,7 +38,7 @@ function BashCommand({ onCopy }: { onCopy: () => void }) {
|
||||
<button
|
||||
onClick={onCopy}
|
||||
className="inline-flex items-center text-muted-foreground hover:text-foreground transition-colors"
|
||||
title="Copy to clipboard"
|
||||
title={copyTitle}
|
||||
>
|
||||
<RiFileCopyLine className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -53,6 +54,7 @@ export function LocalSetupScreen({
|
||||
isFromRecovery = false,
|
||||
onSwitchToRemote,
|
||||
}: LocalSetupScreenProps) {
|
||||
const { t } = useI18n();
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const [showHint, setShowHint] = React.useState(false);
|
||||
const [isDesktopApp, setIsDesktopApp] = React.useState(false);
|
||||
@@ -156,7 +158,7 @@ export function LocalSetupScreen({
|
||||
|
||||
try {
|
||||
const selected = await tauri.dialog.open({
|
||||
title: 'Select opencode binary',
|
||||
title: t('onboarding.localSetup.dialog.selectOpencodeBinary'),
|
||||
multiple: false,
|
||||
directory: false,
|
||||
});
|
||||
@@ -166,7 +168,7 @@ export function LocalSetupScreen({
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, [isDesktopApp]);
|
||||
}, [isDesktopApp, t]);
|
||||
|
||||
const handleApplyPath = React.useCallback(async () => {
|
||||
setIsRetrying(true);
|
||||
@@ -205,14 +207,14 @@ export function LocalSetupScreen({
|
||||
// CLI is available, proceed to main screen
|
||||
onCliAvailable?.();
|
||||
} else {
|
||||
setCheckError('OpenCode CLI is not ready yet. Please confirm installation is complete and try again.');
|
||||
setCheckError(t('onboarding.localSetup.errors.cliNotReady'));
|
||||
}
|
||||
} catch (err) {
|
||||
setCheckError(err instanceof Error ? err.message : 'Detection failed');
|
||||
setCheckError(err instanceof Error ? err.message : t('onboarding.localSetup.errors.detectionFailed'));
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
}, [checkCliAvailability, onCliAvailable]);
|
||||
}, [checkCliAvailability, onCliAvailable, t]);
|
||||
|
||||
const docsUrl = platform === 'windows' ? WINDOWS_WSL_DOCS_URL : DOCS_URL;
|
||||
const binaryPlaceholder =
|
||||
@@ -234,26 +236,26 @@ export function LocalSetupScreen({
|
||||
onClick={onBack}
|
||||
className="p-0 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
← Back
|
||||
{t('onboarding.common.actions.back')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-3xl font-semibold tracking-tight text-foreground">
|
||||
Setting Up OpenCode
|
||||
{t('onboarding.localSetup.title')}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Install OpenCode CLI to continue.
|
||||
{t('onboarding.localSetup.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{platform === 'windows' && (
|
||||
<div className="mx-auto max-w-2xl rounded-lg border border-border bg-background/50 p-4 text-left">
|
||||
<div className="text-sm text-foreground">Windows setup (WSL recommended)</div>
|
||||
<div className="text-sm text-foreground">{t('onboarding.localSetup.windows.title')}</div>
|
||||
<ol className="mt-2 list-decimal space-y-1 pl-5 text-sm text-muted-foreground">
|
||||
<li>Install WSL (if needed) with <code className="text-foreground/80">wsl --install</code> in PowerShell.</li>
|
||||
<li>Run the install command below inside your WSL terminal.</li>
|
||||
<li>If OpenChamber does not detect OpenCode automatically, set the binary path below.</li>
|
||||
<li>{t('onboarding.localSetup.windows.stepInstallWsl')} <code className="text-foreground/80">wsl --install</code> {t('onboarding.localSetup.windows.stepInstallWslSuffix')}</li>
|
||||
<li>{t('onboarding.localSetup.windows.stepRunInstallInWsl')}</li>
|
||||
<li>{t('onboarding.localSetup.windows.stepSetBinaryPath')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
)}
|
||||
@@ -263,10 +265,10 @@ export function LocalSetupScreen({
|
||||
{copied ? (
|
||||
<div className="flex items-center justify-center gap-2" style={{ color: 'var(--status-success)' }}>
|
||||
<RiCheckLine className="h-4 w-4" />
|
||||
Copied to clipboard
|
||||
{t('onboarding.common.status.copiedToClipboard')}
|
||||
</div>
|
||||
) : (
|
||||
<BashCommand onCopy={handleCopy} />
|
||||
<BashCommand onCopy={handleCopy} copyTitle={t('onboarding.common.copyToClipboard')} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -277,7 +279,7 @@ export function LocalSetupScreen({
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors inline-flex items-center gap-1 justify-center"
|
||||
>
|
||||
{platform === 'windows' ? 'View Windows + WSL documentation' : 'View documentation'}
|
||||
{platform === 'windows' ? t('onboarding.localSetup.docs.windows') : t('onboarding.localSetup.docs.default')}
|
||||
<RiExternalLinkLine className="h-3 w-3" />
|
||||
</a>
|
||||
|
||||
@@ -295,17 +297,17 @@ export function LocalSetupScreen({
|
||||
className="w-full max-w-xs"
|
||||
size="lg"
|
||||
>
|
||||
{isChecking ? 'Checking...' : "I've completed installation, check and continue"}
|
||||
{isChecking ? t('onboarding.localSetup.actions.checking') : t('onboarding.localSetup.actions.checkAndContinue')}
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Click to check if OpenCode CLI is available. If successful, you'll automatically enter the main screen.
|
||||
{t('onboarding.localSetup.helper.checkAndContinue')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto w-full max-w-xl pt-4">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-muted-foreground">Already installed? Set the OpenCode CLI path:</div>
|
||||
<div className="text-sm text-muted-foreground">{t('onboarding.localSetup.field.alreadyInstalled')}</div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={opencodeBinary}
|
||||
@@ -320,30 +322,30 @@ export function LocalSetupScreen({
|
||||
onClick={handleBrowse}
|
||||
disabled={isRetrying || !isDesktopApp || !isTauriShell()}
|
||||
>
|
||||
Browse
|
||||
{t('onboarding.localSetup.actions.browse')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleApplyPath}
|
||||
disabled={isRetrying}
|
||||
>
|
||||
Apply
|
||||
{t('onboarding.localSetup.actions.apply')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground/70">Saves to OpenChamber settings and reloads OpenCode configuration.</div>
|
||||
<div className="text-xs text-muted-foreground/70">{t('onboarding.localSetup.helper.saveAndReload')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isFromRecovery && onSwitchToRemote && (
|
||||
<div className="text-center pt-4">
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
Prefer to use a remote server?
|
||||
{t('onboarding.localSetup.remotePreference')}
|
||||
</p>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={onSwitchToRemote}
|
||||
>
|
||||
Connect to Remote Server →
|
||||
{t('onboarding.localSetup.actions.connectRemoteServer')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -354,22 +356,22 @@ export function LocalSetupScreen({
|
||||
{platform === 'windows' ? (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground/70">
|
||||
On Windows, install and run OpenCode in WSL for best compatibility.
|
||||
{t('onboarding.localSetup.windows.hintInstallInWsl')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground/70">
|
||||
If detection fails, set a native path (<code className="text-foreground/70">opencode.cmd</code>/<code className="text-foreground/70">opencode.exe</code>), <code className="text-foreground/70">wsl.exe</code>, or <code className="text-foreground/70">wsl:/usr/local/bin/opencode</code>.
|
||||
{t('onboarding.localSetup.windows.hintDetectionFailed')}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground/70">
|
||||
Already installed? Make sure <code className="text-foreground/70">opencode</code> is in your PATH
|
||||
{t('onboarding.localSetup.hint.ensurePath')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground/70">
|
||||
or set <code className="text-foreground/70">OPENCODE_BINARY</code> environment variable.
|
||||
{t('onboarding.localSetup.hint.setEnv')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground/70">
|
||||
If you see <code className="text-foreground/70">env: node: No such file or directory</code> or <code className="text-foreground/70">env: bun: No such file or directory</code>, install that runtime or ensure it is on PATH.
|
||||
{t('onboarding.localSetup.hint.missingRuntime')}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { isTauriShell } from '@/lib/desktop';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
type ConnectionState = 'idle' | 'testing' | 'success' | 'error';
|
||||
|
||||
@@ -30,16 +31,16 @@ export interface RemoteConnectionFormProps {
|
||||
|
||||
type ProbeStatus = HostProbeResult['status'] | null;
|
||||
|
||||
function getProbeStatusMessage(status: ProbeStatus): string | null {
|
||||
function getProbeStatusMessageKey(status: ProbeStatus): string | null {
|
||||
switch (status) {
|
||||
case 'ok':
|
||||
return null; // Success is shown separately
|
||||
case 'auth':
|
||||
return 'Server requires authentication. You can still connect, but may need to provide credentials.';
|
||||
return 'onboarding.remoteConnection.probe.authMessage';
|
||||
case 'wrong-service':
|
||||
return 'Server responded but is not running OpenChamber. Verify the address points to an OpenChamber server.';
|
||||
return 'onboarding.remoteConnection.probe.wrongServiceMessage';
|
||||
case 'unreachable':
|
||||
return 'Server is unreachable. Check your network connection and verify the server address.';
|
||||
return 'onboarding.remoteConnection.probe.unreachableMessage';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -58,6 +59,7 @@ export function RemoteConnectionForm({
|
||||
onConnect,
|
||||
onSwitchToLocal,
|
||||
}: RemoteConnectionFormProps) {
|
||||
const { t } = useI18n();
|
||||
const [url, setUrl] = useState(initialUrl);
|
||||
const [label, setLabel] = useState(initialLabel);
|
||||
const [state, setState] = useState<ConnectionState>('idle');
|
||||
@@ -89,10 +91,10 @@ export function RemoteConnectionForm({
|
||||
setProbeResult(result);
|
||||
setState(result.status === 'ok' ? 'success' : 'error');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Connection test failed');
|
||||
setError(err instanceof Error ? err.message : t('onboarding.remoteConnection.errors.connectionTestFailed'));
|
||||
setState('error');
|
||||
}
|
||||
}, [normalizedUrl]);
|
||||
}, [normalizedUrl, t]);
|
||||
|
||||
const handleConnect = useCallback(async () => {
|
||||
if (!normalizedUrl) return;
|
||||
@@ -144,16 +146,16 @@ export function RemoteConnectionForm({
|
||||
await tauri?.core?.invoke?.('desktop_restart');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to save connection');
|
||||
setError(err instanceof Error ? err.message : t('onboarding.remoteConnection.errors.failedToSaveConnection'));
|
||||
setState('error');
|
||||
}
|
||||
}, [normalizedUrl, label, onConnect]);
|
||||
}, [normalizedUrl, label, onConnect, t]);
|
||||
|
||||
const isTesting = state === 'testing';
|
||||
const canTest = normalizedUrl !== null && !isTesting;
|
||||
const canConnect = normalizedUrl !== null && !isTesting && !isBlockingStatus(probeResult?.status ?? null);
|
||||
|
||||
const probeMessage = getProbeStatusMessage(probeResult?.status ?? null);
|
||||
const probeMessageKey = getProbeStatusMessageKey(probeResult?.status ?? null);
|
||||
const isSuccess = probeResult?.status === 'ok';
|
||||
const isAuth = probeResult?.status === 'auth';
|
||||
const isBlocking = isBlockingStatus(probeResult?.status ?? null);
|
||||
@@ -164,47 +166,47 @@ export function RemoteConnectionForm({
|
||||
{showBackButton && (
|
||||
<div className="flex items-center">
|
||||
<Button variant="ghost" onClick={onBack} className="p-0 text-muted-foreground hover:text-foreground">
|
||||
← Back
|
||||
{t('onboarding.common.actions.back')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="typography-ui-header text-xl font-semibold text-foreground">
|
||||
{isRecoveryMode ? 'Connect to a Different Server' : 'Connect to Remote Server'}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<h1 className="typography-ui-header text-xl font-semibold text-foreground">
|
||||
{isRecoveryMode
|
||||
? 'Enter the address of an OpenChamber server to connect to.'
|
||||
: 'Enter the address of an OpenChamber server to connect to.'}
|
||||
</p>
|
||||
</div>
|
||||
? t('onboarding.remoteConnection.titleRecovery')
|
||||
: t('onboarding.remoteConnection.title')}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t('onboarding.remoteConnection.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="remote-url" className="text-sm text-foreground">
|
||||
Server Address
|
||||
{t('onboarding.remoteConnection.field.serverAddress')}
|
||||
</label>
|
||||
<Input
|
||||
id="remote-url"
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={handleUrlChange}
|
||||
placeholder="https://your-server.example.com:4096"
|
||||
placeholder={t('onboarding.remoteConnection.field.serverAddressPlaceholder')}
|
||||
disabled={isTesting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="remote-label" className="text-sm text-foreground">
|
||||
Name (optional)
|
||||
{t('onboarding.remoteConnection.field.nameOptional')}
|
||||
</label>
|
||||
<Input
|
||||
id="remote-label"
|
||||
type="text"
|
||||
value={label}
|
||||
onChange={handleLabelChange}
|
||||
placeholder="My Remote Server"
|
||||
placeholder={t('onboarding.remoteConnection.field.namePlaceholder')}
|
||||
disabled={isTesting}
|
||||
/>
|
||||
</div>
|
||||
@@ -219,7 +221,7 @@ export function RemoteConnectionForm({
|
||||
color: 'var(--status-success)',
|
||||
}}
|
||||
>
|
||||
Connected successfully ({probeResult.latencyMs}ms)
|
||||
{t('onboarding.remoteConnection.status.connectedSuccessfully', { latencyMs: probeResult.latencyMs })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -232,7 +234,7 @@ export function RemoteConnectionForm({
|
||||
color: 'var(--status-warning)',
|
||||
}}
|
||||
>
|
||||
Server requires authentication. You can still connect.
|
||||
{t('onboarding.remoteConnection.status.authWarning')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -246,13 +248,13 @@ export function RemoteConnectionForm({
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div className="font-semibold mb-1">Connection Failed</div>
|
||||
<div className="opacity-90">{probeMessage}</div>
|
||||
<div className="font-semibold mb-1">{t('onboarding.remoteConnection.status.connectionFailed')}</div>
|
||||
<div className="opacity-90">{probeMessageKey ? t(probeMessageKey as Parameters<typeof t>[0]) : null}</div>
|
||||
</div>
|
||||
<div className="text-xs opacity-80">
|
||||
{probeResult.status === 'unreachable'
|
||||
? 'Suggestions: Check the server address, verify the server is running, or check your network connection.'
|
||||
: 'Suggestions: Verify the URL points to an OpenChamber server, or contact the server administrator.'}
|
||||
? t('onboarding.remoteConnection.status.suggestionsUnreachable')
|
||||
: t('onboarding.remoteConnection.status.suggestionsWrongService')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -276,20 +278,20 @@ export function RemoteConnectionForm({
|
||||
onClick={handleTest}
|
||||
disabled={!canTest}
|
||||
>
|
||||
{isTesting ? 'Testing\u2026' : 'Test Connection'}
|
||||
{isTesting ? t('onboarding.remoteConnection.actions.testing') : t('onboarding.remoteConnection.actions.testConnection')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConnect}
|
||||
disabled={!canConnect}
|
||||
>
|
||||
Connect & Restart
|
||||
{t('onboarding.remoteConnection.actions.connectAndRestart')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Suggested actions when connection is blocked */}
|
||||
{isBlocking && (
|
||||
<div className="flex flex-col gap-2 pt-2 border-t border-border">
|
||||
<div className="text-xs text-muted-foreground text-center">What would you like to do?</div>
|
||||
<div className="text-xs text-muted-foreground text-center">{t('onboarding.remoteConnection.actions.whatToDo')}</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -297,7 +299,7 @@ export function RemoteConnectionForm({
|
||||
onClick={onBack}
|
||||
className="flex-1"
|
||||
>
|
||||
Choose Different Server
|
||||
{t('onboarding.remoteConnection.actions.chooseDifferentServer')}
|
||||
</Button>
|
||||
{!isRecoveryMode && onSwitchToLocal && (
|
||||
<Button
|
||||
@@ -306,7 +308,7 @@ export function RemoteConnectionForm({
|
||||
onClick={onSwitchToLocal}
|
||||
className="flex-1"
|
||||
>
|
||||
Use Local Instead
|
||||
{t('onboarding.remoteConnection.actions.useLocalInstead')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -10,15 +10,21 @@ export type RecoveryVariant =
|
||||
export type DesktopRecoveryConfig = {
|
||||
title: string;
|
||||
description: string;
|
||||
titleKey: string;
|
||||
descriptionKey: string;
|
||||
descriptionParams?: Record<string, string>;
|
||||
iconKey: 'local' | 'remote';
|
||||
showRetry: boolean;
|
||||
retryLabel?: string;
|
||||
retryLabelKey?: string;
|
||||
showUseLocal: boolean;
|
||||
showUseRemote: boolean;
|
||||
/** Label for the "use local" primary action button */
|
||||
useLocalLabel: string;
|
||||
useLocalLabelKey: string;
|
||||
/** Label for the "use remote" primary action button */
|
||||
useRemoteLabel: string;
|
||||
useRemoteLabelKey: string;
|
||||
};
|
||||
|
||||
function formatHostDisplay(hostLabel?: string, hostUrl?: string): string | undefined {
|
||||
@@ -36,27 +42,35 @@ export function getDesktopRecoveryConfig(
|
||||
case 'local-unavailable':
|
||||
return {
|
||||
title: 'Local OpenCode Unavailable',
|
||||
description:
|
||||
'OpenCode CLI could not be started or is not installed. Install OpenCode or connect to a remote server instead.',
|
||||
description: 'OpenCode CLI could not be started or is not installed. Install OpenCode or connect to a remote server instead.',
|
||||
titleKey: 'onboarding.desktopRecovery.localUnavailable.title',
|
||||
descriptionKey: 'onboarding.desktopRecovery.localUnavailable.description',
|
||||
iconKey: 'local',
|
||||
showRetry: true,
|
||||
retryLabel: 'Retry Local',
|
||||
retryLabelKey: 'onboarding.desktopRecovery.localUnavailable.retry',
|
||||
showUseLocal: true,
|
||||
showUseRemote: true,
|
||||
useLocalLabel: 'Set Up Local',
|
||||
useLocalLabelKey: 'onboarding.desktopRecovery.localUnavailable.useLocal',
|
||||
useRemoteLabel: 'Use Remote',
|
||||
useRemoteLabelKey: 'onboarding.desktopRecovery.common.useRemote',
|
||||
};
|
||||
|
||||
case 'remote-missing':
|
||||
return {
|
||||
title: 'No Default Connection',
|
||||
description: 'Your saved default connection could not be found. Choose how you want to connect.',
|
||||
titleKey: 'onboarding.desktopRecovery.noDefaultConnection.title',
|
||||
descriptionKey: 'onboarding.desktopRecovery.noDefaultConnection.description',
|
||||
iconKey: 'local',
|
||||
showRetry: false,
|
||||
showUseLocal: true,
|
||||
showUseRemote: true,
|
||||
useLocalLabel: 'Use Local',
|
||||
useLocalLabelKey: 'onboarding.desktopRecovery.common.useLocal',
|
||||
useRemoteLabel: 'Use Remote',
|
||||
useRemoteLabelKey: 'onboarding.desktopRecovery.common.useRemote',
|
||||
};
|
||||
|
||||
case 'remote-unreachable': {
|
||||
@@ -64,13 +78,19 @@ export function getDesktopRecoveryConfig(
|
||||
return {
|
||||
title: 'Remote Server Unreachable',
|
||||
description: `Could not connect to "${host || 'the remote server'}". Check your network connection and verify the server address.`,
|
||||
titleKey: 'onboarding.desktopRecovery.remoteUnreachable.title',
|
||||
descriptionKey: 'onboarding.desktopRecovery.remoteUnreachable.description',
|
||||
descriptionParams: host ? { host } : undefined,
|
||||
iconKey: 'remote',
|
||||
showRetry: true,
|
||||
retryLabel: 'Retry Connection',
|
||||
retryLabelKey: 'onboarding.desktopRecovery.remoteUnreachable.retry',
|
||||
showUseLocal: true,
|
||||
showUseRemote: true,
|
||||
useLocalLabel: 'Use Local',
|
||||
useLocalLabelKey: 'onboarding.desktopRecovery.common.useLocal',
|
||||
useRemoteLabel: 'Use Remote',
|
||||
useRemoteLabelKey: 'onboarding.desktopRecovery.common.useRemote',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,12 +99,17 @@ export function getDesktopRecoveryConfig(
|
||||
return {
|
||||
title: 'Incompatible Server',
|
||||
description: `The server at "${host || 'unknown'}" is not running OpenChamber. Verify the address points to an OpenChamber server.`,
|
||||
titleKey: 'onboarding.desktopRecovery.incompatibleServer.title',
|
||||
descriptionKey: 'onboarding.desktopRecovery.incompatibleServer.description',
|
||||
descriptionParams: host ? { host } : undefined,
|
||||
iconKey: 'remote',
|
||||
showRetry: false,
|
||||
showUseLocal: true,
|
||||
showUseRemote: true,
|
||||
useLocalLabel: 'Use Local',
|
||||
useLocalLabelKey: 'onboarding.desktopRecovery.common.useLocal',
|
||||
useRemoteLabel: 'Use Remote',
|
||||
useRemoteLabelKey: 'onboarding.desktopRecovery.common.useRemote',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,12 +117,16 @@ export function getDesktopRecoveryConfig(
|
||||
return {
|
||||
title: 'No Default Connection',
|
||||
description: 'Your saved default connection could not be found. Choose how you want to connect.',
|
||||
titleKey: 'onboarding.desktopRecovery.noDefaultConnection.title',
|
||||
descriptionKey: 'onboarding.desktopRecovery.noDefaultConnection.description',
|
||||
iconKey: 'local',
|
||||
showRetry: false,
|
||||
showUseLocal: true,
|
||||
showUseRemote: true,
|
||||
useLocalLabel: 'Use Local',
|
||||
useLocalLabelKey: 'onboarding.desktopRecovery.common.useLocal',
|
||||
useRemoteLabel: 'Use Remote',
|
||||
useRemoteLabelKey: 'onboarding.desktopRecovery.common.useRemote',
|
||||
};
|
||||
|
||||
default: {
|
||||
|
||||
Reference in New Issue
Block a user