From d4ea1b6f28460ac42e7fafc5ec1a7f33f4c39e03 Mon Sep 17 00:00:00 2001 From: stevessr Date: Tue, 10 Feb 2026 20:12:35 +0800 Subject: [PATCH 1/2] =?UTF-8?q?Feat:=20=E4=B8=BA=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E7=9A=84=E9=80=82=E9=85=8D=E5=99=A8=E7=9A=84?= =?UTF-8?q?=E5=85=83=E6=95=B0=E6=8D=AE&i18n=E6=8F=90=E4=BE=9B=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E9=80=9A=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/platform/platform_metadata.py | 5 +++ astrbot/core/platform/register.py | 5 +++ astrbot/dashboard/routes/config.py | 44 +++++++++++++++++- .../components/platform/AddNewPlatform.vue | 9 +++- dashboard/src/i18n/composables.ts | 45 +++++++++++++++++++ dashboard/src/main.ts | 8 ++++ dashboard/src/views/ConfigPage.vue | 18 ++++++++ dashboard/src/views/ExtensionPage.vue | 18 +++++++- dashboard/src/views/PlatformPage.vue | 18 +++++++- 9 files changed, 165 insertions(+), 5 deletions(-) diff --git a/astrbot/core/platform/platform_metadata.py b/astrbot/core/platform/platform_metadata.py index 00e7dc966..26926564b 100644 --- a/astrbot/core/platform/platform_metadata.py +++ b/astrbot/core/platform/platform_metadata.py @@ -24,3 +24,8 @@ class PlatformMetadata: module_path: str | None = None """注册该适配器的模块路径,用于插件热重载时清理""" + i18n_resources: dict[str, dict] | None = None + """国际化资源数据,如 {"zh-CN": {...}, "en-US": {...}}""" + + config_metadata: dict | None = None + """配置项元数据,用于 WebUI 生成表单。对应 config_metadata.json 的内容""" diff --git a/astrbot/core/platform/register.py b/astrbot/core/platform/register.py index 3bbec4809..62ec5070a 100644 --- a/astrbot/core/platform/register.py +++ b/astrbot/core/platform/register.py @@ -15,11 +15,14 @@ def register_platform_adapter( adapter_display_name: str | None = None, logo_path: str | None = None, support_streaming_message: bool = True, + i18n_resources: dict[str, dict] | None = None, + config_metadata: dict | None = None, ): """用于注册平台适配器的带参装饰器。 default_config_tmpl 指定了平台适配器的默认配置模板。用户填写好后将会作为 platform_config 传入你的 Platform 类的实现类。 logo_path 指定了平台适配器的 logo 文件路径,是相对于插件目录的路径。 + config_metadata 指定了配置项的元数据,用于 WebUI 生成表单。如果不指定,WebUI 将会把配置项渲染为原始的键值对编辑框。 """ def decorator(cls): @@ -49,6 +52,8 @@ def decorator(cls): logo_path=logo_path, support_streaming_message=support_streaming_message, module_path=module_path, + i18n_resources=i18n_resources, + config_metadata=config_metadata, ) platform_registry.append(pm) platform_cls_map[adapter_name] = cls diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index efea4c7cf..6d60fb6de 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -1290,6 +1290,30 @@ async def _register_platform_logo(self, platform, platform_default_tmpl) -> None f"Unexpected error registering logo for platform {platform.name}: {e}", ) + def _inject_platform_metadata_with_i18n( + self, platform, metadata, platform_i18n_translations: dict + ): + """将配置元数据注入到 metadata 中并处理国际化键转换。""" + metadata["platform_group"]["metadata"]["platform"].setdefault("items", {}) + platform_items_to_inject = copy.deepcopy(platform.config_metadata) + + if platform.i18n_resources: + i18n_prefix = f"platform_group.platform.{platform.name}" + + for lang, lang_data in platform.i18n_resources.items(): + platform_i18n_translations.setdefault(lang, {}).setdefault( + "platform_group", {} + ).setdefault("platform", {})[platform.name] = lang_data + + for field_key, field_value in platform_items_to_inject.items(): + for key in ("description", "hint", "labels"): + if key in field_value: + field_value[key] = f"{i18n_prefix}.{field_key}.{key}" + + metadata["platform_group"]["metadata"]["platform"]["items"].update( + platform_items_to_inject + ) + async def _get_astrbot_config(self): config = self.config metadata = copy.deepcopy(CONFIG_METADATA_2) @@ -1311,11 +1335,23 @@ async def _get_astrbot_config(self): "config_template" ] + # 收集平台的 i18n 翻译数据 + platform_i18n_translations = {} + # 收集需要注册logo的平台 logo_registration_tasks = [] for platform in platform_registry: if platform.default_config_tmpl: - platform_default_tmpl[platform.name] = platform.default_config_tmpl + platform_default_tmpl[platform.name] = copy.deepcopy( + platform.default_config_tmpl + ) + + # 注入配置元数据(在 convert_to_i18n_keys 之后,使用国际化键) + if platform.config_metadata: + self._inject_platform_metadata_with_i18n( + platform, metadata, platform_i18n_translations + ) + # 收集logo注册任务 if platform.logo_path: logo_registration_tasks.append( @@ -1334,7 +1370,11 @@ async def _get_astrbot_config(self): if provider.default_config_tmpl: provider_default_tmpl[provider.type] = provider.default_config_tmpl - return {"metadata": metadata, "config": config} + return { + "metadata": metadata, + "config": config, + "platform_i18n_translations": platform_i18n_translations, + } async def _get_plugin_config(self, plugin_name: str): ret: dict = {"metadata": None, "config": None} diff --git a/dashboard/src/components/platform/AddNewPlatform.vue b/dashboard/src/components/platform/AddNewPlatform.vue index 6595bf129..e771a56e5 100644 --- a/dashboard/src/components/platform/AddNewPlatform.vue +++ b/dashboard/src/components/platform/AddNewPlatform.vue @@ -522,7 +522,14 @@ export default { } }, methods: { - getPlatformIcon, + getPlatformIcon(platformType) { + // Check for plugin-provided logo_token first + const template = this.platformTemplates?.[platformType]; + if (template && template.logo_token) { + return `/api/file/${template.logo_token}`; + } + return getPlatformIcon(platformType); + }, getPlatformDescription, resetForm() { this.selectedPlatformType = null; diff --git a/dashboard/src/i18n/composables.ts b/dashboard/src/i18n/composables.ts index 7b76881b8..131010bc3 100644 --- a/dashboard/src/i18n/composables.ts +++ b/dashboard/src/i18n/composables.ts @@ -89,6 +89,13 @@ export function useI18n() { // 保存到localStorage localStorage.setItem('astrbot-locale', newLocale); + + // 触发自定义事件,通知相关页面重新加载配置数据 + // 这是因为插件适配器的 i18n 数据是通过后端 API 注入的, + // 需要根据 Accept-Language 头重新获取 + window.dispatchEvent(new CustomEvent('astrbot-locale-changed', { + detail: { locale: newLocale } + })); } }; @@ -171,6 +178,44 @@ export function useLanguageSwitcher() { }; } +/** + * 将动态翻译数据(如插件提供的 i18n)合并到当前翻译中。 + * @param modulePath 模块路径,如 'features.config-metadata' + * @param allLocaleData 所有语言的翻译数据,如 { "zh-CN": {...}, "en-US": {...} } + */ +export function mergeDynamicTranslations(modulePath: string, allLocaleData: Record) { + const locale = currentLocale.value; + const localeData = allLocaleData[locale]; + if (!localeData || typeof localeData !== 'object') return; + + const pathParts = modulePath.split('.'); + let target: any = translations.value; + for (const part of pathParts) { + if (!(part in target) || typeof target[part] !== 'object') { + target[part] = {}; + } + target = target[part]; + } + + deepMerge(target, localeData); + + // 触发响应式更新 + translations.value = { ...translations.value }; +} + +function deepMerge(target: Record, source: Record) { + for (const key of Object.keys(source)) { + if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { + if (!(key in target) || typeof target[key] !== 'object') { + target[key] = {}; + } + deepMerge(target[key], source[key]); + } else { + target[key] = source[key]; + } + } +} + // 初始化函数(在应用启动时调用) export async function setupI18n() { // 从localStorage获取保存的语言设置 diff --git a/dashboard/src/main.ts b/dashboard/src/main.ts index 451f1616b..687166654 100644 --- a/dashboard/src/main.ts +++ b/dashboard/src/main.ts @@ -84,6 +84,10 @@ axios.interceptors.request.use((config) => { if (token) { config.headers['Authorization'] = `Bearer ${token}`; } + const locale = localStorage.getItem('astrbot-locale'); + if (locale) { + config.headers['Accept-Language'] = locale; + } return config; }); @@ -98,6 +102,10 @@ window.fetch = (input: RequestInfo | URL, init?: RequestInit) => { if (!headers.has('Authorization')) { headers.set('Authorization', `Bearer ${token}`); } + const locale = localStorage.getItem('astrbot-locale'); + if (locale && !headers.has('Accept-Language')) { + headers.set('Accept-Language', locale); + } return _origFetch(input, { ...init, headers }); }; diff --git a/dashboard/src/views/ConfigPage.vue b/dashboard/src/views/ConfigPage.vue index 4c27081f3..dd4bbd715 100644 --- a/dashboard/src/views/ConfigPage.vue +++ b/dashboard/src/views/ConfigPage.vue @@ -303,8 +303,26 @@ export default { this.getConfigInfoList(targetConfigId); // 初始化配置类型状态 this.configType = this.isSystemConfig ? 'system' : 'normal'; + + // 监听语言切换事件,重新加载配置以获取插件的 i18n 数据 + window.addEventListener('astrbot-locale-changed', this.handleLocaleChange); + }, + + beforeUnmount() { + // 移除语言切换事件监听器 + window.removeEventListener('astrbot-locale-changed', this.handleLocaleChange); }, methods: { + // 处理语言切换事件,重新加载配置以获取插件的 i18n 数据 + handleLocaleChange() { + // 重新加载当前配置 + if (this.selectedConfigID) { + this.getConfig(this.selectedConfigID); + } else if (this.isSystemConfig) { + this.getConfig(); + } + }, + getConfigInfoList(abconf_id) { // 获取配置列表 axios.get('/api/config/abconfs').then((res) => { diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue index c62637e3c..7429ae197 100644 --- a/dashboard/src/views/ExtensionPage.vue +++ b/dashboard/src/views/ExtensionPage.vue @@ -14,7 +14,7 @@ import { useCommonStore } from "@/stores/common"; import { useI18n, useModuleI18n } from "@/i18n/composables"; import defaultPluginIcon from "@/assets/images/plugin_icon.png"; -import { ref, computed, onMounted, reactive, watch } from "vue"; +import { ref, computed, onMounted, onUnmounted, reactive, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; const commonStore = useCommonStore(); @@ -1054,6 +1054,22 @@ onMounted(async () => { } }); +// 处理语言切换事件,重新加载插件配置以获取插件的 i18n 数据 +const handleLocaleChange = () => { + // 如果配置对话框是打开的,重新加载当前插件的配置 + if (configDialog.value && currentConfigPlugin.value) { + openExtensionConfig(currentConfigPlugin.value); + } +}; + +// 监听语言切换事件 +window.addEventListener("astrbot-locale-changed", handleLocaleChange); + +// 清理事件监听器 +onUnmounted(() => { + window.removeEventListener("astrbot-locale-changed", handleLocaleChange); +}); + // 搜索防抖处理 let searchDebounceTimer = null; watch(marketSearch, (newVal) => { diff --git a/dashboard/src/views/PlatformPage.vue b/dashboard/src/views/PlatformPage.vue index abf0c1d4d..f50df9554 100644 --- a/dashboard/src/views/PlatformPage.vue +++ b/dashboard/src/views/PlatformPage.vue @@ -195,7 +195,7 @@ import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue'; import ItemCard from '@/components/shared/ItemCard.vue'; import AddNewPlatform from '@/components/platform/AddNewPlatform.vue'; import { useCommonStore } from '@/stores/common'; -import { useI18n, useModuleI18n } from '@/i18n/composables'; +import { useI18n, useModuleI18n, mergeDynamicTranslations } from '@/i18n/composables'; import { getPlatformIcon, getTutorialLink } from '@/utils/platformUtils'; import { askForConfirmation as askForConfirmationDialog, @@ -280,15 +280,25 @@ export default { this.statsRefreshInterval = setInterval(() => { this.getPlatformStats(); }, 10000); + + // 监听语言切换事件,重新加载配置以获取插件的 i18n 数据 + window.addEventListener('astrbot-locale-changed', this.handleLocaleChange); }, beforeUnmount() { if (this.statsRefreshInterval) { clearInterval(this.statsRefreshInterval); } + // 移除语言切换事件监听器 + window.removeEventListener('astrbot-locale-changed', this.handleLocaleChange); }, methods: { + // 处理语言切换事件,重新加载配置以获取插件的 i18n 数据 + handleLocaleChange() { + this.getConfig(); + }, + // 从工具函数导入 getPlatformIcon(platform_id) { // 首先检查是否有来自插件的 logo_token @@ -305,6 +315,12 @@ export default { this.config_data = res.data.data.config; this.fetched = true this.metadata = res.data.data.metadata; + + // 将插件平台适配器的 i18n 翻译注入到前端 i18n 系统中 + const platformI18n = res.data.data.platform_i18n_translations; + if (platformI18n && typeof platformI18n === 'object') { + mergeDynamicTranslations('features.config-metadata', platformI18n); + } }).catch((err) => { this.showError(err); }); From 51408780da812335e6c1c9794f53477bcd7dcab9 Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:48:11 +0800 Subject: [PATCH 2/2] chore: update docstrings with pull request references Added references to pull request 5045 in docstrings. --- astrbot/core/platform/platform_metadata.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/astrbot/core/platform/platform_metadata.py b/astrbot/core/platform/platform_metadata.py index 26926564b..2d01b921d 100644 --- a/astrbot/core/platform/platform_metadata.py +++ b/astrbot/core/platform/platform_metadata.py @@ -25,7 +25,13 @@ class PlatformMetadata: module_path: str | None = None """注册该适配器的模块路径,用于插件热重载时清理""" i18n_resources: dict[str, dict] | None = None - """国际化资源数据,如 {"zh-CN": {...}, "en-US": {...}}""" + """国际化资源数据,如 {"zh-CN": {...}, "en-US": {...}} + + 参考 https://github.com/AstrBotDevs/AstrBot/pull/5045 + """ config_metadata: dict | None = None - """配置项元数据,用于 WebUI 生成表单。对应 config_metadata.json 的内容""" + """配置项元数据,用于 WebUI 生成表单。对应 config_metadata.json 的内容 + + 参考 https://github.com/AstrBotDevs/AstrBot/pull/5045 + """