Skip to content

Commit faf8efa

Browse files
committed
feat: enhance plugin page internationalization
- Updated PluginRoute to read initial context from JWT and set it in the bridge SDK. - Added methods to retrieve locale and plugin metadata for better i18n support. - Enhanced pluginI18n utility to resolve page-specific translations and added new functions for page titles and descriptions. - Modified PluginPagePage and PluginDetailPage to utilize new i18n features for dynamic content rendering. - Improved documentation for plugin page i18n structure and usage. - Added tests to verify the correct integration of i18n in plugin pages and context handling.
1 parent 319f50b commit faf8efa

10 files changed

Lines changed: 946 additions & 68 deletions

File tree

astrbot/dashboard/plugin_page_bridge.js

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const SELF_ORIGIN = window.location.origin;
44
const pendingRequests = new Map();
55
const sseHandlers = new Map();
6+
const contextHandlers = new Set();
67
let requestCounter = 0;
78
let subscriptionCounter = 0;
89
let context = null;
@@ -13,7 +14,11 @@
1314
});
1415

1516
function getTargetOrigin() {
16-
if (typeof parentOrigin === "string" && parentOrigin && parentOrigin !== "null") {
17+
if (
18+
typeof parentOrigin === "string" &&
19+
parentOrigin &&
20+
parentOrigin !== "null"
21+
) {
1722
return parentOrigin;
1823
}
1924
if (SELF_ORIGIN !== "null") {
@@ -70,6 +75,63 @@
7075
}
7176
}
7277

78+
function getByPath(source, key) {
79+
if (!source || typeof source !== "object" || !key) {
80+
return undefined;
81+
}
82+
83+
return String(key)
84+
.split(".")
85+
.reduce((current, part) => {
86+
if (!current || typeof current !== "object" || !(part in current)) {
87+
return undefined;
88+
}
89+
return current[part];
90+
}, source);
91+
}
92+
93+
function translate(key, fallback) {
94+
const locale = context?.locale;
95+
const messages = context?.i18n;
96+
const locales = [locale, "zh-CN", "en-US"].filter(Boolean);
97+
let value;
98+
for (const candidateLocale of locales) {
99+
value = getByPath(messages?.[candidateLocale], key);
100+
if (value !== undefined && value !== null) {
101+
break;
102+
}
103+
}
104+
if (value === undefined || value === null) {
105+
return fallback || "";
106+
}
107+
return typeof value === "string" ? value : String(value);
108+
}
109+
110+
function notifyContextHandlers() {
111+
contextHandlers.forEach((handler) => {
112+
try {
113+
handler(context);
114+
} catch (error) {
115+
console.error("AstrBotPluginPage context handler failed:", error);
116+
}
117+
});
118+
}
119+
120+
function applyContext(nextContext) {
121+
if (!nextContext || typeof nextContext !== "object") {
122+
return;
123+
}
124+
context = {
125+
...(context || {}),
126+
...nextContext,
127+
};
128+
if (resolveReady) {
129+
resolveReady(context);
130+
resolveReady = null;
131+
}
132+
notifyContextHandlers();
133+
}
134+
73135
window.addEventListener("message", (event) => {
74136
if (event.source !== window.parent) {
75137
return;
@@ -87,11 +149,7 @@
87149
}
88150

89151
if (message.kind === "context") {
90-
context = message.context || null;
91-
if (resolveReady) {
92-
resolveReady(context);
93-
resolveReady = null;
94-
}
152+
applyContext(message.context);
95153
return;
96154
}
97155

@@ -104,7 +162,9 @@
104162
if (message.ok) {
105163
pending.resolve(message.data);
106164
} else {
107-
pending.reject(new Error(message.error || "Plugin bridge request failed."));
165+
pending.reject(
166+
new Error(message.error || "Plugin bridge request failed."),
167+
);
108168
}
109169
return;
110170
}
@@ -139,6 +199,30 @@
139199
getContext() {
140200
return context;
141201
},
202+
getLocale() {
203+
return context?.locale || "zh-CN";
204+
},
205+
getI18n() {
206+
return context?.i18n || {};
207+
},
208+
t(key, fallback) {
209+
return translate(key, fallback);
210+
},
211+
onContext(handler) {
212+
if (typeof handler !== "function") {
213+
return () => {};
214+
}
215+
contextHandlers.add(handler);
216+
if (context) {
217+
handler(context);
218+
}
219+
return () => {
220+
contextHandlers.delete(handler);
221+
};
222+
},
223+
__setInitialContext(nextContext) {
224+
applyContext(nextContext);
225+
},
142226
apiGet(endpoint, params) {
143227
return makeRequest("api:get", { endpoint, params });
144228
},

astrbot/dashboard/routes/plugin.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,13 @@ async def get_plugin_page_bridge_sdk(self):
189189
return await self._plugin_page_error_response(
190190
404, "Plugin Page bridge SDK not found"
191191
)
192-
bridge_js = await self._read_plugin_page_binary(_PLUGIN_PAGE_BRIDGE_FILE)
192+
bridge_js = await self._read_plugin_page_text(_PLUGIN_PAGE_BRIDGE_FILE)
193+
initial_context = self._get_plugin_page_initial_context()
194+
if initial_context:
195+
context_json = json.dumps(initial_context, ensure_ascii=False)
196+
bridge_js += (
197+
f"\n;window.AstrBotPluginPage?.__setInitialContext({context_json});\n"
198+
)
193199
response = cast(
194200
QuartResponse,
195201
await make_response(
@@ -204,6 +210,82 @@ def _get_plugin_metadata_by_name(self, plugin_name: str) -> StarMetadata | None:
204210
return plugin
205211
return None
206212

213+
@staticmethod
214+
def _get_by_path(source: dict | None, key: str):
215+
if not isinstance(source, dict) or not key:
216+
return None
217+
current = source
218+
for part in key.split("."):
219+
if not isinstance(current, dict) or part not in current:
220+
return None
221+
current = current[part]
222+
return current
223+
224+
@staticmethod
225+
def _get_request_locale(default: str = "zh-CN") -> str:
226+
raw_locale = request.headers.get("Accept-Language", "").strip()
227+
locale = raw_locale.split(",", 1)[0].split(";", 1)[0].strip()
228+
if not locale or len(locale) > 32:
229+
return default
230+
return locale
231+
232+
def _get_plugin_page_initial_context(self) -> dict | None:
233+
asset_token = request.args.get("asset_token", "").strip()
234+
if not asset_token:
235+
return None
236+
jwt_secret = self.config.get("dashboard", {}).get("jwt_secret")
237+
if not isinstance(jwt_secret, str) or not jwt_secret.strip():
238+
return None
239+
240+
try:
241+
payload = jwt.decode(asset_token, jwt_secret, algorithms=["HS256"])
242+
except jwt.InvalidTokenError:
243+
return None
244+
if payload.get("token_type") != _PLUGIN_PAGE_ASSET_TOKEN_TYPE:
245+
return None
246+
247+
plugin_name = payload.get("plugin_name")
248+
page_name = payload.get("page_name")
249+
if not isinstance(plugin_name, str) or not isinstance(page_name, str):
250+
return None
251+
252+
plugin = self._get_plugin_metadata_by_name(plugin_name)
253+
if not plugin:
254+
return None
255+
256+
locale = (
257+
payload.get("locale")
258+
if isinstance(payload.get("locale"), str)
259+
else self._get_request_locale()
260+
)
261+
plugin_i18n = plugin.i18n or {}
262+
try:
263+
plugin_root = self._get_plugin_root_dir(plugin)
264+
fresh_i18n = PluginManager._load_plugin_i18n(str(plugin_root))
265+
if fresh_i18n:
266+
plugin_i18n = fresh_i18n
267+
except (OSError, ValueError):
268+
pass
269+
270+
locale_data = plugin_i18n.get(locale)
271+
display_name = (
272+
self._get_by_path(locale_data, "metadata.display_name")
273+
or plugin.display_name
274+
or plugin.name
275+
)
276+
page_title = (
277+
self._get_by_path(locale_data, f"pages.{page_name}.title") or page_name
278+
)
279+
280+
return {
281+
"pluginName": plugin.name,
282+
"displayName": display_name,
283+
"pageName": page_name,
284+
"pageTitle": page_title,
285+
"locale": locale,
286+
"i18n": plugin_i18n,
287+
}
288+
207289
@staticmethod
208290
def _normalize_plugin_page_path(
209291
raw_path: str,
@@ -634,6 +716,7 @@ async def _serialize_plugin_page(
634716
page_data = {
635717
"name": page.name,
636718
"title": page.title,
719+
"i18n_key": f"pages.{page.name}",
637720
}
638721
if include_content_path:
639722
asset_token = (
@@ -675,6 +758,7 @@ def _issue_plugin_page_asset_token(
675758
"token_type": _PLUGIN_PAGE_ASSET_TOKEN_TYPE,
676759
"plugin_name": plugin_name,
677760
"page_name": page_name,
761+
"locale": self._get_request_locale(),
678762
"iat": now,
679763
"exp": now + timedelta(seconds=_PLUGIN_PAGE_ASSET_TOKEN_TTL_SECONDS),
680764
}
@@ -1285,6 +1369,7 @@ async def get_plugin_page_components(self, plugin) -> list[dict]:
12851369
"name": page["title"],
12861370
"title": page["title"],
12871371
"page_name": page["name"],
1372+
"i18n_key": page["i18n_key"],
12881373
"description": "Plugin Page entry",
12891374
"plugin_name": plugin.name,
12901375
}

0 commit comments

Comments
 (0)