Skip to content

Commit 473e01a

Browse files
stevessrSoulter
andauthored
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>
1 parent cd5312b commit 473e01a

9 files changed

Lines changed: 171 additions & 5 deletions

File tree

astrbot/core/platform/platform_metadata.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,14 @@ class PlatformMetadata:
2424

2525
module_path: str | None = None
2626
"""注册该适配器的模块路径,用于插件热重载时清理"""
27+
i18n_resources: dict[str, dict] | None = None
28+
"""国际化资源数据,如 {"zh-CN": {...}, "en-US": {...}}
29+
30+
参考 https://github.com/AstrBotDevs/AstrBot/pull/5045
31+
"""
32+
33+
config_metadata: dict | None = None
34+
"""配置项元数据,用于 WebUI 生成表单。对应 config_metadata.json 的内容
35+
36+
参考 https://github.com/AstrBotDevs/AstrBot/pull/5045
37+
"""

astrbot/core/platform/register.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ def register_platform_adapter(
1515
adapter_display_name: str | None = None,
1616
logo_path: str | None = None,
1717
support_streaming_message: bool = True,
18+
i18n_resources: dict[str, dict] | None = None,
19+
config_metadata: dict | None = None,
1820
):
1921
"""用于注册平台适配器的带参装饰器。
2022
2123
default_config_tmpl 指定了平台适配器的默认配置模板。用户填写好后将会作为 platform_config 传入你的 Platform 类的实现类。
2224
logo_path 指定了平台适配器的 logo 文件路径,是相对于插件目录的路径。
25+
config_metadata 指定了配置项的元数据,用于 WebUI 生成表单。如果不指定,WebUI 将会把配置项渲染为原始的键值对编辑框。
2326
"""
2427

2528
def decorator(cls):
@@ -49,6 +52,8 @@ def decorator(cls):
4952
logo_path=logo_path,
5053
support_streaming_message=support_streaming_message,
5154
module_path=module_path,
55+
i18n_resources=i18n_resources,
56+
config_metadata=config_metadata,
5257
)
5358
platform_registry.append(pm)
5459
platform_cls_map[adapter_name] = cls

astrbot/dashboard/routes/config.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,6 +1290,30 @@ async def _register_platform_logo(self, platform, platform_default_tmpl) -> None
12901290
f"Unexpected error registering logo for platform {platform.name}: {e}",
12911291
)
12921292

1293+
def _inject_platform_metadata_with_i18n(
1294+
self, platform, metadata, platform_i18n_translations: dict
1295+
):
1296+
"""将配置元数据注入到 metadata 中并处理国际化键转换。"""
1297+
metadata["platform_group"]["metadata"]["platform"].setdefault("items", {})
1298+
platform_items_to_inject = copy.deepcopy(platform.config_metadata)
1299+
1300+
if platform.i18n_resources:
1301+
i18n_prefix = f"platform_group.platform.{platform.name}"
1302+
1303+
for lang, lang_data in platform.i18n_resources.items():
1304+
platform_i18n_translations.setdefault(lang, {}).setdefault(
1305+
"platform_group", {}
1306+
).setdefault("platform", {})[platform.name] = lang_data
1307+
1308+
for field_key, field_value in platform_items_to_inject.items():
1309+
for key in ("description", "hint", "labels"):
1310+
if key in field_value:
1311+
field_value[key] = f"{i18n_prefix}.{field_key}.{key}"
1312+
1313+
metadata["platform_group"]["metadata"]["platform"]["items"].update(
1314+
platform_items_to_inject
1315+
)
1316+
12931317
async def _get_astrbot_config(self):
12941318
config = self.config
12951319
metadata = copy.deepcopy(CONFIG_METADATA_2)
@@ -1311,11 +1335,23 @@ async def _get_astrbot_config(self):
13111335
"config_template"
13121336
]
13131337

1338+
# 收集平台的 i18n 翻译数据
1339+
platform_i18n_translations = {}
1340+
13141341
# 收集需要注册logo的平台
13151342
logo_registration_tasks = []
13161343
for platform in platform_registry:
13171344
if platform.default_config_tmpl:
1318-
platform_default_tmpl[platform.name] = platform.default_config_tmpl
1345+
platform_default_tmpl[platform.name] = copy.deepcopy(
1346+
platform.default_config_tmpl
1347+
)
1348+
1349+
# 注入配置元数据(在 convert_to_i18n_keys 之后,使用国际化键)
1350+
if platform.config_metadata:
1351+
self._inject_platform_metadata_with_i18n(
1352+
platform, metadata, platform_i18n_translations
1353+
)
1354+
13191355
# 收集logo注册任务
13201356
if platform.logo_path:
13211357
logo_registration_tasks.append(
@@ -1334,7 +1370,11 @@ async def _get_astrbot_config(self):
13341370
if provider.default_config_tmpl:
13351371
provider_default_tmpl[provider.type] = provider.default_config_tmpl
13361372

1337-
return {"metadata": metadata, "config": config}
1373+
return {
1374+
"metadata": metadata,
1375+
"config": config,
1376+
"platform_i18n_translations": platform_i18n_translations,
1377+
}
13381378

13391379
async def _get_plugin_config(self, plugin_name: str):
13401380
ret: dict = {"metadata": None, "config": None}

dashboard/src/components/platform/AddNewPlatform.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,14 @@ export default {
522522
}
523523
},
524524
methods: {
525-
getPlatformIcon,
525+
getPlatformIcon(platformType) {
526+
// Check for plugin-provided logo_token first
527+
const template = this.platformTemplates?.[platformType];
528+
if (template && template.logo_token) {
529+
return `/api/file/${template.logo_token}`;
530+
}
531+
return getPlatformIcon(platformType);
532+
},
526533
getPlatformDescription,
527534
resetForm() {
528535
this.selectedPlatformType = null;

dashboard/src/i18n/composables.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ export function useI18n() {
8989

9090
// 保存到localStorage
9191
localStorage.setItem('astrbot-locale', newLocale);
92+
93+
// 触发自定义事件,通知相关页面重新加载配置数据
94+
// 这是因为插件适配器的 i18n 数据是通过后端 API 注入的,
95+
// 需要根据 Accept-Language 头重新获取
96+
window.dispatchEvent(new CustomEvent('astrbot-locale-changed', {
97+
detail: { locale: newLocale }
98+
}));
9299
}
93100
};
94101

@@ -171,6 +178,44 @@ export function useLanguageSwitcher() {
171178
};
172179
}
173180

181+
/**
182+
* 将动态翻译数据(如插件提供的 i18n)合并到当前翻译中。
183+
* @param modulePath 模块路径,如 'features.config-metadata'
184+
* @param allLocaleData 所有语言的翻译数据,如 { "zh-CN": {...}, "en-US": {...} }
185+
*/
186+
export function mergeDynamicTranslations(modulePath: string, allLocaleData: Record<string, any>) {
187+
const locale = currentLocale.value;
188+
const localeData = allLocaleData[locale];
189+
if (!localeData || typeof localeData !== 'object') return;
190+
191+
const pathParts = modulePath.split('.');
192+
let target: any = translations.value;
193+
for (const part of pathParts) {
194+
if (!(part in target) || typeof target[part] !== 'object') {
195+
target[part] = {};
196+
}
197+
target = target[part];
198+
}
199+
200+
deepMerge(target, localeData);
201+
202+
// 触发响应式更新
203+
translations.value = { ...translations.value };
204+
}
205+
206+
function deepMerge(target: Record<string, any>, source: Record<string, any>) {
207+
for (const key of Object.keys(source)) {
208+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
209+
if (!(key in target) || typeof target[key] !== 'object') {
210+
target[key] = {};
211+
}
212+
deepMerge(target[key], source[key]);
213+
} else {
214+
target[key] = source[key];
215+
}
216+
}
217+
}
218+
174219
// 初始化函数(在应用启动时调用)
175220
export async function setupI18n() {
176221
// 从localStorage获取保存的语言设置

dashboard/src/main.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ axios.interceptors.request.use((config) => {
8484
if (token) {
8585
config.headers['Authorization'] = `Bearer ${token}`;
8686
}
87+
const locale = localStorage.getItem('astrbot-locale');
88+
if (locale) {
89+
config.headers['Accept-Language'] = locale;
90+
}
8791
return config;
8892
});
8993

@@ -98,6 +102,10 @@ window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
98102
if (!headers.has('Authorization')) {
99103
headers.set('Authorization', `Bearer ${token}`);
100104
}
105+
const locale = localStorage.getItem('astrbot-locale');
106+
if (locale && !headers.has('Accept-Language')) {
107+
headers.set('Accept-Language', locale);
108+
}
101109
return _origFetch(input, { ...init, headers });
102110
};
103111

dashboard/src/views/ConfigPage.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,26 @@ export default {
303303
this.getConfigInfoList(targetConfigId);
304304
// 初始化配置类型状态
305305
this.configType = this.isSystemConfig ? 'system' : 'normal';
306+
307+
// 监听语言切换事件,重新加载配置以获取插件的 i18n 数据
308+
window.addEventListener('astrbot-locale-changed', this.handleLocaleChange);
309+
},
310+
311+
beforeUnmount() {
312+
// 移除语言切换事件监听器
313+
window.removeEventListener('astrbot-locale-changed', this.handleLocaleChange);
306314
},
307315
methods: {
316+
// 处理语言切换事件,重新加载配置以获取插件的 i18n 数据
317+
handleLocaleChange() {
318+
// 重新加载当前配置
319+
if (this.selectedConfigID) {
320+
this.getConfig(this.selectedConfigID);
321+
} else if (this.isSystemConfig) {
322+
this.getConfig();
323+
}
324+
},
325+
308326
getConfigInfoList(abconf_id) {
309327
// 获取配置列表
310328
axios.get('/api/config/abconfs').then((res) => {

dashboard/src/views/ExtensionPage.vue

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { useCommonStore } from "@/stores/common";
1414
import { useI18n, useModuleI18n } from "@/i18n/composables";
1515
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
1616
17-
import { ref, computed, onMounted, reactive, watch } from "vue";
17+
import { ref, computed, onMounted, onUnmounted, reactive, watch } from "vue";
1818
import { useRoute, useRouter } from "vue-router";
1919
2020
const commonStore = useCommonStore();
@@ -1054,6 +1054,22 @@ onMounted(async () => {
10541054
}
10551055
});
10561056
1057+
// 处理语言切换事件,重新加载插件配置以获取插件的 i18n 数据
1058+
const handleLocaleChange = () => {
1059+
// 如果配置对话框是打开的,重新加载当前插件的配置
1060+
if (configDialog.value && currentConfigPlugin.value) {
1061+
openExtensionConfig(currentConfigPlugin.value);
1062+
}
1063+
};
1064+
1065+
// 监听语言切换事件
1066+
window.addEventListener("astrbot-locale-changed", handleLocaleChange);
1067+
1068+
// 清理事件监听器
1069+
onUnmounted(() => {
1070+
window.removeEventListener("astrbot-locale-changed", handleLocaleChange);
1071+
});
1072+
10571073
// 搜索防抖处理
10581074
let searchDebounceTimer = null;
10591075
watch(marketSearch, (newVal) => {

dashboard/src/views/PlatformPage.vue

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
195195
import ItemCard from '@/components/shared/ItemCard.vue';
196196
import AddNewPlatform from '@/components/platform/AddNewPlatform.vue';
197197
import { useCommonStore } from '@/stores/common';
198-
import { useI18n, useModuleI18n } from '@/i18n/composables';
198+
import { useI18n, useModuleI18n, mergeDynamicTranslations } from '@/i18n/composables';
199199
import { getPlatformIcon, getTutorialLink } from '@/utils/platformUtils';
200200
import {
201201
askForConfirmation as askForConfirmationDialog,
@@ -280,15 +280,25 @@ export default {
280280
this.statsRefreshInterval = setInterval(() => {
281281
this.getPlatformStats();
282282
}, 10000);
283+
284+
// 监听语言切换事件,重新加载配置以获取插件的 i18n 数据
285+
window.addEventListener('astrbot-locale-changed', this.handleLocaleChange);
283286
},
284287
285288
beforeUnmount() {
286289
if (this.statsRefreshInterval) {
287290
clearInterval(this.statsRefreshInterval);
288291
}
292+
// 移除语言切换事件监听器
293+
window.removeEventListener('astrbot-locale-changed', this.handleLocaleChange);
289294
},
290295
291296
methods: {
297+
// 处理语言切换事件,重新加载配置以获取插件的 i18n 数据
298+
handleLocaleChange() {
299+
this.getConfig();
300+
},
301+
292302
// 从工具函数导入
293303
getPlatformIcon(platform_id) {
294304
// 首先检查是否有来自插件的 logo_token
@@ -305,6 +315,12 @@ export default {
305315
this.config_data = res.data.data.config;
306316
this.fetched = true
307317
this.metadata = res.data.data.metadata;
318+
319+
// 将插件平台适配器的 i18n 翻译注入到前端 i18n 系统中
320+
const platformI18n = res.data.data.platform_i18n_translations;
321+
if (platformI18n && typeof platformI18n === 'object') {
322+
mergeDynamicTranslations('features.config-metadata', platformI18n);
323+
}
308324
}).catch((err) => {
309325
this.showError(err);
310326
});

0 commit comments

Comments
 (0)