Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions astrbot/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

# 初始化数据存储文件夹
Expand Down
1 change: 0 additions & 1 deletion astrbot/core/agent/handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions astrbot/core/lang.py
Original file line number Diff line number Diff line change
@@ -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")
2 changes: 2 additions & 0 deletions astrbot/dashboard/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .subagent import SubAgentRoute
from .tools import ToolsRoute
from .update import UpdateRoute
from .lang_route import LangRoute

__all__ = [
"AuthRoute",
Expand All @@ -42,4 +43,5 @@
"ToolsRoute",
"SkillsRoute",
"UpdateRoute",
"LangRoute"
]
29 changes: 29 additions & 0 deletions astrbot/dashboard/routes/lang_route.py
Original file line number Diff line number Diff line change
@@ -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),
Comment on lines +8 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): 路由注册为 GET,但前端使用的是带 JSON body 的 POST 请求,导致方法不匹配。

前端调用的是 axios.post("/api/setLang", { lang: ... }),但当前路由注册的是 "GET"。由于 set_Lang 使用了 await request.get_json(),因此应将其暴露为 POST 接口,例如:

self.routes = {
    "/setLang": ("POST", self.set_Lang),
}
Original comment in English

issue (bug_risk): Route is registered as GET but the frontend uses POST with a JSON body, leading to a method mismatch.

The frontend calls axios.post("/api/setLang", { lang: ... }), but this route is registered as "GET". Because set_Lang uses await request.get_json(), this should be exposed as a POST endpoint instead:

self.routes = {
    "/setLang": ("POST", 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__
2 changes: 2 additions & 0 deletions astrbot/dashboard/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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/<path:subpath>",
Expand Down
21 changes: 21 additions & 0 deletions dashboard/src/i18n/composables.ts
Original file line number Diff line number Diff line change
@@ -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<Locale>('zh-CN');
Expand Down Expand Up @@ -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);
});
}
Comment on lines +101 to 110
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: 语言设置的调用在 setLocalesetupI18n 中都各写了一遍,这可能导致逻辑漂移,也会让之后修改行为变得更复杂。

axios.post("/api/setLang", ...) 在初始化阶段和语言切换时都实现了一遍。为了避免重复和潜在的不一致,可以抽取一个类似 syncLangWithServer(locale) 的帮助函数,让 setLocalesetupI18n 共同使用,这样未来的改动(比如错误处理、重试机制)只需要维护一个地方。

建议实现:

import { translations as staticTranslations } from './translations';
import type { Locale } from './types';
import axios from 'axios';

const syncLangWithServer = (locale: Locale) => {
  return axios.post('/api/setLang', { lang: locale })
    .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);
    });
};

// 全局状态
const currentLocale = ref<Locale>('zh-CN');
      window.dispatchEvent(new CustomEvent('astrbot-locale-changed', {
        detail: { locale: newLocale }
      }));

      syncLangWithServer(newLocale);

setupI18n 的初始化逻辑中还有第二个 axios.post("/api/setLang", ...) 调用(用于首次加载 / 初始化),该代码片段没有出现在当前截取的内容里。那一块也应该替换为 syncLangWithServer(initialLocale)(或使用对应的初始语言变量),这样初始化和运行时的语言同步逻辑及错误处理就能共用同一套实现。

Original comment in English

suggestion: The language-setting call is duplicated in both setLocale and setupI18n, which could drift or complicate behavior changes.

The axios.post("/api/setLang", ...) call is implemented both on init and on locale change. To avoid duplication and potential drift, extract this into a helper like syncLangWithServer(locale) that both setLocale and setupI18n use, so future changes (e.g., error handling, retries) stay consistent in one place.

Suggested implementation:

import { translations as staticTranslations } from './translations';
import type { Locale } from './types';
import axios from 'axios';

const syncLangWithServer = (locale: Locale) => {
  return axios.post('/api/setLang', { lang: locale })
    .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);
    });
};

// 全局状态
const currentLocale = ref<Locale>('zh-CN');
      window.dispatchEvent(new CustomEvent('astrbot-locale-changed', {
        detail: { locale: newLocale }
      }));

      syncLangWithServer(newLocale);

There is a second axios.post("/api/setLang", ...) call in the initialization logic of setupI18n (used on first load / init) that is not shown in the snippet. That block should also be replaced with syncLangWithServer(initialLocale) (or the appropriate variable used for the initial locale) so both init and runtime changes share the same implementation and error handling.

};

Expand Down Expand Up @@ -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);
});
}
9 changes: 9 additions & 0 deletions locales/en/main.ftl
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 9 additions & 0 deletions locales/zh-cn/main.ftl
Original file line number Diff line number Diff line change
@@ -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}。
Comment on lines +2 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): zh-CN 的 FTL 文件使用了非 Fluent 的占位符语法,无法被正确替换。

在 Fluent 中,外部参数必须使用带有 $ 前缀的形式引用,例如 { $webui_dir }, { $v }, { $version }, { $error }。当前 zh-CN 条目省略了 $,并使用 VERSION / e,这和你在 Python 中传入的变量名并不匹配。请将它们与英文文件以及 Python 里的关键字参数对齐,例如:

main-use-specified-webui = 使用指定的 WebUI 目录: { $webui_dir }
main-webui-not-found = 指定的 WebUI 目录 { $webui_dir } 不存在,将使用默认逻辑。
main-webui-version-mismatch = 检测到 WebUI 版本 ({ $v }) 与当前 AstrBot 版本 (v{ $version }) 不符。
main-download-dashboard-failed = 下载管理面板文件失败: { $error }。

这样 Fluent 运行时时才能正确插入这些变量值。

Original comment in English

issue (bug_risk): The zh-CN FTL file uses non-Fluent placeholder syntax that won’t be substituted correctly.

In Fluent, external arguments must be referenced with a leading $, e.g. { $webui_dir }, { $v }, { $version }, { $error }. The current zh-CN entries omit the $ and use VERSION/e, which don’t match the variables you’ll pass from Python. Please align them with the en file and your Python kwargs, for example:

main-use-specified-webui = 使用指定的 WebUI 目录: { $webui_dir }
main-webui-not-found = 指定的 WebUI 目录 { $webui_dir } 不存在,将使用默认逻辑。
main-webui-version-mismatch = 检测到 WebUI 版本 ({ $v }) 与当前 AstrBot 版本 (v{ $version }) 不符。
main-download-dashboard-failed = 下载管理面板文件失败: { $error }。

This way the Fluent runtime can correctly interpolate the values.

main-download-dashboard-success = 管理面板下载完成。
main-argparse-webui-dir-help = 指定 WebUI 静态文件目录路径
22 changes: 10 additions & 12 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -68,34 +68,32 @@ 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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Fluent 的翻译调用使用了位置参数,这会触发 TypeError,并且也与 FTL 文件中的变量名不匹配。

Lang.__call__ 的定义是 def __call__(self, key: str, **kwargs),因此 t("main-use-specified-webui", webui_dir) 会因为多出的那个位置参数触发 TypeError。FTL 条目也期望使用具名变量($webui_dir, $v, $version, $error),因此应该通过关键字参数传入,例如:

logger.info(t("main-use-specified-webui", webui_dir=webui_dir))
logger.warning(t("main-webui-not-found", webui_dir=webui_dir))
logger.warning(t("main-webui-version-mismatch", v=v, version=VERSION))
logger.critical(t("main-download-dashboard-failed", error=e))

这样可以避免运行时错误,并与 Fluent 的变量命名保持一致。

Original comment in English

issue (bug_risk): Fluent translation calls use positional args, which will raise TypeError and also don't match the variable names in the FTL files.

Lang.__call__ is defined as def __call__(self, key: str, **kwargs), so t("main-use-specified-webui", webui_dir) will raise TypeError due to the extra positional argument. The FTL entries also expect named variables ($webui_dir, $v, $version, $error), so these should be passed as keyword args, e.g.:

logger.info(t("main-use-specified-webui", webui_dir=webui_dir))
logger.warning(t("main-webui-not-found", webui_dir=webui_dir))
logger.warning(t("main-webui-version-mismatch", v=v, version=VERSION))
logger.critical(t("main-download-dashboard-failed", error=e))

This avoids the runtime error and matches Fluent's variable naming.

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):
v = await get_dashboard_version()
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


Expand All @@ -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()
Expand Down
Loading