diff --git a/astrbot/core/config/i18n_utils.py b/astrbot/core/config/i18n_utils.py index aa441c0c16..cb6b6429b5 100644 --- a/astrbot/core/config/i18n_utils.py +++ b/astrbot/core/config/i18n_utils.py @@ -42,6 +42,55 @@ def convert_to_i18n_keys(metadata: dict[str, Any]) -> dict[str, Any]: """ result = {} + def convert_items( + group: str, section: str, items: dict[str, Any], prefix: str = "" + ) -> dict[str, Any]: + items_result: dict[str, Any] = {} + + for field_key, field_data in items.items(): + if not isinstance(field_data, dict): + items_result[field_key] = field_data + continue + + field_name = field_key + field_path = f"{prefix}.{field_name}" if prefix else field_name + + field_result = { + key: value + for key, value in field_data.items() + if key not in {"description", "hint", "labels", "name"} + } + + if "description" in field_data: + field_result["description"] = ( + f"{group}.{section}.{field_path}.description" + ) + if "hint" in field_data: + field_result["hint"] = f"{group}.{section}.{field_path}.hint" + if "labels" in field_data: + field_result["labels"] = f"{group}.{section}.{field_path}.labels" + if "name" in field_data: + field_result["name"] = f"{group}.{section}.{field_path}.name" + + if "items" in field_data and isinstance(field_data["items"], dict): + field_result["items"] = convert_items( + group, section, field_data["items"], field_path + ) + + if "template_schema" in field_data and isinstance( + field_data["template_schema"], dict + ): + field_result["template_schema"] = convert_items( + group, + section, + field_data["template_schema"], + f"{field_path}.template_schema", + ) + + items_result[field_key] = field_result + + return items_result + for group_key, group_data in metadata.items(): group_result = { "name": f"{group_key}.name", @@ -50,59 +99,19 @@ def convert_to_i18n_keys(metadata: dict[str, Any]) -> dict[str, Any]: for section_key, section_data in group_data.get("metadata", {}).items(): section_result = { - "description": f"{group_key}.{section_key}.description", - "type": section_data.get("type"), + key: value + for key, value in section_data.items() + if key not in {"description", "hint", "labels", "name"} } + section_result["description"] = f"{group_key}.{section_key}.description" - # 复制其他属性 - for key in ["items", "condition", "_special", "invisible"]: - if key in section_data: - section_result[key] = section_data[key] - - # 处理 hint if "hint" in section_data: section_result["hint"] = f"{group_key}.{section_key}.hint" - # 处理 items 中的字段 if "items" in section_data and isinstance(section_data["items"], dict): - items_result = {} - for field_key, field_data in section_data["items"].items(): - # 处理嵌套的点号字段名(如 provider_settings.enable) - field_name = field_key - - field_result = {} - - # 复制基本属性 - for attr in [ - "type", - "condition", - "_special", - "invisible", - "options", - "slider", - ]: - if attr in field_data: - field_result[attr] = field_data[attr] - - # 转换文本属性为国际化键 - if "description" in field_data: - field_result["description"] = ( - f"{group_key}.{section_key}.{field_name}.description" - ) - - if "hint" in field_data: - field_result["hint"] = ( - f"{group_key}.{section_key}.{field_name}.hint" - ) - - if "labels" in field_data: - field_result["labels"] = ( - f"{group_key}.{section_key}.{field_name}.labels" - ) - - items_result[field_key] = field_result - - section_result["items"] = items_result + section_result["items"] = convert_items( + group_key, section_key, section_data["items"] + ) group_result["metadata"][section_key] = section_result diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index c5998682c8..e018d64755 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -1,4 +1,5 @@ import asyncio +import copy import inspect import os import traceback @@ -407,8 +408,19 @@ async def update_provider_source(self): return Response().ok(message="更新 provider source 成功").__dict__ async def get_provider_template(self): + provider_metadata = ConfigMetadataI18n.convert_to_i18n_keys( + { + "provider_group": { + "metadata": { + "provider": CONFIG_METADATA_2["provider_group"]["metadata"][ + "provider" + ] + } + } + } + ) config_schema = { - "provider": CONFIG_METADATA_2["provider_group"]["metadata"]["provider"] + "provider": provider_metadata["provider_group"]["metadata"]["provider"] } data = { "config_schema": config_schema, @@ -1278,11 +1290,24 @@ async def _register_platform_logo(self, platform, platform_default_tmpl): async def _get_astrbot_config(self): config = self.config + metadata = copy.deepcopy(CONFIG_METADATA_2) + platform_i18n = ConfigMetadataI18n.convert_to_i18n_keys( + { + "platform_group": { + "metadata": { + "platform": metadata["platform_group"]["metadata"]["platform"] + } + } + } + ) + metadata["platform_group"]["metadata"]["platform"] = platform_i18n[ + "platform_group" + ]["metadata"]["platform"] # 平台适配器的默认配置模板注入 - platform_default_tmpl = CONFIG_METADATA_2["platform_group"]["metadata"][ - "platform" - ]["config_template"] + platform_default_tmpl = metadata["platform_group"]["metadata"]["platform"][ + "config_template" + ] # 收集需要注册logo的平台 logo_registration_tasks = [] @@ -1300,14 +1325,14 @@ async def _get_astrbot_config(self): await asyncio.gather(*logo_registration_tasks, return_exceptions=True) # 服务提供商的默认配置模板注入 - provider_default_tmpl = CONFIG_METADATA_2["provider_group"]["metadata"][ - "provider" - ]["config_template"] + provider_default_tmpl = metadata["provider_group"]["metadata"]["provider"][ + "config_template" + ] for provider in provider_registry: if provider.default_config_tmpl: provider_default_tmpl[provider.type] = provider.default_config_tmpl - return {"metadata": CONFIG_METADATA_2, "config": config} + return {"metadata": metadata, "config": config} 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 118aa202a8..3be4d92f60 100644 --- a/dashboard/src/components/platform/AddNewPlatform.vue +++ b/dashboard/src/components/platform/AddNewPlatform.vue @@ -9,14 +9,14 @@

- 选择消息平台类别 + {{ tm('createDialog.step1Title') }}

- 想把机器人接入到哪里?如 QQ、企业微信、飞书、Discord、Telegram 等。 + {{ tm('createDialog.step1Hint') }}
- *消息下发时,根据会话来源按顺序从上到下匹配首个符合条件的配置文件。使用 * 表示匹配所有。使用 /sid 指令获取会话 - ID。全部不匹配时将使用默认配置文件。 + {{ tm('createDialog.routeHint') }}
@@ -266,10 +265,10 @@ - 无视警告并继续创建 + {{ tm('createDialog.warningContinue') }} - 重新修改 + {{ tm('createDialog.warningEditAgain') }} @@ -286,9 +285,9 @@
- 配置文件管理 + {{ tm('createDialog.configDrawerTitle') }}
- ID: {{ configDrawerTargetId }} + {{ tm('createDialog.configDrawerIdLabel') }}: {{ configDrawerTargetId }}
@@ -359,23 +358,9 @@ export default { // 平台配置文件表格(已弃用,改用 platformRoutes) platformConfigs: [], - configTableHeaders: [ - { title: '与此实例关联的配置文件 ID', key: 'name', sortable: false }, - { title: '在此实例下的应用范围', key: 'scope', sortable: false }, - ], // 平台路由表 platformRoutes: [], - routeTableHeaders: [ - { title: '消息会话来源(消息类型:会话 ID)', key: 'source', sortable: false, width: '60%' }, - { title: '使用配置文件', key: 'configId', sortable: false, width: '20%' }, - { title: '操作', key: 'actions', sortable: false, align: 'center', width: '20%' }, - ], - messageTypeOptions: [ - { label: '全部消息', value: '*' }, - { label: '群组消息(GroupMessage)', value: 'GroupMessage' }, - { label: '私聊消息(FriendMessage)', value: 'FriendMessage' }, - ], isEditingRoutes: false, // 编辑模式开关 // ID冲突确认对话框 @@ -437,6 +422,26 @@ export default { } return false; + }, + configTableHeaders() { + return [ + { title: this.tm('createDialog.configTableHeaders.configId'), key: 'name', sortable: false }, + { title: this.tm('createDialog.configTableHeaders.scope'), key: 'scope', sortable: false }, + ]; + }, + routeTableHeaders() { + return [ + { title: this.tm('createDialog.routeTableHeaders.source'), key: 'source', sortable: false, width: '60%' }, + { title: this.tm('createDialog.routeTableHeaders.config'), key: 'configId', sortable: false, width: '20%' }, + { title: this.tm('createDialog.routeTableHeaders.actions'), key: 'actions', sortable: false, align: 'center', width: '20%' }, + ]; + }, + messageTypeOptions() { + return [ + { label: this.tm('createDialog.messageTypeOptions.all'), value: '*' }, + { label: this.tm('createDialog.messageTypeOptions.group'), value: 'GroupMessage' }, + { label: this.tm('createDialog.messageTypeOptions.friend'), value: 'FriendMessage' }, + ]; } }, watch: { @@ -603,7 +608,7 @@ export default { const targetId = configId || 'default'; if (configId && this.configInfoList.findIndex(c => c.id === configId) === -1) { - this.showError('目标配置文件不存在,已打开配置页面以便检查。'); + this.showError(this.tm('messages.configNotFoundOpenConfig')); } this.configDrawerTargetId = targetId; @@ -637,7 +642,7 @@ export default { const id = this.originalUpdatingPlatformId || this.updatingPlatformConfig.id; if (!id) { this.loading = false; - this.showError('更新失败,缺少平台 ID。'); + this.showError(this.tm('messages.updateMissingPlatformId')); return; } @@ -655,7 +660,7 @@ export default { }) if (resp.data.status === 'error') { - throw new Error(resp.data.message || '平台更新失败'); + throw new Error(resp.data.message || this.tm('messages.platformUpdateFailed')); } // 同时更新路由表 @@ -665,7 +670,7 @@ export default { this.showDialog = false; this.resetForm(); this.$emit('refresh-config'); - this.showSuccess('更新成功'); + this.showSuccess(this.tm('messages.updateSuccess')); } catch (err) { this.loading = false; this.showError(err.response?.data?.message || err.message); @@ -710,7 +715,7 @@ export default { this.showDialog = false; this.resetForm(); this.$emit('refresh-config'); - this.showSuccess(res.data.message || '平台添加成功,配置文件已更新'); + this.showSuccess(res.data.message || this.tm('messages.addSuccessWithConfig')); } catch (err) { this.loading = false; this.showError(err.response?.data?.message || err.message); @@ -738,7 +743,7 @@ export default { } if (!configId) { - throw new Error('无法获取配置文件ID'); + throw new Error(this.tm('messages.configIdMissing')); } // 第二步:统一更新路由表 @@ -755,7 +760,8 @@ export default { console.log(`成功更新路由表: ${umop} -> ${configId}`); } catch (err) { console.error('更新路由表失败:', err); - throw new Error(`更新路由表失败: ${err.response?.data?.message || err.message}`); + const errorMessage = err.response?.data?.message || err.message; + throw new Error(this.tm('messages.routingUpdateFailed', { message: errorMessage })); } }, @@ -778,7 +784,8 @@ export default { return newConfigId; } catch (err) { console.error('创建新配置文件失败:', err); - throw new Error(`创建新配置文件失败: ${err.response?.data?.message || err.message}`); + const errorMessage = err.response?.data?.message || err.message; + throw new Error(this.tm('messages.createConfigFailed', { message: errorMessage })); } }, @@ -922,7 +929,7 @@ export default { const newPlatformId = this.updatingPlatformConfig?.id || originalPlatformId; if (!originalPlatformId && !newPlatformId) { - throw new Error('无法获取平台 ID'); + throw new Error(this.tm('messages.platformIdMissing')); } try { @@ -958,7 +965,8 @@ export default { }); } catch (err) { console.error('保存路由表失败:', err); - throw new Error(`保存路由表失败: ${err.response?.data?.message || err.message}`); + const errorMessage = err.response?.data?.message || err.message; + throw new Error(this.tm('messages.routingSaveFailed', { message: errorMessage })); } }, @@ -987,10 +995,10 @@ export default { // 获取消息类型标签 getMessageTypeLabel(messageType) { const typeMap = { - '*': '全部消息', - '': '全部消息', - 'GroupMessage': '群组消息', - 'FriendMessage': '私聊消息' + '*': this.tm('createDialog.messageTypeLabels.all'), + '': this.tm('createDialog.messageTypeLabels.all'), + 'GroupMessage': this.tm('createDialog.messageTypeLabels.group'), + 'FriendMessage': this.tm('createDialog.messageTypeLabels.friend') }; return typeMap[messageType] || messageType; }, diff --git a/dashboard/src/components/provider/ProviderSourcesPanel.vue b/dashboard/src/components/provider/ProviderSourcesPanel.vue index 6f65af67b2..adca108325 100644 --- a/dashboard/src/components/provider/ProviderSourcesPanel.vue +++ b/dashboard/src/components/provider/ProviderSourcesPanel.vue @@ -14,7 +14,7 @@ rounded="xl" size="small" > - 新增 + {{ tm('providerSources.add') }} diff --git a/dashboard/src/components/shared/AstrBotConfig.vue b/dashboard/src/components/shared/AstrBotConfig.vue index 22dc03807e..12cb0bee1c 100644 --- a/dashboard/src/components/shared/AstrBotConfig.vue +++ b/dashboard/src/components/shared/AstrBotConfig.vue @@ -3,7 +3,7 @@ import { VueMonacoEditor } from '@guolao/vue-monaco-editor' import { ref, computed } from 'vue' import ConfigItemRenderer from './ConfigItemRenderer.vue' import TemplateListEditor from './TemplateListEditor.vue' -import { useI18n } from '@/i18n/composables' +import { useI18n, useModuleI18n } from '@/i18n/composables' import axios from 'axios' import { useToast } from '@/utils/toast' @@ -35,6 +35,12 @@ const props = defineProps({ }) const { t } = useI18n() +const { tm, getRaw } = useModuleI18n('features/config-metadata') + +const translateIfKey = (value) => { + if (!value || typeof value !== 'string') return value + return getRaw(value) ? tm(value) : value +} const filteredIterable = computed(() => { if (!props.iterable) return {} @@ -134,11 +140,11 @@ function hasVisibleItemsAfter(items, currentIndex) {