diff --git a/astrbot/core/__init__.py b/astrbot/core/__init__.py index 6400d6fa4..258b04d27 100644 --- a/astrbot/core/__init__.py +++ b/astrbot/core/__init__.py @@ -8,7 +8,9 @@ from astrbot.core.utils.shared_preferences import SharedPreferences from astrbot.core.utils.t2i.renderer import HtmlRenderer + from .log import LogBroker, LogManager # noqa +from .lang import t from .utils.astrbot_path import get_astrbot_data_path # 初始化数据存储文件夹 diff --git a/astrbot/core/agent/handoff.py b/astrbot/core/agent/handoff.py index 761c50269..0491c367d 100644 --- a/astrbot/core/agent/handoff.py +++ b/astrbot/core/agent/handoff.py @@ -15,7 +15,6 @@ def __init__( tool_description: str | None = None, **kwargs, ) -> None: - # Avoid passing duplicate `description` to the FunctionTool dataclass. # Some call sites (e.g. SubAgentOrchestrator) pass `description` via kwargs # to override what the main agent sees, while we also compute a default diff --git a/astrbot/core/lang.py b/astrbot/core/lang.py new file mode 100644 index 000000000..7c4483766 --- /dev/null +++ b/astrbot/core/lang.py @@ -0,0 +1,49 @@ +# lang.py +from pathlib import Path +from fluent.runtime import FluentLocalization, FluentResourceLoader +from astrbot.core.utils.astrbot_path import get_astrbot_path + +class Lang: + def __init__(self, locale = "zh-cn", files = None): + self.locale = locale + self.files = files + self.load_locale(self.locale, self.files) + + def load_locale(self, locale = "zh-cn", files = None): + # 1. 定位 locales 文件夹 + base_dir = Path(get_astrbot_path()) / "locales" + + # 2. 搜索所有可用的语言文件夹 (作为语言包选项) + self.available_locales = [d.name for d in base_dir.iterdir() if d.is_dir()] + + # 寻找匹配的 locale (忽略大小写) + matched_locale = next( + (l for l in self.available_locales if l.lower() == locale.lower()), locale + ) + + # 3. 默认搜索语言包下所有 .ftl 文件 + if files is None: + files_set = set() + for loc in self.available_locales: + for ftl_file in (base_dir / loc).glob("*.ftl"): + files_set.add(ftl_file.name) + files = list(files_set) + + # 4. 初始化 Loader 和 Localization + loader = FluentResourceLoader(str(base_dir / "{locale}")) + + # 优先级: 指定的 locale -> 默认 zh-cn (如果存在) + locales_preference = [matched_locale] + if "zh-cn" in self.available_locales and matched_locale.lower() != "zh-cn": + locales_preference.append("zh-cn") + + self._l10n = FluentLocalization(locales_preference, files, loader) + + def __call__(self, key: str, **kwargs) -> str: + """ + 让对象可以直接像函数一样调用:t("key") + 同时利用 **kwargs 简化参数传递 + """ + return self._l10n.format_value(key, kwargs) + +t = Lang(locale="zh-cn") diff --git a/astrbot/dashboard/routes/__init__.py b/astrbot/dashboard/routes/__init__.py index 481be2f89..fe12feff7 100644 --- a/astrbot/dashboard/routes/__init__.py +++ b/astrbot/dashboard/routes/__init__.py @@ -19,6 +19,7 @@ from .subagent import SubAgentRoute from .tools import ToolsRoute from .update import UpdateRoute +from .lang_route import LangRoute __all__ = [ "AuthRoute", @@ -42,4 +43,5 @@ "ToolsRoute", "SkillsRoute", "UpdateRoute", + "LangRoute" ] diff --git a/astrbot/dashboard/routes/lang_route.py b/astrbot/dashboard/routes/lang_route.py new file mode 100644 index 000000000..e8568c6df --- /dev/null +++ b/astrbot/dashboard/routes/lang_route.py @@ -0,0 +1,29 @@ +from astrbot.core.lang import t +from astrbot.dashboard.routes.route import Response, Route, RouteContext +from quart import request + +class LangRoute(Route): + def __init__(self, context: RouteContext) -> None: + super().__init__(context) + self.routes = { + "/setLang": ("GET", self.set_Lang), + } + self.register_routes() + + async def set_Lang(self): + data = await request.get_json() + lang = data.get("lang") + if lang is None: + return Response().error("lang 为必填参数。").__dict__ + try: + t.load_locale( + locale = lang.lower(), + files = None + ) + except ValueError as exc: + return Response().error(str(exc)).__dict__ + payload = { + "lang": lang.lower(), + "message": f"语言已设置为 {lang}" + } + return Response().ok(payload).__dict__ diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index 6a588a5d3..5c2a49349 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -28,6 +28,7 @@ from .routes.session_management import SessionManagementRoute from .routes.subagent import SubAgentRoute from .routes.t2i import T2iRoute +from .routes.lang_route import LangRoute class _AddrWithPort(Protocol): @@ -107,6 +108,7 @@ def __init__( self.platform_route = PlatformRoute(self.context, core_lifecycle) self.backup_route = BackupRoute(self.context, db, core_lifecycle) self.live_chat_route = LiveChatRoute(self.context, db, core_lifecycle) + self.lang_route = LangRoute(self.context) self.app.add_url_rule( "/api/plug/", diff --git a/dashboard/src/i18n/composables.ts b/dashboard/src/i18n/composables.ts index 131010bc3..36e419bed 100644 --- a/dashboard/src/i18n/composables.ts +++ b/dashboard/src/i18n/composables.ts @@ -1,6 +1,7 @@ import { ref, computed } from 'vue'; import { translations as staticTranslations } from './translations'; import type { Locale } from './types'; +import axios from 'axios'; // 全局状态 const currentLocale = ref('zh-CN'); @@ -96,6 +97,16 @@ export function useI18n() { window.dispatchEvent(new CustomEvent('astrbot-locale-changed', { detail: { locale: newLocale } })); + + axios.post("/api/setLang", { lang: newLocale }) + .then(response => { + if (response.data.code !== 200) { + console.error('Failed to set language on server:', response.data.message); + } + }) + .catch(error => { + console.error('Error setting language on server:', error); + }); } }; @@ -225,4 +236,14 @@ export async function setupI18n() { : 'zh-CN'; await initI18n(initialLocale); + + axios.post("/api/setLang", { lang: initialLocale }) + .then(response => { + if (response.data.code !== 200) { + console.error('Failed to set language on server:', response.data.message); + } + }) + .catch(error => { + console.error('Error setting language on server:', error); + }); } \ No newline at end of file diff --git a/locales/en/main.ftl b/locales/en/main.ftl new file mode 100644 index 000000000..a39d059ee --- /dev/null +++ b/locales/en/main.ftl @@ -0,0 +1,9 @@ +main-python-version-error = Please use Python 3.10+ to run this project. +main-use-specified-webui = Using specified WebUI directory: { $webui_dir } +main-webui-not-found = The specified WebUI directory { $webui_dir } does not exist, using default logic. +main-webui-latest = WebUI version is up to date. +main-webui-version-mismatch = Detected WebUI version ({ $v }) does not match current AstrBot version (v{ $version }). +main-downloading-dashboard = Starting to download dashboard files... Peak hours (evening) may result in slower speeds. If downloads fail repeatedly, please go to https://github.com/AstrBotDevs/AstrBot/releases/latest to download dist.zip and extract the dist folder into the data directory. +main-download-dashboard-failed = Failed to download dashboard files: { $error }. +main-download-dashboard-success = Dashboard download complete. +main-argparse-webui-dir-help = Specify the WebUI static file directory path. \ No newline at end of file diff --git a/locales/zh-cn/main.ftl b/locales/zh-cn/main.ftl new file mode 100644 index 000000000..7767c291b --- /dev/null +++ b/locales/zh-cn/main.ftl @@ -0,0 +1,9 @@ +main-python-version-error = 请使用 Python3.10+ 运行本项目。 +main-use-specified-webui = 使用指定的 WebUI 目录: {webui_dir} +main-webui-not-found = 指定的 WebUI 目录 {webui_dir} 不存在,将使用默认逻辑。 +main-webui-latest = WebUI 版本已是最新 +main-webui-version-mismatch = 检测到 WebUI 版本 ({v}) 与当前 AstrBot 版本 (v{VERSION}) 不符。 +main-downloading-dashboard = 开始下载管理面板文件...高峰期(晚上)可能导致较慢的速度。如多次下载失败,请前往 https://github.com/AstrBotDevs/AstrBot/releases/latest 下载 dist.zip,并将其中的 dist 文件夹解压至 data 目录下。 +main-download-dashboard-failed = 下载管理面板文件失败: {e}。 +main-download-dashboard-success = 管理面板下载完成。 +main-argparse-webui-dir-help = 指定 WebUI 静态文件目录路径 \ No newline at end of file diff --git a/main.py b/main.py index be188140c..d47c495f0 100644 --- a/main.py +++ b/main.py @@ -9,7 +9,7 @@ runtime_bootstrap.initialize_runtime_bootstrap() -from astrbot.core import LogBroker, LogManager, db_helper, logger # noqa: E402 +from astrbot.core import LogBroker, LogManager, db_helper, logger, t # noqa: E402 from astrbot.core.config.default import VERSION # noqa: E402 from astrbot.core.initial_loader import InitialLoader # noqa: E402 from astrbot.core.utils.astrbot_path import ( # noqa: E402 @@ -41,7 +41,7 @@ def check_env() -> None: if not (sys.version_info.major == 3 and sys.version_info.minor >= 10): - logger.error("请使用 Python3.10+ 运行本项目。") + logger.error(t("main-python-version-error")) exit() astrbot_root = get_astrbot_root() @@ -68,9 +68,9 @@ async def check_dashboard_files(webui_dir: str | None = None): # 指定webui目录 if webui_dir: if os.path.exists(webui_dir): - logger.info(f"使用指定的 WebUI 目录: {webui_dir}") + logger.info(t("main-use-specified-webui", webui_dir)) return webui_dir - logger.warning(f"指定的 WebUI 目录 {webui_dir} 不存在,将使用默认逻辑。") + logger.warning(t("main-webui-not-found"), webui_dir) data_dist_path = os.path.join(get_astrbot_data_path(), "dist") if os.path.exists(data_dist_path): @@ -78,24 +78,22 @@ async def check_dashboard_files(webui_dir: str | None = None): if v is not None: # 存在文件 if v == f"v{VERSION}": - logger.info("WebUI 版本已是最新。") + logger.info(t("main-webui-latest")) else: logger.warning( - f"检测到 WebUI 版本 ({v}) 与当前 AstrBot 版本 (v{VERSION}) 不符。", + t("main-webui-version-mismatch", v), ) return data_dist_path - logger.info( - "开始下载管理面板文件...高峰期(晚上)可能导致较慢的速度。如多次下载失败,请前往 https://github.com/AstrBotDevs/AstrBot/releases/latest 下载 dist.zip,并将其中的 dist 文件夹解压至 data 目录下。", - ) + logger.info(t("main-downloading-dashboard")) try: await download_dashboard(version=f"v{VERSION}", latest=False) except Exception as e: - logger.critical(f"下载管理面板文件失败: {e}。") + logger.critical(t("main-download-dashboard-failed", e)) return None - logger.info("管理面板下载完成。") + logger.info(t("main-download-dashboard-success")) return data_dist_path @@ -104,7 +102,7 @@ async def check_dashboard_files(webui_dir: str | None = None): parser.add_argument( "--webui-dir", type=str, - help="指定 WebUI 静态文件目录路径", + help=t("main-argparse-webui-dir-help"), default=None, ) args = parser.parse_args()