Skip to content

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

Closed
antares127 wants to merge 3 commits into
AstrBotDevs:masterfrom
antares127:feat/plugin-readme-locale
Closed

antares127 wants to merge 3 commits into
AstrBotDevs:masterfrom
antares127:feat/plugin-readme-locale

Conversation

@antares127

@antares127 antares127 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 localized plugin READMEs with secure locale fallback and a dashboard language switcher.

New Features:

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

Bug Fixes:

  • Prevent stale plugin, README, and changelog responses from overwriting content after plugin or locale changes.

Enhancements:

  • Validate README paths and preserve legacy README behavior for plugins without localized declarations.
  • Expose resolved and available README locales through the plugin APIs.

Documentation:

  • Document the localized README metadata contract and fallback behavior in plugin publishing guides.

Tests:

  • Add coverage for locale matching, fallback behavior, path validation, symlink protection, and locale normalization.

Review fix (Sourcery finding)

  • fetchPluginDetail() now carries a request id + plugin identity guard: a slow detail response for plugin A can no longer overwrite pluginDetail after the user switched to plugin B, and the watcher continuation only starts README/changelog fetches when the detail request still belongs to the current selection.
  • The README staleness guard now checks the plugin identity in addition to the request id, and the market README branch (which previously had no guard at all) applies the same checks.

Supersedes #10613 (re-created from the antares127 fork).

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.
- Identity-check fetchPluginDetail responses: a slow response for plugin A
  can no longer overwrite pluginDetail after the user switched to plugin B
- Guard the watcher continuation so README/changelog fetches are only
  started when the detail request still belongs to the current selection
- Extend the README request guard to also verify the plugin identity, not
  just the request id, and apply the same staleness checks to the market
  README branch which previously had none

@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="astrbot/dashboard/services/plugin_service.py" line_range="2030-2037" />
<code_context>
+        candidates: list[tuple[str | None, str]] = []
+        if requested:
+            matched_locale = None
+            if requested in locale_paths:
+                matched_locale = requested
+            elif "-" in requested:
+                base_language = requested.split("-", 1)[0]
+                if base_language in locale_paths:
+                    matched_locale = base_language
+            if matched_locale:
+                candidates.append((matched_locale, locale_paths[matched_locale]))
+        candidates.append((None, declared.get("default") or "README.md"))
+
</code_context>
<issue_to_address>
**Base-language fallback is skipped**

When the requested exact-locale README is declared but missing or invalid, while a base-language README and default README are available, `get_plugin_readme()` skips the base-language candidate when the exact locale is declared, then falls back to the default after the exact file fails resolution. Users see the default README instead of the available base-language version.

Try the base-language README after an exact-locale candidate fails, before trying the default.

Also at `astrbot/dashboard/services/plugin_service.py:2038`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and if README path selection or containment is wrong, the authenticated endpoint could serve unintended Markdown contents from the filesystem; reverting stops future reads but cannot retract content already disclosed.

Blocking findings: astrbot/dashboard/services/plugin_service.py:2037


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

Comment on lines +2030 to +2037
if requested in locale_paths:
matched_locale = requested
elif "-" in requested:
base_language = requested.split("-", 1)[0]
if base_language in locale_paths:
matched_locale = base_language
if matched_locale:
candidates.append((matched_locale, locale_paths[matched_locale]))

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 · Base-language fallback is skipped

When the requested exact-locale README is declared but missing or invalid, while a base-language README and default README are available, get_plugin_readme() skips the base-language candidate when the exact locale is declared, then falls back to the default after the exact file fails resolution. Users see the default README instead of the available base-language version.

Try the base-language README after an exact-locale candidate fails, before trying the default.

Also at astrbot/dashboard/services/plugin_service.py:2038.

Prompt for AI agents
In `astrbot/dashboard/services/plugin_service.py` at lines 2030-2037:

**Base-language fallback is skipped**

When the requested exact-locale README is declared but missing or invalid, while a base-language README and default README are available, `get_plugin_readme()` skips the base-language candidate when the exact locale is declared, then falls back to the default after the exact file fails resolution. Users see the default README instead of the available base-language version.

Try the base-language README after an exact-locale candidate fails, before trying the default.

Also at `astrbot/dashboard/services/plugin_service.py:2038`.

- Guard fetchChangelog with a request id + plugin identity check so a slow
  changelog response for plugin A can no longer overwrite plugin B's page
  (same race class that was fixed for the README fetch)
- Skip the duplicate README fetch triggered when the plugin-switch watcher
  resets readmeLocale back to the default
- Return the originally declared locale key (e.g. zh-CN) as resolved_locale
  instead of the normalized lowercase tag, keeping it consistent with
  available_locales and the documented format
- Also catch ValueError when resolving declared README paths so an entry
  with an embedded NUL byte degrades gracefully instead of failing the
  request
@antares127

Copy link
Copy Markdown
Author

Closing this: the feature has since been implemented upstream, so this PR is no longer needed. Thanks for the reviews!

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和插件配置加个语言切换按钮

2 participants