From a203e939783f994bfbcd662dbcf1a72ed71912dd Mon Sep 17 00:00:00 2001 From: zhongrenfei1-hub <231221504+zhongrenfei1-hub@users.noreply.github.com> Date: Tue, 26 May 2026 14:06:22 +0800 Subject: [PATCH] i18n(routines): localize "Agent completed without producing any output" error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes part of #2902. Addresses the specific example error called out in the issue body; further daemon-surfaced messages can be wired in incrementally by appending to `KNOWN_DAEMON_ERROR_PREFIXES`. When a Scheduled Routine run fails because the agent process exited cleanly without producing output, the daemon emits a structured SSE error payload (`AGENT_EXECUTION_FAILED`, see `apps/daemon/src/server.ts` around line 11334) with an English message: "Agent completed without producing any output. The model or provider may have returned an empty response — check the agent logs for upstream errors." The web side surfaces `run.error` as a plain string via `runFailureReason` in `apps/web/src/components/RoutinesSection.tsx`, which bypasses i18n entirely. Result: even on a `zh-CN` / `zh-TW` locale the failure shows in English, breaking the Chinese-language experience for what is otherwise one of the most user-visible Routines surfaces (the run history error row and the per-routine card). Changes: - Add an i18n key `routines.error.agentEmptyOutput` with the exact English source string copied verbatim from the daemon, plus explicit zh-CN and zh-TW translations. zh-CN gets an explicit entry (rather than relying on `...en` spread) so the tier-1 parity lock from #2920 stays green. Other locales fall back via the existing `...en` spread shape; their dedicated translations can be filled in by native speakers in follow-ups. - Add `localizeRoutineError(t, raw)` in `RoutinesSection.tsx` with a `KNOWN_DAEMON_ERROR_PREFIXES` table mapping stable daemon message prefixes onto i18n keys. Match on `startsWith` so any trailing context the daemon may append still routes to the same translation. - Thread `t` into `runFailureReason` so both callsites (the run history list and the per-routine card) get the localized text. - Unknown daemon errors pass through unchanged — untranslated failures still surface, they just stay in English until a matching i18n key is added. No behavior regression for any existing error. Verified locally: - `git diff main --stat`: 5 files, +47 / -8. - New key present in `en.ts`, `zh-CN.ts`, `zh-TW.ts`; `Dict` updated in `types.ts`. - Tier-1 lock dry-run: `zh-CN.ts` declares 2311 explicit keys matching `en.ts`'s 2311 value-bearing keys, with no `...en` spread. Drafted with Claude (Anthropic) assistance; reviewed and submitted by me. --- apps/web/src/components/RoutinesSection.tsx | 58 ++++++++++++++++++--- apps/web/src/i18n/locales/en.ts | 2 + apps/web/src/i18n/locales/zh-CN.ts | 2 + apps/web/src/i18n/locales/zh-TW.ts | 2 + apps/web/src/i18n/types.ts | 6 +++ 5 files changed, 62 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/RoutinesSection.tsx b/apps/web/src/components/RoutinesSection.tsx index e2f435da0..30ffc85e3 100644 --- a/apps/web/src/components/RoutinesSection.tsx +++ b/apps/web/src/components/RoutinesSection.tsx @@ -172,14 +172,56 @@ function formatRunTimestamp(ts: number): string { }); } -function runFailureReason(run: { - status: RoutineRun['status']; - error?: string | null; - summary?: string | null; -} | null | undefined): string | null { +// Maps known daemon-surfaced failure message prefixes onto i18n keys, so +// users running the app in a non-English locale see a translated reason +// instead of the raw English string the daemon ships to web (see e.g. +// `apps/daemon/src/server.ts` `createSseErrorPayload` callsites). We match +// on a stable prefix rather than the full string so any trailing context +// the daemon may append (e.g. `: `) is preserved verbatim +// after the localized prefix — losing it would hide debugging context the +// caller may rely on. Anything not in this list passes through untouched — +// untranslated failures continue to surface so behavior never regresses, +// they just stay in English until a matching i18n key is added here. +// Each entry is the *full current daemon message* (copied verbatim from +// the corresponding `createSseErrorPayload` call) paired with the i18n +// key that translates it. We match on full-string prefix so the current +// contract resolves to `t(key)` exactly; if the daemon later appends +// extra context (e.g. `: `), the extra text is +// preserved as a suffix on the translated message rather than dropped. +const KNOWN_DAEMON_ERROR_PREFIXES: ReadonlyArray = [ + [ + // Source: apps/daemon/src/server.ts `AGENT_EXECUTION_FAILED` payload. + 'Agent completed without producing any output. The model or provider may have returned an empty response — check the agent logs for upstream errors.', + 'routines.error.agentEmptyOutput', + ], +]; + +function localizeRoutineError(t: TranslateFn, raw: string): string { + for (const [prefix, key] of KNOWN_DAEMON_ERROR_PREFIXES) { + if (raw.startsWith(prefix)) { + // Preserve any suffix the daemon may have appended (e.g. provider + // detail after the known prefix). When the daemon emits exactly + // the prefix string today, `suffix` is empty and the result is + // just `t(key)`; nothing changes for the current contract. + const suffix = raw.slice(prefix.length); + return `${t(key)}${suffix}`; + } + } + return raw; +} + +function runFailureReason( + t: TranslateFn, + run: { + status: RoutineRun['status']; + error?: string | null; + summary?: string | null; + } | null | undefined, +): string | null { if (!run || run.status !== 'failed') return null; const reason = (run.error || run.summary || '').trim(); - return reason || null; + if (!reason) return null; + return localizeRoutineError(t, reason); } type FormState = { @@ -412,7 +454,7 @@ function RunHistory({ return (
    {runs.map((r) => { - const failureReason = runFailureReason(r); + const failureReason = runFailureReason(t, r); return (
  • @@ -754,7 +796,7 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) { : t('routines.targetCreate'); const isBusy = busyId === r.id; const isExpanded = expandedId === r.id; - const failureReason = runFailureReason(r.lastRun); + const failureReason = runFailureReason(t, r.lastRun); return (
  • diff --git a/apps/web/src/i18n/locales/en.ts b/apps/web/src/i18n/locales/en.ts index 49ac1ba70..0c8a9fae7 100644 --- a/apps/web/src/i18n/locales/en.ts +++ b/apps/web/src/i18n/locales/en.ts @@ -900,6 +900,8 @@ export const en: Dict = { 'Delete this automation? Past runs and their projects are kept.', 'routines.errorPickProject': 'Pick a project to reuse, or switch to “Create new each run”', + 'routines.error.agentEmptyOutput': + 'Agent completed without producing any output. The model or provider may have returned an empty response — check the agent logs for upstream errors.', 'entry.helpAria': 'Help', 'entry.helpMenuAria': 'Help menu', 'entry.helpGetHelp': 'Get help on GitHub', diff --git a/apps/web/src/i18n/locales/zh-CN.ts b/apps/web/src/i18n/locales/zh-CN.ts index 50732a343..9bd0bbe7f 100644 --- a/apps/web/src/i18n/locales/zh-CN.ts +++ b/apps/web/src/i18n/locales/zh-CN.ts @@ -897,6 +897,8 @@ export const zhCN: Dict = { 'routines.status.canceled': '已取消', 'routines.confirmDelete': '删除此自动化?过往运行记录及其项目将予以保留。', 'routines.errorPickProject': '请选择要复用的项目,或切换为“每次运行新建项目”。', + 'routines.error.agentEmptyOutput': + '智能体执行完成但未产生任何输出。模型或服务商可能返回了空响应——请查看智能体日志以排查上游错误。', 'entry.helpAria': '帮助', 'entry.helpMenuAria': '帮助菜单', 'entry.helpGetHelp': '在 GitHub 上获取帮助', diff --git a/apps/web/src/i18n/locales/zh-TW.ts b/apps/web/src/i18n/locales/zh-TW.ts index 442af51e6..b21226b53 100644 --- a/apps/web/src/i18n/locales/zh-TW.ts +++ b/apps/web/src/i18n/locales/zh-TW.ts @@ -643,6 +643,8 @@ export const zhTW: Dict = { 'routines.status.canceled': '已取消', 'routines.confirmDelete': '刪除此自動化?過往執行記錄及其專案將予以保留。', 'routines.errorPickProject': '請選擇要重複使用的專案,或切換為「每次執行建立新專案」。', + 'routines.error.agentEmptyOutput': + '代理執行完成但未產生任何輸出。模型或服務商可能傳回了空回應——請查看代理日誌以排查上游錯誤。', 'newproj.tabPrototype': '原型', 'newproj.tabLiveArtifact': '即時成品', diff --git a/apps/web/src/i18n/types.ts b/apps/web/src/i18n/types.ts index c9ce3b2f5..e9d3d6287 100644 --- a/apps/web/src/i18n/types.ts +++ b/apps/web/src/i18n/types.ts @@ -1210,6 +1210,12 @@ export interface Dict { 'routines.status.canceled': string; 'routines.confirmDelete': string; 'routines.errorPickProject': string; + // Routine run failure messages surfaced from the daemon. Each key + // corresponds to a well-known daemon-side error string we recognize + // and localize in `localizeRoutineError` (apps/web/src/components/ + // RoutinesSection.tsx). Unknown daemon errors keep their raw string + // so untranslated failures still surface, just in English. + 'routines.error.agentEmptyOutput': string; // Bottom-of-rail help menu 'entry.helpAria': string; 'entry.helpMenuAria': string;