diff --git a/apps/app/src/app/components/create-workspace-modal.tsx b/apps/app/src/app/components/create-workspace-modal.tsx index 081ba1810..2419ba7fc 100644 --- a/apps/app/src/app/components/create-workspace-modal.tsx +++ b/apps/app/src/app/components/create-workspace-modal.tsx @@ -110,7 +110,7 @@ export default function CreateWorkspaceModal(props: { onClick={props.onClose} disabled={submitting()} class={`flex h-8 w-8 items-center justify-center rounded-full text-dls-secondary transition-colors hover:bg-dls-hover hover:text-dls-text ${submitting() ? "cursor-not-allowed opacity-50" : ""}`.trim()} - aria-label="Close create workspace modal" + aria-label={translate("dashboard.create_workspace_close")} > @@ -120,7 +120,7 @@ export default function CreateWorkspaceModal(props: {
-
Workspace folder
+
{translate("dashboard.create_workspace_folder")}
@@ -137,7 +137,7 @@ export default function CreateWorkspaceModal(props: { }> - {hasSelectedFolder() ? translate("dashboard.change") : "Select folder"} + {hasSelectedFolder() ? translate("dashboard.change") : translate("dashboard.create_workspace_select_folder")}
@@ -152,7 +152,7 @@ export default function CreateWorkspaceModal(props: { }> - Sandbox setup + {translate("dashboard.create_workspace_sandbox_setup")}
{p().stage}
{elapsedSeconds()}s
@@ -162,7 +162,7 @@ export default function CreateWorkspaceModal(props: { class="shrink-0 rounded px-2 py-1 text-xs text-gray-10 transition-colors hover:bg-gray-4 hover:text-gray-12" onClick={() => setShowProgressDetails((prev) => !prev)} > - {showProgressDetails() ? "Hide logs" : "Show logs"} + {showProgressDetails() ? translate("dashboard.create_workspace_hide_logs") : translate("dashboard.create_workspace_show_logs")} @@ -211,7 +211,7 @@ export default function CreateWorkspaceModal(props: { 0}>
-
Live Logs
+
{translate("dashboard.create_workspace_live_logs")}
@@ -244,7 +244,7 @@ export default function CreateWorkspaceModal(props: {
0}>
- Docker debug details + {translate("dashboard.create_workspace_docker_debug")}
{(line) =>
{line}
} @@ -296,7 +296,7 @@ export default function CreateWorkspaceModal(props: { - Creating... + {translate("dashboard.create_workspace_creating")} diff --git a/apps/app/src/app/components/den-settings-panel.tsx b/apps/app/src/app/components/den-settings-panel.tsx index 174fe3421..d9af8003f 100644 --- a/apps/app/src/app/components/den-settings-panel.tsx +++ b/apps/app/src/app/components/den-settings-panel.tsx @@ -15,6 +15,7 @@ import { } from "../lib/den"; import { isDesktopDeployment } from "../lib/openwork-deployment"; import { usePlatform } from "../context/platform"; +import { t } from "../../i18n"; type DenSettingsPanelProps = { developerMode: boolean; @@ -43,18 +44,18 @@ function workerStatusMeta(status: string) { const normalized = status.trim().toLowerCase(); switch (normalized) { case "healthy": - return { label: "Ready", tone: "ready" as const, canOpen: true }; + return { label: t("den.status_ready"), tone: "ready" as const, canOpen: true }; case "provisioning": - return { label: "Provisioning", tone: "warning" as const, canOpen: false }; + return { label: t("den.status_provisioning"), tone: "warning" as const, canOpen: false }; case "failed": - return { label: "Failed", tone: "error" as const, canOpen: false }; + return { label: t("den.status_failed"), tone: "error" as const, canOpen: false }; case "stopped": - return { label: "Stopped", tone: "neutral" as const, canOpen: false }; + return { label: t("den.status_stopped"), tone: "neutral" as const, canOpen: false }; default: return { label: normalized ? `${normalized.slice(0, 1).toUpperCase()}${normalized.slice(1)}` - : "Unknown", + : t("den.status_unknown"), tone: "neutral" as const, canOpen: normalized === "ready", }; @@ -116,10 +117,10 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { }); const summaryLabel = createMemo(() => { - if (authError()) return "Needs attention"; - if (sessionBusy()) return "Checking session"; - if (isSignedIn()) return "Connected"; - return "Signed out"; + if (authError()) return t("den.summary_needs_attention"); + if (sessionBusy()) return t("den.summary_checking"); + if (isSignedIn()) return t("den.summary_connected"); + return t("den.summary_signed_out"); }); createEffect(() => { @@ -152,8 +153,8 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { platform.openLink(target.toString()); setStatusMessage( mode === "sign-up" - ? "Finish account creation in your browser to connect OpenWork." - : "Finish signing in in your browser to connect OpenWork.", + ? t("den.finish_creation") + : t("den.finish_sign_in"), ); setAuthError(null); }; @@ -184,7 +185,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { const applyBaseUrl = () => { const normalized = normalizeDenBaseUrl(baseUrlDraft()); if (!normalized) { - setBaseUrlError("Enter a valid http:// or https:// Den control plane URL."); + setBaseUrlError(t("den.invalid_url")); return; } @@ -197,7 +198,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { setBaseUrl(resolved.baseUrl); setBaseUrlDraft(resolved.baseUrl); - clearSignedInState("Updated the Den control plane URL. Sign in again to continue."); + clearSignedInState(t("den.url_updated")); }; const refreshOrgs = async (quiet = false) => { @@ -219,11 +220,11 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { setActiveOrgId(next); if (!quiet && response.orgs.length > 0) { setStatusMessage( - `Loaded ${response.orgs.length} org${response.orgs.length === 1 ? "" : "s"}.`, + t("den.loaded_orgs").replace("{count}", String(response.orgs.length)), ); } } catch (error) { - setOrgsError(error instanceof Error ? error.message : "Failed to load orgs."); + setOrgsError(error instanceof Error ? error.message : t("den.failed_load_orgs")); } finally { setOrgsBusy(false); } @@ -243,14 +244,15 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { const nextWorkers = await client().listWorkers(orgId, 20); setWorkers(nextWorkers); if (!quiet) { + const orgName = activeOrg()?.name ?? "this org"; setStatusMessage( nextWorkers.length > 0 - ? `Loaded ${nextWorkers.length} worker${nextWorkers.length === 1 ? "" : "s"} for ${activeOrg()?.name ?? "this org"}.` - : `No workers found for ${activeOrg()?.name ?? "this org"}.`, + ? t("den.loaded_workers").replace("{count}", String(nextWorkers.length)).replace("{org}", orgName) + : t("den.no_workers_found").replace("{org}", orgName), ); } } catch (error) { - setWorkersError(error instanceof Error ? error.message : "Failed to load workers."); + setWorkersError(error instanceof Error ? error.message : t("den.failed_load_workers")); } finally { setWorkersBusy(false); } @@ -276,7 +278,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { .then((nextUser) => { if (cancelled) return; setUser(nextUser); - setStatusMessage(`Signed in as ${nextUser.email}.`); + setStatusMessage(t("den.signed_in_as").replace("{email}", nextUser.email)); }) .catch((error) => { if (cancelled) return; @@ -286,7 +288,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { clearSessionState(); } setAuthError( - error instanceof Error ? error.message : "No active Den session found.", + error instanceof Error ? error.message : t("den.no_session"), ); }) .finally(() => { @@ -324,13 +326,13 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { setAuthError(null); setStatusMessage( customEvent.detail.email?.trim() - ? `Connected OpenWork Den as ${customEvent.detail.email.trim()}.` - : "Connected OpenWork Den.", + ? t("den.connected_as").replace("{email}", customEvent.detail.email.trim()) + : t("den.connected"), ); } else if (customEvent.detail?.status === "error") { setAuthError( customEvent.detail.message?.trim() || - "Failed to finish OpenWork Den sign-in.", + t("den.sign_in_failed"), ); } }; @@ -360,15 +362,13 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { setAuthBusy(false); } - clearSignedInState( - "Signed out and cleared your OpenWork Den session on this device.", - ); + clearSignedInState(t("den.signed_out_msg")); }; const handleOpenWorker = async (workerId: string, workerName: string) => { const orgId = activeOrgId().trim(); if (!orgId) { - setWorkersError("Choose an org before opening a worker."); + setWorkersError(t("den.choose_org")); return; } @@ -381,9 +381,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { const accessToken = tokens.ownerToken?.trim() || tokens.clientToken?.trim() || ""; if (!openworkUrl || !accessToken) { - throw new Error( - "Worker is not ready to open yet. Try again after provisioning finishes.", - ); + throw new Error(t("den.worker_not_ready")); } const ok = await props.connectRemoteWorkspace({ @@ -393,13 +391,13 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { displayName: workerName, }); if (!ok) { - throw new Error(`Failed to open ${workerName} in OpenWork.`); + throw new Error(t("den.failed_open_worker").replace("{name}", workerName)); } - setStatusMessage(`Opened ${workerName} in OpenWork.`); + setStatusMessage(t("den.opened_worker").replace("{name}", workerName)); } catch (error) { setWorkersError( - error instanceof Error ? error.message : `Failed to open ${workerName}.`, + error instanceof Error ? error.message : t("den.failed_open").replace("{name}", workerName), ); } finally { setOpeningWorkerId(null); @@ -422,15 +420,14 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
- OpenWork Den + {t("den.title")}
- Sign in, pick an org, and open Den workers from Settings. + {t("den.description")}
- Sign in to OpenWork Den to keep your tasks alive even when your - computer sleeps. + {t("den.tagline")}
@@ -445,11 +442,11 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
setBaseUrlDraft(event.currentTarget.value)} placeholder={DEFAULT_DEN_BASE_URL} - hint="Developer mode only. Use this to target a local or self-hosted Den control plane. Changing it signs you out so the app can re-hydrate against the new control plane." + hint={t("den.control_plane_url_hint")} disabled={authBusy() || sessionBusy()} />
@@ -459,7 +456,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { onClick={() => setBaseUrlDraft(baseUrl())} disabled={authBusy() || sessionBusy()} > - Reset + {t("den.reset")}
@@ -502,17 +499,16 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
- Sign in to OpenWork Den + {t("den.sign_in_title")}
- Sign in to OpenWork Den to keep your tasks alive even when your - computer sleeps. + {t("den.tagline")}
@@ -534,8 +530,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
- Finish auth in your browser and OpenWork will reconnect here - automatically. + {t("den.finish_auth")}
@@ -544,9 +539,9 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
-
Den Account
+
{t("den.account_title")}
- Manage your connected account and organization. + {t("den.account_description")}
@@ -567,15 +562,15 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { disabled={authBusy() || sessionBusy()} > - {authBusy() ? "Signing out..." : "Sign out"} + {authBusy() ? t("den.signing_out") : t("den.sign_out")}
-
Active org
+
{t("den.active_org")}
- Workers are scoped to the selected org. + {t("den.workers_scoped")}
@@ -585,7 +580,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { onChange={(event) => { setActiveOrgId(event.currentTarget.value); setStatusMessage( - `Switched to ${activeOrg()?.name ?? "the selected org"}.`, + t("den.switched_org").replace("{org}", activeOrg()?.name ?? "the selected org"), ); }} disabled={orgsBusy() || orgs().length === 0} @@ -593,7 +588,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { {(org) => ( )} @@ -624,17 +619,16 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
- Den workers + {t("den.workers_title")}
- Open workers directly into OpenWork using the same - remote-connect flow the app already uses elsewhere. + {t("den.workers_description")}
- {activeOrg()?.name || "No org selected"} + {activeOrg()?.name || t("den.no_org_selected")}
@@ -658,8 +652,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
- No cloud workers are visible for this org yet. Create one in - Den, then refresh this tab. + {t("den.no_workers_visible")}
@@ -681,12 +674,12 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { - Mine + {t("den.mine_badge")}
- {worker.provider ? `${worker.provider} worker` : "Cloud worker"} + {worker.provider ? t("den.worker_type").replace("{provider}", worker.provider) : t("den.cloud_worker")} {(value) => · {value()}} @@ -699,9 +692,9 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) { void handleOpenWorker(worker.workerId, worker.workerName) } disabled={openingWorkerId() !== null || !status().canOpen} - title={!status().canOpen ? "This worker is not ready to open yet." : undefined} + title={!status().canOpen ? t("den.worker_not_ready_tooltip") : undefined} > - {openingWorkerId() === worker.workerId ? "Opening..." : "Open"} + {openingWorkerId() === worker.workerId ? t("den.opening") : t("den.open")}
); diff --git a/apps/app/src/app/components/mcp-auth-modal.tsx b/apps/app/src/app/components/mcp-auth-modal.tsx index 2ffde2846..32bcf7f34 100644 --- a/apps/app/src/app/components/mcp-auth-modal.tsx +++ b/apps/app/src/app/components/mcp-auth-modal.tsx @@ -168,7 +168,7 @@ export default function McpAuthModal(props: McpAuthModalProps) { statusPoll = window.setInterval(async () => { if (Date.now() - startedAt >= MCP_AUTH_TIMEOUT_MS) { stopStatusPolling(); - setError("Request timed out."); + setError(translate("mcp.request_timed_out")); return; } @@ -589,7 +589,7 @@ export default function McpAuthModal(props: McpAuthModalProps) { setStatusChecking(false); }; - const serverName = () => props.entry?.name ?? "MCP Server"; + const serverName = () => props.entry?.name ?? translate("mcp.server_fallback"); return ( @@ -694,7 +694,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
-

Already Connected

+

{translate("mcp.already_connected")}

{translate("mcp.auth.already_connected_description", { server: serverName() })}

@@ -804,7 +804,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
-
Authorization link
+
{translate("mcp.auth_link_label")}
{authorizationUrl()}
@@ -814,7 +814,7 @@ export default function McpAuthModal(props: McpAuthModalProps) { class="text-xs" onClick={handleCopyAuthorizationUrl} > - {authUrlCopied() ? "Copied" : "Copy link"} + {authUrlCopied() ? translate("mcp.copied") : translate("mcp.copy_link")}
-

Opening your browser

+

{translate("mcp.step_opening_browser")}

{translate("mcp.auth.step1_description", { server: serverName() })}

@@ -863,7 +863,7 @@ export default function McpAuthModal(props: McpAuthModalProps) { 2
-

Authorize OpenWork

+

{translate("mcp.step_authorize")}

{translate("mcp.auth.step2_description")}

@@ -875,7 +875,7 @@ export default function McpAuthModal(props: McpAuthModalProps) { 3
-

Return here when you're done

+

{translate("mcp.step_return")}

{translate("mcp.auth.step3_description")}

diff --git a/apps/app/src/app/components/mobile-sidebar-drawer.tsx b/apps/app/src/app/components/mobile-sidebar-drawer.tsx index 74d4b5c77..d7397e986 100644 --- a/apps/app/src/app/components/mobile-sidebar-drawer.tsx +++ b/apps/app/src/app/components/mobile-sidebar-drawer.tsx @@ -1,5 +1,6 @@ import { Show, createEffect, onCleanup } from "solid-js"; import type { JSX } from "solid-js"; +import { t, currentLocale } from "../../i18n"; type MobileSidebarDrawerProps = { open: boolean; @@ -8,6 +9,7 @@ type MobileSidebarDrawerProps = { }; export default function MobileSidebarDrawer(props: MobileSidebarDrawerProps) { + const translate = (key: string) => t(key, currentLocale()); createEffect(() => { if (!props.open || typeof window === "undefined" || typeof document === "undefined") return; @@ -35,7 +37,7 @@ export default function MobileSidebarDrawer(props: MobileSidebarDrawerProps) { type="button" class="absolute inset-0 bg-gray-1/60 backdrop-blur-sm" onClick={props.onClose} - aria-label="Close sidebar" + aria-label={translate("session.close_sidebar")} />
{props.children} diff --git a/apps/app/src/app/components/model-picker-modal.tsx b/apps/app/src/app/components/model-picker-modal.tsx index 4f208564e..b993930e7 100644 --- a/apps/app/src/app/components/model-picker-modal.tsx +++ b/apps/app/src/app/components/model-picker-modal.tsx @@ -306,9 +306,9 @@ export default function ModelPickerModal(props: ModelPickerModalProps) { {provider.title}
- Connect this provider to browse and save models + {translate("model_picker.connect_provider")} - {provider.matchCount} {provider.matchCount === 1 ? "model" : "models"} + {provider.matchCount} {provider.matchCount === 1 ? translate("model_picker.model_singular") : translate("model_picker.model_plural")}
@@ -324,12 +324,12 @@ export default function ModelPickerModal(props: ModelPickerModalProps) {

- {props.target === "default" ? "Default model" : "Chat model"} + {props.target === "default" ? translate("model_picker.default_label") : translate("model_picker.chat_label")}

{props.target === "default" - ? "Choose the default model for new chats. If a model supports reasoning profiles, configure them on its card." - : "Choose the model for this chat. If a model supports reasoning profiles, configure them on its card."} + ? translate("model_picker.default_desc") + : translate("model_picker.chat_desc")}

diff --git a/apps/app/src/app/components/provider-auth-modal.tsx b/apps/app/src/app/components/provider-auth-modal.tsx index 411e011c1..b1037058b 100644 --- a/apps/app/src/app/components/provider-auth-modal.tsx +++ b/apps/app/src/app/components/provider-auth-modal.tsx @@ -4,10 +4,13 @@ import type { ProviderListItem } from "../types"; import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"; import { isTauriRuntime } from "../utils"; import { compareProviders } from "../utils/providers"; +import { t, currentLocale } from "../../i18n"; import Button from "./button"; import TextInput from "./text-input"; +const translate = (key: string) => t(key, currentLocale()); + export type ProviderAuthMethod = { type: "oauth" | "api"; label: string; @@ -126,7 +129,7 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) { }); const methodLabel = (method: ProviderAuthMethod) => - method.label || (method.type === "oauth" ? "OAuth" : "API key"); + method.label || (method.type === "oauth" ? translate("provider.oauth_method") : translate("provider.api_key_method")); const actionDisabled = () => props.loading || props.submitting; @@ -595,14 +598,14 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) {
-

Connect providers

-

Sign in to services you want OpenWork to use.

+

{translate("provider.connect_title")}

+

{translate("provider.connect_desc")}

@@ -615,7 +618,7 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) { fallback={
- Loading providers... + {translate("provider.loading")}
} @@ -635,7 +638,7 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) { { setSearchQuery(event.currentTarget.value); @@ -653,7 +656,7 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) { when={filteredEntries().length} fallback={
- {entries().length ? "No providers match your search." : "No providers available."} + {entries().length ? translate("provider.no_match") : translate("provider.none_available")}
} > @@ -706,7 +709,7 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) { >
- Connected + {translate("provider.connected")}
@@ -735,7 +738,7 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) { -
Arrow keys to navigate, Enter to select.
+
{translate("provider.keyboard_hint")}
@@ -744,10 +747,10 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) {
{selectedEntry()!.name}
-
Choose how you'd like to connect.
+
{translate("provider.choose_connect")}
@@ -780,13 +783,13 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) {
Paste your API key to connect.
{ setApiKeyInput(event.currentTarget.value); @@ -799,7 +802,7 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) { /> 0}>
- Env vars: {selectedEntry()!.env.join(", ")} + {translate("provider.env_vars_label")}{selectedEntry()!.env.join(", ")}
@@ -825,7 +828,7 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) {
Finish OAuth by pasting the authorization code.
@@ -837,9 +840,9 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) {
{ setOauthCodeInput(event.currentTarget.value); @@ -882,16 +885,16 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) {
{selectedEntry()!.name}
-
Waiting for browser confirmation.
+
{translate("provider.waiting_browser")}
Sign in in the browser tab we just opened. We will complete the connection automatically.
+
{translate("provider.browser_signin_desc")}
} >
@@ -900,13 +903,13 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) { The first time you do this you'll need to enable Device auth in your account settings.
ChatGPT > Account Settings > Security > Enable device code authorization
-
When you're ready, copy the code below, and click "Open Browser".
+
{translate("provider.copy_code_desc")}
-
Confirmation code
+
{translate("provider.confirmation_code_label")}
{oauthDisplayCode()}
@@ -946,7 +949,7 @@ export default function ProviderAuthModal(props: ProviderAuthModalProps) { {submittingLabel()} diff --git a/apps/app/src/app/components/question-modal.tsx b/apps/app/src/app/components/question-modal.tsx index 4b3b070bd..52a0269d5 100644 --- a/apps/app/src/app/components/question-modal.tsx +++ b/apps/app/src/app/components/question-modal.tsx @@ -2,6 +2,7 @@ import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "so import type { QuestionInfo } from "@opencode-ai/sdk/v2/client"; import { Check, ChevronRight, HelpCircle } from "lucide-solid"; +import { t, currentLocale } from "../../i18n"; import Button from "./button"; @@ -14,6 +15,7 @@ export type QuestionModalProps = { }; export default function QuestionModal(props: QuestionModalProps) { + const translate = (key: string) => t(key, currentLocale()); const [currentIndex, setCurrentIndex] = createSignal(0); const [answers, setAnswers] = createSignal([]); const [currentSelection, setCurrentSelection] = createSignal([]); @@ -138,7 +140,7 @@ export default function QuestionModal(props: QuestionModalProps) {

- {currentQuestion()!.header || "Question"} + {currentQuestion()!.header || translate("question.fallback_header")}

Question {currentIndex() + 1} of {props.questions.length} @@ -186,14 +188,14 @@ export default function QuestionModal(props: QuestionModalProps) {
setCustomInput(e.currentTarget.value)} class="w-full px-4 py-3 rounded-xl bg-dls-surface border border-dls-border focus:border-dls-accent focus:ring-4 focus:ring-[rgba(var(--dls-accent-rgb),0.2)] focus:outline-none text-sm text-dls-text placeholder:text-dls-secondary transition-shadow" - placeholder="Type your answer here..." + placeholder={translate("question.placeholder")} onKeyDown={(e) => { if (e.key === "Enter") { if (e.isComposing || e.keyCode === 229) return; @@ -209,15 +211,15 @@ export default function QuestionModal(props: QuestionModalProps) {
↑↓ - navigate + {translate("question.navigate_hint")} - select + {translate("question.select_hint")}
@@ -1752,7 +1754,7 @@ export default function Composer(props: ComposerProps) { class="shrink-0 rounded-md border border-gray-6 bg-gray-2 px-2 py-1 text-[10px] text-gray-11 hover:bg-gray-3" onClick={() => inboxFileInputRef?.click()} > - Upload to shared folder + {translate("session.upload_shared_folder")}
@@ -1764,7 +1766,7 @@ export default function Composer(props: ComposerProps) {
- Describe your task... + {translate("session.task_placeholder")}
- Run task + {translate("session.run_task")} } > @@ -1848,10 +1850,10 @@ export default function Composer(props: ComposerProps) { type="button" onClick={() => props.onStop()} class="inline-flex items-center gap-2 rounded-full bg-gray-12 px-4 py-2 text-[13px] font-medium text-gray-1 transition-colors hover:bg-gray-11" - title="Stop" + title={translate("session.stop")} > - Stop + {translate("session.stop")}
@@ -1872,7 +1874,7 @@ export default function Composer(props: ComposerProps) { onClick={props.onToggleAgentPicker} disabled={props.busy} aria-expanded={props.agentPickerOpen} - title="Agent" + title={translate("session.agent_label")} > {props.agentLabel} @@ -1881,14 +1883,14 @@ export default function Composer(props: ComposerProps) {
- Agent + {translate("session.agent_label")}
event.preventDefault()}> Loading agents...
+
{translate("session.loading_agents")}
} > @@ -1903,7 +1905,7 @@ export default function Composer(props: ComposerProps) { props.onSelectAgent(null); }} > - Default agent + {translate("session.default_agent")} diff --git a/apps/app/src/app/components/session/context-panel.tsx b/apps/app/src/app/components/session/context-panel.tsx index e89ffe40a..beb7905ba 100644 --- a/apps/app/src/app/components/session/context-panel.tsx +++ b/apps/app/src/app/components/session/context-panel.tsx @@ -1,6 +1,7 @@ import { For, Show } from "solid-js"; import { ChevronDown, Circle, File, Folder, Package } from "lucide-solid"; +import { t } from "../../../i18n"; import { SUGGESTED_PLUGINS } from "../../constants"; import type { McpServerEntry, McpStatus, McpStatusMap, SkillCard } from "../../types"; import { stripPluginVersion } from "../../utils/plugins"; @@ -108,19 +109,19 @@ const getSmartFileName = (files: string[], file: string): string => { }; const mcpStatusLabel = (status?: McpStatus, disabled?: boolean) => { - if (disabled) return "Disabled"; - if (!status) return "Disconnected"; + if (disabled) return t("session.ctx_disabled"); + if (!status) return t("session.ctx_disconnected"); switch (status.status) { case "connected": - return "Connected"; + return t("session.ctx_connected"); case "needs_auth": - return "Needs auth"; + return t("session.ctx_needs_auth"); case "needs_client_registration": - return "Register client"; + return t("session.ctx_register_client"); case "failed": - return "Failed"; + return t("session.ctx_failed"); default: - return "Disconnected"; + return t("session.ctx_disconnected"); } }; @@ -152,7 +153,7 @@ export default function ContextPanel(props: ContextPanelProps) { class="w-full px-4 py-3 flex items-center justify-between text-sm text-gray-12 font-medium" onClick={() => props.onToggleSection("context")} > - Context + {t("session.ctx_context")}
- Working files + {t("session.ctx_working_files")}
None yet.
} + fallback={
{t("session.ctx_none_yet")}
} > {(file) => { @@ -204,7 +205,7 @@ export default function ContextPanel(props: ContextPanelProps) { class="w-full px-4 py-3 flex items-center justify-between text-sm text-gray-12 font-medium" onClick={() => props.onToggleSection("plugins")} > - Plugins + {t("session.ctx_plugins")} - {props.activePluginStatus ?? "No plugins loaded."} + {props.activePluginStatus ?? t("session.ctx_no_plugins")}
} > @@ -254,7 +255,7 @@ export default function ContextPanel(props: ContextPanelProps) { class="w-full px-4 py-3 flex items-center justify-between text-sm text-gray-12 font-medium" onClick={() => props.onToggleSection("mcp")} > - MCP + {t("session.ctx_mcp")} - {props.mcpStatus ?? "No MCP servers loaded."} + {props.mcpStatus ?? t("session.ctx_no_mcp")}
} > @@ -304,7 +305,7 @@ export default function ContextPanel(props: ContextPanelProps) { class="w-full px-4 py-3 flex items-center justify-between text-sm text-gray-12 font-medium" onClick={() => props.onToggleSection("skills")} > - Skills + {t("session.ctx_skills")} - {props.skillsStatus ?? "No skills loaded."} + {props.skillsStatus ?? t("session.ctx_no_skills")}
} > @@ -353,7 +354,7 @@ export default function ContextPanel(props: ContextPanelProps) { class="w-full px-4 py-3 flex items-center justify-between text-sm text-gray-12 font-medium" onClick={() => props.onToggleSection("authorizedFolders")} > - Authorized folders + {t("session.ctx_authorized_folders")} None yet.
} + fallback={
{t("session.ctx_none_yet")}
} > {(folder) => ( diff --git a/apps/app/src/app/components/session/inbox-panel.tsx b/apps/app/src/app/components/session/inbox-panel.tsx index 0d6b51ef8..48fe44e52 100644 --- a/apps/app/src/app/components/session/inbox-panel.tsx +++ b/apps/app/src/app/components/session/inbox-panel.tsx @@ -1,6 +1,7 @@ import { For, Show, createEffect, createMemo, createSignal } from "solid-js"; import { Download, RefreshCw, UploadCloud } from "lucide-solid"; +import { t } from "../../../i18n"; import { getOpenWorkDeployment } from "../../lib/openwork-deployment"; import type { OpenworkInboxItem, OpenworkServerClient } from "../../lib/openwork-server"; import WebUnavailableSurface from "../web-unavailable-surface"; @@ -48,7 +49,7 @@ export default function InboxPanel(props: InboxPanelProps) { }); const connected = createMemo(() => Boolean(props.client && (props.workspaceId ?? "").trim())); - const helperText = "Share files with this worker from the app."; + const helperText = t("session.inbox_share_hint"); const visibleItems = createMemo(() => (items() ?? []).slice(0, maxPreview())); const hiddenCount = createMemo(() => Math.max(0, (items() ?? []).length - visibleItems().length)); @@ -72,7 +73,7 @@ export default function InboxPanel(props: InboxPanelProps) { setItems(result.items ?? []); } catch (err) { const message = - err instanceof Error ? err.message : "Failed to load shared folder"; + err instanceof Error ? err.message : t("session.inbox_load_failed"); setError(message); setItems([]); } finally { @@ -84,7 +85,7 @@ export default function InboxPanel(props: InboxPanelProps) { const client = props.client; const workspaceId = (props.workspaceId ?? "").trim(); if (!client || !workspaceId) { - toast("Connect to a worker to upload files to the shared folder."); + toast(t("session.inbox_connect_to_upload")); return; } if (!files.length) return; @@ -93,15 +94,15 @@ export default function InboxPanel(props: InboxPanelProps) { setError(null); try { const label = files.length === 1 ? files[0]?.name ?? "file" : `${files.length} files`; - toast(`Uploading ${label}...`); + toast(t("session.inbox_uploading").replace("{label}", label)); for (const file of files) { await client.uploadInbox(workspaceId, file); } - toast("Uploaded to the shared folder."); + toast(t("session.inbox_uploaded")); await refresh(); } catch (err) { const message = - err instanceof Error ? err.message : "Shared folder upload failed"; + err instanceof Error ? err.message : t("session.inbox_upload_failed"); setError(message); toast(message); } finally { @@ -115,7 +116,7 @@ export default function InboxPanel(props: InboxPanelProps) { await navigator.clipboard.writeText(path); toast(`Copied: ${path}`); } catch { - toast("Copy failed. Your browser may block clipboard access."); + toast(t("session.inbox_copy_failed")); } }; @@ -123,12 +124,12 @@ export default function InboxPanel(props: InboxPanelProps) { const client = props.client; const workspaceId = (props.workspaceId ?? "").trim(); if (!client || !workspaceId) { - toast("Connect to a worker to download shared files."); + toast(t("session.inbox_connect_to_download")); return; } const id = String(item.id ?? "").trim(); if (!id) { - toast("Missing shared file id."); + toast(t("session.inbox_missing_file_id")); return; } @@ -144,7 +145,7 @@ export default function InboxPanel(props: InboxPanelProps) { a.remove(); URL.revokeObjectURL(url); } catch (err) { - const message = err instanceof Error ? err.message : "Download failed"; + const message = err instanceof Error ? err.message : t("session.inbox_download_failed"); toast(message); } }; @@ -160,7 +161,7 @@ export default function InboxPanel(props: InboxPanelProps) {
- Shared folder + {t("session.inbox_shared_folder")}
0}> @@ -171,8 +172,8 @@ export default function InboxPanel(props: InboxPanelProps) { type="button" class="rounded-md p-1 text-gray-9 hover:text-gray-11 hover:bg-gray-3 transition-colors" onClick={() => void refresh()} - title="Refresh shared folder" - aria-label="Refresh shared folder" + title={t("session.inbox_refresh")} + aria-label={t("session.inbox_refresh")} disabled={!connected() || loading()} > @@ -214,12 +215,12 @@ export default function InboxPanel(props: InboxPanelProps) { if (files.length) void uploadFiles(files); }} disabled={uploading()} - title={connected() ? "Drop files here to upload" : "Connect to a worker to upload"} + title={connected() ? t("session.inbox_drop_to_upload") : t("session.inbox_connect_upload")} >
- {uploading() ? "Uploading..." : "Drop files or click to upload"} + {uploading() ? t("session.inbox_uploading_label") : t("session.inbox_drop_or_click")} {helperText}
@@ -234,8 +235,8 @@ export default function InboxPanel(props: InboxPanelProps) { when={visibleItems().length > 0} fallback={
- - No shared files yet. + + {t("session.inbox_no_files")}
} @@ -253,8 +254,8 @@ export default function InboxPanel(props: InboxPanelProps) { type="button" class="min-w-0 flex-1 text-left" onClick={() => void copyPath(item)} - title={rel() ? `Copy ${INBOX_PREFIX}${rel()}` : "Copy shared folder path"} - aria-label={rel() ? `Copy ${INBOX_PREFIX}${rel()}` : "Copy shared folder path"} + title={rel() ? `${t("session.inbox_copy_path")}: ${INBOX_PREFIX}${rel()}` : t("session.inbox_copy_path")} + aria-label={rel() ? `${t("session.inbox_copy_path")}: ${INBOX_PREFIX}${rel()}` : t("session.inbox_copy_path")} disabled={!connected()} >
{name()}
@@ -275,8 +276,8 @@ export default function InboxPanel(props: InboxPanelProps) { type="button" class="shrink-0 rounded-md p-1 text-gray-9 opacity-0 group-hover:opacity-100 hover:text-gray-11 hover:bg-gray-3" onClick={() => void downloadItem(item)} - title="Download" - aria-label="Download" + title={t("session.inbox_download")} + aria-label={t("session.inbox_download")} disabled={!connected()} > @@ -288,7 +289,7 @@ export default function InboxPanel(props: InboxPanelProps) {
0}> -
Showing first {maxPreview()}.
+
{t("session.inbox_showing_first").replace("{count}", String(maxPreview()))}
diff --git a/apps/app/src/app/components/session/message-list.tsx b/apps/app/src/app/components/session/message-list.tsx index 9226ebb43..b201a2442 100644 --- a/apps/app/src/app/components/session/message-list.tsx +++ b/apps/app/src/app/components/session/message-list.tsx @@ -31,6 +31,7 @@ import { } from "../../utils"; import PartView from "../part-view"; import { perfNow, recordPerfLog } from "../../lib/perf-log"; +import { t } from "../../../i18n"; export type MessageListProps = { messages: MessageWithParts[]; @@ -228,57 +229,57 @@ function toolHeadline(part: Part) { const description = pick("description"); if (description) return compactText(description); const command = pick("command", "cmd"); - return command ? compactText(`Run ${command}`, 48) : "Run command"; + return command ? compactText(t("session.tool_run_command").replace("{cmd}", command), 48) : t("session.tool_run_command_fallback"); } if (tool === "read") { const file = target("filePath", "path", "file"); - return file ? `Reviewed ${file}` : "Reviewed file"; + return file ? t("session.tool_reviewed_file").replace("{file}", file) : t("session.tool_reviewed_file_fallback"); } if (tool === "edit") { const file = target("filePath", "path", "file"); - return file ? `Updated ${file}` : "Updated file"; + return file ? t("session.tool_updated_file").replace("{file}", file) : t("session.tool_updated_file_fallback"); } if (tool === "write" || tool === "apply_patch") { const file = target("filePath", "path", "file"); - return file ? `Update ${file}` : "Update file"; + return file ? t("session.tool_update_file").replace("{file}", file) : t("session.tool_update_file_fallback"); } if (tool === "grep" || tool === "glob" || tool === "search") { const pattern = pick("pattern", "query"); - return pattern ? `Searched ${compactText(pattern, 36)}` : "Searched code"; + return pattern ? t("session.tool_searched").replace("{pattern}", compactText(pattern, 36)) : t("session.tool_searched_fallback"); } if (tool === "list" || tool === "list_files") { const path = target("path"); - return path ? `Reviewed ${path}` : "Reviewed files"; + return path ? t("session.tool_reviewed_files").replace("{path}", path) : t("session.tool_reviewed_files_fallback"); } if (tool === "task") { const description = pick("description"); if (description) return compactText(description); const agent = pick("subagent_type"); - return agent ? `Delegate ${agent}` : "Delegate task"; + return agent ? t("session.tool_delegate").replace("{agent}", agent) : t("session.tool_delegate_fallback"); } if (tool === "todowrite") { - return "Update todo list"; + return t("session.tool_update_todo"); } if (tool === "todoread") { - return "Read todo list"; + return t("session.tool_read_todo"); } if (tool === "webfetch") { const url = pick("url"); - return url ? `Checked ${compactText(url, 36)}` : "Checked web page"; + return url ? t("session.tool_checked_url").replace("{url}", compactText(url, 36)) : t("session.tool_checked_url_fallback"); } if (tool === "skill") { const name = pick("name"); - return name ? `Load skill ${name}` : "Load skill"; + return name ? t("session.tool_load_skill").replace("{name}", name) : t("session.tool_load_skill_fallback"); } const fallback = tool @@ -697,16 +698,16 @@ export default function MessageList(props: MessageListProps) { if (title) return title; if (task().description) return task().description!; if (task().agentType) return `${task().agentType} task`; - return "Subagent session"; + return t("session.subagent_session"); }); const statusLabel = createMemo(() => { - if (loading()) return "Loading transcript"; - if (streaming()) return "Running"; + if (loading()) return t("session.loading_transcript"); + if (streaming()) return t("session.subagent_running"); if (childMessages().length > 0) { const count = childMessages().length; return `${count} message${count === 1 ? "" : "s"}`; } - return "Waiting for transcript"; + return t("session.waiting_transcript"); }); createEffect(() => { @@ -754,7 +755,7 @@ export default function MessageList(props: MessageListProps) {
0} - fallback={
Waiting for the subagent transcript to arrive.
} + fallback={
{t("session.waiting_subagent")}
} >
-
Request
+
{t("session.request_label")}
{formatStructuredValue(toolInput())}
-
Result
+
{t("session.result_label")}
{formatStructuredValue(toolOutput())}
diff --git a/apps/app/src/app/components/session/sidebar.tsx b/apps/app/src/app/components/session/sidebar.tsx index b95f17ba8..b95536f32 100644 --- a/apps/app/src/app/components/session/sidebar.tsx +++ b/apps/app/src/app/components/session/sidebar.tsx @@ -3,6 +3,7 @@ import { Check, ChevronDown, GripVertical, Loader2, Plus, RefreshCcw, Settings, import type { TodoItem, WorkspaceConnectionState } from "../../types"; import type { WorkspaceInfo } from "../../lib/tauri"; +import { t } from "../../../i18n"; type SessionSummary = { id: string; @@ -283,21 +284,21 @@ export default function SessionSidebar(props: SidebarProps) { disabled={props.newTaskDisabled} > - New task + {t("session.sidebar_new_task")}
-
Workspaces
+
{t("session.sidebar_workspaces")}
0} fallback={
- No workspaces in this session yet. Add one to get started. + {t("session.sidebar_no_workspaces")}
} > @@ -373,7 +374,7 @@ export default function SessionSidebar(props: SidebarProps) { - {isSandboxWorkspace() ? "Sandbox" : "Remote"} + {isSandboxWorkspace() ? t("session.sidebar_sandbox") : t("session.sidebar_remote")}
@@ -390,11 +391,11 @@ export default function SessionSidebar(props: SidebarProps) { - Needs attention + {t("session.sidebar_needs_attention")} - Switch}> - Active + {t("session.sidebar_switch")}}> + {t("session.sidebar_active")} @@ -406,7 +407,7 @@ export default function SessionSidebar(props: SidebarProps) { type="button" class="p-1 rounded-md text-gray-9 hover:text-gray-12 hover:bg-gray-2" onClick={() => toggleWorkspaceCollapse(group.workspace.id)} - title={collapsed() ? "Expand" : "Collapse"} + title={collapsed() ? t("session.sidebar_expand") : t("session.sidebar_collapse")} > handleDragStart(event, group.workspace.id)} onDragEnd={handleDragEnd} @@ -441,7 +442,7 @@ export default function SessionSidebar(props: SidebarProps) { disabled={isActivelyConnecting()} > - Edit connection + {t("session.sidebar_edit_connection")} @@ -461,7 +462,7 @@ export default function SessionSidebar(props: SidebarProps) { disabled={isActivelyConnecting()} > - Stop sandbox + {t("session.sidebar_stop_sandbox")}
0} fallback={
- No sessions yet. + {t("session.sidebar_no_sessions")}
} > @@ -535,8 +536,8 @@ export default function SessionSidebar(props: SidebarProps) { onClick={() => toggleShowAllSessions(group.workspace.id)} > {showingAll() - ? "Show fewer" - : `Show ${sessions().length - MAX_SESSIONS_PREVIEW} more`} + ? t("session.sidebar_show_fewer") + : t("session.sidebar_show_more").replace("{count}", String(sessions().length - MAX_SESSIONS_PREVIEW))}
@@ -557,7 +558,7 @@ export default function SessionSidebar(props: SidebarProps) { onDrop={(event) => handleDrop(event, null)} > - Add new workspace + {t("session.sidebar_add_workspace")}
@@ -570,7 +571,7 @@ export default function SessionSidebar(props: SidebarProps) { }} > - New worker + {t("session.sidebar_new_worker")}
@@ -608,7 +609,7 @@ export default function SessionSidebar(props: SidebarProps) { class="w-full px-4 py-3 flex items-center justify-between text-sm text-gray-12 font-medium" onClick={() => props.onToggleSection("progress")} > - Progress + {t("session.sidebar_progress")} - New task + {t("session.sidebar_new_task_menu")}
diff --git a/apps/app/src/app/components/session/workspace-session-list.tsx b/apps/app/src/app/components/session/workspace-session-list.tsx index 4d546de96..b00ec4da4 100644 --- a/apps/app/src/app/components/session/workspace-session-list.tsx +++ b/apps/app/src/app/components/session/workspace-session-list.tsx @@ -27,6 +27,7 @@ import { getWorkspaceTaskLoadErrorDisplay, isWindowsPlatform, } from "../../utils"; +import { t } from "../../../i18n"; type Props = { workspaceSessionGroups: WorkspaceSessionGroup[]; @@ -166,16 +167,16 @@ const workspaceLabel = (workspace: WorkspaceInfo) => workspace.openworkWorkspaceName?.trim() || workspace.name?.trim() || workspace.path?.trim() || - "Workspace"; + t("session.workspace_default"); const workspaceKindLabel = (workspace: WorkspaceInfo) => workspace.workspaceType === "remote" ? workspace.sandboxBackend === "docker" || Boolean(workspace.sandboxRunId?.trim()) || Boolean(workspace.sandboxContainerName?.trim()) - ? "Sandbox" - : "Remote" - : "Local"; + ? t("session.kind_sandbox") + : t("session.kind_remote") + : t("session.kind_local"); const WORKSPACE_SWATCHES = ["#2563eb", "#5a67d8", "#f97316", "#10b981"]; @@ -191,8 +192,8 @@ const workspaceSwatchColor = (seed: string) => { export default function WorkspaceSessionList(props: Props) { const revealLabel = isWindowsPlatform() - ? "Reveal in Explorer" - : "Reveal in Finder"; + ? t("session.reveal_explorer") + : t("session.reveal_finder"); const newWorkspaceDesktopOnly = getOpenWorkDeployment() === "web"; const [expandedWorkspaceIds, setExpandedWorkspaceIds] = createSignal< Set @@ -296,7 +297,7 @@ export default function WorkspaceSessionList(props: Props) { const showMoreLabel = (workspaceId: string, totalRoots: number) => { const remaining = Math.max(0, totalRoots - previewCount(workspaceId)); const nextCount = Math.min(MAX_SESSIONS_PREVIEW, remaining); - return nextCount > 0 ? `Show ${nextCount} more` : "Show more"; + return nextCount > 0 ? t("session.show_more").replace("{count}", String(nextCount)) : t("session.show_more_generic"); }; createEffect(() => { @@ -402,7 +403,7 @@ export default function WorkspaceSessionList(props: Props) { @@ -473,7 +474,7 @@ export default function WorkspaceSessionList(props: Props) { props.onOpenDeleteSession?.(); }} > - Delete session + {t("session.delete_session")}
@@ -515,9 +516,9 @@ export default function WorkspaceSessionList(props: Props) { getWorkspaceTaskLoadErrorDisplay(workspace(), group.error); const statusLabel = () => { if (group.status === "error") return taskLoadError().label; - if (isConnectionActionBusy()) return "Connecting"; + if (isConnectionActionBusy()) return t("session.status_connecting"); if (!props.developerMode) return ""; - if (props.activeWorkspaceId === workspace().id) return "Active"; + if (props.activeWorkspaceId === workspace().id) return t("session.status_active"); return workspaceKindLabel(workspace()); }; const statusTone = () => { @@ -591,7 +592,7 @@ export default function WorkspaceSessionList(props: Props) { props.onCreateTaskInWorkspace(workspace().id); }} disabled={props.newTaskDisabled} - aria-label="New task" + aria-label={t("session.new_task_label")} > @@ -607,7 +608,7 @@ export default function WorkspaceSessionList(props: Props) { : workspace().id, ); }} - aria-label="Workspace options" + aria-label={t("session.workspace_options")} > @@ -618,8 +619,8 @@ export default function WorkspaceSessionList(props: Props) { class="rounded-md p-1 text-gray-9 hover:bg-gray-3/80 hover:text-gray-11" aria-label={ isWorkspaceExpanded(workspace().id) - ? "Collapse" - : "Expand" + ? t("session.collapse") + : t("session.expand") } onClick={(event) => { event.stopPropagation(); @@ -650,7 +651,7 @@ export default function WorkspaceSessionList(props: Props) { setWorkspaceMenuId(null); }} > - Edit name + {t("session.edit_name")}
@@ -809,10 +810,10 @@ export default function WorkspaceSessionList(props: Props) { disabled={props.newTaskDisabled} > - No tasks yet. + {t("session.no_tasks_yet")} @@ -843,7 +844,7 @@ export default function WorkspaceSessionList(props: Props) { } >
- Loading tasks... + {t("session.loading_tasks")}
@@ -866,7 +867,7 @@ export default function WorkspaceSessionList(props: Props) { onClick={() => setAddWorkspaceMenuOpen((prev) => !prev)} > - Add workspace + {t("session.add_workspace")} @@ -881,7 +882,7 @@ export default function WorkspaceSessionList(props: Props) { disabled={newWorkspaceDesktopOnly} title={ newWorkspaceDesktopOnly - ? "Create local workspaces in the desktop app." + ? t("session.create_local_desktop_only") : undefined } onClick={() => { @@ -890,7 +891,7 @@ export default function WorkspaceSessionList(props: Props) { }} > - New workspace + {t("session.new_workspace")} @@ -904,7 +905,7 @@ export default function WorkspaceSessionList(props: Props) { }} > - Connect remote workspace + {t("session.connect_remote_workspace")}
diff --git a/apps/app/src/app/components/share-workspace-modal.tsx b/apps/app/src/app/components/share-workspace-modal.tsx index 2b802f524..971275be5 100644 --- a/apps/app/src/app/components/share-workspace-modal.tsx +++ b/apps/app/src/app/components/share-workspace-modal.tsx @@ -1,4 +1,5 @@ import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js"; +import { t, currentLocale } from "../../i18n"; import { ArrowLeft, Check, @@ -28,9 +29,9 @@ const isCollaboratorField = (label: string) => /collaborator token/i.test(label) const isPasswordField = (label: string) => /owner token|connected token|access token|password/i.test(label); const isWorkerUrlField = (label: string) => /worker url/i.test(label); -const displayFieldLabel = (field: ShareField) => { - if (isPasswordField(field.label)) return "Password"; - if (isWorkerUrlField(field.label)) return "Worker URL"; +const displayFieldLabel = (field: ShareField, translate: (key: string) => string) => { + if (isPasswordField(field.label)) return translate("share.password_field"); + if (isWorkerUrlField(field.label)) return translate("share.worker_url_field"); return field.label; }; @@ -58,12 +59,14 @@ export default function ShareWorkspaceModal(props: { exportDisabledReason?: string | null; onOpenBots?: () => void; }) { + const translate = (key: string) => t(key, currentLocale()); + const [activeView, setActiveView] = createSignal("chooser"); const [revealedByIndex, setRevealedByIndex] = createSignal>({}); const [copiedKey, setCopiedKey] = createSignal(null); const [collaboratorExpanded, setCollaboratorExpanded] = createSignal(false); - const title = createMemo(() => props.title ?? "Share workspace"); + const title = createMemo(() => props.title ?? translate("share.title")); const note = createMemo(() => props.note?.trim() ?? ""); const accessFields = createMemo(() => props.fields.filter((field) => !isInviteField(field.label))); const collaboratorField = createMemo(() => accessFields().find((field) => isCollaboratorField(field.label)) ?? null); @@ -115,7 +118,7 @@ export default function ShareWorkspaceModal(props: { return (
}> @@ -146,7 +149,7 @@ export default function ShareWorkspaceModal(props: { onClick={() => handleCopy(field.value, key())} disabled={!field.value} class="p-1.5 text-gray-10 hover:text-gray-12 hover:bg-gray-3 rounded-md transition-colors disabled:opacity-50" - title="Copy" + title={translate("share.copy")} > }> @@ -179,7 +182,7 @@ export default function ShareWorkspaceModal(props: { disabled={Boolean(disabledReason) || !createAction || busy} class="mt-3 w-full rounded-full bg-dls-text px-5 py-3 text-[13px] font-medium text-dls-surface shadow-sm transition-colors hover:bg-gray-12 active:scale-[0.99] disabled:opacity-50" > - {busy ? "Publishing..." : createLabel} + {busy ? translate("share.publishing") : createLabel} } > @@ -193,7 +196,7 @@ export default function ShareWorkspaceModal(props: { ); @@ -222,8 +225,8 @@ export default function ShareWorkspaceModal(props: { @@ -232,8 +235,8 @@ export default function ShareWorkspaceModal(props: { @@ -243,8 +246,8 @@ export default function ShareWorkspaceModal(props: {

{title()} - Share a template - Access workspace remotely + {translate("share.tab_template")} + {translate("share.tab_remote")}

{props.workspaceName}
@@ -263,9 +266,9 @@ export default function ShareWorkspaceModal(props: {
-

Share a template

+

{translate("share.tab_template")}

- Share your setup and defaults so someone else can start from the same environment. + {translate("share.template_desc")}

@@ -279,9 +282,9 @@ export default function ShareWorkspaceModal(props: {
-

Access workspace remotely

+

{translate("share.tab_remote")}

- Copy the connection details needed to reach this live workspace from another machine or messaging surface. + {translate("share.access_section_desc")}

@@ -291,15 +294,15 @@ export default function ShareWorkspaceModal(props: {
- Share a reusable setup without granting live access to this running workspace. + {translate("share.reusable_desc")}
-

Workspace template

-

Share the core setup and workspace defaults.

+

{translate("share.template_section_title")}

+

{translate("share.template_section_desc")}

@@ -317,8 +320,8 @@ export default function ShareWorkspaceModal(props: { "share-workspace-profile", props.onShareWorkspaceProfile, props.shareWorkspaceProfileBusy, - "Create Template Link", - "Regenerate Link", + translate("share.create_template_link"), + translate("share.regenerate_link"), props.onShareWorkspaceProfile, props.shareWorkspaceProfileDisabledReason, )} @@ -330,15 +333,15 @@ export default function ShareWorkspaceModal(props: {
⚠️ - Share with trusted people only. These credentials grant live access to this workspace. + {translate("share.live_access_warning")}
-

Connect messaging

-

Use this workspace from Slack, Telegram, and others.

+

{translate("share.connect_messaging_title")}

+

{translate("share.connect_messaging_desc")}

@@ -365,7 +368,7 @@ export default function ShareWorkspaceModal(props: { onClick={() => setCollaboratorExpanded((value) => !value)} aria-expanded={collaboratorExpanded()} > - Optional collaborator access + {translate("share.collaborator_access")}
-
Routine access without permission approvals.
+
{translate("share.collaborator_desc")}
{renderCredentialField(field(), () => 0, "collaborator")}
diff --git a/apps/app/src/app/components/workspace-right-sidebar.tsx b/apps/app/src/app/components/workspace-right-sidebar.tsx index 2eea397e6..c596a6a1f 100644 --- a/apps/app/src/app/components/workspace-right-sidebar.tsx +++ b/apps/app/src/app/components/workspace-right-sidebar.tsx @@ -1,4 +1,5 @@ import { Show, type JSX } from "solid-js"; +import { t, currentLocale } from "../../i18n"; import { Box, ChevronLeft, @@ -38,6 +39,7 @@ type Props = { }; export default function WorkspaceRightSidebar(props: Props) { + const translate = (key: string) => t(key, currentLocale()); const mobile = () => props.mobile ?? false; const showSelection = () => props.showSelection ?? true; const closeMobile = () => props.onCloseMobile?.(); @@ -88,8 +90,8 @@ export default function WorkspaceRightSidebar(props: Props) { type="button" class="flex h-10 w-10 items-center justify-center rounded-[16px] text-gray-10 transition-colors hover:bg-dls-surface hover:text-dls-text" onClick={mobile() ? closeMobile : props.onToggleExpanded} - title={mobile() ? "Close sidebar" : props.expanded ? "Collapse sidebar" : "Expand sidebar"} - aria-label={mobile() ? "Close sidebar" : props.expanded ? "Collapse sidebar" : "Expand sidebar"} + title={mobile() ? translate("session.close_sidebar") : props.expanded ? translate("session.collapse_sidebar") : translate("session.expand_sidebar")} + aria-label={mobile() ? translate("session.close_sidebar") : props.expanded ? translate("session.collapse_sidebar") : translate("session.expand_sidebar")} >
{sidebarButton( - "Automations", + translate("session.sidebar_automations"), , showSelection() && props.tab === "scheduled", props.onOpenAutomations, @@ -114,7 +116,7 @@ export default function WorkspaceRightSidebar(props: Props) { props.onOpenSkills, )} {sidebarButton( - "Extensions", + translate("session.sidebar_extensions"), , showSelection() && (props.tab === "mcp" || props.tab === "plugins"), props.onOpenExtensions, diff --git a/apps/app/src/app/pages/dashboard.tsx b/apps/app/src/app/pages/dashboard.tsx index a5ed2b7dd..680d13c7b 100644 --- a/apps/app/src/app/pages/dashboard.tsx +++ b/apps/app/src/app/pages/dashboard.tsx @@ -74,6 +74,7 @@ import { Zap, } from "lucide-solid"; import type { Language } from "../../i18n"; +import { t, currentLocale } from "../../i18n"; export type DashboardViewProps = { tab: DashboardTab; @@ -373,6 +374,7 @@ type SkillsSetBundleV1 = { }; export default function DashboardView(props: DashboardViewProps) { + const translate = (key: string) => t(key, currentLocale()); const platform = usePlatform(); const webDeployment = createMemo(() => getOpenWorkDeployment() === "web"); const [mobileRightSidebarOpen, setMobileRightSidebarOpen] = createSignal(false); @@ -1132,8 +1134,8 @@ export default function DashboardView(props: DashboardViewProps) {