Skip to content

feat(plugin): support localized plugin READMEs with a dashboard switcher - #10613

Closed
mjy1113451 wants to merge 1 commit into
AstrBotDevs:masterfrom
mjy1113451:feat/plugin-readme-locale
Closed

mjy1113451 wants to merge 1 commit into
AstrBotDevs:masterfrom
mjy1113451:feat/plugin-readme-locale

Conversation

@mjy1113451

@mjy1113451 mjy1113451 commented Oct 18, 2026 •

Copy link
Copy Markdown

Motivation

Closes #10593.

Following the analysis in the issue, the two halves of the request are handled differently:

  • Plugin config already follows the WebUI locale through .astrbot-plugin/i18n/<locale>.json and AstrBotConfig. No second language switch was added there, so there is still exactly one source of truth for config strings.
  • Plugin README had no locale protocol at all (get_plugin_readme() always read README.md). This PR adds the declaration, the server-side resolution and the dashboard switcher.

What changed

Plugin contract

metadata.yaml may declare README variants:

readmes:
  default: README.md
  zh-CN: README_zh.md
  en-US: README_en.md

StarMetadata.readmes stores the normalized mapping; invalid entries are dropped at load time so a typo cannot break plugin loading.

Resolution

PluginService.get_plugin_readme(plugin_name, locale) resolves in this order:

  1. exact locale (zh-CN),
  2. base language (zh-TW → zh),
  3. default (or README.md when no default is declared).

Every declared path must be a relative Markdown file inside the plugin directory: .., absolute paths, non-Markdown files and symlinks that resolve outside the plugin root are rejected, and a missing variant falls back to default. Plugins without readmes keep the exact legacy behavior.

The response now also returns resolved_locale and available_locales (additive, content unchanged).

API and dashboard

  • GET /api/v1/plugins/readme?plugin_id=...&locale=... (the route the dashboard uses) and GET /api/v1/plugins/{plugin_id}/readme?locale=...; the legacy /api/plugin/readme also forwards locale. OpenAPI spec updated and the generated client regenerated.
  • PluginDetailPage.vue shows a language select only when the plugin declares more than one README. The default option follows the WebUI locale; picking a variant overrides it for the current detail page. Locale changes are part of the refresh dependencies and responses are guarded by a request id so a slow older response cannot overwrite a newer one.
  • i18n keys added for all four dashboard locales.

Verification

  • ruff format --check / ruff check clean on all touched Python files.
  • python -m py_compile on touched files.
  • pnpm run i18n:check and pnpm run build (vue-tsc + vite) pass.
  • New tests/test_plugin_readme_locale.py covers: legacy single README, exact locale, language fallback (zh-TW → zh), unknown locale → default, missing variant → default, ../ escape, absolute path, non-Markdown file, symlink escape, and locale normalization.

Notes

  • Marketplace plugins keep using readme_url as before; per-locale marketplace URLs are a separate data-contract change on the marketplace side and are intentionally not included here.
  • The switcher is hidden for plugins that ship a single README, so the page looks unchanged for existing plugins.

Summary by Sourcery

Support locale-aware plugin README delivery and dashboard selection while preserving compatibility with existing plugins.

New Features:

  • Add localized plugin README declarations with locale-aware API resolution and dashboard language switching.

Bug Fixes:

  • Prevent invalid or unsafe README paths, including traversal, absolute paths, non-Markdown files, and escaping symlinks, from being served.

Enhancements:

  • Preserve legacy README behavior for plugins without locale metadata while returning resolved and available locale information.
  • Regenerate API types and add localized dashboard labels for the README language selector.

Documentation:

  • Document the localized README metadata contract, fallback behavior, and path restrictions in English and Chinese plugin publishing guides.

Tests:

  • Add coverage for locale matching, fallback behavior, legacy compatibility, path validation, symlink safety, and locale normalization.

Plugins can declare readmes in metadata.yaml. The dashboard resolves the requested locale (exact, then base language, then default), validates that declared paths stay inside the plugin directory, and shows a language selector when more than one README exists.
@mjy1113451
mjy1113451 force-pushed the feat/plugin-readme-locale branch from 3d43641 to 6e02a77 Compare October 18, 2026 09:04

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="dashboard/src/views/extension/PluginDetailPage.vue" line_range="761" />
<code_context>
+      readmeLocale.value || locale.value,
+    );
+    // Ignore slow responses that belong to an older locale selection.
+    if (requestId !== readmeRequestId) return;

     if (res.data.status !== "ok") {
</code_context>
<issue_to_address>
**Stale plugin README replaces current selection**

When plugin A's detail request is still pending when the user switches to plugin B, and A's request completes after B's detail request, a delayed `fetchPluginDetail` for plugin A can finish after the user selects plugin B, then its watcher continuation calls `fetchReadme` with A's detail and assigns that read to the newest `readmeRequestId`; the guard accepts it, so A's README appears on B's page.

Cancel or identity-check each detail request and its continuation before assigning `pluginDetail` or starting a README fetch.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and the README endpoint now serves files selected through plugin metadata, so a defect in the path-confinement checks could let a caller with plugin scope read Markdown files outside the plugin directory. Reverting would stop future reads, but any data already returned cannot be undisclosed.

Blocking findings: dashboard/src/views/extension/PluginDetailPage.vue:761


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

readmeLocale.value || locale.value,
);
// Ignore slow responses that belong to an older locale selection.
if (requestId !== readmeRequestId) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Stale plugin README replaces current selection

When plugin A's detail request is still pending when the user switches to plugin B, and A's request completes after B's detail request, a delayed fetchPluginDetail for plugin A can finish after the user selects plugin B, then its watcher continuation calls fetchReadme with A's detail and assigns that read to the newest readmeRequestId; the guard accepts it, so A's README appears on B's page.

Cancel or identity-check each detail request and its continuation before assigning pluginDetail or starting a README fetch.

Prompt for AI agents
In `dashboard/src/views/extension/PluginDetailPage.vue` at line 761:

**Stale plugin README replaces current selection**

When plugin A's detail request is still pending when the user switches to plugin B, and A's request completes after B's detail request, a delayed `fetchPluginDetail` for plugin A can finish after the user selects plugin B, then its watcher continuation calls `fetchReadme` with A's detail and assigns that read to the newest `readmeRequestId`; the guard accepts it, so A's README appears on B's page.

Cancel or identity-check each detail request and its continuation before assigning `pluginDetail` or starting a README fetch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] 给不同语言README和插件配置加个语言切换按钮

1 participant