mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
i18n(routines): localize "Agent completed without producing any output" error
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.
This commit is contained in:
parent
52f6ca0cf5
commit
a203e93978
5 changed files with 62 additions and 8 deletions
|
|
@ -172,14 +172,56 @@ function formatRunTimestamp(ts: number): string {
|
|||
});
|
||||
}
|
||||
|
||||
function runFailureReason(run: {
|
||||
// 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. `: <provider detail>`) 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. `: <provider detail>`), the extra text is
|
||||
// preserved as a suffix on the translated message rather than dropped.
|
||||
const KNOWN_DAEMON_ERROR_PREFIXES: ReadonlyArray<readonly [string, keyof Dict]> = [
|
||||
[
|
||||
// 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 {
|
||||
} | 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 (
|
||||
<ul className="routines-history">
|
||||
{runs.map((r) => {
|
||||
const failureReason = runFailureReason(r);
|
||||
const failureReason = runFailureReason(t, r);
|
||||
return (
|
||||
<li key={r.id} className="routines-history-row">
|
||||
<StatusPill status={r.status} t={t} />
|
||||
|
|
@ -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 (
|
||||
<li key={r.id} className={`routines-card routines-item${r.enabled ? '' : ' is-disabled'}`}>
|
||||
<div className="routines-item-head">
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 上获取帮助',
|
||||
|
|
|
|||
|
|
@ -643,6 +643,8 @@ export const zhTW: Dict = {
|
|||
'routines.status.canceled': '已取消',
|
||||
'routines.confirmDelete': '刪除此自動化?過往執行記錄及其專案將予以保留。',
|
||||
'routines.errorPickProject': '請選擇要重複使用的專案,或切換為「每次執行建立新專案」。',
|
||||
'routines.error.agentEmptyOutput':
|
||||
'代理執行完成但未產生任何輸出。模型或服務商可能傳回了空回應——請查看代理日誌以排查上游錯誤。',
|
||||
|
||||
'newproj.tabPrototype': '原型',
|
||||
'newproj.tabLiveArtifact': '即時成品',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue