feat(i18n): add English support for host-side chat messages

Wrap all user-facing chat text in src/channels with a host-side t()
translator (Chinese is the identity default, so untranslated text is
always sent verbatim in Chinese). English is selected via the host
plugin config `language: en` or the DSH_IM_LANGUAGE environment
variable, and the settings UI keeps following the DSH web locale.

- add src/channels/shared/i18n.mjs with setImHostLanguage/t() and a
  Chinese-key -> English dictionary split by area (shared + channels)
- complete the English dictionary for every t() key in src/channels,
  including the feishu channel (welcome/help, repair flow, interactive
  cards, watch flow) and the shared command/approval/stream text
- wrap previously untranslated chat-facing strings in feishu bridge,
  feishu runtime, editable-message-stream, harness-client,
  connection-test and image-prompt
- dedupe dictionary entries that existed in multiple channel files and
  canonicalize conflicting translations into the shared dictionaries
- extend the i18n unit test to decode string-literal escapes so keys
  containing \n are checked correctly
- document the language switch in README.md, README.en.md and
  CHANGELOG.md, and rebuild lib/index.js

Added English support for bot chat messages across all nine IM
channels and AI Office without changing any Chinese output.
This commit is contained in:
Zaakir 2026-08-23 22:34:11 +05:00
parent 3ff8c2a519
commit dcd7cd64d8
78 changed files with 2283 additions and 992 deletions

View file

@ -1,5 +1,6 @@
import { setTimeout as sleep } from 'node:timers/promises';
import { t } from '../shared/i18n.mjs';
import { harnessApprovalText } from '../shared/harness-approval.mjs';
import {
harnessAnswerForQuestion,
@ -21,23 +22,23 @@ function clean(value, max = 8_000) {
function safeFailure(error) {
if (error?.code === 'office-job-alias-invalid') return error.message;
if (error?.code === 'turn-stopped') return '本机 Harness 已停止这次执行。';
if (error?.code === 'office-job-conflict') return 'Office Job 已被领取、取消或结束。';
return '本机 Harness 未能完成任务;请检查 Harness 会话后重试。';
if (error?.code === 'turn-stopped') return t('本机 Harness 已停止这次执行。');
if (error?.code === 'office-job-conflict') return t('Office Job 已被领取、取消或结束。');
return t('本机 Harness 未能完成任务;请检查 Harness 会话后重试。');
}
function renderPrompt(job, preset) {
return [
'# AI Office Handoff',
'',
'你正在本机 DeepSeek Harness 中继续一个来自 AI Office 的任务。',
'只在当前 Workspace 内行动。完成后必须返回:结果摘要、改动文件、验证证据、未解决风险。',
t('你正在本机 DeepSeek Harness 中继续一个来自 AI Office 的任务。'),
t('只在当前 Workspace 内行动。完成后必须返回:结果摘要、改动文件、验证证据、未解决风险。'),
'',
'## 本机 Instruction Preset',
t('## 本机 Instruction Preset'),
preset,
'',
...(clean(job.instruction) ? ['## 本轮补充指令', clean(job.instruction), ''] : []),
'## Office 时间线',
...(clean(job.instruction) ? [t('## 本轮补充指令'), clean(job.instruction), ''] : []),
t('## Office 时间线'),
clean(job.markdown, 200_000),
].join('\n');
}
@ -54,7 +55,7 @@ function approvalRequest(interaction) {
return {
id,
kind: 'approval',
title: `批准 ${clean(interaction.payload?.toolName, 120) || 'Harness 工具'}`,
title: t('批准 {tool}', { tool: clean(interaction.payload?.toolName, 120) || t('Harness 工具') }),
prompt,
toolName: clean(interaction.payload?.toolName, 120),
};
@ -66,7 +67,7 @@ function approvalRequest(interaction) {
return {
id,
kind: 'question',
title: questions.length > 1 ? `Harness 需要 ${questions.length} 项补充信息` : clean(questions[0].header || questions[0].question, 160),
title: questions.length > 1 ? t('Harness 需要 {count} 项补充信息', { count: questions.length }) : clean(questions[0].header || questions[0].question, 160),
prompt: questions.map((question, index) => harnessQuestionText(question, index, questions.length)).join('\n\n---\n\n'),
questions,
};
@ -216,14 +217,14 @@ export class OfficeJobExecutor {
const workspace = this.#config.workspaces[job.workspaceAlias];
const preset = this.#config.instructionPresets[job.instructionPreset];
if (!workspace || !preset) {
const error = new Error('Office Job 引用了本机未配置的 Workspace/Preset alias。');
const error = new Error(t('Office Job 引用了本机未配置的 Workspace/Preset alias。'));
error.code = 'office-job-alias-invalid';
throw error;
}
entry.harness = this.#createHarness({ workspace });
await this.#progress(jobId, entry, { kind: 'status', message: `已领取 Job,准备 Workspace alias:${job.workspaceAlias}` }, true);
await this.#progress(jobId, entry, { kind: 'status', message: t('已领取 Job,准备 Workspace alias:{alias}', { alias: job.workspaceAlias }) }, true);
entry.sessionId = await entry.harness.createSession({ signal, workspace });
await this.#progress(jobId, entry, { kind: 'status', message: 'Harness Session 已创建。', sessionId: entry.sessionId }, true);
await this.#progress(jobId, entry, { kind: 'status', message: t('Harness Session 已创建。'), sessionId: entry.sessionId }, true);
const answer = await entry.harness.ask(entry.sessionId, renderPrompt(job, preset), {
timeoutMs: 30 * 60_000,
signal,
@ -300,7 +301,7 @@ export class OfficeJobExecutor {
}
async #handleUpdate(jobId, entry, update) {
const message = update?.type === 'tool' ? `正在使用 ${clean(update.name, 160) || 'Harness 工具'}…`
const message = update?.type === 'tool' ? t('正在使用 {tool}…', { tool: clean(update.name, 160) || t('Harness 工具') })
: clean(update?.text, 4_000);
if (!message) return;
const key = `${update.type}:${message}`;

View file

@ -1,5 +1,6 @@
import { setTimeout as sleep } from 'node:timers/promises';
import { t } from '../shared/i18n.mjs';
import { OfficeTransport } from './office-transport.mjs';
import { OFFICE_PROTOCOL_VERSION } from './protocol.mjs';
import { OfficeJobExecutor } from './office-job-executor.mjs';
@ -9,12 +10,12 @@ const RETRY_DELAYS = Object.freeze([1_000, 3_000, 10_000, 30_000]);
function safeConnectionError(error) {
const code = typeof error?.code === 'string' ? error.code : 'office-connection-failed';
const messages = {
'invalid-device-token': 'AI Office 拒绝了 Device Token。',
'office-hook-unavailable': 'AI Office Connector Hook 尚未就绪。',
'office-protocol-mismatch': 'AI Office Connector 协议版本不兼容。',
'office-transport-failed': '本机暂时无法访问 AI Office。',
'invalid-device-token': t('AI Office 拒绝了 Device Token。'),
'office-hook-unavailable': t('AI Office Connector Hook 尚未就绪。'),
'office-protocol-mismatch': t('AI Office Connector 协议版本不兼容。'),
'office-transport-failed': t('本机暂时无法访问 AI Office。'),
};
return { code, message: messages[code] ?? 'AI Office 连接已中断。' };
return { code, message: messages[code] ?? t('AI Office 连接已中断。') };
}
export class OfficeRuntime {