From 133f27422d49b897bae5f7abf194832a93c7fe22 Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Wed, 26 Nov 2025 16:40:58 +0800 Subject: [PATCH] feat: implement i18n of astrbot config (#3772) * feat: implement i18n of astrbot config * feat(config): update configuration metadata with i18n details and future deprecation notes --- astrbot/core/config/default.py | 25 +- astrbot/core/config/i18n_utils.py | 110 +++++ astrbot/dashboard/routes/config.py | 36 +- .../config/AstrBotCoreConfigWrapper.vue | 14 +- .../components/provider/AddNewProvider.vue | 2 +- .../src/components/shared/AstrBotConfigV4.vue | 45 +- .../shared/KnowledgeBaseSelector.vue | 22 +- .../components/shared/PluginSetSelector.vue | 28 +- .../components/shared/ProviderSelector.vue | 18 +- dashboard/src/i18n/loader.ts | 2 + .../src/i18n/locales/en-US/core/shared.json | 45 ++ .../en-US/features/config-metadata.json | 452 ++++++++++++++++++ .../i18n/locales/en-US/features/config.json | 28 ++ .../src/i18n/locales/zh-CN/core/shared.json | 45 ++ .../zh-CN/features/config-metadata.json | 452 ++++++++++++++++++ .../i18n/locales/zh-CN/features/config.json | 27 ++ dashboard/src/i18n/translations.ts | 12 +- dashboard/src/views/ConfigPage.vue | 36 +- 18 files changed, 1319 insertions(+), 80 deletions(-) create mode 100644 astrbot/core/config/i18n_utils.py create mode 100644 dashboard/src/i18n/locales/en-US/core/shared.json create mode 100644 dashboard/src/i18n/locales/en-US/features/config-metadata.json create mode 100644 dashboard/src/i18n/locales/zh-CN/core/shared.json create mode 100644 dashboard/src/i18n/locales/zh-CN/features/config-metadata.json diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index ae9bbecb7..09deb4aaf 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -145,7 +145,16 @@ DEFAULT_CONFIG = { } -# 配置项的中文描述、值类型 +""" +AstrBot v3 时代的配置元数据,目前仅承担以下功能: + +1. 保存配置时,配置项的类型验证 +2. WebUI 展示提供商和平台适配器模版 + +WebUI 的配置文件在 `CONFIG_METADATA_3` 中。 + +未来将会逐步淘汰此配置元数据。 +""" CONFIG_METADATA_2 = { "platform_group": { "metadata": { @@ -1091,7 +1100,7 @@ CONFIG_METADATA_2 = { "api_base": "", "model": "whisper-1", }, - "Whisper(本地加载)": { + "Whisper(Local)": { "hint": "启用前请 pip 安装 openai-whisper 库(N卡用户大约下载 2GB,主要是 torch 和 cuda,CPU 用户大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。", "provider": "openai", "type": "openai_whisper_selfhost", @@ -1100,7 +1109,7 @@ CONFIG_METADATA_2 = { "id": "whisper_selfhost", "model": "tiny", }, - "SenseVoice(本地加载)": { + "SenseVoice(Local)": { "hint": "启用前请 pip 安装 funasr、funasr_onnx、torchaudio、torch、modelscope、jieba 库(默认使用CPU,大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。", "type": "sensevoice_stt_selfhost", "provider": "sensevoice", @@ -1135,7 +1144,7 @@ CONFIG_METADATA_2 = { "pitch": "+0Hz", "timeout": 20, }, - "GSV TTS(本地加载)": { + "GSV TTS(Local)": { "id": "gsv_tts", "enable": False, "provider": "gpt_sovits", @@ -2189,6 +2198,14 @@ CONFIG_METADATA_2 = { } +""" +v4.7.0 之后,name, description, hint 等字段已经实现 i18n 国际化。国际化资源文件位于: + +- dashboard/src/i18n/locales/en-US/features/config-metadata.json +- dashboard/src/i18n/locales/zh-CN/features/config-metadata.json + +如果在此文件中添加了新的配置字段,请务必同步更新上述两个国际化资源文件。 +""" CONFIG_METADATA_3 = { "ai_group": { "name": "AI 配置", diff --git a/astrbot/core/config/i18n_utils.py b/astrbot/core/config/i18n_utils.py new file mode 100644 index 000000000..d61073f20 --- /dev/null +++ b/astrbot/core/config/i18n_utils.py @@ -0,0 +1,110 @@ +""" +配置元数据国际化工具 + +提供配置元数据的国际化键转换功能 +""" + +from typing import Any + + +class ConfigMetadataI18n: + """配置元数据国际化转换器""" + + @staticmethod + def _get_i18n_key(group: str, section: str, field: str, attr: str) -> str: + """ + 生成国际化键 + + Args: + group: 配置组,如 'ai_group', 'platform_group' + section: 配置节,如 'agent_runner', 'general' + field: 字段名,如 'enable', 'default_provider' + attr: 属性类型,如 'description', 'hint', 'labels' + + Returns: + 国际化键,格式如: 'ai_group.agent_runner.enable.description' + """ + if field: + return f"{group}.{section}.{field}.{attr}" + else: + return f"{group}.{section}.{attr}" + + @staticmethod + def convert_to_i18n_keys(metadata: dict[str, Any]) -> dict[str, Any]: + """ + 将配置元数据转换为使用国际化键 + + Args: + metadata: 原始配置元数据字典 + + Returns: + 使用国际化键的配置元数据字典 + """ + result = {} + + for group_key, group_data in metadata.items(): + group_result = { + "name": f"{group_key}.name", + "metadata": {}, + } + + 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"), + } + + # 复制其他属性 + 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", + ]: + 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 + + group_result["metadata"][section_key] = section_result + + result[group_key] = group_result + + return result diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index b947d26f2..6d68bf4c3 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -14,6 +14,7 @@ from astrbot.core.config.default import ( DEFAULT_CONFIG, DEFAULT_VALUE_MAP, ) +from astrbot.core.config.i18n_utils import ConfigMetadataI18n from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.platform.register import platform_cls_map, platform_registry from astrbot.core.provider import Provider @@ -133,7 +134,9 @@ def save_config(post_config: dict, config: AstrBotConfig, is_core: bool = False) is_core, ) else: - errors, post_config = validate_config(post_config, config.schema, is_core) + errors, post_config = validate_config( + post_config, getattr(config, "schema", {}), is_core + ) except BaseException as e: logger.error(traceback.format_exc()) logger.warning(f"验证配置时出现异常: {e}") @@ -247,11 +250,8 @@ class ConfigRoute(Route): async def get_default_config(self): """获取默认配置文件""" - return ( - Response() - .ok({"config": DEFAULT_CONFIG, "metadata": CONFIG_METADATA_3}) - .__dict__ - ) + metadata = ConfigMetadataI18n.convert_to_i18n_keys(CONFIG_METADATA_3) + return Response().ok({"config": DEFAULT_CONFIG, "metadata": metadata}).__dict__ async def get_abconf_list(self): """获取所有 AstrBot 配置文件的列表""" @@ -282,17 +282,15 @@ class ConfigRoute(Route): try: if system_config: abconf = self.acm.confs["default"] - return ( - Response() - .ok({"config": abconf, "metadata": CONFIG_METADATA_3_SYSTEM}) - .__dict__ + metadata = ConfigMetadataI18n.convert_to_i18n_keys( + CONFIG_METADATA_3_SYSTEM ) + return Response().ok({"config": abconf, "metadata": metadata}).__dict__ + if abconf_id is None: + raise ValueError("abconf_id cannot be None") abconf = self.acm.confs[abconf_id] - return ( - Response() - .ok({"config": abconf, "metadata": CONFIG_METADATA_3}) - .__dict__ - ) + metadata = ConfigMetadataI18n.convert_to_i18n_keys(CONFIG_METADATA_3) + return Response().ok({"config": abconf, "metadata": metadata}).__dict__ except ValueError as e: return Response().error(str(e)).__dict__ @@ -598,9 +596,15 @@ class ConfigRoute(Route): return Response().error("缺少参数 provider_id").__dict__ prov_mgr = self.core_lifecycle.provider_manager - provider: Provider | None = prov_mgr.inst_map.get(provider_id, None) + provider = prov_mgr.inst_map.get(provider_id, None) if not provider: return Response().error(f"未找到 ID 为 {provider_id} 的提供商").__dict__ + if not isinstance(provider, Provider): + return ( + Response() + .error(f"提供商 {provider_id} 类型不支持获取模型列表") + .__dict__ + ) try: models = await provider.get_models() diff --git a/dashboard/src/components/config/AstrBotCoreConfigWrapper.vue b/dashboard/src/components/config/AstrBotCoreConfigWrapper.vue index f3d5abe14..2a84989df 100644 --- a/dashboard/src/components/config/AstrBotCoreConfigWrapper.vue +++ b/dashboard/src/components/config/AstrBotCoreConfigWrapper.vue @@ -4,7 +4,7 @@ :align-tabs="$vuetify.display.mobile ? 'left' : 'start'" color="deep-purple-accent-4" class="config-tabs"> - {{ metadata[key]['name'] }} + {{ tm(metadata[key]['name']) }} @@ -59,7 +59,17 @@ export default { } }, setup() { - const { tm } = useModuleI18n('features/config'); + const { tm: tmConfig } = useModuleI18n('features/config'); + const { tm: tmMetadata } = useModuleI18n('features/config-metadata'); + + const tm = (key) => { + const metadataResult = tmMetadata(key); + if (!metadataResult.startsWith('[MISSING:') && !metadataResult.startsWith('[INVALID:')) { + return metadataResult; + } + return tmConfig(key); + }; + return { tm }; diff --git a/dashboard/src/components/provider/AddNewProvider.vue b/dashboard/src/components/provider/AddNewProvider.vue index f876779fb..f59c24942 100644 --- a/dashboard/src/components/provider/AddNewProvider.vue +++ b/dashboard/src/components/provider/AddNewProvider.vue @@ -58,7 +58,7 @@ - {{ t('dialogs.addProvider.noTemplates') }} + {{ tm('dialogs.addProvider.noTemplates') }} diff --git a/dashboard/src/components/shared/AstrBotConfigV4.vue b/dashboard/src/components/shared/AstrBotConfigV4.vue index e55f6f998..55a2a34ed 100644 --- a/dashboard/src/components/shared/AstrBotConfigV4.vue +++ b/dashboard/src/components/shared/AstrBotConfigV4.vue @@ -8,7 +8,7 @@ import PersonaSelector from './PersonaSelector.vue' import KnowledgeBaseSelector from './KnowledgeBaseSelector.vue' import PluginSetSelector from './PluginSetSelector.vue' import T2ITemplateEditor from './T2ITemplateEditor.vue' -import { useI18n } from '@/i18n/composables' +import { useI18n, useModuleI18n } from '@/i18n/composables' const props = defineProps({ @@ -27,6 +27,34 @@ const props = defineProps({ }) const { t } = useI18n() +const { tm } = useModuleI18n('features/config-metadata') + +// 翻译器函数 - 如果是国际化键则翻译,否则原样返回 +const translateIfKey = (value) => { + if (!value || typeof value !== 'string') return value + return tm(value) +} + +// 处理labels翻译 - labels可以是数组或国际化键 +const getTranslatedLabels = (itemMeta) => { + if (!itemMeta?.labels) return null + + // 如果labels是字符串(国际化键) + if (typeof itemMeta.labels === 'string') { + const translatedLabels = tm(itemMeta.labels) + // 如果翻译成功且是数组,返回翻译结果 + if (Array.isArray(translatedLabels)) { + return translatedLabels + } + } + + // 如果labels是数组,直接返回 + if (Array.isArray(itemMeta.labels)) { + return itemMeta.labels + } + + return null +} const dialog = ref(false) const currentEditingKey = ref('') @@ -158,11 +186,11 @@ function getSpecialSubtype(value) { rounded="md" variant="outlined"> - {{ metadata[metadataKey]?.description }} + {{ translateIfKey(metadata[metadataKey]?.description) }} ‼️ - {{ metadata[metadataKey]?.hint }} + {{ translateIfKey(metadata[metadataKey]?.hint) }} @@ -176,13 +204,13 @@ function getSpecialSubtype(value) { - {{ itemMeta?.description || itemKey }} + {{ translateIfKey(itemMeta?.description) || itemKey }} ({{ itemKey }}) ‼️ - {{ itemMeta?.hint }} + {{ translateIfKey(itemMeta?.hint) }} @@ -190,7 +218,12 @@ function getSpecialSubtype(value) { diff --git a/dashboard/src/components/shared/KnowledgeBaseSelector.vue b/dashboard/src/components/shared/KnowledgeBaseSelector.vue index e959b948a..8c8dae6ae 100644 --- a/dashboard/src/components/shared/KnowledgeBaseSelector.vue +++ b/dashboard/src/components/shared/KnowledgeBaseSelector.vue @@ -3,7 +3,7 @@ - 未选择 + {{ tm('knowledgeBaseSelector.notSelected') }} - 选择知识库 + {{ tm('knowledgeBaseSelector.dialogTitle') }} @@ -50,9 +50,9 @@ {{ kb.kb_name }} - {{ kb.description || '无描述' }} - - {{ kb.doc_count }} 个文档 - - {{ kb.chunk_count }} 个块 + {{ kb.description || tm('knowledgeBaseSelector.noDescription') }} + - {{ tm('knowledgeBaseSelector.documentCount', { count: kb.doc_count }) }} + - {{ tm('knowledgeBaseSelector.chunkCount', { count: kb.chunk_count }) }} @@ -68,9 +68,9 @@ mdi-database-off - 暂无知识库 + {{ tm('knowledgeBaseSelector.noKnowledgeBases') }} - 创建知识库 + {{ tm('knowledgeBaseSelector.createKnowledgeBase') }} @@ -78,14 +78,14 @@ - 已选择 {{ selectedKnowledgeBases.length }} 个知识库 + {{ tm('knowledgeBaseSelector.selectedCount', { count: selectedKnowledgeBases.length }) }} - 取消 + {{ tm('knowledgeBaseSelector.cancelSelection') }} - 确认选择 + {{ tm('knowledgeBaseSelector.confirmSelection') }} @@ -96,6 +96,7 @@ import { ref, watch } from 'vue' import axios from 'axios' import { useRouter } from 'vue-router' +import { useModuleI18n } from '@/i18n/composables' const props = defineProps({ modelValue: { @@ -110,6 +111,7 @@ const props = defineProps({ const emit = defineEmits(['update:modelValue']) const router = useRouter() +const { tm } = useModuleI18n('core.shared') const dialog = ref(false) const knowledgeBaseList = ref([]) diff --git a/dashboard/src/components/shared/PluginSetSelector.vue b/dashboard/src/components/shared/PluginSetSelector.vue index d37b089ce..97c7059c2 100644 --- a/dashboard/src/components/shared/PluginSetSelector.vue +++ b/dashboard/src/components/shared/PluginSetSelector.vue @@ -4,13 +4,13 @@ - 未启用任何插件 + {{ tm('pluginSetSelector.notSelected') }} - 启用所有插件 (*) + {{ tm('pluginSetSelector.allPlugins') }} - 已选择 {{ modelValue.length }} 个插件 + {{ tm('pluginSetSelector.selectedCount', { count: modelValue.length }) }} @@ -23,7 +23,7 @@ - 选择插件集合 + {{ tm('pluginSetSelector.dialogTitle') }} @@ -34,17 +34,17 @@ @@ -68,21 +68,21 @@ {{ plugin.name }} - {{ plugin.desc || '无描述' }} + {{ plugin.desc || tm('pluginSetSelector.noDescription') }} - 未激活 + {{ tm('pluginSetSelector.notActivated') }} - *不显示系统插件和已经在插件页禁用的插件。 + {{ tm('pluginSetSelector.note') }} mdi-puzzle-outline - 暂无可用的插件 + {{ tm('pluginSetSelector.noPlugins') }} @@ -90,11 +90,11 @@ - 取消 + {{ tm('pluginSetSelector.cancelSelection') }} - 确认选择 + {{ tm('pluginSetSelector.confirmSelection') }} @@ -104,6 +104,7 @@
暂无知识库
{{ tm('knowledgeBaseSelector.noKnowledgeBases') }}
暂无可用的插件
{{ tm('pluginSetSelector.noPlugins') }}