From 49a499d417fa30d51199f447eea537cb9cdcb995 Mon Sep 17 00:00:00 2001 From: Estrella Pan Date: Fri, 9 Oct 2026 13:14:33 +0200 Subject: [PATCH] =?UTF-8?q?fix(webui):=20=E5=86=85=E7=BD=AE=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E7=9A=84=E5=90=8D=E7=A7=B0=E3=80=81=E6=8F=8F=E8=BF=B0?= =?UTF-8?q?=E4=B8=8E=E9=85=8D=E7=BD=AE=E5=AD=97=E6=AE=B5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=8B=B1=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 内置插件的清单与 config_model 只有中文,英文界面里直接显示中文。前端在 builtin_plugins. 下为 4 个内置插件配齐中英文案,按插件 id 查找,查不到 (第三方插件)时仍显示后端文字。插件的停用原因改为代码(not_enabled、 unsigned_blocked),由前端翻译;“未启用”不再重复显示为提示框。 --- backend/src/module/plugin/manager.py | 6 +- backend/src/test/test_plugin_runtime.py | 6 +- .../src/components/setting/config-plugins.vue | 26 ++++-- webui/src/i18n/en.json | 83 ++++++++++++++++++- webui/src/i18n/zh-CN.json | 83 ++++++++++++++++++- webui/src/utils/id-label.ts | 7 +- webui/src/utils/plugin-text.ts | 52 ++++++++++++ 7 files changed, 246 insertions(+), 17 deletions(-) create mode 100644 webui/src/utils/plugin-text.ts diff --git a/backend/src/module/plugin/manager.py b/backend/src/module/plugin/manager.py index 19e40ac1..3896b568 100644 --- a/backend/src/module/plugin/manager.py +++ b/backend/src/module/plugin/manager.py @@ -331,12 +331,12 @@ class PluginManager: del self._failed[plugin_id] def _blocked_reason(self, candidate: PluginCandidate) -> str | None: - """插件不应运行的原因;None 表示应启用。""" + """插件不应运行的原因代码(前端按代码翻译);None 表示应启用。""" conf = self._settings.plugins if not self._enabled(candidate): - return "未启用" + return "not_enabled" if not candidate.signed and not conf.allow_unsigned: - return "未签名插件需要开启 plugins.allow_unsigned" + return "unsigned_blocked" return None def _enabled(self, candidate: PluginCandidate) -> bool: diff --git a/backend/src/test/test_plugin_runtime.py b/backend/src/test/test_plugin_runtime.py index 7e6cd8d7..2fbbb66c 100644 --- a/backend/src/test/test_plugin_runtime.py +++ b/backend/src/test/test_plugin_runtime.py @@ -686,7 +686,7 @@ class TestManager: await manager.start() [status] = manager.statuses() assert status.state == "disabled" - assert "allow_unsigned" in (status.error or "") + assert status.error == "unsigned_blocked" settings_obj.plugins.allow_unsigned = True await manager.apply_settings() assert manager.statuses()[0].state == "active" @@ -834,7 +834,7 @@ class TestManager: settings_obj.plugins.enabled = {"demo": False} await manager.apply_settings() [status] = manager.statuses() - assert (status.state, status.error) == ("disabled", "未启用") + assert (status.state, status.error) == ("disabled", "not_enabled") async def test_unknown_extension_point_fails_load(self, tmp_path): recorder = Recorder() @@ -984,7 +984,7 @@ class TestPluginsApi: source="local", signed=False, state="disabled", - error="未签名插件需要开启 plugins.allow_unsigned", + error="unsigned_blocked", ) ctx = SimpleNamespace(plugins=SimpleNamespace(statuses=lambda: [status])) monkeypatch.setattr(settings, "plugins", Plugins()) diff --git a/webui/src/components/setting/config-plugins.vue b/webui/src/components/setting/config-plugins.vue index ba599647..af9b759a 100644 --- a/webui/src/components/setting/config-plugins.vue +++ b/webui/src/components/setting/config-plugins.vue @@ -13,6 +13,12 @@ import { refreshPluginProviders } from '@/hooks/usePluginProviders'; import { refreshPluginUi } from '@/hooks/usePluginUi'; import { idLabel } from '@/utils/id-label'; import { fillSchemaDefaults, schemaFields } from '@/utils/plugin-schema'; +import { + localizeFields, + pluginDescription, + pluginName, + pluginReason, +} from '@/utils/plugin-text'; // 插件卡片不参与全局保存:每次改动直接调 /plugins 接口落盘并应用, // 随后刷新 config store 的 plugins 段,避免全局保存用旧值覆盖。 @@ -138,7 +144,7 @@ async function setEnabled(plugin: PluginInfo, enabled: boolean) { impact && !(await confirm({ title: t('config.plugins_set.disable_confirm_title', { - name: plugin.name, + name: pluginName(t, plugin.id, plugin.name), }), body: impact, confirmText: t('config.plugins_set.disable'), @@ -187,7 +193,7 @@ async function install(entry: CatalogEntry) { async function uninstall(plugin: PluginInfo) { const confirmed = await confirm({ title: t('config.plugins_set.uninstall_confirm_title', { - name: plugin.name, + name: pluginName(t, plugin.id, plugin.name), }), body: [impactText(plugin), t('config.plugins_set.uninstall_confirm_body')] .filter(Boolean) @@ -246,7 +252,7 @@ onMounted(load); >
- {{ plugin.name }} + {{ pluginName(t, plugin.id, plugin.name) }} {{ plugin.id }} · v{{ plugin.version }} @@ -255,7 +261,9 @@ onMounted(load); :model-value="plugin.enabled" :loading="busy.has(plugin.id)" :aria-label=" - $t('config.plugins_set.enabled_for', { name: plugin.name }) + $t('config.plugins_set.enabled_for', { + name: pluginName(t, plugin.id, plugin.name), + }) " @update:model-value="setEnabled(plugin, $event)" /> @@ -272,14 +280,14 @@ onMounted(load);

- {{ plugin.description }} + {{ pluginDescription(t, plugin.id, plugin.description) }}

- {{ plugin.error }} + {{ pluginReason(t, plugin.error) }}

( (node, part) => (node as Record | undefined)?.[part], en ); - return typeof found === 'string' ? t(key) : id; + return typeof found === 'string'; } diff --git a/webui/src/utils/plugin-text.ts b/webui/src/utils/plugin-text.ts new file mode 100644 index 00000000..f68d8675 --- /dev/null +++ b/webui/src/utils/plugin-text.ts @@ -0,0 +1,52 @@ +import { hasKey } from './id-label'; +import type { SchemaField } from './plugin-schema'; + +type Translate = (key: string) => string; + +/** + * 内置插件的名称、描述与配置字段文案:清单和 config_model 里只有中文,前端在 + * `builtin_plugins.` 下为它们各配一份翻译。没有收录的插件(第三方)原样显示 + * 后端给的文字。 + */ +function root(pluginId: string): string { + return `builtin_plugins.${pluginId.replaceAll('-', '_')}`; +} + +function pick(t: Translate, key: string, fallback: string): string { + return hasKey(key) ? t(key) : fallback; +} + +export function pluginName(t: Translate, id: string, fallback: string): string { + return pick(t, `${root(id)}.name`, fallback); +} + +export function pluginDescription( + t: Translate, + id: string, + fallback: string +): string { + return pick(t, `${root(id)}.description`, fallback); +} + +/** 字段文案:`fields..title / description`,对象数组的子字段嵌套在父字段下 */ +export function localizeFields( + t: Translate, + id: string, + fields: SchemaField[], + parent = `${root(id)}.fields` +): SchemaField[] { + return fields.map((field) => { + const base = `${parent}.${field.key}`; + return { + ...field, + label: pick(t, `${base}.title`, field.label), + description: pick(t, `${base}.description`, field.description), + itemFields: localizeFields(t, id, field.itemFields, base), + }; + }); +} + +/** 后端 blocked 原因代码(如 not_enabled)的文案;其它错误原样显示 */ +export function pluginReason(t: Translate, error: string): string { + return pick(t, `config.plugins_set.reason.${error}`, error); +}