From 80a3997ee71b45af8ba9c1d774d1ac99389f645c Mon Sep 17 00:00:00 2001 From: JoeanAmier Date: Sun, 22 Dec 2024 11:13:28 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E7=BF=BB=E8=AF=91=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locale/README.md | 31 ++++++++++++---- locale/po_to_mo.py | 1 - source/CLI/main.py | 33 +++++++++++------ source/TUI/about.py | 12 +++---- source/TUI/app.py | 31 +++++++--------- source/TUI/index.py | 28 +++++++-------- source/TUI/loading.py | 9 +++-- source/TUI/monitor.py | 12 +++---- source/TUI/record.py | 14 ++++---- source/TUI/setting.py | 60 +++++++++++++++---------------- source/TUI/update.py | 18 +++++----- source/application/app.py | 57 +++++++++++++---------------- source/application/download.py | 24 ++++++------- source/application/explore.py | 11 +++--- source/application/request.py | 14 ++++---- source/expansion/browser.py | 19 ++++++---- source/expansion/cleaner.py | 7 +++- source/module/__init__.py | 3 -- source/module/manager.py | 44 +++++++++-------------- source/module/recorder.py | 2 +- source/module/settings.py | 5 --- source/module/static.py | 13 ------- source/module/translator.py | 27 -------------- source/translation/__init__.py | 1 + source/translation/translate.py | 63 +++++++++++++++++++++++++++++++++ static/Release_Notes.md | 14 ++++---- 26 files changed, 286 insertions(+), 267 deletions(-) delete mode 100644 source/module/translator.py create mode 100644 source/translation/__init__.py create mode 100644 source/translation/translate.py diff --git a/locale/README.md b/locale/README.md index 1f7cbba..8955261 100644 --- a/locale/README.md +++ b/locale/README.md @@ -1,13 +1,30 @@ +# 命令参考 + +**运行命令前,确保已经安装了 `gettext` 软件包,并配置好环境变量。** + +**Before running the command, ensure that the `gettext` package is installed and the environment variables are properly +configured.** + +* `xgettext --files-from=py_files.txt -d xhs -o xhs.pot` +* `mkdir zh_CN\LC_MESSAGES` +* `msginit -l zh_CN -o zh_CN/LC_MESSAGES/xhs.po -i xhs.pot` +* `mkdir en_US\LC_MESSAGES` +* `msginit -l en_US -o en_US/LC_MESSAGES/xhs.po -i xhs.pot` +* `msgmerge -U zh_CN/LC_MESSAGES/xhs.po xhs.pot` +* `msgmerge -U en_US/LC_MESSAGES/xhs.po xhs.pot` + # 翻译贡献指南 -* 如果想要贡献支持更多语言,请参考 `zh_CN` 与 `en_US` 的文件夹层级结构,复制 `locale/zh_CN/LC_MESSAGES/xhs.po` 文件并编辑翻译。 +* 如果想要贡献支持更多语言,请在终端切换至 `locale` 文件夹,运行命令 + `msginit -l 语言代码 -o 语言代码/LC_MESSAGES/xhs.po -i xhs.pot` + 生成 po 文件并编辑翻译。 * 如果想要贡献改进翻译结果,请直接编辑 `xhs.po` 文件内容。 -* 不需要提交 `xhs.mo` 文件,提交 `xhs.po` 文件后,作者会转换格式并合并。 +* 仅需提交 `xhs.po` 文件,作者会转换格式并合并。 # Translation Contribution Guide -* If you want to contribute by supporting more languages, please refer to the folder structure of `zh_CN` and `en_US`, - copy the `locale/zh_CN/LC_MESSAGES/xhs.po` file, and edit the translation. -* If you want to contribute by improving the translation results, please edit the content of the `xhs.po` file directly. -* There is no need to submit the `xhs.mo` file; after submitting the `xhs.po` file, the author will convert the format - and merge it. +* If you want to contribute support for more languages, please switch to the `locale` folder in the terminal and run the + command `msginit -l language_code -o language_code/LC_MESSAGES/xhs.po -i xhs.pot` to generate the po file and edit the + translation. +* If you want to contribute to improving the translation, please directly edit the content of the `xhs.po` file. +* Only the `xhs.po` file needs to be submitted, and the author will convert the format and merge it. diff --git a/locale/po_to_mo.py b/locale/po_to_mo.py index 729e1f8..a571bad 100644 --- a/locale/po_to_mo.py +++ b/locale/po_to_mo.py @@ -1,7 +1,6 @@ from pathlib import Path from subprocess import run -__all__ = [] ROOT = Path(__file__).resolve().parent diff --git a/source/CLI/main.py b/source/CLI/main.py index 234aff3..7ad5297 100644 --- a/source/CLI/main.py +++ b/source/CLI/main.py @@ -23,7 +23,7 @@ from source.module import ( PROJECT, ) from source.module import Settings -from source.module import Translate +from source.translation import switch_language, _ __all__ = ["cli"] @@ -106,7 +106,6 @@ class CLI: @staticmethod @check_value def help_(ctx: Context, param, value) -> None: - _ = Translate("").message() table = Table(highlight=True, box=None, show_header=True) # 添加表格的列名 @@ -115,7 +114,6 @@ class CLI: table.add_column("type", no_wrap=True, style="bold") table.add_column("description", no_wrap=True, ) - # TODO: 语言设置未生效 options = ( ("--url", "-u", "str", _("小红书作品链接")), ("--index", "-i", "str", _("下载指定序号的图片文件,仅对图文作品生效;多个序号输入示例:\"1 3 5 7\"")), @@ -158,7 +156,6 @@ class CLI: border_style="bold", title="XHS-Downloader CLI Parameters", title_align="left")) - ctx.exit() @command(name="XHS-Downloader", help=PROJECT) @@ -170,8 +167,6 @@ class CLI: ) @option("--folder_name", "-fn", ) @option("--name_format", "-nf", ) -# @option("--sec_ch_ua", "-su", ) -# @option("--sec_ch_ua_platform", "-sp", ) @option("--user_agent", "-ua", ) @option("--cookie", "-ck", ) @option("--proxy", "-p", ) @@ -192,19 +187,35 @@ class CLI: @option("--update_settings", "-us", type=bool, is_flag=True, ) @option("-h", - is_flag=True, - is_eager=True, - expose_value=False, - callback=CLI.help_, ) + "--help", + is_flag=True, ) @option("--version", "-v", is_flag=True, is_eager=True, expose_value=False, callback=CLI.version, ) @pass_context -def cli(ctx, **kwargs): +def cli(ctx, help, language, **kwargs): + # Step 1: 切换语言 + if language: + switch_language(language) + + # Step 2: 如果请求了帮助信息,则显示帮助并退出 + if help: + ctx.obj = kwargs # 保留当前上下文的参数 + CLI.help_(ctx, None, help) + return + + # Step 3: 主逻辑 async def main(): async with CLI(ctx, **kwargs) as xhs: await xhs.run() run(main()) + + +if __name__ == "__main__": + from click.testing import CliRunner + + runner = CliRunner() + result = runner.invoke(cli, ['-l', 'en_US', '-h']) diff --git a/source/TUI/about.py b/source/TUI/about.py index 67f5853..c0a4c9f 100644 --- a/source/TUI/about.py +++ b/source/TUI/about.py @@ -1,5 +1,3 @@ -from typing import Callable - from rich.text import Text from textual.app import ComposeResult from textual.binding import Binding @@ -14,6 +12,7 @@ from ..module import ( MASTER, INFO, ) +from ..translation import _ __all__ = ["About"] @@ -34,17 +33,16 @@ class About(Screen): description="返回首页/Back"), ] - def __init__(self, message: Callable[[str], str]): + def __init__(self, ): super().__init__() - self.message = message def compose(self) -> ComposeResult: yield Header() - yield Label(Text(self.message("如果 XHS-Downloader 对您有帮助,请考虑为它点个 Star,感谢您的支持!"), style=INFO), + yield Label(Text(_("如果 XHS-Downloader 对您有帮助,请考虑为它点个 Star,感谢您的支持!"), style=INFO), classes="prompt", ) yield Label(Text("Discord 社区", style=PROMPT), classes="prompt", ) - yield Label(f"{self.message("邀请链接:")}https://discord.com/invite/ZYtmgKud9Y") - yield Label(Text(self.message("作者的其他开源项目"), style=PROMPT), classes="prompt", ) + yield Label(f"{_("邀请链接:")}https://discord.com/invite/ZYtmgKud9Y") + yield Label(Text(_("作者的其他开源项目"), style=PROMPT), classes="prompt", ) yield Label(Text("TikTokDownloader (抖音 / TikTok)", style=MASTER), classes="prompt", ) yield Label("https://github.com/JoeanAmier/TikTokDownloader") yield Label(Text("KS-Downloader (快手)", style=MASTER), classes="prompt", ) diff --git a/source/TUI/app.py b/source/TUI/app.py index e82cce6..3915929 100644 --- a/source/TUI/app.py +++ b/source/TUI/app.py @@ -1,5 +1,3 @@ -from typing import Callable - from textual.app import App from textual.widgets import RichLog @@ -15,8 +13,8 @@ from ..module import ( ERROR, ) from ..module import Settings -from ..module import Translate from ..module import logging +from ..translation import _ __all__ = ["XHSDownloader"] @@ -28,7 +26,6 @@ class XHSDownloader(App): def __init__(self): super().__init__() self.parameter: dict - self.message: Callable[[str], str] self.APP: XHS self.__initialization() @@ -41,10 +38,8 @@ class XHSDownloader(App): def __initialization(self) -> None: self.parameter = self.SETTINGS.run() - self.message = Translate(self.parameter["language"]).message() self.APP = XHS( **self.parameter, - transition=self.message, _print=False, ) @@ -52,18 +47,18 @@ class XHSDownloader(App): self.install_screen( Setting( self.parameter, - self.message), + ), name="setting") - self.install_screen(Index(self.APP, self.message), name="index") - self.install_screen(Loading(self.message), name="loading") - self.install_screen(About(self.message), name="about") - self.install_screen(Record(self.APP, self.message), name="record") + self.install_screen(Index(self.APP, ), name="index") + self.install_screen(Loading(), name="loading") + self.install_screen(About(), name="about") + self.install_screen(Record(self.APP, ), name="record") await self.push_screen("index") self.SETTINGS.check_keys( self.parameter, logging, self.query_one(RichLog), - self.message("配置文件 settings.json 缺少必要的参数,请删除该文件,然后重新运行程序,自动生成默认配置文件!") + + _("配置文件 settings.json 缺少必要的参数,请删除该文件,然后重新运行程序,自动生成默认配置文件!") + f"\n{ ">" * 50}", @@ -88,15 +83,15 @@ class XHSDownloader(App): self.uninstall_screen("loading") self.uninstall_screen("about") self.uninstall_screen("record") - self.install_screen(Index(self.APP, self.message), name="index") + self.install_screen(Index(self.APP, ), name="index") self.install_screen( Setting( self.parameter, - self.message), + ), name="setting") - self.install_screen(Loading(self.message), name="loading") - self.install_screen(About(self.message), name="about") - self.install_screen(Record(self.APP, self.message), name="record") + self.install_screen(Loading(), name="loading") + self.install_screen(About(), name="about") + self.install_screen(Record(self.APP, ), name="record") await self.push_screen("index") def update_result(self, tip: str) -> None: @@ -105,7 +100,7 @@ class XHSDownloader(App): log.write(">" * 50) async def action_check_update(self): - await self.push_screen(Update(self.APP, self.message), callback=self.update_result) + await self.push_screen(Update(self.APP, ), callback=self.update_result) async def action_update_and_return(self): await self.push_screen("index") diff --git a/source/TUI/index.py b/source/TUI/index.py index e250f4f..a3149ac 100644 --- a/source/TUI/index.py +++ b/source/TUI/index.py @@ -1,5 +1,3 @@ -from typing import Callable - from pyperclip import paste from rich.text import Text from textual import on @@ -28,6 +26,7 @@ from ..module import ( REPOSITORY, GENERAL, ) +from ..translation import _ __all__ = ["Index"] @@ -42,10 +41,9 @@ class Index(Screen): Binding(key="A", action="about", description="关于项目/About"), ] - def __init__(self, app: XHS, message: Callable[[str], str]): + def __init__(self, app: XHS, ): super().__init__() self.xhs = app - self.message = message self.url = None self.tip = None @@ -54,24 +52,24 @@ class Index(Screen): yield ScrollableContainer( Label( Text( - f"{self.message("开源协议")}: {LICENCE}", + f"{_("开源协议")}: {LICENCE}", style=MASTER) ), Label( Text( - f"{self.message("项目地址")}{REPOSITORY}", + f"{_("项目地址")}{REPOSITORY}", style=MASTER) ), Label( Text( - self.message("请输入小红书图文/视频作品链接"), + _("请输入小红书图文/视频作品链接"), style=PROMPT), classes="prompt", ), - Input(placeholder=self.message("多个链接之间使用空格分隔")), + Input(placeholder=_("多个链接之间使用空格分隔")), HorizontalScroll( - Button(self.message("下载无水印作品文件"), id="deal"), - Button(self.message("读取剪贴板"), id="paste"), - Button(self.message("清空输入框"), id="reset"), + Button(_("下载无水印作品文件"), id="deal"), + Button(_("读取剪贴板"), id="paste"), + Button(_("清空输入框"), id="reset"), ), ) yield RichLog(markup=True, ) @@ -83,7 +81,7 @@ class Index(Screen): self.tip = self.query_one(RichLog) self.tip.write( Text( - self.message("免责声明\n") + + _("免责声明\n") + f"\n{ ">" * 50}", @@ -96,7 +94,7 @@ class Index(Screen): if self.url.value: self.deal() else: - self.tip.write(Text(self.message("未输入任何小红书作品链接"), style=WARNING)) + self.tip.write(Text(_("未输入任何小红书作品链接"), style=WARNING)) self.tip.write(Text(">" * 50, style=GENERAL)) @on(Button.Pressed, "#reset") @@ -113,7 +111,7 @@ class Index(Screen): if any(await self.xhs.extract(self.url.value, True, log=self.tip, data=False, )): self.url.value = "" else: - self.tip.write(Text(self.message("下载小红书作品文件失败"), style=ERROR)) + self.tip.write(Text(_("下载小红书作品文件失败"), style=ERROR)) self.tip.write(Text(">" * 50, style=GENERAL)) self.app.pop_screen() @@ -127,7 +125,7 @@ class Index(Screen): await self.app.run_action("settings") async def action_monitor(self): - await self.app.push_screen(Monitor(self.xhs, self.message)) + await self.app.push_screen(Monitor(self.xhs, )) async def action_about(self): await self.app.push_screen("about") diff --git a/source/TUI/loading.py b/source/TUI/loading.py index 9dd35c4..6f3d09a 100644 --- a/source/TUI/loading.py +++ b/source/TUI/loading.py @@ -1,22 +1,21 @@ -from typing import Callable - from textual.app import ComposeResult from textual.containers import Grid from textual.screen import ModalScreen from textual.widgets import Label from textual.widgets import LoadingIndicator +from ..translation import _ + __all__ = ["Loading"] class Loading(ModalScreen): - def __init__(self, message: Callable[[str], str]): + def __init__(self, ): super().__init__() - self.message = message def compose(self) -> ComposeResult: yield Grid( - Label(self.message("程序处理中...")), + Label(_("程序处理中...")), LoadingIndicator(), classes="loading", ) diff --git a/source/TUI/monitor.py b/source/TUI/monitor.py index 3ab3879..5edf442 100644 --- a/source/TUI/monitor.py +++ b/source/TUI/monitor.py @@ -1,5 +1,3 @@ -from typing import Callable - from rich.text import Text from textual import on from textual import work @@ -18,6 +16,7 @@ from ..module import ( MASTER, INFO, ) +from ..translation import _ __all__ = ["Monitor"] @@ -28,16 +27,15 @@ class Monitor(Screen): Binding(key="C", action="close", description="关闭监听/Close"), ] - def __init__(self, app: XHS, message: Callable[[str], str]): + def __init__(self, app: XHS, ): super().__init__() self.xhs = app - self.message = message def compose(self) -> ComposeResult: yield Header() - yield Label(Text(self.message("已启动监听剪贴板模式"), style=INFO), classes="prompt") + yield Label(Text(_("已启动监听剪贴板模式"), style=INFO), classes="prompt") yield RichLog(markup=True, wrap=True) - yield Button(self.message("退出监听剪贴板模式"), id="close") + yield Button(_("退出监听剪贴板模式"), id="close") yield Footer() @on(Button.Pressed, "#close") @@ -52,7 +50,7 @@ class Monitor(Screen): def on_mount(self) -> None: self.title = PROJECT self.query_one(RichLog).write( - Text(self.message( + Text(_( "程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件,如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!"), style=MASTER)) self.run_monitor() diff --git a/source/TUI/record.py b/source/TUI/record.py index 5a95ff9..8d80d53 100644 --- a/source/TUI/record.py +++ b/source/TUI/record.py @@ -1,5 +1,3 @@ -from typing import Callable - from textual import on from textual.app import ComposeResult from textual.containers import Grid @@ -10,24 +8,24 @@ from textual.widgets import Input from textual.widgets import Label from ..application import XHS +from ..translation import _ __all__ = ["Record"] class Record(ModalScreen): - def __init__(self, app: XHS, message: Callable[[str], str]): + def __init__(self, app: XHS, ): super().__init__() self.xhs = app - self.message = message def compose(self) -> ComposeResult: yield Grid( - Label(self.message("请输入待删除的小红书作品链接或作品 ID"), classes="prompt"), - Input(placeholder=self.message("支持输入作品 ID 或包含作品 ID 的作品链接,多个链接或 ID 之间使用空格分隔"), + Label(_("请输入待删除的小红书作品链接或作品 ID"), classes="prompt"), + Input(placeholder=_("支持输入作品 ID 或包含作品 ID 的作品链接,多个链接或 ID 之间使用空格分隔"), id="id", ), HorizontalScroll( - Button(self.message("删除指定作品 ID"), id="enter", ), - Button(self.message("返回首页"), id="close"), ), + Button(_("删除指定作品 ID"), id="enter", ), + Button(_("返回首页"), id="close"), ), id="record", ) diff --git a/source/TUI/setting.py b/source/TUI/setting.py index 0d1b0f0..1b34621 100644 --- a/source/TUI/setting.py +++ b/source/TUI/setting.py @@ -1,5 +1,3 @@ -from typing import Callable - from textual import on from textual.app import ComposeResult from textual.binding import Binding @@ -14,6 +12,8 @@ from textual.widgets import Input from textual.widgets import Label from textual.widgets import Select +from ..translation import _ + __all__ = ["Setting"] @@ -23,50 +23,49 @@ class Setting(Screen): Binding(key="B", action="index", description="返回首页/Back"), ] - def __init__(self, data: dict, message: Callable[[str], str]): + def __init__(self, data: dict, ): super().__init__() self.data = data - self.message = message def compose(self) -> ComposeResult: yield Header() yield ScrollableContainer( - Label(self.message("作品数据 / 文件保存根路径"), classes="params", ), - Input(self.data["work_path"], placeholder=self.message("程序根路径"), valid_empty=True, + Label(_("作品数据 / 文件保存根路径"), classes="params", ), + Input(self.data["work_path"], placeholder=_("程序根路径"), valid_empty=True, id="work_path", ), - Label(self.message("作品文件储存文件夹名称"), classes="params", ), + Label(_("作品文件储存文件夹名称"), classes="params", ), Input(self.data["folder_name"], placeholder="Download", id="folder_name", ), - Label(self.message("作品文件名称格式"), classes="params", ), - Input(self.data["name_format"], placeholder=self.message("发布时间 作者昵称 作品标题"), valid_empty=True, + Label(_("作品文件名称格式"), classes="params", ), + Input(self.data["name_format"], placeholder=_("发布时间 作者昵称 作品标题"), valid_empty=True, id="name_format", ), - Label(self.message("User-Agent"), classes="params", ), - Input(self.data["user_agent"], placeholder=self.message("内置 Chrome User Agent"), valid_empty=True, + Label(_("User-Agent"), classes="params", ), + Input(self.data["user_agent"], placeholder=_("内置 Chrome User Agent"), valid_empty=True, id="user_agent", ), - Label(self.message("小红书网页版 Cookie"), classes="params", ), + Label(_("小红书网页版 Cookie"), classes="params", ), Input(placeholder=self.__check_cookie(), valid_empty=True, id="cookie", ), - Label(self.message("网络代理"), classes="params", ), - Input(self.data["proxy"], placeholder=self.message("不使用代理"), valid_empty=True, id="proxy", ), - Label(self.message("请求数据超时限制,单位:秒"), classes="params", ), + Label(_("网络代理"), classes="params", ), + Input(self.data["proxy"], placeholder=_("不使用代理"), valid_empty=True, id="proxy", ), + Label(_("请求数据超时限制,单位:秒"), classes="params", ), Input(str(self.data["timeout"]), placeholder="10", type="integer", id="timeout", ), - Label(self.message("下载文件时,每次从服务器获取的数据块大小,单位:字节"), classes="params", ), + Label(_("下载文件时,每次从服务器获取的数据块大小,单位:字节"), classes="params", ), Input(str(self.data["chunk"]), placeholder="1048576", type="integer", id="chunk", ), - Label(self.message("请求数据失败时,重试的最大次数"), classes="params", ), + Label(_("请求数据失败时,重试的最大次数"), classes="params", ), Input(str(self.data["max_retry"]), placeholder="5", type="integer", id="max_retry", ), Label(), Container( - Checkbox(self.message("记录作品详细数据"), id="record_data", value=self.data["record_data"], ), - Checkbox(self.message("作品文件夹归档模式"), id="folder_mode", value=self.data["folder_mode"], ), - Checkbox(self.message("视频作品下载开关"), id="video_download", value=self.data["video_download"], ), - Checkbox(self.message("图文作品下载开关"), id="image_download", value=self.data["image_download"], ), + Checkbox(_("记录作品详细数据"), id="record_data", value=self.data["record_data"], ), + Checkbox(_("作品文件夹归档模式"), id="folder_mode", value=self.data["folder_mode"], ), + Checkbox(_("视频作品下载开关"), id="video_download", value=self.data["video_download"], ), + Checkbox(_("图文作品下载开关"), id="image_download", value=self.data["image_download"], ), classes="horizontal-layout"), Label(), Container( - Checkbox(self.message("动图文件下载开关"), id="live_download", value=self.data["live_download"], ), - Checkbox(self.message("作品下载记录开关"), id="download_record", value=self.data["download_record"], ), + Checkbox(_("动图文件下载开关"), id="live_download", value=self.data["live_download"], ), + Checkbox(_("作品下载记录开关"), id="download_record", value=self.data["download_record"], ), classes="horizontal-layout"), Container( - Label(self.message("图片下载格式"), classes="params", ), - Label(self.message("程序语言"), classes="params", ), + Label(_("图片下载格式"), classes="params", ), + Label(_("程序语言"), classes="params", ), classes="horizontal-layout", ), Label(), @@ -83,19 +82,19 @@ class Setting(Screen): id="language", ), classes="horizontal-layout"), Container( - Button(self.message("保存配置"), id="save", ), - Button(self.message("放弃更改"), id="abandon", ), + Button(_("保存配置"), id="save", ), + Button(_("放弃更改"), id="abandon", ), classes="settings_button", ), ) yield Footer() def __check_cookie(self) -> str: if self.data["cookie"]: - return self.message("小红书网页版 Cookie,无需登录,参数已设置") - return self.message("小红书网页版 Cookie,无需登录,参数未设置") + return _("小红书网页版 Cookie,无需登录,参数已设置") + return _("小红书网页版 Cookie,无需登录,参数未设置") def on_mount(self) -> None: - self.title = self.message("程序设置") + self.title = _("程序设置") @on(Button.Pressed, "#save") def save_settings(self): @@ -117,7 +116,6 @@ class Setting(Screen): "video_download": self.query_one("#video_download").value, "live_download": self.query_one("#live_download").value, "download_record": self.query_one("#download_record").value, - # "server": False, }) @on(Button.Pressed, "#abandon") diff --git a/source/TUI/update.py b/source/TUI/update.py index 46d7d1b..e5de80c 100644 --- a/source/TUI/update.py +++ b/source/TUI/update.py @@ -1,5 +1,3 @@ -from typing import Callable - from rich.text import Text from textual import work from textual.app import ComposeResult @@ -15,19 +13,19 @@ from ..module import ( INFO, RELEASES, ) +from ..translation import _ __all__ = ["Update"] class Update(ModalScreen): - def __init__(self, app: XHS, message: Callable[[str], str]): + def __init__(self, app: XHS, ): super().__init__() self.xhs = app - self.message = message def compose(self) -> ComposeResult: yield Grid( - Label(self.message("正在检查新版本,请稍等...")), + Label(_("正在检查新版本,请稍等...")), LoadingIndicator(), classes="loading", ) @@ -39,24 +37,24 @@ class Update(ModalScreen): version = url.split("/")[-1] match self.compare_versions(f"{XHS.VERSION_MAJOR}.{XHS.VERSION_MINOR}", version, XHS.VERSION_BETA): case 4: - tip = Text(f"{self.message("检测到新版本:{0}.{1}").format( + tip = Text(f"{_("检测到新版本:{0}.{1}").format( XHS.VERSION_MAJOR, XHS.VERSION_MINOR)}\n{RELEASES}", style=WARNING) case 3: tip = Text( - f"{self.message("当前版本为开发版, 可更新至正式版")}\n{RELEASES}", + f"{_("当前版本为开发版, 可更新至正式版")}\n{RELEASES}", style=WARNING) case 2: tip = Text( - self.message("当前已是最新开发版"), + _("当前已是最新开发版"), style=WARNING) case 1: tip = Text( - self.message("当前已是最新正式版"), + _("当前已是最新正式版"), style=INFO) case _: raise ValueError except ValueError: - tip = Text(self.message("检测新版本失败"), style=ERROR) + tip = Text(_("检测新版本失败"), style=ERROR) self.dismiss(tip) def on_mount(self) -> None: diff --git a/source/application/app.py b/source/application/app.py index af72861..a63929f 100644 --- a/source/application/app.py +++ b/source/application/app.py @@ -6,7 +6,6 @@ from asyncio import sleep from contextlib import suppress from datetime import datetime from re import compile -from typing import Callable from urllib.parse import urlparse from fastapi import FastAPI @@ -36,9 +35,9 @@ from source.module import ( VERSION_MINOR, VERSION_BETA, ) -from source.module import Translate from source.module import logging from source.module import sleep_time +from source.translation import switch_language, _ from .download import Download from .explore import Explore from .image import Image @@ -80,8 +79,6 @@ class XHS: work_path="", folder_name="Download", name_format="发布时间 作者昵称 作品标题", - # sec_ch_ua: str = "", - # sec_ch_ua_platform: str = "", user_agent: str = None, cookie: str = None, proxy: str | dict = None, @@ -96,22 +93,18 @@ class XHS: folder_mode=False, download_record=True, language="zh_CN", - # server=False, - transition: Callable[[str], str] = None, read_cookie: int | str = None, _print: bool = True, *args, **kwargs, ): - self.message = transition or Translate(language).message() + switch_language(language) self.manager = Manager( ROOT, work_path, folder_name, name_format, chunk, - # sec_ch_ua, - # sec_ch_ua_platform, user_agent, self.read_browser_cookie(read_cookie) or cookie, proxy, @@ -124,8 +117,6 @@ class XHS: live_download, download_record, folder_mode, - # server, - self.message, _print, ) self.html = Html(self.manager) @@ -163,7 +154,7 @@ class XHS: if (u := container["下载地址"]) and download: if await self.skip_download(i := container["作品ID"]): logging( - log, self.message("作品 {0} 存在下载记录,跳过下载").format(i)) + log, _("作品 {0} 存在下载记录,跳过下载").format(i)) else: path, result = await self.download.run( u, @@ -176,7 +167,7 @@ class XHS: ) await self.__add_record(i, result) elif not u: - logging(log, self.message("提取作品文件下载地址失败"), ERROR) + logging(log, _("提取作品文件下载地址失败"), ERROR) await self.save_data(container) @_data_cache @@ -202,10 +193,10 @@ class XHS: # return # 调试代码 urls = await self.__extract_links(url, log) if not urls: - logging(log, self.message("提取小红书作品链接失败"), WARNING) + logging(log, _("提取小红书作品链接失败"), WARNING) else: logging( - log, self.message("共 {0} 个小红书作品待处理...").format(len(urls))) + log, _("共 {0} 个小红书作品待处理...").format(len(urls))) # return urls # 调试代码 return [await self.__deal_extract(i, download, index, log, bar, data, ) for i in urls] @@ -220,7 +211,7 @@ class XHS: ) -> None: url = await self.__extract_links(url, log) if not url: - logging(log, self.message("提取小红书作品链接失败"), WARNING) + logging(log, _("提取小红书作品链接失败"), WARNING) else: await self.__deal_extract(url[0], download, index, log, bar, data, ) @@ -250,29 +241,29 @@ class XHS: cookie: str = None, ): if await self.skip_download(i := self.__extract_link_id(url)) and not data: - msg = self.message("作品 {0} 存在下载记录,跳过处理").format(i) + msg = _("作品 {0} 存在下载记录,跳过处理").format(i) logging(log, msg) return {"message": msg} - logging(log, self.message("开始处理作品:{0}").format(i)) + logging(log, _("开始处理作品:{0}").format(i)) html = await self.html.request_url(url, log=log, cookie=cookie, ) namespace = self.__generate_data_object(html) if not namespace: - logging(log, self.message("{0} 获取数据失败").format(i), ERROR) + logging(log, _("{0} 获取数据失败").format(i), ERROR) return {} data = self.explore.run(namespace) # logging(log, data) # 调试代码 if not data: - logging(log, self.message("{0} 提取数据失败").format(i), ERROR) + logging(log, _("{0} 提取数据失败").format(i), ERROR) return {} match data["作品类型"]: - case "视频": + case _("视频"): self.__extract_video(data, namespace) - case "图文": + case _("图文"): self.__extract_image(data, namespace) case _: data["下载地址"] = [] await self.__download_files(data, download, index, log, bar) - logging(log, self.message("作品处理完成:{0}").format(i)) + logging(log, _("作品处理完成:{0}").format(i)) await sleep_time() return data @@ -332,7 +323,7 @@ class XHS: ) -> None: logging( None, - self.message( + _( "程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件,如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!"), style=MASTER, ) @@ -392,13 +383,13 @@ class XHS: # skip = data.get("skip", False) # url = await self.__extract_links(url, None) # if not url: - # msg = self.message("提取小红书作品链接失败") + # msg = _("提取小红书作品链接失败") # data = None # else: # if data := await self.__deal_extract(url[0], download, index, None, None, not skip, ): - # msg = self.message("获取小红书作品数据成功") + # msg = _("获取小红书作品数据成功") # else: - # msg = self.message("获取小红书作品数据失败") + # msg = _("获取小红书作品数据失败") # data = None # return web.json_response(dict(message=msg, url=url[0], data=data)) @@ -420,12 +411,12 @@ class XHS: # await self.runner.setup() # self.site = web.TCPSite(self.runner, "0.0.0.0") # await self.site.start() - # logging(log, self.message("Web API 服务器已启动!")) - # logging(log, self.message("服务器主机及端口: {0}".format(self.site.name, ))) + # logging(log, _("Web API 服务器已启动!")) + # logging(log, _("服务器主机及端口: {0}".format(self.site.name, ))) # async def close_server(self, log=None, ): # await self.runner.cleanup() - # logging(log, self.message("Web API 服务器已关闭!")) + # logging(log, _("Web API 服务器已关闭!")) async def run_server(self, host="0.0.0.0", port=8000, log_level="info", ): self.server = FastAPI( @@ -451,7 +442,7 @@ class XHS: async def handle(extract: ExtractParams): url = await self.__extract_links(extract.url, None) if not url: - msg = self.message("提取小红书作品链接失败") + msg = _("提取小红书作品链接失败") data = None else: if data := await self.__deal_extract( @@ -463,9 +454,9 @@ class XHS: not extract.skip, extract.cookie, ): - msg = self.message("获取小红书作品数据成功") + msg = _("获取小红书作品数据成功") else: - msg = self.message("获取小红书作品数据失败") + msg = _("获取小红书作品数据失败") data = None return ExtractData( message=msg, diff --git a/source/application/download.py b/source/application/download.py index 9e19ebd..f2a400e 100644 --- a/source/application/download.py +++ b/source/application/download.py @@ -18,6 +18,7 @@ from ..module import Manager from ..module import logging from ..module import retry as re_download from ..module import sleep_time +from ..translation import _ if TYPE_CHECKING: from httpx import AsyncClient @@ -44,7 +45,6 @@ class Download: self.client: "AsyncClient" = manager.download_client self.headers = manager.blank_headers self.retry = manager.retry - self.message = manager.message self.folder_mode = manager.folder_mode self.video_format = "mp4" self.live_format = "mp4" @@ -72,14 +72,14 @@ class Download: ) -> tuple[Path, list[Any]]: path = self.__generate_path(name) match type_: - case "视频": + case _("视频"): tasks = self.__ready_download_video( urls, path, name, log, ) - case "图文": + case _("图文"): tasks = self.__ready_download_image( urls, lives, @@ -115,7 +115,7 @@ class Download: name: str, log) -> list: if not self.video_download: - logging(log, self.message("视频作品下载功能已关闭,跳过下载")) + logging(log, _("视频作品下载功能已关闭,跳过下载")) return [] if self.__check_exists_path(path, f"{name}.{self.video_format}", log): return [] @@ -131,7 +131,7 @@ class Download: log) -> list: tasks = [] if not self.image_download: - logging(log, self.message("图文作品下载功能已关闭,跳过下载")) + logging(log, _("图文作品下载功能已关闭,跳过下载")) return tasks for i, j in enumerate(zip(urls, lives), start=1): if index and i not in index: @@ -158,7 +158,7 @@ class Download: def __check_exists_glob(self, path: Path, name: str, log, ) -> bool: if any(path.glob(name)): logging( - log, self.message( + log, _( "{0} 文件已存在,跳过下载").format(name)) return True return False @@ -166,7 +166,7 @@ class Download: def __check_exists_path(self, path: Path, name: str, log, ) -> bool: if path.joinpath(name).exists(): logging( - log, self.message( + log, _( "{0} 文件已存在,跳过下载").format(name)) return True return False @@ -192,7 +192,7 @@ class Download: # except HTTPError as error: # logging( # log, - # self.message( + # _( # "网络异常,{0} 请求失败,错误信息: {1}").format(name, repr(error)), # ERROR, # ) @@ -205,7 +205,7 @@ class Download: await sleep_time() if response.status_code == 416: raise CacheError( - self.message("文件 {0} 缓存异常,重新下载").format(temp.name), + _("文件 {0} 缓存异常,重新下载").format(temp.name), ) response.raise_for_status() # self.__create_progress( @@ -228,13 +228,13 @@ class Download: ) self.manager.move(temp, real) # self.__create_progress(bar, None) - logging(log, self.message("文件 {0} 下载成功").format(real.name)) + logging(log, _("文件 {0} 下载成功").format(real.name)) return True except HTTPError as error: # self.__create_progress(bar, None) logging( log, - self.message( + _( "网络异常,{0} 下载失败,错误信息: {1}").format(name, repr(error)), ERROR, ) @@ -308,7 +308,7 @@ class Download: except Exception as error: logging( log, - self.message("文件 {0} 格式判断失败,错误信息:{1}").format(temp.name, repr(error)), + _("文件 {0} 格式判断失败,错误信息:{1}").format(temp.name, repr(error)), ERROR, ) return path.joinpath(f"{name}.{default_suffix}") diff --git a/source/application/explore.py b/source/application/explore.py index 6f3044c..75fcf3d 100644 --- a/source/application/explore.py +++ b/source/application/explore.py @@ -1,13 +1,14 @@ from datetime import datetime -from source.expansion import Namespace +from ..expansion import Namespace +from ..translation import _ __all__ = ['Explore'] class Explore: time_format = "%Y-%m-%d_%H:%M:%S" - explore_type = {"video": "视频", "normal": "图文"} + explore_type = {"video": _("视频"), "normal": _("图文")} def run(self, data: Namespace) -> dict: return self.__extract_data(data) @@ -44,7 +45,7 @@ class Explore: container["作品标题"] = data.safe_extract("title") container["作品描述"] = data.safe_extract("desc") container["作品类型"] = self.explore_type.get( - data.safe_extract("type"), "未知") + data.safe_extract("type"), _("未知")) # container["IP归属地"] = data.safe_extract("ipLocation") def __extract_time(self, container: dict, data: Namespace): @@ -52,12 +53,12 @@ class Explore: time / 1000).strftime( self.time_format) if ( - time := data.safe_extract("time")) else "未知" + time := data.safe_extract("time")) else _("未知") container["最后更新时间"] = datetime.fromtimestamp( last / 1000).strftime( self.time_format) if ( - last := data.safe_extract("lastUpdateTime")) else "未知" + last := data.safe_extract("lastUpdateTime")) else _("未知") @staticmethod def __extract_user(container: dict, data: Namespace): diff --git a/source/application/request.py b/source/application/request.py index 92c570d..cc806a9 100644 --- a/source/application/request.py +++ b/source/application/request.py @@ -1,10 +1,11 @@ from httpx import HTTPError -from source.module import ERROR -from source.module import Manager -from source.module import logging -from source.module import retry -from source.module import sleep_time +from ..module import ERROR +from ..module import Manager +from ..module import logging +from ..module import retry +from ..module import sleep_time +from ..translation import _ __all__ = ["Html"] @@ -12,7 +13,6 @@ __all__ = ["Html"] class Html: def __init__(self, manager: Manager, ): self.retry = manager.retry - self.message = manager.message self.client = manager.request_client self.headers = manager.headers self.blank_headers = manager.blank_headers @@ -41,7 +41,7 @@ class Html: except HTTPError as error: logging( log, - self.message("网络异常,{0} 请求失败: {1}").format(url, repr(error)), + _("网络异常,{0} 请求失败: {1}").format(url, repr(error)), ERROR ) return "" diff --git a/source/expansion/browser.py b/source/expansion/browser.py index e8d784b..28a5752 100644 --- a/source/expansion/browser.py +++ b/source/expansion/browser.py @@ -15,6 +15,11 @@ from rookiepy import ( vivaldi, ) +try: + from source.translation import _ +except ImportError: + _ = lambda s: s + __all__ = ["BrowserCookie"] @@ -37,23 +42,23 @@ class BrowserCookie: console = console or Console() options = "\n".join(f"{i}. {k}: {v[1]}" for i, (k, v) in enumerate(cls.SUPPORT_BROWSER.items(), start=1)) if browser := console.input( - f"读取指定浏览器的 Cookie 并写入配置文件\n" - f"Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 Cookie!\n" - f"{options}\n请输入浏览器名称或序号:", ): + _("读取指定浏览器的 Cookie 并写入配置文件\n" + "Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 Cookie!\n" + "{options}\n请输入浏览器名称或序号:").format(options=options), ): return cls.get(browser, domains, console, ) - console.print("未选择浏览器!") + console.print(_("未选择浏览器!")) @classmethod def get(cls, browser: str | int, domains: list[str], console: Console = None, ) -> str: console = console or Console() if not (browser := cls.__browser_object(browser)): - console.print("浏览器名称或序号输入错误!") + console.print(_("浏览器名称或序号输入错误!")) return "" try: cookies = browser(domains=domains) return "; ".join(f"{i["name"]}={i["value"]}" for i in cookies) except RuntimeError: - console.print("获取 Cookie 失败,未找到 Cookie 数据!") + console.print(_("获取 Cookie 失败,未找到 Cookie 数据!")) return "" @classmethod @@ -91,4 +96,4 @@ match platform: case "win32": pass case _: - print("从浏览器读取 Cookie 功能不支持当前平台!") + print(_("从浏览器读取 Cookie 功能不支持当前平台!")) diff --git a/source/expansion/cleaner.py b/source/expansion/cleaner.py index 83da36f..68bb661 100644 --- a/source/expansion/cleaner.py +++ b/source/expansion/cleaner.py @@ -5,6 +5,11 @@ from warnings import warn from emoji import replace_emoji +try: + from source.translation import _ +except ImportError: + _ = lambda s: s + class Cleaner: CONTROL_CHARACTERS = compile(r"[\x00-\x1F\x7F]") @@ -37,7 +42,7 @@ class Cleaner: "\x00": "", } # Linux 系统 else: - warn("不受支持的操作系统类型,可能无法正常去除非法字符!") + warn(_("不受支持的操作系统类型,可能无法正常去除非法字符!")) rule = {} cache = {i: "" for i in whitespace[1:]} # 补充换行符等非法字符 return rule | cache diff --git a/source/module/__init__.py b/source/module/__init__.py index fbe330f..abf2ba0 100644 --- a/source/module/__init__.py +++ b/source/module/__init__.py @@ -26,8 +26,6 @@ from .static import ( HEADERS, PROJECT, USERAGENT, - SEC_CH_UA, - SEC_CH_UA_PLATFORM, FILE_SIGNATURES, FILE_SIGNATURES_LENGTH, MAX_WORKERS, @@ -37,4 +35,3 @@ from .tools import ( logging, sleep_time, ) -from .translator import Translate diff --git a/source/module/manager.py b/source/module/manager.py index f89b089..98cb779 100644 --- a/source/module/manager.py +++ b/source/module/manager.py @@ -3,7 +3,6 @@ from re import compile from re import sub from shutil import move from shutil import rmtree -from typing import Callable from httpx import AsyncClient from httpx import AsyncHTTPTransport @@ -14,11 +13,10 @@ from httpx import get from source.expansion import remove_empty_directories from .static import HEADERS -# from .static import SEC_CH_UA -# from .static import SEC_CH_UA_PLATFORM from .static import USERAGENT from .static import WARNING from .tools import logging +from ..translation import _ __all__ = ["Manager"] @@ -26,19 +24,19 @@ __all__ = ["Manager"] class Manager: NAME = compile(r"[^\u4e00-\u9fffa-zA-Z0-9-_!?,。;:“”()《》]") NAME_KEYS = ( - '收藏数量', - '评论数量', - '分享数量', - '点赞数量', - '作品标签', - '作品ID', - '作品标题', - '作品描述', - '作品类型', - '发布时间', - '最后更新时间', - '作者昵称', - '作者ID', + "收藏数量", + "评论数量", + "分享数量", + "点赞数量", + "作品标签", + "作品ID", + "作品标题", + "作品描述", + "作品类型", + "发布时间", + "最后更新时间", + "作者昵称", + "作者ID", ) NO_PROXY = { "http://": None, @@ -55,8 +53,6 @@ class Manager: folder: str, name_format: str, chunk: int, - # sec_ch_ua: str, - # sec_ch_ua_platform: str, user_agent: str, cookie: str, proxy: str | dict, @@ -69,19 +65,14 @@ class Manager: live_download: bool, download_record: bool, folder_mode: bool, - # server: bool, - transition: Callable[[str], str], _print: bool, ): self.root = root self.temp = root.joinpath("./temp") self.path = self.__check_path(path) self.folder = self.__check_folder(folder) - self.message = transition self.blank_headers = HEADERS | { 'user-agent': user_agent or USERAGENT, - # 'sec-ch-ua': sec_ch_ua or SEC_CH_UA, - # 'sec-ch-ua-platform': sec_ch_ua_platform or SEC_CH_UA_PLATFORM, } self.headers = self.blank_headers | { 'cookie': cookie, @@ -121,7 +112,6 @@ class Manager: self.image_download = self.check_bool(image_download, True) self.video_download = self.check_bool(video_download, True) self.live_download = self.check_bool(live_download, True) - # self.server = self.check_bool(server, False) def __check_path(self, path: str) -> Path: if not path: @@ -211,11 +201,11 @@ class Manager: } ) response.raise_for_status() - self.proxy_tip = (self.message("代理 {0} 测试成功").format(proxy),) + self.proxy_tip = (_("代理 {0} 测试成功").format(proxy),) return proxy except TimeoutException: self.proxy_tip = ( - self.message("代理 {0} 测试超时").format(proxy), + _("代理 {0} 测试超时").format(proxy), WARNING, ) except ( @@ -223,7 +213,7 @@ class Manager: HTTPStatusError, ) as e: self.proxy_tip = ( - self.message("代理 {0} 测试失败:{1}").format( + _("代理 {0} 测试失败:{1}").format( proxy, e, ), diff --git a/source/module/recorder.py b/source/module/recorder.py index e1234df..75fd4cd 100644 --- a/source/module/recorder.py +++ b/source/module/recorder.py @@ -4,7 +4,7 @@ from re import compile from aiosqlite import connect -from source.module import Manager +from ..module import Manager __all__ = ["IDRecorder", "DataRecorder", ] diff --git a/source/module/settings.py b/source/module/settings.py index 07ce0f6..0258ed9 100644 --- a/source/module/settings.py +++ b/source/module/settings.py @@ -4,8 +4,6 @@ from pathlib import Path from platform import system from .static import ROOT -# from .static import SEC_CH_UA -# from .static import SEC_CH_UA_PLATFORM from .static import USERAGENT __all__ = ['Settings'] @@ -16,8 +14,6 @@ class Settings: "work_path": "", "folder_name": "Download", "name_format": "发布时间 作者昵称 作品标题", - # "sec_ch_ua": SEC_CH_UA, - # "sec_ch_ua_platform": SEC_CH_UA_PLATFORM, "user_agent": USERAGENT, "cookie": "", "proxy": None, @@ -32,7 +28,6 @@ class Settings: "folder_mode": False, "download_record": True, "language": "zh_CN", - # "server": False, } encode = "UTF-8-SIG" if system() == "Windows" else "UTF-8" diff --git a/source/module/static.py b/source/module/static.py index b477cd0..628009c 100644 --- a/source/module/static.py +++ b/source/module/static.py @@ -15,24 +15,11 @@ USERSCRIPT = "https://raw.githubusercontent.com/JoeanAmier/XHS-Downloader/master USERAGENT = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 ' 'Safari/537.36 Edg/128.0.0.0') -SEC_CH_UA = '"Chromium";v="128", "Not;A=Brand";v="24", "Microsoft Edge";v="128"' -SEC_CH_UA_PLATFORM = '"Windows"' HEADERS = { 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - # 'accept-language': 'zh-CN,zh;q=0.9', 'cache-control': 'no-cache', - # 'dnt': '1', 'pragma': 'no-cache', - # 'priority': 'u=0, i', - # 'sec-ch-ua': SEC_CH_UA, - # 'sec-ch-ua-mobile': '?0', - # 'sec-ch-ua-platform': SEC_CH_UA_PLATFORM, - # 'sec-fetch-dest': 'document', - # 'sec-fetch-mode': 'navigate', - # 'sec-fetch-site': 'none', - # 'sec-fetch-user': '?1', - # 'upgrade-insecure-requests': '1', 'user-agent': USERAGENT, } diff --git a/source/module/translator.py b/source/module/translator.py deleted file mode 100644 index cfb52f3..0000000 --- a/source/module/translator.py +++ /dev/null @@ -1,27 +0,0 @@ -from gettext import translation - -from ..module import ROOT - -__all__ = ["Translate"] - - -class Translate: - SUPPORT = { - "zh_CN", - "en_US", - } - - def __init__(self, language: str): - self.language = self.__check_language(language) - self.translate = translation( - "xhs", - localedir=ROOT.joinpath("locale"), - languages=[self.language], - fallback=True, - ) - - def __check_language(self, language: str) -> str: - return language if language in self.SUPPORT else "zh_CN" - - def message(self): - return self.translate.gettext diff --git a/source/translation/__init__.py b/source/translation/__init__.py new file mode 100644 index 0000000..399e0e0 --- /dev/null +++ b/source/translation/__init__.py @@ -0,0 +1 @@ +from .translate import switch_language, _ diff --git a/source/translation/translate.py b/source/translation/translate.py new file mode 100644 index 0000000..cd2a475 --- /dev/null +++ b/source/translation/translate.py @@ -0,0 +1,63 @@ +from gettext import translation +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent.parent + + +class TranslationManager: + """管理gettext翻译的类""" + + _instance = None # 单例实例 + + def __new__(cls, *args, **kwargs): + if not cls._instance: + cls._instance = super(TranslationManager, cls).__new__(cls) + return cls._instance + + def __init__(self, domain="xhs", localedir=None): + self.domain = domain + if not localedir: + localedir = ROOT.joinpath('locale') + self.localedir = Path(localedir) + self.current_translator = self.setup_translation() + + def setup_translation(self, language: str = "zh_CN"): + """设置gettext翻译环境""" + try: + return translation( + self.domain, + localedir=self.localedir, + languages=[language], + fallback=True, + ) + except FileNotFoundError as e: + print(f"Warning: Translation files for '{self.domain}' not found. Error: {e}") + return translation(self.domain, fallback=True) + + def switch_language(self, language: str = "en_US"): + """切换当前使用的语言""" + self.current_translator = self.setup_translation(language) + + def gettext(self, message): + """提供gettext方法""" + return self.current_translator.gettext(message) + + +# 初始化TranslationManager单例实例 +translation_manager = TranslationManager() + + +def _translate(message): + """辅助函数来简化翻译调用""" + return translation_manager.gettext(message) + + +def switch_language(language: str = "en_US"): + """切换语言并刷新翻译函数""" + global _ + translation_manager.switch_language(language) + _ = translation_manager.gettext + + +# 设置默认翻译函数 +_ = _translate diff --git a/static/Release_Notes.md b/static/Release_Notes.md index 8aa7092..93e0c17 100644 --- a/static/Release_Notes.md +++ b/static/Release_Notes.md @@ -1,8 +1,10 @@ **项目更新内容:** -1. 优化文件名称非法字符处理 -2. 支持 API 模式传入 Cookie -3. 适配新版本 HTTPX 库 -4. 更正英语语言代码 -5. 优化文件下载功能 -6. 移除内置延时机制 +1. 修复命令行模式语言不生效的问题 +2. 优化文件名称非法字符处理 +3. 支持 API 模式传入 Cookie +4. 适配新版本 HTTPX 库 +5. 重构项目翻译模块 +6. 更正英语语言代码 +7. 优化文件下载功能 +8. 移除内置延时机制