feat: add i18n supports for custom platform adapters (#5045)
* Feat: 为插件提供的适配器的元数据&i18n提供数据通路 * chore: update docstrings with pull request references Added references to pull request 5045 in docstrings. --------- Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
This commit is contained in:
@@ -24,3 +24,14 @@ class PlatformMetadata:
|
||||
|
||||
module_path: str | None = None
|
||||
"""注册该适配器的模块路径,用于插件热重载时清理"""
|
||||
i18n_resources: dict[str, dict] | None = None
|
||||
"""国际化资源数据,如 {"zh-CN": {...}, "en-US": {...}}
|
||||
|
||||
参考 https://github.com/AstrBotDevs/AstrBot/pull/5045
|
||||
"""
|
||||
|
||||
config_metadata: dict | None = None
|
||||
"""配置项元数据,用于 WebUI 生成表单。对应 config_metadata.json 的内容
|
||||
|
||||
参考 https://github.com/AstrBotDevs/AstrBot/pull/5045
|
||||
"""
|
||||
|
||||
@@ -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 register_platform_adapter(
|
||||
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
|
||||
|
||||
@@ -1290,6 +1290,30 @@ class ConfigRoute(Route):
|
||||
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 @@ class ConfigRoute(Route):
|
||||
"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 @@ class ConfigRoute(Route):
|
||||
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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string, any>) {
|
||||
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<string, any>, source: Record<string, any>) {
|
||||
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获取保存的语言设置
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user