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

@ -64,6 +64,18 @@ export const name = 'im-settings';
export const inject = ['slots', 'connection', 'locale', 'workspaces'];
export const IM_PLUGIN_VERSION = manifest.version;
/** Push the active DSH UI locale to the Host so bot chat copy stays aligned. */
function syncHostChatLanguage(ctx, language) {
if (typeof language !== 'string' || !language.trim()) return;
void ctx.connection.rpc.call(
UPDATE_RPC_CHANNEL,
'host.language.set',
{ language },
).catch(() => {
// Host may be offline or on an older build without the endpoint.
});
}
function callWorkspaceDirectoryApi(ctx, method, ...args) {
// Current DSH owns directory operations on uiWorkspace; legacy Hosts keep them on workspaces.
const uiWorkspace = typeof ctx.get === 'function' ? ctx.get('uiWorkspace') : undefined;
@ -332,6 +344,13 @@ export function apply(ctx) {
const t = ctx.locale.bind(IM_LOCALE_NAMESPACE);
setImTranslator(t);
ctx.effect(() => {
syncHostChatLanguage(ctx, ctx.locale.getLocale().active);
return ctx.on('locale/change', (snapshot) => {
syncHostChatLanguage(ctx, snapshot?.active);
});
}, 'im-settings: sync host chat language with DSH locale');
ctx.effect(() => {
const disposers = [
installFeishuStyles(),

View file

@ -1,3 +1,4 @@
import { getImHostLanguage, setImHostLanguage } from '../../src/channels/shared/i18n.mjs';
import { createUpdateRuntime } from './update-runtime.mjs';
import { createUpdateService } from './update-service.mjs';
import { installConnectionRpcChannel } from './connection-rpc-mount.mjs';
@ -5,8 +6,20 @@ import { resolveRpcAuthority } from './rpc-authority.mjs';
export const UPDATE_RPC_CHANNEL = '/dsh-im';
export const UPDATE_ENDPOINTS = Object.freeze(['update.status', 'update.check', 'update.install']);
export const HOST_LANGUAGE_SET_ENDPOINT = 'host.language.set';
function validLanguagePayload(payload) {
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return false;
const keys = Object.keys(payload);
return keys.length === 1
&& keys[0] === 'language'
&& typeof payload.language === 'string'
&& payload.language.trim().length > 0
&& payload.language.length <= 32;
}
function validPayload(endpoint, payload) {
if (endpoint === HOST_LANGUAGE_SET_ENDPOINT) return validLanguagePayload(payload);
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return false;
const keys = Object.keys(payload);
if (endpoint !== 'update.install') return keys.length === 0;
@ -25,9 +38,14 @@ const PUBLIC_ERRORS = new Set([
export function createUpdateRpcHandler(service) {
return async (endpoint, payload, signal) => {
if (!UPDATE_ENDPOINTS.includes(endpoint) || !validPayload(endpoint, payload)) {
if (endpoint !== HOST_LANGUAGE_SET_ENDPOINT && !UPDATE_ENDPOINTS.includes(endpoint)
|| !validPayload(endpoint, payload)) {
return { ok: false, error: { code: 'bad-request', message: 'Invalid update request.' } };
}
if (endpoint === HOST_LANGUAGE_SET_ENDPOINT) {
setImHostLanguage(payload.language);
return { ok: true, value: { language: getImHostLanguage() } };
}
if (signal?.aborted) return { ok: false, error: { code: 'cancelled', message: 'Request cancelled.' } };
try {
// The submitted install belongs to the Host, not the lifetime of this browser request.