-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[Feat]添加跟随前端变化的i18n设置 #5213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feat]添加跟随前端变化的i18n设置 #5213
Changes from all commits
bb7dfcc
05beedc
2f1d666
b7bfc7e
8827d1b
ec29027
c386396
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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") |
| 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), | ||
| } | ||
| 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__ | ||
| 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'); | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: 语言设置的调用在
建议实现: 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);在 Original comment in Englishsuggestion: The language-setting call is duplicated in both The 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 |
||
| }; | ||
|
|
||
|
|
@@ -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); | ||
| }); | ||
| } | ||
| 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. |
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): zh-CN 的 FTL 文件使用了非 Fluent 的占位符语法,无法被正确替换。 在 Fluent 中,外部参数必须使用带有 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 Englishissue (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 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 静态文件目录路径 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,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)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): Fluent 的翻译调用使用了位置参数,这会触发 TypeError,并且也与 FTL 文件中的变量名不匹配。
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 Englishissue (bug_risk): Fluent translation calls use positional args, which will raise TypeError and also don't match the variable names in the FTL files.
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 | ||
|
|
||
|
|
||
|
|
@@ -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() | ||
|
|
||
There was a problem hiding this comment.
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 接口,例如: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". Becauseset_Langusesawait request.get_json(), this should be exposed as a POST endpoint instead: