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

@ -744,7 +744,10 @@ export function parsePendingIdFromNotifyText(text) {
return match?.[1] ?? null;
}
/** Notify / ack copy (zh). */
/**
* Notify / ack copy keys (Chinese source literals for host `t()`).
* Callers must pass through `t()` after `setImHostLanguage` — do not send raw.
*/
export const ACCESS_GRANT_COPY = Object.freeze({
pendingAckDirect: '已提交私聊访问申请,请等待全局管理员审批。',
pendingAckGroup: '已提交本群访问申请,请等待管理员审批。',
@ -755,26 +758,38 @@ export const ACCESS_GRANT_COPY = Object.freeze({
denied: '访问申请未通过。',
adminApproved: '已批准该访问申请。',
adminDenied: '已拒绝该访问申请。',
processFailed: '无法处理该审批。',
});
/** Identity translator that still fills `{name}` placeholders (zh default). */
function fillPlaceholders(text, params) {
if (typeof text !== 'string' || params == null) return text;
return text.replace(/\{(\w+)\}/g, (match, name) =>
Object.hasOwn(params, name) ? String(params[name]) : match);
}
/**
* @param {object} pending
* @param {(text: string, params?: Record<string, unknown>) => string} [translate]
* @returns {string}
*/
export function formatPendingNotifyBody(pending) {
export function formatPendingNotifyBody(pending, translate = fillPlaceholders) {
const scene = pending.kind === 'group'
? `群聊 ${pending.groupJid ?? ''}`
: '私聊';
? translate('群聊 {jid}', { jid: pending.groupJid ?? '' })
: translate('私聊');
const unknown = translate('未知');
const who = pending.pushName
? `${pending.pushName} (${pending.phone || pending.lid || '未知'})`
: (pending.phone || pending.lid || '未知');
const text = pending.requestText ? `\n原文:${pending.requestText.slice(0, 200)}` : '';
? `${pending.pushName} (${pending.phone || pending.lid || unknown})`
: (pending.phone || pending.lid || unknown);
const text = pending.requestText
? `\n${translate('原文:{text}', { text: pending.requestText.slice(0, 200) })}`
: '';
return [
ACCESS_GRANT_COPY.notifyTitle,
`场景:${scene}`,
`申请人:${who}`,
`请求编号:${pending.id}`,
'请引用本条消息回复「同意」或「拒绝」。',
translate(ACCESS_GRANT_COPY.notifyTitle),
translate('场景:{scene}', { scene }),
translate('申请人:{who}', { who }),
translate('请求编号:{id}', { id: pending.id }),
translate('请引用本条消息回复「同意」或「拒绝」。'),
text,
].filter(Boolean).join('\n');
}

View file

@ -14,4 +14,33 @@ export default {
'WhatsApp账号': 'WhatsApp account',
' Web 关联设备': ' Web linked device',
// Access-grant / whitelist approval (WhatsApp DM to admins & requesters)
'已提交私聊访问申请,请等待全局管理员审批。':
'Your DM access request was submitted. Please wait for a global admin to approve.',
'已提交本群访问申请,请等待管理员审批。':
'Your group access request was submitted. Please wait for an admin to approve.',
'已记录访问申请,但尚未解析到电话号码;请管理员在设置中补全后再批准。':
'Access request recorded, but the phone number is unresolved; an admin must complete it in settings before approving.',
'[dsh-im-ops] 访问申请': '[dsh-im-ops] Access request',
'私聊访问已批准,现在可以直接对话。': 'DM access approved. You can chat directly now.',
'本群访问已批准,请在本群 @ 机器人继续。': 'Group access approved. @ the bot in this group to continue.',
'访问申请未通过。': 'Access request denied.',
'已批准该访问申请。': 'Access request approved.',
'已拒绝该访问申请。': 'Access request rejected.',
'无法处理该审批。': 'Unable to process this approval.',
'找不到待批申请。': 'No pending request found.',
'该申请已失效。': 'This request is no longer valid.',
'无权审批该申请。': 'You are not allowed to approve this request.',
'审批动作无效。': 'Invalid approval action.',
'申请人电话尚未解析,请在设置中补全后再批准。':
'Requester phone is unresolved; complete it in settings before approving.',
'私聊': 'Direct chat',
'群聊 {jid}': 'Group {jid}',
'场景:{scene}': 'Scene: {scene}',
'申请人:{who}': 'Requester: {who}',
'请求编号:{id}': 'Request: {id}',
'原文:{text}': 'Original message: {text}',
'请引用本条消息回复「同意」或「拒绝」。':
'Quote this message and reply with 同意 (approve) or 拒绝 (reject). Also: yes / no.',
};

View file

@ -22,6 +22,7 @@ import {
upsertAccessContact,
} from '../shared/access-grant.mjs';
import { isSharedLocalCommand } from '../shared/command-permission.mjs';
import { t } from '../shared/i18n.mjs';
import { isWhatsappLidJid } from './whatsapp-identity.mjs';
/**
@ -178,17 +179,17 @@ export async function tryHandleWhatsappApprovalReply({
}
await sendText(
{ jid: message.replyTarget?.jid ?? `${phone}@s.whatsapp.net` },
intent === 'approve' ? ACCESS_GRANT_COPY.adminApproved : ACCESS_GRANT_COPY.adminDenied,
t(intent === 'approve' ? ACCESS_GRANT_COPY.adminApproved : ACCESS_GRANT_COPY.adminDenied),
);
const requesterJid = resolved.phone
? `${resolved.phone}@s.whatsapp.net`
: (resolved.lid ?? null);
if (requesterJid) {
const body = intent === 'approve'
const body = t(intent === 'approve'
? (resolved.kind === 'group'
? ACCESS_GRANT_COPY.approvedGroup
: ACCESS_GRANT_COPY.approvedDirect)
: ACCESS_GRANT_COPY.denied;
: ACCESS_GRANT_COPY.denied);
try {
await sendText({ jid: requesterJid }, body);
} catch {
@ -201,7 +202,7 @@ export async function tryHandleWhatsappApprovalReply({
|| error?.code === 'pending-unresolved' || error?.code === 'pending-not-found') {
await sendText(
{ jid: message.replyTarget?.jid ?? `${phone}@s.whatsapp.net` },
error.message || '无法处理该审批。',
t(error.message || ACCESS_GRANT_COPY.processFailed),
);
return true;
}
@ -322,11 +323,11 @@ export async function gateWhatsappInbound({
await workspaces.setAccessGrant(botId, current);
}
const ack = !phone
const ack = t(!phone
? ACCESS_GRANT_COPY.pendingUnresolved
: (scene === 'group'
? ACCESS_GRANT_COPY.pendingAckGroup
: ACCESS_GRANT_COPY.pendingAckDirect);
: ACCESS_GRANT_COPY.pendingAckDirect));
try {
await sendText(message.replyTarget, ack);
} catch {
@ -335,7 +336,7 @@ export async function gateWhatsappInbound({
if (created) {
const admins = approverPhonesForPending(current, pending);
const body = formatPendingNotifyBody(pending);
const body = formatPendingNotifyBody(pending, t);
const refs = [];
for (const adminPhone of admins) {
// Never DM the linked bot account as if it were a peer when it is the only self-chat path.