diff --git a/apps/app/src/app/components/mcp-auth-modal.tsx b/apps/app/src/app/components/mcp-auth-modal.tsx index 8d0b04a1b..2ffde2846 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(translate("mcp.auth.request_timed_out")); + setError("Request timed out."); return; } @@ -694,7 +694,7 @@ export default function McpAuthModal(props: McpAuthModalProps) {
-

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

+

Already Connected

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

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

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

+

Opening your browser

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

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

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

+

Authorize OpenWork

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

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

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

+

Return here when you're done

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

diff --git a/apps/app/src/app/components/question-modal.tsx b/apps/app/src/app/components/question-modal.tsx index 94448f56b..4b3b070bd 100644 --- a/apps/app/src/app/components/question-modal.tsx +++ b/apps/app/src/app/components/question-modal.tsx @@ -4,7 +4,6 @@ import type { QuestionInfo } from "@opencode-ai/sdk/v2/client"; import { Check, ChevronRight, HelpCircle } from "lucide-solid"; import Button from "./button"; -import { t } from "../../i18n"; export type QuestionModalProps = { open: boolean; @@ -139,10 +138,10 @@ export default function QuestionModal(props: QuestionModalProps) {

- {currentQuestion()!.header || t("common.question")} + {currentQuestion()!.header || "Question"}

- {t("question_modal.question_counter", undefined, { current: currentIndex() + 1, total: props.questions.length })} + Question {currentIndex() + 1} of {props.questions.length}
@@ -187,14 +186,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={t("question_modal.custom_answer_placeholder")} + placeholder="Type your answer here..." onKeyDown={(e) => { if (e.key === "Enter") { if (e.isComposing || e.keyCode === 229) return; @@ -210,15 +209,15 @@ export default function QuestionModal(props: QuestionModalProps) {
↑↓ - {t("common.navigate")} + navigate - {t("common.select")} + select
- {cmd.source === "skill" ? t("composer.skill_source") : cmd.source === "mcp" ? "MCP" : ""} + {cmd.source === "skill" ? "Skill" : cmd.source === "mcp" ? "MCP" : ""} @@ -1697,7 +1697,7 @@ export default function Composer(props: ComposerProps) {
{attachment.name}
- {attachment.kind === "image" ? t("composer.image_kind") : attachment.mimeType || t("composer.file_kind")} + {attachment.kind === "image" ? "Image" : attachment.mimeType || "File"}
} > @@ -1805,10 +1805,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={t("composer.stop")} + title="Stop" > - {t("composer.stop")} + Stop
@@ -1829,7 +1829,7 @@ export default function Composer(props: ComposerProps) { onClick={props.onToggleAgentPicker} disabled={props.busy} aria-expanded={props.agentPickerOpen} - title={t("composer.agent_label")} + title="Agent" > {props.agentLabel} @@ -1838,14 +1838,14 @@ export default function Composer(props: ComposerProps) {
- {t("composer.agent_label")} + Agent
event.preventDefault()}> {t("composer.loading_agents")}
+
Loading agents...
} > @@ -1860,7 +1860,7 @@ export default function Composer(props: ComposerProps) { props.onSelectAgent(null); }} > - {t("composer.default_agent")} + Default agent @@ -1931,7 +1931,7 @@ export default function Composer(props: ComposerProps) {
- {t("composer.behavior_label")} + Behavior
diff --git a/apps/app/src/app/components/session/context-panel.tsx b/apps/app/src/app/components/session/context-panel.tsx index c5339e5b7..40633ca7f 100644 --- a/apps/app/src/app/components/session/context-panel.tsx +++ b/apps/app/src/app/components/session/context-panel.tsx @@ -3,7 +3,6 @@ import { ChevronDown, Circle, File, Folder, Package } from "lucide-solid"; import { useConnections } from "../../connections/provider"; import { SUGGESTED_PLUGINS } from "../../constants"; -import { t } from "../../../i18n"; import type { McpStatus, SkillCard } from "../../types"; import { stripPluginVersion } from "../../utils/plugins"; @@ -107,19 +106,19 @@ const getSmartFileName = (files: string[], file: string): string => { }; const mcpStatusLabel = (status?: McpStatus, disabled?: boolean) => { - if (disabled) return t("context_panel.mcp_disabled"); - if (!status) return t("context_panel.mcp_disconnected"); + if (disabled) return "Disabled"; + if (!status) return "Disconnected"; switch (status.status) { case "connected": - return t("context_panel.mcp_connected"); + return "Connected"; case "needs_auth": - return t("context_panel.mcp_needs_auth"); + return "Needs auth"; case "needs_client_registration": - return t("context_panel.mcp_register_client"); + return "Register client"; case "failed": - return t("context_panel.mcp_failed"); + return "Failed"; default: - return t("context_panel.mcp_disconnected"); + return "Disconnected"; } }; @@ -152,7 +151,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")} > - {t("context_panel.context")} + Context
- {t("context_panel.working_files")} + Working files
{t("context_panel.none_yet")}
} + fallback={
None yet.
} > {(file) => { @@ -183,7 +182,7 @@ export default function ContextPanel(props: ContextPanelProps) { : "cursor-default opacity-70" }`.trim()} onClick={() => props.onFileClick?.(file)} - title={canOpen() ? t("context_panel.open_file", undefined, { path: displayPath() }) : displayPath()} + title={canOpen() ? `Open ${displayPath()}` : displayPath()} disabled={!canOpen()} > @@ -204,7 +203,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")} > - {t("context_panel.plugins")} + Plugins - {props.activePluginStatus ?? t("context_panel.no_plugins")} + {props.activePluginStatus ?? "No plugins loaded."}
} > @@ -254,7 +253,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")} > - {t("context_panel.mcp")} + MCP - {connections.mcpStatus() ?? t("context_panel.no_mcp")} + {connections.mcpStatus() ?? "No MCP servers loaded."}
} > @@ -304,7 +303,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")} > - {t("context_panel.skills")} + Skills - {props.skillsStatus ?? t("context_panel.no_skills")} + {props.skillsStatus ?? "No skills loaded."}
} > @@ -353,7 +352,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")} > - {t("context_panel.authorized_folders")} + Authorized folders {t("context_panel.none_yet")}
} + fallback={
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 f81f74a78..0d6b51ef8 100644 --- a/apps/app/src/app/components/session/inbox-panel.tsx +++ b/apps/app/src/app/components/session/inbox-panel.tsx @@ -2,7 +2,6 @@ import { For, Show, createEffect, createMemo, createSignal } from "solid-js"; import { Download, RefreshCw, UploadCloud } from "lucide-solid"; import { getOpenWorkDeployment } from "../../lib/openwork-deployment"; -import { t } from "../../../i18n"; import type { OpenworkInboxItem, OpenworkServerClient } from "../../lib/openwork-server"; import WebUnavailableSurface from "../web-unavailable-surface"; import { formatBytes, formatRelativeTime } from "../../utils"; @@ -49,7 +48,7 @@ export default function InboxPanel(props: InboxPanelProps) { }); const connected = createMemo(() => Boolean(props.client && (props.workspaceId ?? "").trim())); - const helperText = () => t("inbox_panel.helper_text"); + const helperText = "Share files with this worker from the app."; const visibleItems = createMemo(() => (items() ?? []).slice(0, maxPreview())); const hiddenCount = createMemo(() => Math.max(0, (items() ?? []).length - visibleItems().length)); @@ -73,7 +72,7 @@ export default function InboxPanel(props: InboxPanelProps) { setItems(result.items ?? []); } catch (err) { const message = - err instanceof Error ? err.message : t("inbox_panel.load_failed"); + err instanceof Error ? err.message : "Failed to load shared folder"; setError(message); setItems([]); } finally { @@ -85,7 +84,7 @@ export default function InboxPanel(props: InboxPanelProps) { const client = props.client; const workspaceId = (props.workspaceId ?? "").trim(); if (!client || !workspaceId) { - toast(t("inbox_panel.upload_needs_worker")); + toast("Connect to a worker to upload files to the shared folder."); return; } if (!files.length) return; @@ -94,15 +93,15 @@ export default function InboxPanel(props: InboxPanelProps) { setError(null); try { const label = files.length === 1 ? files[0]?.name ?? "file" : `${files.length} files`; - toast(t("inbox_panel.uploading_label", undefined, { label })); + toast(`Uploading ${label}...`); for (const file of files) { await client.uploadInbox(workspaceId, file); } - toast(t("inbox_panel.upload_success")); + toast("Uploaded to the shared folder."); await refresh(); } catch (err) { const message = - err instanceof Error ? err.message : t("inbox_panel.upload_failed"); + err instanceof Error ? err.message : "Shared folder upload failed"; setError(message); toast(message); } finally { @@ -114,9 +113,9 @@ export default function InboxPanel(props: InboxPanelProps) { const path = toInboxWorkspacePath(item); try { await navigator.clipboard.writeText(path); - toast(t("inbox_panel.copied_path", undefined, { path })); + toast(`Copied: ${path}`); } catch { - toast(t("inbox_panel.copy_failed")); + toast("Copy failed. Your browser may block clipboard access."); } }; @@ -124,12 +123,12 @@ export default function InboxPanel(props: InboxPanelProps) { const client = props.client; const workspaceId = (props.workspaceId ?? "").trim(); if (!client || !workspaceId) { - toast(t("inbox_panel.connect_to_download")); + toast("Connect to a worker to download shared files."); return; } const id = String(item.id ?? "").trim(); if (!id) { - toast(t("inbox_panel.missing_file_id")); + toast("Missing shared file id."); return; } @@ -145,7 +144,7 @@ export default function InboxPanel(props: InboxPanelProps) { a.remove(); URL.revokeObjectURL(url); } catch (err) { - const message = err instanceof Error ? err.message : t("inbox_panel.download_failed"); + const message = err instanceof Error ? err.message : "Download failed"; toast(message); } }; @@ -161,7 +160,7 @@ export default function InboxPanel(props: InboxPanelProps) {
- {t("inbox_panel.shared_folder")} + Shared folder
0}> @@ -172,8 +171,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={t("inbox_panel.refresh_tooltip")} - aria-label={t("inbox_panel.refresh_tooltip")} + title="Refresh shared folder" + aria-label="Refresh shared folder" disabled={!connected() || loading()} > @@ -215,14 +214,14 @@ export default function InboxPanel(props: InboxPanelProps) { if (files.length) void uploadFiles(files); }} disabled={uploading()} - title={connected() ? t("inbox_panel.drop_to_upload") : t("inbox_panel.connect_to_upload")} + title={connected() ? "Drop files here to upload" : "Connect to a worker to upload"} >
- {uploading() ? t("inbox_panel.uploading") : t("inbox_panel.upload_prompt")} + {uploading() ? "Uploading..." : "Drop files or click to upload"} - {helperText()} + {helperText}
@@ -235,8 +234,8 @@ export default function InboxPanel(props: InboxPanelProps) { when={visibleItems().length > 0} fallback={
- - {t("inbox_panel.no_files")} + + No shared files yet.
} @@ -276,8 +275,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={t("inbox_panel.download")} - aria-label={t("inbox_panel.download")} + title="Download" + aria-label="Download" disabled={!connected()} > @@ -289,7 +288,7 @@ export default function InboxPanel(props: InboxPanelProps) {
0}> -
{t("inbox_panel.showing_first", undefined, { count: maxPreview() })}
+
Showing first {maxPreview()}.
diff --git a/apps/app/src/app/components/session/message-list.tsx b/apps/app/src/app/components/session/message-list.tsx index 0644c97cd..489f804b5 100644 --- a/apps/app/src/app/components/session/message-list.tsx +++ b/apps/app/src/app/components/session/message-list.tsx @@ -31,7 +31,6 @@ import { } from "../../utils"; import PartView from "../part-view"; import { perfNow, recordPerfLog } from "../../lib/perf-log"; -import { t } from "../../../i18n"; export type MessageListProps = { messages: MessageWithParts[]; @@ -229,57 +228,57 @@ function toolHeadline(part: Part) { const description = pick("description"); if (description) return compactText(description); const command = pick("command", "cmd"); - return command ? compactText(t("message_list.tool_run_command", undefined, { command }), 48) : t("message_list.tool_run_command_fallback"); + return command ? compactText(`Run ${command}`, 48) : "Run command"; } if (tool === "read") { const file = target("filePath", "path", "file"); - return file ? t("message_list.tool_reviewed_file", undefined, { file }) : t("message_list.tool_reviewed_file_fallback"); + return file ? `Reviewed ${file}` : "Reviewed file"; } if (tool === "edit") { const file = target("filePath", "path", "file"); - return file ? t("message_list.tool_updated_file", undefined, { file }) : t("message_list.tool_updated_file_fallback"); + return file ? `Updated ${file}` : "Updated file"; } if (tool === "write" || tool === "apply_patch") { const file = target("filePath", "path", "file"); - return file ? t("message_list.tool_update_file", undefined, { file }) : t("message_list.tool_update_file_fallback"); + return file ? `Update ${file}` : "Update file"; } if (tool === "grep" || tool === "glob" || tool === "search") { const pattern = pick("pattern", "query"); - return pattern ? t("message_list.tool_searched_pattern", undefined, { pattern: compactText(pattern, 36) }) : t("message_list.tool_searched_code_fallback"); + return pattern ? `Searched ${compactText(pattern, 36)}` : "Searched code"; } if (tool === "list" || tool === "list_files") { const path = target("path"); - return path ? t("message_list.tool_reviewed_path", undefined, { path }) : t("message_list.tool_reviewed_files_fallback"); + return path ? `Reviewed ${path}` : "Reviewed files"; } if (tool === "task") { const description = pick("description"); if (description) return compactText(description); const agent = pick("subagent_type"); - return agent ? t("message_list.tool_delegate_agent", undefined, { agent }) : t("message_list.tool_delegate_task_fallback"); + return agent ? `Delegate ${agent}` : "Delegate task"; } if (tool === "todowrite") { - return t("message_list.tool_update_todo"); + return "Update todo list"; } if (tool === "todoread") { - return t("message_list.tool_read_todo"); + return "Read todo list"; } if (tool === "webfetch") { const url = pick("url"); - return url ? t("message_list.tool_checked_url", undefined, { url: compactText(url, 36) }) : t("message_list.tool_checked_web_fallback"); + return url ? `Checked ${compactText(url, 36)}` : "Checked web page"; } if (tool === "skill") { const name = pick("name"); - return name ? t("message_list.tool_load_skill_named", undefined, { name }) : t("message_list.tool_load_skill_fallback"); + return name ? `Load skill ${name}` : "Load skill"; } const fallback = tool @@ -696,17 +695,17 @@ export default function MessageList(props: MessageListProps) { const title = session()?.title?.trim(); if (title) return title; if (task().description) return task().description!; - if (task().agentType) return t("message_list.subagent_type_task", undefined, { agentType: task().agentType! }); - return t("message_list.subagent_session_fallback"); + if (task().agentType) return `${task().agentType} task`; + return "Subagent session"; }); const statusLabel = createMemo(() => { - if (loading()) return t("message_list.subagent_loading_transcript"); - if (streaming()) return t("message_list.subagent_running"); + if (loading()) return "Loading transcript"; + if (streaming()) return "Running"; if (childMessages().length > 0) { const count = childMessages().length; - return t("message_list.subagent_message_count", undefined, { count, plural: count === 1 ? "" : "s" }); + return `${count} message${count === 1 ? "" : "s"}`; } - return t("message_list.subagent_waiting_transcript"); + return "Waiting for transcript"; }); createEffect(() => { @@ -746,7 +745,7 @@ export default function MessageList(props: MessageListProps) { props.openSessionById?.(id); }} > - {t("message_list.open_session")} + Open session
@@ -754,7 +753,7 @@ export default function MessageList(props: MessageListProps) {
0} - fallback={
{t("message.waiting_subagent")}
} + fallback={
Waiting for the subagent transcript to arrive.
} > { if (rowProps.part.type !== "reasoning") return ""; @@ -855,13 +854,13 @@ export default function MessageList(props: MessageListProps) {
-
{t("message.tool_request_label")}
+
Request
{formatStructuredValue(toolInput())}
-
{t("message.tool_result_label")}
+
Result
{formatStructuredValue(toolOutput())}
@@ -1143,7 +1142,7 @@ export default function MessageList(props: MessageListProps) {
-
{t("dashboard.workspaces")}
+
Workspaces
0} fallback={
- {t("sidebar.no_workspaces")} + No workspaces in this session yet. Add one to get started.
} > @@ -357,7 +356,7 @@ export default function SessionSidebar(props: SidebarProps) { - {isSandboxWorkspace() ? t("workspace.sandbox_badge") : t("workspace.remote_badge")} + {isSandboxWorkspace() ? "Sandbox" : "Remote"}
@@ -374,11 +373,11 @@ export default function SessionSidebar(props: SidebarProps) { - {t("sidebar.needs_attention")} + Needs attention - {t("sidebar.switch")}}> - {t("sidebar.active")} + Switch}> + Active @@ -390,7 +389,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() ? t("sidebar.expand") : t("sidebar.collapse")} + title={collapsed() ? "Expand" : "Collapse"} > handleDragStart(event, group.workspace.id)} onDragEnd={handleDragEnd} @@ -425,7 +424,7 @@ export default function SessionSidebar(props: SidebarProps) { disabled={isActivelyConnecting()} > - {t("sidebar.edit_connection")} + Edit connection @@ -445,7 +444,7 @@ export default function SessionSidebar(props: SidebarProps) { disabled={isActivelyConnecting()} > - {t("sidebar.stop_sandbox")} + Stop sandbox
0} fallback={
- {t("sidebar.no_sessions_yet")} + No sessions yet.
} > @@ -520,7 +519,9 @@ export default function SessionSidebar(props: SidebarProps) { class="w-full px-3 py-2 rounded-lg text-xs text-gray-9 hover:text-gray-12 hover:bg-gray-2 transition-colors" onClick={() => toggleShowAllSessions(group.workspace.id)} > - {showingAll() ? t("sidebar.show_fewer") : t("sidebar.show_more", undefined, { count: sessions().length - MAX_SESSIONS_PREVIEW })} + {showingAll() + ? "Show fewer" + : `Show ${sessions().length - MAX_SESSIONS_PREVIEW} more`}
@@ -541,7 +542,7 @@ export default function SessionSidebar(props: SidebarProps) { onDrop={(event) => handleDrop(event, null)} > - {t("sidebar.add_workspace")} + Add workspace
@@ -554,7 +555,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")} > - {t("sidebar.progress")} + Progress - {t("session.new_task")} + New task
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 fe2d17a48..520e10c31 100644 --- a/apps/app/src/app/components/session/workspace-session-list.tsx +++ b/apps/app/src/app/components/session/workspace-session-list.tsx @@ -25,7 +25,6 @@ import { getWorkspaceTaskLoadErrorDisplay, isWindowsPlatform, } from "../../utils"; -import { t } from "../../../i18n"; type Props = { workspaceSessionGroups: WorkspaceSessionGroup[]; @@ -58,6 +57,7 @@ type Props = { }; const MAX_SESSIONS_PREVIEW = 6; +const COLLAPSED_SESSIONS_PREVIEW = MAX_SESSIONS_PREVIEW; type SessionListItem = WorkspaceSessionGroup["sessions"][number]; type FlattenedSessionRow = { session: SessionListItem; depth: number }; @@ -160,16 +160,16 @@ const workspaceLabel = (workspace: WorkspaceInfo) => workspace.openworkWorkspaceName?.trim() || workspace.name?.trim() || workspace.path?.trim() || - t("workspace_list.workspace_fallback"); + "Workspace"; const workspaceKindLabel = (workspace: WorkspaceInfo) => workspace.workspaceType === "remote" ? workspace.sandboxBackend === "docker" || Boolean(workspace.sandboxRunId?.trim()) || Boolean(workspace.sandboxContainerName?.trim()) - ? t("workspace.sandbox_badge") - : t("workspace.remote_badge") - : t("workspace.local_badge"); + ? "Sandbox" + : "Remote" + : "Local"; const WORKSPACE_SWATCHES = ["#2563eb", "#5a67d8", "#f97316", "#10b981"]; @@ -184,9 +184,9 @@ const workspaceSwatchColor = (seed: string) => { }; export default function WorkspaceSessionList(props: Props) { - const revealLabel = () => isWindowsPlatform() - ? t("workspace_list.reveal_explorer") - : t("workspace_list.reveal_finder"); + const revealLabel = isWindowsPlatform() + ? "Reveal in Explorer" + : "Reveal in Finder"; const [expandedWorkspaceIds, setExpandedWorkspaceIds] = createSignal< Set >(new Set()); @@ -238,8 +238,13 @@ export default function WorkspaceSessionList(props: Props) { expandWorkspace(props.selectedWorkspaceId); }); - const previewCount = (workspaceId: string) => - previewCountByWorkspaceId()[workspaceId] ?? MAX_SESSIONS_PREVIEW; + const previewCount = (workspaceId: string) => { + const base = + previewCountByWorkspaceId()[workspaceId] ?? MAX_SESSIONS_PREVIEW; + return isWorkspaceExpanded(workspaceId) + ? base + : Math.min(COLLAPSED_SESSIONS_PREVIEW, base); + }; const previewSessions = ( workspaceId: string, @@ -282,7 +287,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 ? t("workspace_list.show_more", undefined, { count: nextCount }) : t("workspace_list.show_more_fallback"); + return nextCount > 0 ? `Show ${nextCount} more` : "Show more"; }; createEffect(() => { @@ -406,7 +411,7 @@ export default function WorkspaceSessionList(props: Props) {
@@ -477,7 +482,7 @@ export default function WorkspaceSessionList(props: Props) { props.onOpenDeleteSession?.(); }} > - {t("workspace_list.delete_session")} + Delete session @@ -519,9 +524,9 @@ export default function WorkspaceSessionList(props: Props) { getWorkspaceTaskLoadErrorDisplay(workspace(), group.error); const statusLabel = () => { if (group.status === "error") return taskLoadError().label; - if (isConnectionActionBusy()) return t("workspace_list.connecting"); + if (isConnectionActionBusy()) return "Connecting"; if (!props.developerMode) return ""; - if (props.selectedWorkspaceId === workspace().id) return t("workspace.selected"); + if (props.selectedWorkspaceId === workspace().id) return "Selected"; return workspaceKindLabel(workspace()); }; const statusTone = () => { @@ -595,7 +600,7 @@ export default function WorkspaceSessionList(props: Props) { props.onCreateTaskInWorkspace(workspace().id); }} disabled={props.newTaskDisabled} - aria-label={t("session.new_task")} + aria-label="New task" > @@ -611,7 +616,7 @@ export default function WorkspaceSessionList(props: Props) { : workspace().id, ); }} - aria-label={t("workspace_list.workspace_options")} + aria-label="Workspace options" > @@ -622,8 +627,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) - ? t("sidebar.collapse") - : t("sidebar.expand") + ? "Collapse" + : "Expand" } onClick={(event) => { event.stopPropagation(); @@ -654,7 +659,7 @@ export default function WorkspaceSessionList(props: Props) { setWorkspaceMenuId(null); }} > - {t("workspace_list.edit_name")} + Edit name @@ -691,7 +696,7 @@ export default function WorkspaceSessionList(props: Props) { }} disabled={isConnectionActionBusy()} > - {t("workspace_list.recover")} + Recover - -
-
- 0} - fallback={ - -
- {taskLoadError().message} -
-
- } +
+
+ 0}> + - + renderSessionRow( workspace().id, - group.sessions, + row, tree, forcedExpandedSessionIds, )} - > - {(row) => - renderSessionRow( + + + + previewCount(workspace().id) + } + > + + + + } + > + 0} + fallback={ + +
- - {t("workspace.no_tasks")} - - - + {taskLoadError().message} +
+ } + > + + {(row) => + renderSessionRow( + workspace().id, + row, + tree, + forcedExpandedSessionIds, + )} + - - previewCount(workspace().id) + + + + + + previewCount(workspace().id) + } + > + - + ) + } + > + {showMoreLabel( + workspace().id, + getRootSessions(group.sessions).length, + )} + - } - > -
- {t("workspace.loading_tasks")} -
-
-
+ + } + > +
+ Loading tasks... +
+ +
-
+
); }} @@ -845,7 +895,7 @@ export default function WorkspaceSessionList(props: Props) { onClick={props.onOpenCreateWorkspace} > - {t("workspace_list.add_workspace")} + Add workspace diff --git a/apps/app/src/app/components/status-bar.tsx b/apps/app/src/app/components/status-bar.tsx index d2013cbd1..8d229bf5c 100644 --- a/apps/app/src/app/components/status-bar.tsx +++ b/apps/app/src/app/components/status-bar.tsx @@ -1,7 +1,6 @@ import { Show, createMemo } from "solid-js"; import { MessageCircle, Settings } from "lucide-solid"; -import { t } from "../../i18n"; import { useConnections } from "../connections/provider"; import type { OpenworkServerStatus } from "../lib/openwork-server"; @@ -48,19 +47,19 @@ export default function StatusBar(props: StatusBarProps) { if (props.clientConnected) { const detailBits: string[] = []; if (providers > 0) { - detailBits.push(t("status.providers_connected", undefined, { count: providers, plural: providers === 1 ? "" : "s" })); + detailBits.push(`${providers} provider${providers === 1 ? "" : "s"} connected`); } if (mcp > 0) { - detailBits.push(t("status.mcp_connected", undefined, { count: mcp })); + detailBits.push(`${mcp} MCP connected`); } if (!detailBits.length) { - detailBits.push(t("status.ready_for_tasks")); + detailBits.push("Ready for new tasks"); } if (props.developerMode) { - detailBits.push(t("status.developer_mode")); + detailBits.push("Developer mode"); } return { - label: t("status.openwork_ready"), + label: "OpenWork Ready", detail: detailBits.join(" · "), dotClass: "bg-green-9", pingClass: "bg-green-9/45 animate-ping", @@ -70,11 +69,11 @@ export default function StatusBar(props: StatusBarProps) { if (props.openworkServerStatus === "limited") { return { - label: t("status.limited_mode"), + label: "Limited Mode", detail: mcp > 0 - ? t("status.limited_mcp_hint", undefined, { count: mcp }) - : t("status.limited_hint"), + ? `${mcp} MCP connected · reconnect for full features` + : "Reconnect to restore full OpenWork features", dotClass: "bg-amber-9", pingClass: "bg-amber-9/35", pulse: false, @@ -82,8 +81,8 @@ export default function StatusBar(props: StatusBarProps) { } return { - label: t("status.disconnected_label"), - detail: t("status.disconnected_hint"), + label: "Disconnected", + detail: "Open settings to reconnect", dotClass: "bg-red-9", pingClass: "bg-red-9/35", pulse: false, @@ -109,19 +108,19 @@ export default function StatusBar(props: StatusBarProps) { type="button" class="inline-flex h-8 items-center gap-1.5 rounded-md px-2 text-dls-secondary transition-colors hover:bg-dls-hover hover:text-dls-text" onClick={props.onSendFeedback} - title={t("status.send_feedback")} - aria-label={t("status.send_feedback")} + title="Send feedback" + aria-label="Send feedback" > - {t("status.feedback")} + Feedback diff --git a/apps/app/src/app/context/automations.ts b/apps/app/src/app/context/automations.ts index 8ad926ffc..77966544d 100644 --- a/apps/app/src/app/context/automations.ts +++ b/apps/app/src/app/context/automations.ts @@ -5,7 +5,6 @@ import { schedulerDeleteJob, schedulerListJobs } from "../lib/tauri"; import { isTauriRuntime } from "../utils"; import { createWorkspaceContextKey } from "./workspace-context"; import type { OpenworkServerStore } from "../connections/openwork-server-store"; -import { t } from "../../i18n"; export type AutomationsStore = ReturnType; @@ -34,10 +33,10 @@ const buildCreateAutomationPrompt = ( const schedule = input.schedule.trim(); const prompt = normalizeSentence(input.prompt); if (!schedule) { - return { ok: false, error: t("automations.schedule_required") }; + return { ok: false, error: "Schedule is required." }; } if (!prompt) { - return { ok: false, error: t("automations.prompt_required") }; + return { ok: false, error: "Prompt is required." }; } const workdir = (input.workdir ?? "").trim(); const nameSegment = name ? ` named \"${name}\"` : ""; @@ -59,7 +58,7 @@ const buildRunAutomationPrompt = ( if (job.run?.prompt || job.prompt) { const promptBody = (job.run?.prompt ?? job.prompt ?? "").trim(); if (!promptBody) { - return { ok: false, error: t("automations.prompt_empty") }; + return { ok: false, error: "Automation prompt is empty." }; } return { ok: true, @@ -137,10 +136,10 @@ export function createAutomationsStore(options: { if (scheduledJobsContextKey() !== requestContextKey) return "skipped"; const status = options.openworkServer.openworkServerStatus() === "disconnected" - ? t("automations.server_unavailable") + ? "OpenWork server unavailable. Connect to sync scheduled tasks." : options.openworkServer.openworkServerStatus() === "limited" - ? t("automations.server_needs_token") - : t("automations.server_not_ready"); + ? "OpenWork server needs a token to load scheduled tasks." + : "OpenWork server not ready."; setScheduledJobsStatus(status); return "unavailable"; } @@ -156,7 +155,7 @@ export function createAutomationsStore(options: { } catch (error) { if (scheduledJobsContextKey() !== requestContextKey) return "skipped"; const message = error instanceof Error ? error.message : String(error); - setScheduledJobsStatus(message || t("automations.failed_to_load")); + setScheduledJobsStatus(message || "Failed to load scheduled tasks."); return "error"; } finally { setScheduledJobsBusy(false); @@ -181,7 +180,7 @@ export function createAutomationsStore(options: { } catch (error) { if (scheduledJobsContextKey() !== requestContextKey) return "skipped"; const message = error instanceof Error ? error.message : String(error); - setScheduledJobsStatus(message || t("automations.failed_to_load")); + setScheduledJobsStatus(message || "Failed to load scheduled tasks."); return "error"; } finally { setScheduledJobsBusy(false); @@ -193,7 +192,7 @@ export function createAutomationsStore(options: { const client = options.openworkServer.openworkServerClient(); const workspaceId = (options.runtimeWorkspaceId() ?? "").trim(); if (!client || !workspaceId) { - throw new Error(t("automations.server_unavailable")); + throw new Error("OpenWork server unavailable. Connect to sync scheduled tasks."); } const response = await client.deleteScheduledJob(workspaceId, name); setScheduledJobs((current) => current.filter((entry) => entry.slug !== response.job.slug)); @@ -201,7 +200,7 @@ export function createAutomationsStore(options: { } if (!isTauriRuntime()) { - throw new Error(t("automations.desktop_required")); + throw new Error("Scheduled tasks require the desktop app."); } const root = options.selectedWorkspaceRoot().trim(); const job = await schedulerDeleteJob(name, root || undefined); diff --git a/apps/app/src/app/context/providers/store.ts b/apps/app/src/app/context/providers/store.ts index 4396c211a..f2dae9991 100644 --- a/apps/app/src/app/context/providers/store.ts +++ b/apps/app/src/app/context/providers/store.ts @@ -2,7 +2,7 @@ import { createMemo, createSignal, type Accessor } from "solid-js"; import type { ProviderAuthAuthorization, ProviderListResponse } from "@opencode-ai/sdk/v2/client"; -import { t } from "../../../i18n"; +import { t, currentLocale } from "../../../i18n"; import { unwrap, waitForHealthy } from "../../lib/opencode"; import type { Client, ProviderListItem, WorkspaceDisplay } from "../../types"; import { safeStringify } from "../../utils"; @@ -70,7 +70,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { const assertNoClientError = (result: unknown) => { const maybe = result as { error?: unknown } | null | undefined; if (!maybe || maybe.error === undefined) return; - throw new Error(describeProviderError(maybe.error, t("providers.request_failed"))); + throw new Error(describeProviderError(maybe.error, t("app.error_request_failed", currentLocale()))); }; const describeProviderError = (error: unknown, fallback: string) => { @@ -125,9 +125,9 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { const generic = raw && /^unknown\s+error$/i.test(raw); const heading = (() => { - if (status === 401 || status === 403) return t("providers.auth_failed"); - if (status === 429) return t("providers.rate_limit_exceeded"); - if (provider) return t("providers.provider_error", undefined, { provider }); + if (status === 401 || status === 403) return t("app.error_auth_failed", currentLocale()); + if (status === 429) return t("app.error_rate_limit", currentLocale()); + if (provider) return `Provider error (${provider})`; return fallback; })(); @@ -167,7 +167,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { if (!Array.isArray(provider.env) || provider.env.length === 0) continue; const existing = merged[id] ?? []; if (existing.some((method) => method.type === "api")) continue; - merged[id] = [...existing, { type: "api", label: t("providers.api_key_label") }]; + merged[id] = [...existing, { type: "api", label: "API key" }]; } for (const [id, providerMethods] of Object.entries(merged)) { const provider = availableProviders.find((item) => item.id === id); @@ -188,7 +188,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { const loadProviderAuthMethods = async (workerType: "local" | "remote") => { const c = options.client(); if (!c) { - throw new Error(t("providers.not_connected")); + throw new Error(t("app.error_not_connected", currentLocale())); } const methods = unwrap(await c.provider.auth()); return buildProviderAuthMethods( @@ -205,7 +205,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { setProviderAuthError(null); const c = options.client(); if (!c) { - throw new Error(t("providers.not_connected")); + throw new Error(t("app.error_not_connected", currentLocale())); } try { const cachedMethods = providerAuthMethods(); @@ -214,17 +214,17 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { : await loadProviderAuthMethods(providerAuthWorkerType()); const providerIds = Object.keys(authMethods).sort(); if (!providerIds.length) { - throw new Error(t("providers.no_providers_available")); + throw new Error("No providers available"); } const resolved = providerId?.trim() ?? ""; if (!resolved) { - throw new Error(t("providers.provider_id_required")); + throw new Error("Provider ID is required"); } const methods = authMethods[resolved]; if (!methods || !methods.length) { - throw new Error(`${t("providers.unknown_provider")}: ${resolved}`); + throw new Error(`Unknown provider: ${resolved}`); } const oauthIndex = @@ -232,12 +232,12 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { ? methodIndex : methods.find((method) => method.type === "oauth")?.methodIndex ?? -1; if (oauthIndex === -1) { - throw new Error(`${t("providers.no_oauth_prefix")} ${resolved}. ${t("providers.use_api_key_suffix")}`); + throw new Error(`No OAuth flow available for ${resolved}. Use an API key instead.`); } const selectedMethod = methods.find((method) => method.methodIndex === oauthIndex); if (!selectedMethod || selectedMethod.type !== "oauth") { - throw new Error(`${t("providers.not_oauth_flow_prefix")} ${resolved}.`); + throw new Error(`Selected auth method is not an OAuth flow for ${resolved}.`); } const auth = unwrap(await c.provider.oauth.authorize({ providerID: resolved, method: oauthIndex })); @@ -246,7 +246,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { authorization: auth, }; } catch (error) { - const message = describeProviderError(error, t("providers.connect_failed")); + const message = describeProviderError(error, "Failed to connect provider"); setProviderAuthError(message); throw error instanceof Error ? error : new Error(message); } @@ -310,16 +310,16 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { setProviderAuthError(null); const c = options.client(); if (!c) { - throw new Error(t("providers.not_connected")); + throw new Error(t("app.error_not_connected", currentLocale())); } const resolved = providerId?.trim(); if (!resolved) { - throw new Error(t("providers.provider_id_required")); + throw new Error("Provider ID is required"); } if (!Number.isInteger(methodIndex) || methodIndex < 0) { - throw new Error(t("providers.oauth_method_required")); + throw new Error("OAuth method is required"); } const waitForProviderConnection = async (timeoutMs = 15_000, pollMs = 2_000) => { @@ -354,26 +354,26 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { const updated = await refreshProviders({ dispose: true }); const connectedNow = Array.isArray(updated?.connected) && updated.connected.includes(resolved); if (connectedNow) { - return { connected: true, message: `${t("status.connected")} ${resolved}` }; + return { connected: true, message: `Connected ${resolved}` }; } const connected = await waitForProviderConnection(); if (connected) { - return { connected: true, message: `${t("status.connected")} ${resolved}` }; + return { connected: true, message: `Connected ${resolved}` }; } return { connected: false, pending: true }; } catch (error) { if (isPendingOauthError(error)) { const updated = await refreshProviders({ dispose: true }); if (Array.isArray(updated?.connected) && updated.connected.includes(resolved)) { - return { connected: true, message: `${t("status.connected")} ${resolved}` }; + return { connected: true, message: `Connected ${resolved}` }; } const connected = await waitForProviderConnection(); if (connected) { - return { connected: true, message: `${t("status.connected")} ${resolved}` }; + return { connected: true, message: `Connected ${resolved}` }; } return { connected: false, pending: true }; } - const message = describeProviderError(error, t("providers.oauth_failed")); + const message = describeProviderError(error, "Failed to complete OAuth"); setProviderAuthError(message); throw error instanceof Error ? error : new Error(message); } @@ -383,12 +383,12 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { setProviderAuthError(null); const c = options.client(); if (!c) { - throw new Error(t("providers.not_connected")); + throw new Error(t("app.error_not_connected", currentLocale())); } const trimmed = apiKey.trim(); if (!trimmed) { - throw new Error(t("providers.api_key_required")); + throw new Error("API key is required"); } try { @@ -397,9 +397,9 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { auth: { type: "api", key: trimmed }, }); await refreshProviders({ dispose: true }); - return `${t("status.connected")} ${providerId}`; + return `Connected ${providerId}`; } catch (error) { - const message = describeProviderError(error, t("providers.save_api_key_failed")); + const message = describeProviderError(error, "Failed to save API key"); setProviderAuthError(message); throw error instanceof Error ? error : new Error(message); } @@ -409,12 +409,12 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { setProviderAuthError(null); const c = options.client(); if (!c) { - throw new Error(t("providers.not_connected")); + throw new Error(t("app.error_not_connected", currentLocale())); } const resolved = providerId.trim(); if (!resolved) { - throw new Error(t("providers.provider_id_required")); + throw new Error("Provider ID is required"); } const provider = options.providers().find((entry) => entry.id === resolved) as @@ -447,7 +447,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { return; } - throw new Error(t("providers.removal_unsupported")); + throw new Error("Provider auth removal is not supported by this client."); }; const disableProvider = async () => { @@ -492,18 +492,18 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { } if (!Array.isArray(updated?.connected) || !updated.connected.includes(resolved)) { return disabled - ? `${t("providers.disconnected_prefix")} ${resolved} ${t("providers.disabled_in_config_suffix")}` - : `${t("providers.disconnected_prefix")} ${resolved}.`; + ? `Disconnected ${resolved} and disabled it in OpenCode config.` + : `Disconnected ${resolved}.`; } } if (Array.isArray(updated?.connected) && updated.connected.includes(resolved)) { - return `Removed stored credentials for ${resolved}${t("providers.still_connected_suffix")}`; + return `Removed stored credentials for ${resolved}, but the worker still reports it as connected. Clear any remaining API key or OAuth credentials and restart the worker to fully disconnect.`; } removeProviderFromState(resolved); - return `${t("providers.disconnected_prefix")} ${resolved}`; + return `Disconnected ${resolved}`; } catch (error) { - const message = describeProviderError(error, t("providers.disconnect_failed")); + const message = describeProviderError(error, "Failed to disconnect provider"); setProviderAuthError(message); throw error instanceof Error ? error : new Error(message); } @@ -524,7 +524,7 @@ export function createProvidersStore(options: CreateProvidersStoreOptions) { } catch (error) { setProviderAuthPreferredProviderId(null); setProviderAuthReturnFocusTarget("none"); - const message = describeProviderError(error, t("providers.load_failed")); + const message = describeProviderError(error, "Failed to load providers"); setProviderAuthError(message); throw error; } finally { diff --git a/apps/app/src/app/pages/automations.tsx b/apps/app/src/app/pages/automations.tsx index 4ceb54393..600aacb38 100644 --- a/apps/app/src/app/pages/automations.tsx +++ b/apps/app/src/app/pages/automations.tsx @@ -4,7 +4,6 @@ import type { ScheduledJob } from "../types"; import { useAutomations } from "../automations/provider"; import { usePlatform } from "../context/platform"; import { formatRelativeTime, isTauriRuntime } from "../utils"; -import { t } from "../../i18n"; import { BookOpen, @@ -52,7 +51,7 @@ const pillGhostClass = `${pillButtonClass} border border-dls-border bg-dls-surfa const tagClass = "inline-flex items-center rounded-md border border-dls-border bg-dls-hover px-2 py-1 text-[11px] text-dls-secondary"; -const DEFAULT_AUTOMATION_NAME = () => t("scheduled.default_automation_name"); +const DEFAULT_AUTOMATION_NAME = "Daily bug scan"; const DEFAULT_AUTOMATION_PROMPT = "Scan recent commits and flag riskier diffs with the most important follow-ups."; const DEFAULT_SCHEDULE_TIME = "09:00"; @@ -62,79 +61,79 @@ const DEFAULT_INTERVAL_HOURS = 6; const automationTemplates: AutomationTemplate[] = [ { icon: Calendar, - name: t("scheduled.tpl_daily_planning_name"), - description: t("scheduled.tpl_daily_planning_desc"), + name: "Daily planning brief", + description: "Build a focused plan from your tasks and calendar before the day starts.", prompt: "Review my pending tasks and calendar, then draft a practical plan for today with top priorities and one follow-up reminder.", scheduleMode: "daily", scheduleTime: "08:30", scheduleDays: ["mo", "tu", "we", "th", "fr"], - badge: t("scheduled.badge_weekday_morning"), + badge: "Weekday morning", }, { icon: BookOpen, - name: t("scheduled.tpl_inbox_zero_name"), - description: t("scheduled.tpl_inbox_zero_desc"), + name: "Inbox zero helper", + description: "Summarize unread messages and suggest concise replies for the top threads.", prompt: "Summarize unread inbox messages, suggest priority order, and draft concise reply options for the top conversations.", scheduleMode: "daily", scheduleTime: "17:30", scheduleDays: ["mo", "tu", "we", "th", "fr"], - badge: t("scheduled.badge_end_of_day"), + badge: "End-of-day", }, { icon: MessageSquare, - name: t("scheduled.tpl_meeting_prep_name"), - description: t("scheduled.tpl_meeting_prep_desc"), + name: "Meeting prep notes", + description: "Generate prep bullets, context, and unblockers for tomorrow's meetings.", prompt: "Prepare meeting briefs for tomorrow with context, talking points, and questions to unblock decisions.", scheduleMode: "daily", scheduleTime: "18:00", scheduleDays: ["mo", "tu", "we", "th", "fr"], - badge: t("scheduled.badge_weekday_evening"), + badge: "Weekday evening", }, { icon: TrendingUp, - name: t("scheduled.tpl_weekly_wins_name"), - description: t("scheduled.tpl_weekly_wins_desc"), + name: "Weekly wins recap", + description: "Turn the week into wins, blockers, and clear next steps to share.", prompt: "Summarize the week into wins, blockers, and clear next steps I can share with the team.", scheduleMode: "daily", scheduleTime: "16:00", scheduleDays: ["fr"], - badge: t("scheduled.badge_friday_wrapup"), + badge: "Friday wrap-up", }, { icon: Trophy, - name: t("scheduled.tpl_learning_digest_name"), - description: t("scheduled.tpl_learning_digest_desc"), + name: "Learning digest", + description: "Collect saved links and notes into a weekly digest with actions.", prompt: "Collect my saved links and notes, then draft a weekly learning digest with key ideas and follow-up actions.", scheduleMode: "daily", scheduleTime: "10:00", scheduleDays: ["su"], - badge: t("scheduled.badge_weekend_review"), + badge: "Weekend review", }, { icon: Brain, - name: t("scheduled.tpl_habit_checkin_name"), - description: t("scheduled.tpl_habit_checkin_desc"), + name: "Habit check-in", + description: "Run a quick accountability check-in and suggest one concrete next action.", prompt: "Ask me for a quick progress check-in, capture blockers, and suggest one concrete next action.", scheduleMode: "interval", intervalHours: 6, - badge: t("scheduled.badge_every_few_hours"), + badge: "Every few hours", }, ]; const dayOptions = [ - { id: "mo", label: () => t("scheduled.day_mon"), cron: "1" }, - { id: "tu", label: () => t("scheduled.day_tue"), cron: "2" }, - { id: "we", label: () => t("scheduled.day_wed"), cron: "3" }, - { id: "th", label: () => t("scheduled.day_thu"), cron: "4" }, - { id: "fr", label: () => t("scheduled.day_fri"), cron: "5" }, - { id: "sa", label: () => t("scheduled.day_sat"), cron: "6" }, - { id: "su", label: () => t("scheduled.day_sun"), cron: "0" }, + { id: "mo", label: "Mo", cron: "1" }, + { id: "tu", label: "Tu", cron: "2" }, + { id: "we", label: "We", cron: "3" }, + { id: "th", label: "Th", cron: "4" }, + { id: "fr", label: "Fr", cron: "5" }, + { id: "sa", label: "Sa", cron: "6" }, + { id: "su", label: "Su", cron: "0" }, ]; export type AutomationsViewProps = { @@ -180,9 +179,9 @@ const parseCronNumbers = (value: string) => { const humanizeCron = (cron: string) => { const parts = cron.trim().split(/\s+/); - if (parts.length < 5) return t("scheduled.custom_schedule"); + if (parts.length < 5) return "Custom schedule"; const [minuteRaw, hourRaw, dom, mon, dowRaw] = parts; - if (!minuteRaw || !hourRaw || !dom || !mon || !dowRaw) return t("scheduled.custom_schedule"); + if (!minuteRaw || !hourRaw || !dom || !mon || !dowRaw) return "Custom schedule"; if ( minuteRaw === "0" && @@ -193,19 +192,19 @@ const humanizeCron = (cron: string) => { ) { const interval = Number.parseInt(hourRaw.slice(2), 10); if (Number.isFinite(interval) && interval > 0) { - return interval === 1 ? t("scheduled.every_hour") : t("scheduled.every_n_hours", undefined, { interval }); + return interval === 1 ? "Every hour" : `Every ${interval} hours`; } } const hour = Number.parseInt(hourRaw, 10); const minute = Number.parseInt(minuteRaw, 10); - if (!Number.isFinite(hour) || !Number.isFinite(minute)) return t("scheduled.custom_schedule"); - if (dom !== "*" || mon !== "*") return t("scheduled.custom_schedule"); + if (!Number.isFinite(hour) || !Number.isFinite(minute)) return "Custom schedule"; + if (dom !== "*" || mon !== "*") return "Custom schedule"; const timeLabel = `${pad2(hour)}:${pad2(minute)}`; if (dowRaw === "*") { - return t("scheduled.every_day_at", undefined, { time: timeLabel }); + return `Every day at ${timeLabel}`; } const days = parseCronNumbers(dowRaw); @@ -214,28 +213,28 @@ const humanizeCron = (cron: string) => { const weekdayDays = [1, 2, 3, 4, 5]; const weekendDays = [0, 6]; - if (allDays.every((d) => normalized.has(d))) return t("scheduled.every_day_at", undefined, { time: timeLabel }); + if (allDays.every((d) => normalized.has(d))) return `Every day at ${timeLabel}`; if ( weekdayDays.every((d) => normalized.has(d)) && !weekendDays.some((d) => normalized.has(d)) ) { - return t("scheduled.weekdays_at", undefined, { time: timeLabel }); + return `Weekdays at ${timeLabel}`; } if ( weekendDays.every((d) => normalized.has(d)) && !weekdayDays.some((d) => normalized.has(d)) ) { - return t("scheduled.weekends_at", undefined, { time: timeLabel }); + return `Weekends at ${timeLabel}`; } const labels: Record = { - 0: t("scheduled.day_sun"), - 1: t("scheduled.day_mon"), - 2: t("scheduled.day_tue"), - 3: t("scheduled.day_wed"), - 4: t("scheduled.day_thu"), - 5: t("scheduled.day_fri"), - 6: t("scheduled.day_sat"), + 0: "Sun", + 1: "Mon", + 2: "Tue", + 3: "Wed", + 4: "Thu", + 5: "Fri", + 6: "Sat", }; const list = Array.from(normalized) @@ -244,7 +243,7 @@ const humanizeCron = (cron: string) => { .map((d) => labels[d] ?? String(d)) .join(", "); - return list ? t("scheduled.days_at", undefined, { days: list, time: timeLabel }) : t("scheduled.at_time", undefined, { time: timeLabel }); + return list ? `${list} at ${timeLabel}` : `At ${timeLabel}`; }; const buildCronFromDaily = (timeValue: string, days: string[]) => { @@ -277,20 +276,19 @@ const taskSummary = (job: ScheduledJob) => { return `${run.command}${args}`; } const prompt = run?.prompt ?? job.prompt; - return prompt?.trim() || t("scheduled.task_summary_no_prompt"); + return prompt?.trim() || "No prompt or command configured yet."; }; const toRelative = (value?: string | null) => { - if (!value) return t("scheduled.never"); + if (!value) return "Never"; const parsed = Date.parse(value); - if (!Number.isFinite(parsed)) return t("scheduled.never"); + if (!Number.isFinite(parsed)) return "Never"; return formatRelativeTime(parsed); }; const templateScheduleLabel = (template: AutomationTemplate) => { if (template.scheduleMode === "interval") { - const interval = template.intervalHours ?? DEFAULT_INTERVAL_HOURS; - return interval === 1 ? t("scheduled.every_hour") : t("scheduled.every_n_hours", undefined, { interval }); + return `Every ${template.intervalHours ?? DEFAULT_INTERVAL_HOURS} hours`; } return humanizeCron( buildCronFromDaily( @@ -301,10 +299,10 @@ const templateScheduleLabel = (template: AutomationTemplate) => { }; const statusLabel = (status?: string | null) => { - if (!status) return t("scheduled.not_run_yet"); - if (status === "running") return t("scheduled.running_status"); - if (status === "success") return t("scheduled.success_status"); - if (status === "failed") return t("scheduled.failed_status"); + if (!status) return "Not run yet"; + if (status === "running") return "Running"; + if (status === "success") return "Healthy"; + if (status === "failed") return "Needs attention"; return status; }; @@ -346,10 +344,10 @@ const TemplateCard = (props: {
- {t("scheduled.template_badge")} + Template
@@ -389,22 +387,22 @@ const JobCard = (props: {
-
{t("scheduled.last_run_prefix")} {toRelative(props.job.lastRunAt)}
-
{t("scheduled.created_prefix")} {toRelative(props.job.createdAt)}
+
Last run {toRelative(props.job.lastRunAt)}
+
Created {toRelative(props.job.createdAt)}
- {t("scheduled.filter_scheduled")} + Scheduled
@@ -427,7 +425,7 @@ export default function AutomationsView(props: AutomationsViewProps) { const [createModalOpen, setCreateModalOpen] = createSignal(false); const [createBusy, setCreateBusy] = createSignal(false); const [createError, setCreateError] = createSignal(null); - const [automationName, setAutomationName] = createSignal(DEFAULT_AUTOMATION_NAME()); + const [automationName, setAutomationName] = createSignal(DEFAULT_AUTOMATION_NAME); const [automationPrompt, setAutomationPrompt] = createSignal(DEFAULT_AUTOMATION_PROMPT); const [scheduleMode, setScheduleMode] = createSignal("daily"); const [scheduleTime, setScheduleTime] = createSignal(DEFAULT_SCHEDULE_TIME); @@ -446,7 +444,7 @@ export default function AutomationsView(props: AutomationsViewProps) { }; const resetDraft = (template?: AutomationTemplate) => { - setAutomationName(template?.name ?? DEFAULT_AUTOMATION_NAME()); + setAutomationName(template?.name ?? DEFAULT_AUTOMATION_NAME); setAutomationPrompt(template?.prompt ?? DEFAULT_AUTOMATION_PROMPT); setScheduleMode(template?.scheduleMode ?? "daily"); setScheduleTime(template?.scheduleTime ?? DEFAULT_SCHEDULE_TIME); @@ -471,25 +469,25 @@ export default function AutomationsView(props: AutomationsViewProps) { ); const sourceLabel = createMemo(() => - automations.jobsSource() === "remote" ? t("scheduled.source_remote") : t("scheduled.source_local"), + automations.jobsSource() === "remote" ? "OpenWork server" : "Local scheduler", ); const sourceDescription = createMemo(() => automations.jobsSource() === "remote" - ? t("scheduled.subtitle_remote") - : t("scheduled.subtitle_local"), + ? "Scheduled tasks that are currently synced from the connected OpenWork server." + : "Scheduled tasks that are currently registered on this device through the local scheduler.", ); const supportNote = createMemo(() => { if (automations.jobsSource() === "remote") return null; - if (!isTauriRuntime()) return t("scheduled.desktop_required"); + if (!isTauriRuntime()) return "Automations require the desktop app or a connected OpenWork server."; if (!props.schedulerInstalled || schedulerInstallRequested()) return null; return null; }); const lastUpdatedLabel = createMemo(() => { lastUpdatedNow(); - if (!automations.jobsUpdatedAt()) return t("scheduled.not_synced_yet"); + if (!automations.jobsUpdatedAt()) return "Not synced yet"; return formatRelativeTime(automations.jobsUpdatedAt() as number); }); @@ -550,7 +548,7 @@ export default function AutomationsView(props: AutomationsViewProps) { setSchedulerInstallRequested(true); try { await Promise.resolve(props.addPlugin("opencode-scheduler")); - showToast(t("scheduled.scheduler_install_requested"), "success"); + showToast("Scheduler install requested.", "success"); } finally { setInstallingScheduler(false); } @@ -592,10 +590,10 @@ export default function AutomationsView(props: AutomationsViewProps) { try { await Promise.resolve(props.createSessionAndOpen(plan.prompt)); setCreateModalOpen(false); - showToast(t("scheduled.prepared_automation_in_chat"), "success"); + showToast("Prepared automation in chat.", "success"); } catch (error) { setCreateError( - error instanceof Error ? error.message : t("scheduled.prepare_error_fallback"), + error instanceof Error ? error.message : "Failed to prepare automation in chat.", ); } finally { setCreateBusy(false); @@ -610,7 +608,7 @@ export default function AutomationsView(props: AutomationsViewProps) { return; } await Promise.resolve(props.createSessionAndOpen(plan.prompt)); - showToast(t("scheduled.prepared_job_in_chat", undefined, { name: job.name }), "success"); + showToast(`Prepared ${job.name} in chat.`, "success"); }; const confirmDelete = async () => { @@ -621,10 +619,10 @@ export default function AutomationsView(props: AutomationsViewProps) { try { await automations.remove(target.slug); setDeleteTarget(null); - showToast(t("scheduled.removed_job", undefined, { name: target.name }), "success"); + showToast(`Removed ${target.name}.`, "success"); } catch (error) { const message = error instanceof Error ? error.message : String(error); - setDeleteError(message || t("scheduled.delete_error_fallback")); + setDeleteError(message || "Failed to delete automation."); } finally { setDeleteBusy(false); } @@ -648,9 +646,9 @@ export default function AutomationsView(props: AutomationsViewProps) { const jobsEmptyMessage = createMemo(() => { const query = searchQuery().trim(); - if (query) return t("scheduled.no_automations_match", undefined, { query }); - if (schedulerGateActive()) return t("scheduled.install_scheduler_hint"); - return t("scheduled.empty_hint"); + if (query) return `No automations match \"${query}\".`; + if (schedulerGateActive()) return "Install the scheduler or connect to an OpenWork server to start creating automations."; + return "No automations yet. Start with a template or prepare one in chat."; }); return ( @@ -659,21 +657,21 @@ export default function AutomationsView(props: AutomationsViewProps) {
-

{t("scheduled.title")}

+

Automations

- {t("scheduled.page_description")} + Schedule recurring tasks for this worker, monitor what is already registered, and start from a reusable template.

@@ -694,7 +692,7 @@ export default function AutomationsView(props: AutomationsViewProps) { type="text" value={searchQuery()} onInput={(event) => setSearchQuery(event.currentTarget.value)} - placeholder={t("scheduled.search_placeholder")} + placeholder="Search automations or templates" class="w-full rounded-xl border border-dls-border bg-dls-surface py-3 pl-11 pr-4 text-[14px] text-dls-text focus:outline-none focus:ring-2 focus:ring-[rgba(var(--dls-accent-rgb),0.12)]" /> @@ -708,10 +706,10 @@ export default function AutomationsView(props: AutomationsViewProps) { class={activeFilter() === filter ? pillPrimaryClass : pillGhostClass} > {filter === "all" - ? t("scheduled.filter_all") + ? "All" : filter === "scheduled" - ? t("scheduled.filter_scheduled") - : t("scheduled.filter_templates")} + ? "Scheduled" + : "Templates"} )} @@ -729,13 +727,13 @@ export default function AutomationsView(props: AutomationsViewProps) {
{props.schedulerInstalled - ? t("scheduled.reload_activate_title") - : t("scheduled.install_scheduler_title")} + ? "Reload OpenWork to activate automations" + : "Install the scheduler to unlock automations"}

{props.schedulerInstalled - ? t("scheduled.reload_activate_hint") - : t("scheduled.install_scheduler_hint")} + ? "OpenCode loads plugins at startup. Reload OpenWork to activate opencode-scheduler for this workspace." + : "Automations run through the opencode-scheduler plugin today. Add it to this workspace to unlock local scheduling."}

@@ -747,7 +745,7 @@ export default function AutomationsView(props: AutomationsViewProps) { class={pillSecondaryClass} > - {installingScheduler() ? t("scheduled.installing") : t("scheduled.install_scheduler")} + {installingScheduler() ? "Installing…" : "Install scheduler"} @@ -785,11 +783,11 @@ export default function AutomationsView(props: AutomationsViewProps) {
-

{t("scheduled.your_automations")}

+

Your automations

{sourceDescription()}

- {sourceLabel()} · {t("scheduled.last_updated_prefix")} {lastUpdatedLabel()} + {sourceLabel()} · synced {lastUpdatedLabel()}
@@ -824,19 +822,19 @@ export default function AutomationsView(props: AutomationsViewProps) {
-

{t("scheduled.quick_start_templates")}

+

Quick start templates

- {t("scheduled.quick_start_templates_desc")} + Start from a proven recurring workflow, then tailor the prompt before you prepare it in chat.

-
{t("scheduled.template_count", undefined, { count: filteredTemplates().length })}
+
{filteredTemplates().length} templates
- {t("scheduled.no_templates_match")} + No templates match this search.
} > @@ -862,9 +860,9 @@ export default function AutomationsView(props: AutomationsViewProps) {
-

{t("scheduled.delete_confirm_title")}

+

Remove automation?

- {t("scheduled.delete_confirm_desc", undefined, { source: sourceLabel().toLowerCase() })} + This removes the schedule and deletes the job definition from {sourceLabel().toLowerCase()}.

@@ -874,10 +872,10 @@ export default function AutomationsView(props: AutomationsViewProps) {
@@ -890,9 +888,9 @@ export default function AutomationsView(props: AutomationsViewProps) {
-
{t("scheduled.create_title")}
+
Create automation

- {t("scheduled.create_desc")} + The form is ready for direct writes. For now, OpenWork prepares the scheduler command in chat for you.