- {{ page?.title || pageName || tm("buttons.openPages") }}
+ {{ localizedPageTitle }}
diff --git a/dashboard/src/views/extension/PluginDetailPage.vue b/dashboard/src/views/extension/PluginDetailPage.vue
index 32bf88011..10a15b33a 100644
--- a/dashboard/src/views/extension/PluginDetailPage.vue
+++ b/dashboard/src/views/extension/PluginDetailPage.vue
@@ -34,7 +34,12 @@ const props = defineProps({
});
const { tm, router } = props.state;
-const { pluginName, pluginDesc: resolvePluginDesc } = usePluginI18n();
+const {
+ pluginName,
+ pluginDesc: resolvePluginDesc,
+ pluginPageTitle,
+ pluginPageDescription,
+} = usePluginI18n();
const markdown = new MarkdownIt({
html: true,
@@ -426,6 +431,16 @@ const getHandlerCommand = (handler) =>
).trim();
const getHandlerDisplayName = (handler, groupKey) => {
+ if (groupKey === "page") {
+ return pluginPageTitle(
+ pluginData.value,
+ handler,
+ handler?.title ||
+ handler?.name ||
+ handler?.page_name ||
+ tm("status.unknown"),
+ );
+ }
if (handler?.name) {
return handler.name;
}
@@ -450,10 +465,16 @@ const toggleCommandGroup = (key) => {
expandedCommandGroups.value = next;
};
-const getComponentDescription = (component) =>
- String(
- component?.description || component?.desc || tm("status.unknown"),
- ).trim();
+const getComponentDescription = (component) => {
+ const fallback =
+ component?.description || component?.desc || tm("status.unknown");
+ if (getComponentGroupKey(component) === "page") {
+ return String(
+ pluginPageDescription(pluginData.value, component, fallback),
+ ).trim();
+ }
+ return String(fallback).trim();
+};
const openComponentPage = (component) => {
const targetPluginName = component?.plugin_name || pluginData.value?.name;
@@ -691,8 +712,7 @@ const fetchChangelog = async () => {
});
if (res.data.status !== "ok") {
- changelogError.value =
- res.data.message || tm("messages.operationFailed");
+ changelogError.value = res.data.message || tm("messages.operationFailed");
return;
}
diff --git a/docs/en/dev/star/guides/plugin-i18n.md b/docs/en/dev/star/guides/plugin-i18n.md
index 93d598c6e..009a7177e 100644
--- a/docs/en/dev/star/guides/plugin-i18n.md
+++ b/docs/en/dev/star/guides/plugin-i18n.md
@@ -20,6 +20,7 @@ When the current locale has no translation, a field is missing, or the locale fi
- Plugin names, card short descriptions, and descriptions fall back to `display_name`, `short_desc`, and `desc` in `metadata.yaml`.
- Configuration text falls back to `description`, `hint`, and `labels` in `_conf_schema.json`.
+- Page text falls back to the Page directory name, default Page title, or fallback text provided by page code.
## Metadata
@@ -77,6 +78,52 @@ Corresponding `.astrbot-plugin/i18n/zh-CN.json`:
`options` are stored configuration values and should usually not be translated. Use `labels` for select display text.
+## Plugin Pages
+
+`pages` overrides plugin Dashboard Page titles, descriptions, and custom text inside plugin pages. The structure is nested by Page directory name.
+
+Example plugin page directory:
+
+```text
+pages/
+ settings/
+ index.html
+```
+
+Corresponding `.astrbot-plugin/i18n/en-US.json`:
+
+```json
+{
+ "pages": {
+ "settings": {
+ "title": "Settings",
+ "description": "Manage advanced settings for this plugin.",
+ "save": "Save",
+ "reset": "Reset"
+ }
+ }
+}
+```
+
+`title` is used by the WebUI shell title and the Page component name on the plugin detail page. `description` is used by the Page component description on the plugin detail page. Other fields are read by the page through the bridge:
+
+```js
+const bridge = window.AstrBotPluginPage;
+
+function render() {
+ document.getElementById("save").textContent = bridge.t(
+ "pages.settings.save",
+ "Save",
+ );
+}
+
+await bridge.ready();
+render();
+bridge.onContext(render);
+```
+
+Use `onContext()` to react to WebUI language changes; with this listener, the Page usually does not need a refresh.
+
## Nested Configuration
For `object` items in `_conf_schema.json`, translations use the same nested field structure.
diff --git a/docs/en/dev/star/guides/plugin-pages.md b/docs/en/dev/star/guides/plugin-pages.md
index 79845e73a..5bbe60dfc 100644
--- a/docs/en/dev/star/guides/plugin-pages.md
+++ b/docs/en/dev/star/guides/plugin-pages.md
@@ -96,6 +96,10 @@ Inside a plugin Page, use `window.AstrBotPluginPage` directly:
- `ready()`: Wait until the bridge is ready and return the context
- `getContext()`: Read the current context
+- `getLocale()`: Read the current WebUI locale
+- `getI18n()`: Read the current plugin i18n resources
+- `t(key, fallback)`: Read text from plugin i18n resources by key, returning fallback when missing
+- `onContext(handler)`: Listen for context changes, such as rerendering after the WebUI locale changes
- `apiGet(endpoint, params)`: Send a GET request
- `apiPost(endpoint, body)`: Send a POST request
- `upload(endpoint, file)`: Upload one file as `multipart/form-data`
@@ -108,12 +112,62 @@ The current `ready()` context looks like this:
```json
{
"pluginName": "astrbot_plugin_page_demo",
- "displayName": "Plugin Page Demo"
+ "displayName": "Plugin Page Demo",
+ "pageName": "bridge-demo",
+ "pageTitle": "Bridge Demo",
+ "locale": "en-US",
+ "i18n": {}
}
```
`endpoint` must be a plugin-local path. It must not be empty, contain `\`, contain a URL scheme, contain query strings or fragments, or contain `.` / `..` path segments.
+## Page Internationalization
+
+Plugin Pages reuse plugin i18n resource files. Add `pages.