Sync bot chat language with DSH system locale.

Localize WhatsApp access-grant approvals via t() and push Host language from the UI on load and locale change.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-14 22:38:16 +08:00
parent 6107692418
commit 45f430fb28
10 changed files with 181 additions and 33 deletions

View file

@ -191,7 +191,32 @@ test('quote approval helpers parse intent, pending id, and notify refs', () => {
pushName: 'Carol',
requestText: 'hi',
});
assert.match(body, /请求编号:p_abc123/);
assert.equal(parsePendingIdFromNotifyText(body), 'p_abc123');
const enBody = formatPendingNotifyBody({
id: 'p_abc123',
kind: 'direct',
phone: '8618888888888',
pushName: 'Carol',
requestText: 'hi',
}, (text, params) => {
const map = {
'[dsh-im-ops] 访问申请': '[dsh-im-ops] Access request',
'私聊': 'Direct chat',
'场景:{scene}': 'Scene: {scene}',
'申请人:{who}': 'Requester: {who}',
'请求编号:{id}': 'Request: {id}',
'原文:{text}': 'Original message: {text}',
'请引用本条消息回复「同意」或「拒绝」。': 'Quote this message…',
'未知': 'unknown',
};
const translated = map[text] ?? text;
if (params == null) return translated;
return translated.replace(/\{(\w+)\}/g, (match, name) =>
Object.hasOwn(params, name) ? String(params[name]) : match);
});
assert.match(enBody, /Request: p_abc123/);
assert.equal(parsePendingIdFromNotifyText(enBody), 'p_abc123');
const withRef = {
...grant(),
pending: [{

View file

@ -1,7 +1,8 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createUpdateRpcHandler, installUpdateRpc } from '../plugin-src/host/update-rpc.mjs';
import { createUpdateRpcHandler, installUpdateRpc, HOST_LANGUAGE_SET_ENDPOINT } from '../plugin-src/host/update-rpc.mjs';
import { createImHostPlugin } from '../plugin-src/host/index.mjs';
import { getImHostLanguage, setImHostLanguage } from '../src/channels/shared/i18n.mjs';
test('update RPC rejects arbitrary commands, paths, profiles, sources and invalid request identifiers', async () => {
const calls = [];
@ -13,22 +14,53 @@ test('update RPC rejects arbitrary commands, paths, profiles, sources and invali
['update.install', { checkId: 'id', requestId: 'request', command: 'anything' }],
['update.install', { checkId: 'id', requestId: '; rm -rf /' }],
['update.install', { checkId: 'id', requestId: 'request', version: '9.0.0' }],
[HOST_LANGUAGE_SET_ENDPOINT, {}],
[HOST_LANGUAGE_SET_ENDPOINT, { language: 'en', extra: true }],
[HOST_LANGUAGE_SET_ENDPOINT, { language: '' }],
]) {
assert.equal((await handle(endpoint, payload)).error.code, 'bad-request');
}
assert.deepEqual(calls, []);
});
test('update RPC keeps local authority even when other management uses trusted-host', () => {
const calls = [];
test('host.language.set follows DSH locale ids and returns the normalized host language', async () => {
const handle = createUpdateRpcHandler({});
setImHostLanguage('zh');
try {
assert.deepEqual(await handle(HOST_LANGUAGE_SET_ENDPOINT, { language: 'en' }), {
ok: true, value: { language: 'en' },
});
assert.equal(getImHostLanguage(), 'en');
assert.deepEqual(await handle(HOST_LANGUAGE_SET_ENDPOINT, { language: 'zh-CN' }), {
ok: true, value: { language: 'zh' },
});
assert.equal(getImHostLanguage(), 'zh');
} finally {
setImHostLanguage('zh');
}
});
test('update RPC mounts on /dsh-im through webServer', () => {
const routes = [];
const service = { close: async () => {} };
const ctx = {
connection: { rpc: { handle: (...args) => calls.push(args) } },
effect: () => {},
connection: {
requestRejection: () => undefined,
},
webServer: {
register: (route) => {
routes.push(route);
return () => {};
},
},
effect: (factory) => {
const dispose = factory();
return typeof dispose === 'function' ? dispose : () => {};
},
};
installUpdateRpc(ctx, { service, runtime: {} });
assert.equal(calls[0][0], '/dsh-im');
assert.deepEqual(calls[0][2], { authority: 'loopback' });
assert.equal(routes[0]?.path, '/dsh-im');
assert.equal(routes[0]?.kind, 'prefix');
});
test('update RPC returns only safe codes for unanticipated runtime errors', async () => {