diff --git a/apps/app/src/app/pages/automations.tsx b/apps/app/src/app/pages/automations.tsx index 600aacb38..4ceb54393 100644 --- a/apps/app/src/app/pages/automations.tsx +++ b/apps/app/src/app/pages/automations.tsx @@ -4,6 +4,7 @@ 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, @@ -51,7 +52,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 = "Daily bug scan"; +const DEFAULT_AUTOMATION_NAME = () => t("scheduled.default_automation_name"); const DEFAULT_AUTOMATION_PROMPT = "Scan recent commits and flag riskier diffs with the most important follow-ups."; const DEFAULT_SCHEDULE_TIME = "09:00"; @@ -61,79 +62,79 @@ const DEFAULT_INTERVAL_HOURS = 6; const automationTemplates: AutomationTemplate[] = [ { icon: Calendar, - name: "Daily planning brief", - description: "Build a focused plan from your tasks and calendar before the day starts.", + name: t("scheduled.tpl_daily_planning_name"), + description: t("scheduled.tpl_daily_planning_desc"), 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: "Weekday morning", + badge: t("scheduled.badge_weekday_morning"), }, { icon: BookOpen, - name: "Inbox zero helper", - description: "Summarize unread messages and suggest concise replies for the top threads.", + name: t("scheduled.tpl_inbox_zero_name"), + description: t("scheduled.tpl_inbox_zero_desc"), 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: "End-of-day", + badge: t("scheduled.badge_end_of_day"), }, { icon: MessageSquare, - name: "Meeting prep notes", - description: "Generate prep bullets, context, and unblockers for tomorrow's meetings.", + name: t("scheduled.tpl_meeting_prep_name"), + description: t("scheduled.tpl_meeting_prep_desc"), 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: "Weekday evening", + badge: t("scheduled.badge_weekday_evening"), }, { icon: TrendingUp, - name: "Weekly wins recap", - description: "Turn the week into wins, blockers, and clear next steps to share.", + name: t("scheduled.tpl_weekly_wins_name"), + description: t("scheduled.tpl_weekly_wins_desc"), 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: "Friday wrap-up", + badge: t("scheduled.badge_friday_wrapup"), }, { icon: Trophy, - name: "Learning digest", - description: "Collect saved links and notes into a weekly digest with actions.", + name: t("scheduled.tpl_learning_digest_name"), + description: t("scheduled.tpl_learning_digest_desc"), 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: "Weekend review", + badge: t("scheduled.badge_weekend_review"), }, { icon: Brain, - name: "Habit check-in", - description: "Run a quick accountability check-in and suggest one concrete next action.", + name: t("scheduled.tpl_habit_checkin_name"), + description: t("scheduled.tpl_habit_checkin_desc"), prompt: "Ask me for a quick progress check-in, capture blockers, and suggest one concrete next action.", scheduleMode: "interval", intervalHours: 6, - badge: "Every few hours", + badge: t("scheduled.badge_every_few_hours"), }, ]; const dayOptions = [ - { 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" }, + { 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" }, ]; export type AutomationsViewProps = { @@ -179,9 +180,9 @@ const parseCronNumbers = (value: string) => { const humanizeCron = (cron: string) => { const parts = cron.trim().split(/\s+/); - if (parts.length < 5) return "Custom schedule"; + if (parts.length < 5) return t("scheduled.custom_schedule"); const [minuteRaw, hourRaw, dom, mon, dowRaw] = parts; - if (!minuteRaw || !hourRaw || !dom || !mon || !dowRaw) return "Custom schedule"; + if (!minuteRaw || !hourRaw || !dom || !mon || !dowRaw) return t("scheduled.custom_schedule"); if ( minuteRaw === "0" && @@ -192,19 +193,19 @@ const humanizeCron = (cron: string) => { ) { const interval = Number.parseInt(hourRaw.slice(2), 10); if (Number.isFinite(interval) && interval > 0) { - return interval === 1 ? "Every hour" : `Every ${interval} hours`; + return interval === 1 ? t("scheduled.every_hour") : t("scheduled.every_n_hours", undefined, { interval }); } } const hour = Number.parseInt(hourRaw, 10); const minute = Number.parseInt(minuteRaw, 10); - if (!Number.isFinite(hour) || !Number.isFinite(minute)) return "Custom schedule"; - if (dom !== "*" || mon !== "*") return "Custom schedule"; + if (!Number.isFinite(hour) || !Number.isFinite(minute)) return t("scheduled.custom_schedule"); + if (dom !== "*" || mon !== "*") return t("scheduled.custom_schedule"); const timeLabel = `${pad2(hour)}:${pad2(minute)}`; if (dowRaw === "*") { - return `Every day at ${timeLabel}`; + return t("scheduled.every_day_at", undefined, { time: timeLabel }); } const days = parseCronNumbers(dowRaw); @@ -213,28 +214,28 @@ const humanizeCron = (cron: string) => { const weekdayDays = [1, 2, 3, 4, 5]; const weekendDays = [0, 6]; - if (allDays.every((d) => normalized.has(d))) return `Every day at ${timeLabel}`; + if (allDays.every((d) => normalized.has(d))) return t("scheduled.every_day_at", undefined, { time: timeLabel }); if ( weekdayDays.every((d) => normalized.has(d)) && !weekendDays.some((d) => normalized.has(d)) ) { - return `Weekdays at ${timeLabel}`; + return t("scheduled.weekdays_at", undefined, { time: timeLabel }); } if ( weekendDays.every((d) => normalized.has(d)) && !weekdayDays.some((d) => normalized.has(d)) ) { - return `Weekends at ${timeLabel}`; + return t("scheduled.weekends_at", undefined, { time: timeLabel }); } const labels: Record = { - 0: "Sun", - 1: "Mon", - 2: "Tue", - 3: "Wed", - 4: "Thu", - 5: "Fri", - 6: "Sat", + 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"), }; const list = Array.from(normalized) @@ -243,7 +244,7 @@ const humanizeCron = (cron: string) => { .map((d) => labels[d] ?? String(d)) .join(", "); - return list ? `${list} at ${timeLabel}` : `At ${timeLabel}`; + return list ? t("scheduled.days_at", undefined, { days: list, time: timeLabel }) : t("scheduled.at_time", undefined, { time: timeLabel }); }; const buildCronFromDaily = (timeValue: string, days: string[]) => { @@ -276,19 +277,20 @@ const taskSummary = (job: ScheduledJob) => { return `${run.command}${args}`; } const prompt = run?.prompt ?? job.prompt; - return prompt?.trim() || "No prompt or command configured yet."; + return prompt?.trim() || t("scheduled.task_summary_no_prompt"); }; const toRelative = (value?: string | null) => { - if (!value) return "Never"; + if (!value) return t("scheduled.never"); const parsed = Date.parse(value); - if (!Number.isFinite(parsed)) return "Never"; + if (!Number.isFinite(parsed)) return t("scheduled.never"); return formatRelativeTime(parsed); }; const templateScheduleLabel = (template: AutomationTemplate) => { if (template.scheduleMode === "interval") { - return `Every ${template.intervalHours ?? DEFAULT_INTERVAL_HOURS} hours`; + const interval = template.intervalHours ?? DEFAULT_INTERVAL_HOURS; + return interval === 1 ? t("scheduled.every_hour") : t("scheduled.every_n_hours", undefined, { interval }); } return humanizeCron( buildCronFromDaily( @@ -299,10 +301,10 @@ const templateScheduleLabel = (template: AutomationTemplate) => { }; const statusLabel = (status?: string | null) => { - if (!status) return "Not run yet"; - if (status === "running") return "Running"; - if (status === "success") return "Healthy"; - if (status === "failed") return "Needs attention"; + 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"); return status; }; @@ -344,10 +346,10 @@ const TemplateCard = (props: {
- Template + {t("scheduled.template_badge")}
@@ -387,22 +389,22 @@ const JobCard = (props: {
-
Last run {toRelative(props.job.lastRunAt)}
-
Created {toRelative(props.job.createdAt)}
+
{t("scheduled.last_run_prefix")} {toRelative(props.job.lastRunAt)}
+
{t("scheduled.created_prefix")} {toRelative(props.job.createdAt)}
- Scheduled + {t("scheduled.filter_scheduled")}
@@ -425,7 +427,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); @@ -444,7 +446,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); @@ -469,25 +471,25 @@ export default function AutomationsView(props: AutomationsViewProps) { ); const sourceLabel = createMemo(() => - automations.jobsSource() === "remote" ? "OpenWork server" : "Local scheduler", + automations.jobsSource() === "remote" ? t("scheduled.source_remote") : t("scheduled.source_local"), ); const sourceDescription = createMemo(() => automations.jobsSource() === "remote" - ? "Scheduled tasks that are currently synced from the connected OpenWork server." - : "Scheduled tasks that are currently registered on this device through the local scheduler.", + ? t("scheduled.subtitle_remote") + : t("scheduled.subtitle_local"), ); const supportNote = createMemo(() => { if (automations.jobsSource() === "remote") return null; - if (!isTauriRuntime()) return "Automations require the desktop app or a connected OpenWork server."; + if (!isTauriRuntime()) return t("scheduled.desktop_required"); if (!props.schedulerInstalled || schedulerInstallRequested()) return null; return null; }); const lastUpdatedLabel = createMemo(() => { lastUpdatedNow(); - if (!automations.jobsUpdatedAt()) return "Not synced yet"; + if (!automations.jobsUpdatedAt()) return t("scheduled.not_synced_yet"); return formatRelativeTime(automations.jobsUpdatedAt() as number); }); @@ -548,7 +550,7 @@ export default function AutomationsView(props: AutomationsViewProps) { setSchedulerInstallRequested(true); try { await Promise.resolve(props.addPlugin("opencode-scheduler")); - showToast("Scheduler install requested.", "success"); + showToast(t("scheduled.scheduler_install_requested"), "success"); } finally { setInstallingScheduler(false); } @@ -590,10 +592,10 @@ export default function AutomationsView(props: AutomationsViewProps) { try { await Promise.resolve(props.createSessionAndOpen(plan.prompt)); setCreateModalOpen(false); - showToast("Prepared automation in chat.", "success"); + showToast(t("scheduled.prepared_automation_in_chat"), "success"); } catch (error) { setCreateError( - error instanceof Error ? error.message : "Failed to prepare automation in chat.", + error instanceof Error ? error.message : t("scheduled.prepare_error_fallback"), ); } finally { setCreateBusy(false); @@ -608,7 +610,7 @@ export default function AutomationsView(props: AutomationsViewProps) { return; } await Promise.resolve(props.createSessionAndOpen(plan.prompt)); - showToast(`Prepared ${job.name} in chat.`, "success"); + showToast(t("scheduled.prepared_job_in_chat", undefined, { name: job.name }), "success"); }; const confirmDelete = async () => { @@ -619,10 +621,10 @@ export default function AutomationsView(props: AutomationsViewProps) { try { await automations.remove(target.slug); setDeleteTarget(null); - showToast(`Removed ${target.name}.`, "success"); + showToast(t("scheduled.removed_job", undefined, { name: target.name }), "success"); } catch (error) { const message = error instanceof Error ? error.message : String(error); - setDeleteError(message || "Failed to delete automation."); + setDeleteError(message || t("scheduled.delete_error_fallback")); } finally { setDeleteBusy(false); } @@ -646,9 +648,9 @@ export default function AutomationsView(props: AutomationsViewProps) { const jobsEmptyMessage = createMemo(() => { const query = searchQuery().trim(); - 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."; + if (query) return t("scheduled.no_automations_match", undefined, { query }); + if (schedulerGateActive()) return t("scheduled.install_scheduler_hint"); + return t("scheduled.empty_hint"); }); return ( @@ -657,21 +659,21 @@ export default function AutomationsView(props: AutomationsViewProps) {
-

Automations

+

{t("scheduled.title")}

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

@@ -692,7 +694,7 @@ export default function AutomationsView(props: AutomationsViewProps) { type="text" value={searchQuery()} onInput={(event) => setSearchQuery(event.currentTarget.value)} - placeholder="Search automations or templates" + placeholder={t("scheduled.search_placeholder")} 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)]" /> @@ -706,10 +708,10 @@ export default function AutomationsView(props: AutomationsViewProps) { class={activeFilter() === filter ? pillPrimaryClass : pillGhostClass} > {filter === "all" - ? "All" + ? t("scheduled.filter_all") : filter === "scheduled" - ? "Scheduled" - : "Templates"} + ? t("scheduled.filter_scheduled") + : t("scheduled.filter_templates")} )} @@ -727,13 +729,13 @@ export default function AutomationsView(props: AutomationsViewProps) {
{props.schedulerInstalled - ? "Reload OpenWork to activate automations" - : "Install the scheduler to unlock automations"} + ? t("scheduled.reload_activate_title") + : t("scheduled.install_scheduler_title")}

{props.schedulerInstalled - ? "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."} + ? t("scheduled.reload_activate_hint") + : t("scheduled.install_scheduler_hint")}

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

Your automations

+

{t("scheduled.your_automations")}

{sourceDescription()}

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

Quick start templates

+

{t("scheduled.quick_start_templates")}

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

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

Remove automation?

+

{t("scheduled.delete_confirm_title")}

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

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

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