mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-10 14:20:45 +08:00
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:
parent
6107692418
commit
45f430fb28
10 changed files with 181 additions and 33 deletions
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue