mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-07-09 21:37:47 +08:00
support scheduler progress i18n (#6069)
This commit is contained in:
@@ -218,9 +218,31 @@ class LocaleHelper:
|
||||
for pattern, target in cls._load_pattern_matchers(locale):
|
||||
matched = pattern.fullmatch(text)
|
||||
if matched:
|
||||
return cls._format(target, matched.groupdict())
|
||||
return cls._format(
|
||||
target,
|
||||
cls._build_pattern_values(locale, matched.groupdict()),
|
||||
)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _build_pattern_values(cls, locale: str, values: dict[str, str]) -> dict[str, str]:
|
||||
"""
|
||||
为动态模板补充可选的占位值翻译。
|
||||
"""
|
||||
pattern_values = dict(values)
|
||||
catalog = cls._load_catalog(locale)
|
||||
default_catalog = (
|
||||
cls._load_catalog(cls.DEFAULT_LOCALE)
|
||||
if locale != cls.DEFAULT_LOCALE
|
||||
else catalog
|
||||
)
|
||||
for name, value in values.items():
|
||||
translated = cls._lookup_message(catalog, value)
|
||||
if translated is None and locale != cls.DEFAULT_LOCALE:
|
||||
translated = cls._lookup_message(default_catalog, value)
|
||||
pattern_values[f"{name}_i18n"] = translated or value
|
||||
return pattern_values
|
||||
|
||||
@staticmethod
|
||||
@lru_cache(maxsize=16)
|
||||
def _load_pattern_matchers(locale: str) -> list[tuple[re.Pattern[str], str]]:
|
||||
|
||||
@@ -338,6 +338,9 @@
|
||||
"工作流": "Workflow",
|
||||
"正在运行": "Running",
|
||||
"等待": "Waiting",
|
||||
"执行完成": "completed",
|
||||
"执行中": "running",
|
||||
"执行失败": "failed",
|
||||
"同步CookieCloud站点": "Sync CookieCloud Sites",
|
||||
"同步媒体服务器": "Sync Media Servers",
|
||||
"订阅元数据更新": "Update Subscription Metadata",
|
||||
@@ -1087,18 +1090,46 @@
|
||||
"source": "数据表 {name} 清理处理完成",
|
||||
"target": "Data table {name} cleanup completed"
|
||||
},
|
||||
{
|
||||
"source": "{name} 开始执行 ...",
|
||||
"target": "Starting {name_i18n} ..."
|
||||
},
|
||||
{
|
||||
"source": "开始刷新站点资源,共 {count} 个站点 ...",
|
||||
"target": "Starting site resource refresh, {count} sites ..."
|
||||
},
|
||||
{
|
||||
"source": "正在刷新站点资源({index}/{total}){name} ...",
|
||||
"target": "Refreshing site resources ({index}/{total}) {name} ..."
|
||||
},
|
||||
{
|
||||
"source": "开始同步媒体服务器,共 {count} 个 ...",
|
||||
"target": "Starting media server sync, {count} servers ..."
|
||||
},
|
||||
{
|
||||
"source": "正在同步媒体服务器({index}/{total}){name} ...",
|
||||
"target": "Syncing media server ({index}/{total}) {name} ..."
|
||||
},
|
||||
{
|
||||
"source": "媒体服务器 {name} 无可同步媒体库",
|
||||
"target": "Media server {name} has no libraries to sync"
|
||||
},
|
||||
{
|
||||
"source": "正在同步 {server} 媒体库 {library}({finished}/{total})",
|
||||
"target": "Syncing {server} library {library} ({finished}/{total})"
|
||||
},
|
||||
{
|
||||
"source": "正在同步 {server} 媒体库 {library}",
|
||||
"target": "Syncing {server} library {library}"
|
||||
},
|
||||
{
|
||||
"source": "{server} 媒体库({index}/{total}){library} 同步完成",
|
||||
"target": "{server} library ({index}/{total}) {library} sync completed"
|
||||
},
|
||||
{
|
||||
"source": "媒体服务器({index}/{total}){name} 同步完成",
|
||||
"target": "Media server ({index}/{total}) {name} sync completed"
|
||||
},
|
||||
{
|
||||
"source": "开始刷新站点数据,共 {count} 个站点 ...",
|
||||
"target": "Starting site data refresh, {count} sites ..."
|
||||
@@ -1131,6 +1162,10 @@
|
||||
"source": "开始刷新推荐缓存,共 {count} 个数据分页 ...",
|
||||
"target": "Starting recommendation cache refresh, {count} data pages ..."
|
||||
},
|
||||
{
|
||||
"source": "正在刷新推荐缓存({finished}/{total})...",
|
||||
"target": "Refreshing recommendation cache ({finished}/{total}) ..."
|
||||
},
|
||||
{
|
||||
"source": "正在缓存推荐海报({index}/{total})...",
|
||||
"target": "Caching recommendation posters ({index}/{total}) ..."
|
||||
@@ -1227,6 +1262,10 @@
|
||||
"source": "资源预处理完成,开始匹配 {count} 个订阅 ...",
|
||||
"target": "Resource preprocessing completed, matching {count} subscriptions ..."
|
||||
},
|
||||
{
|
||||
"source": "正在匹配订阅({index}/{total}){name} ...",
|
||||
"target": "Matching subscription ({index}/{total}) {name} ..."
|
||||
},
|
||||
{
|
||||
"source": "开始更新订阅元数据,共 {count} 个订阅 ...",
|
||||
"target": "Starting subscription metadata update, {count} subscriptions ..."
|
||||
@@ -1263,6 +1302,26 @@
|
||||
"source": "订阅日历({index}/{total})预缓存完成",
|
||||
"target": "Subscription calendar ({index}/{total}) precache completed"
|
||||
},
|
||||
{
|
||||
"source": "开始执行工作流 {name} ...",
|
||||
"target": "Starting workflow {name} ..."
|
||||
},
|
||||
{
|
||||
"source": "工作流动作({finished}/{total}){name} {status}",
|
||||
"target": "Workflow action ({finished}/{total}) {name} {status_i18n}"
|
||||
},
|
||||
{
|
||||
"source": "工作流 {name} 执行完成",
|
||||
"target": "Workflow {name} completed"
|
||||
},
|
||||
{
|
||||
"source": "{name} 执行完成",
|
||||
"target": "{name_i18n} completed"
|
||||
},
|
||||
{
|
||||
"source": "{name} 执行失败",
|
||||
"target": "{name_i18n} failed"
|
||||
},
|
||||
{
|
||||
"source": "{path} 不存在",
|
||||
"target": "{path} does not exist"
|
||||
|
||||
@@ -338,6 +338,9 @@
|
||||
"工作流": "工作流",
|
||||
"正在运行": "正在執行",
|
||||
"等待": "等待",
|
||||
"执行完成": "執行完成",
|
||||
"执行中": "執行中",
|
||||
"执行失败": "執行失敗",
|
||||
"同步CookieCloud站点": "同步 CookieCloud 站點",
|
||||
"同步媒体服务器": "同步媒體伺服器",
|
||||
"订阅元数据更新": "訂閱元資料更新",
|
||||
@@ -1087,18 +1090,46 @@
|
||||
"source": "数据表 {name} 清理处理完成",
|
||||
"target": "資料表 {name} 清理處理完成"
|
||||
},
|
||||
{
|
||||
"source": "{name} 开始执行 ...",
|
||||
"target": "{name_i18n} 開始執行 ..."
|
||||
},
|
||||
{
|
||||
"source": "开始刷新站点资源,共 {count} 个站点 ...",
|
||||
"target": "開始重新整理站點資源,共 {count} 個站點 ..."
|
||||
},
|
||||
{
|
||||
"source": "正在刷新站点资源({index}/{total}){name} ...",
|
||||
"target": "正在重新整理站點資源({index}/{total}){name} ..."
|
||||
},
|
||||
{
|
||||
"source": "开始同步媒体服务器,共 {count} 个 ...",
|
||||
"target": "開始同步媒體伺服器,共 {count} 個 ..."
|
||||
},
|
||||
{
|
||||
"source": "正在同步媒体服务器({index}/{total}){name} ...",
|
||||
"target": "正在同步媒體伺服器({index}/{total}){name} ..."
|
||||
},
|
||||
{
|
||||
"source": "媒体服务器 {name} 无可同步媒体库",
|
||||
"target": "媒體伺服器 {name} 無可同步媒體庫"
|
||||
},
|
||||
{
|
||||
"source": "正在同步 {server} 媒体库 {library}({finished}/{total})",
|
||||
"target": "正在同步 {server} 媒體庫 {library}({finished}/{total})"
|
||||
},
|
||||
{
|
||||
"source": "正在同步 {server} 媒体库 {library}",
|
||||
"target": "正在同步 {server} 媒體庫 {library}"
|
||||
},
|
||||
{
|
||||
"source": "{server} 媒体库({index}/{total}){library} 同步完成",
|
||||
"target": "{server} 媒體庫({index}/{total}){library} 同步完成"
|
||||
},
|
||||
{
|
||||
"source": "媒体服务器({index}/{total}){name} 同步完成",
|
||||
"target": "媒體伺服器({index}/{total}){name} 同步完成"
|
||||
},
|
||||
{
|
||||
"source": "开始刷新站点数据,共 {count} 个站点 ...",
|
||||
"target": "開始重新整理站點資料,共 {count} 個站點 ..."
|
||||
@@ -1131,6 +1162,10 @@
|
||||
"source": "开始刷新推荐缓存,共 {count} 个数据分页 ...",
|
||||
"target": "開始重新整理推薦快取,共 {count} 個資料分頁 ..."
|
||||
},
|
||||
{
|
||||
"source": "正在刷新推荐缓存({finished}/{total})...",
|
||||
"target": "正在重新整理推薦快取({finished}/{total})..."
|
||||
},
|
||||
{
|
||||
"source": "正在缓存推荐海报({index}/{total})...",
|
||||
"target": "正在快取推薦海報({index}/{total})..."
|
||||
@@ -1227,6 +1262,10 @@
|
||||
"source": "资源预处理完成,开始匹配 {count} 个订阅 ...",
|
||||
"target": "資源預處理完成,開始匹配 {count} 個訂閱 ..."
|
||||
},
|
||||
{
|
||||
"source": "正在匹配订阅({index}/{total}){name} ...",
|
||||
"target": "正在匹配訂閱({index}/{total}){name} ..."
|
||||
},
|
||||
{
|
||||
"source": "开始更新订阅元数据,共 {count} 个订阅 ...",
|
||||
"target": "開始更新訂閱元資料,共 {count} 個訂閱 ..."
|
||||
@@ -1263,6 +1302,26 @@
|
||||
"source": "订阅日历({index}/{total})预缓存完成",
|
||||
"target": "訂閱日曆({index}/{total})預快取完成"
|
||||
},
|
||||
{
|
||||
"source": "开始执行工作流 {name} ...",
|
||||
"target": "開始執行工作流 {name} ..."
|
||||
},
|
||||
{
|
||||
"source": "工作流动作({finished}/{total}){name} {status}",
|
||||
"target": "工作流動作({finished}/{total}){name} {status_i18n}"
|
||||
},
|
||||
{
|
||||
"source": "工作流 {name} 执行完成",
|
||||
"target": "工作流 {name} 執行完成"
|
||||
},
|
||||
{
|
||||
"source": "{name} 执行完成",
|
||||
"target": "{name_i18n} 執行完成"
|
||||
},
|
||||
{
|
||||
"source": "{name} 执行失败",
|
||||
"target": "{name_i18n} 執行失敗"
|
||||
},
|
||||
{
|
||||
"source": "{path} 不存在",
|
||||
"target": "{path} 不存在"
|
||||
|
||||
@@ -53,6 +53,62 @@ def _sample_message_expression(value: ast.AST) -> list[str]:
|
||||
return []
|
||||
|
||||
|
||||
def _sample_progress_text_expression(value: ast.AST) -> list[str]:
|
||||
"""从进度 text 表达式中生成用于翻译校验的样本文本。"""
|
||||
if isinstance(value, ast.Constant) and isinstance(value.value, str):
|
||||
return [value.value]
|
||||
if isinstance(value, ast.JoinedStr):
|
||||
samples = [""]
|
||||
placeholder_index = 0
|
||||
for item in value.values:
|
||||
if isinstance(item, ast.Constant) and isinstance(item.value, str):
|
||||
choices = [item.value]
|
||||
elif isinstance(item, ast.FormattedValue):
|
||||
choices = [
|
||||
sample
|
||||
for sample in _sample_progress_text_expression(item.value)
|
||||
if _has_chinese(sample)
|
||||
]
|
||||
if not choices:
|
||||
placeholder_index += 1
|
||||
choices = [f"样例{placeholder_index}"]
|
||||
else:
|
||||
placeholder_index += 1
|
||||
choices = [f"样例{placeholder_index}"]
|
||||
samples = [prefix + choice for prefix in samples for choice in choices]
|
||||
return [sample for sample in samples if _has_chinese(sample)]
|
||||
if isinstance(value, ast.IfExp):
|
||||
return (
|
||||
_sample_progress_text_expression(value.body)
|
||||
+ _sample_progress_text_expression(value.orelse)
|
||||
)
|
||||
if isinstance(value, ast.BoolOp):
|
||||
samples = []
|
||||
for item in value.values:
|
||||
samples.extend(_sample_progress_text_expression(item))
|
||||
return samples
|
||||
if isinstance(value, ast.BinOp) and isinstance(value.op, ast.Add):
|
||||
left_samples = _sample_progress_text_expression(value.left)
|
||||
right_samples = _sample_progress_text_expression(value.right)
|
||||
if left_samples and right_samples:
|
||||
return [left + right for left in left_samples for right in right_samples]
|
||||
if left_samples:
|
||||
return [f"{left}样例" for left in left_samples]
|
||||
if right_samples:
|
||||
return [f"样例{right}" for right in right_samples]
|
||||
return []
|
||||
|
||||
|
||||
def _is_progress_text_call(node: ast.Call) -> bool:
|
||||
"""判断调用是否可能写入前端展示的进度文本。"""
|
||||
func = ast.unparse(node.func)
|
||||
return (
|
||||
func.endswith("progress_callback")
|
||||
or func.endswith(".update")
|
||||
or func.endswith(".end")
|
||||
)
|
||||
|
||||
|
||||
def test_locale_helper_normalizes_supported_aliases():
|
||||
"""语言别名应规范化为项目支持的语言标识。"""
|
||||
assert LocaleHelper.normalize_locale("en") == "en-US"
|
||||
@@ -262,6 +318,55 @@ def test_schedule_info_auto_fills_i18n_display_fields():
|
||||
assert schedule.progress_detail.error_i18n == "Background service does not exist"
|
||||
|
||||
|
||||
def test_scheduler_progress_patterns_translate_dynamic_texts():
|
||||
"""定时任务进度动态模板应翻译固定词并保留业务变量。"""
|
||||
assert (
|
||||
LocaleHelper.translate_text("同步媒体服务器 开始执行 ...", locale="en-US")
|
||||
== "Starting Sync Media Servers ..."
|
||||
)
|
||||
assert (
|
||||
LocaleHelper.translate_text("同步媒体服务器 执行完成", locale="en-US")
|
||||
== "Sync Media Servers completed"
|
||||
)
|
||||
assert (
|
||||
LocaleHelper.translate_text("同步媒体服务器 执行失败", locale="zh-TW")
|
||||
== "同步媒體伺服器 執行失敗"
|
||||
)
|
||||
assert (
|
||||
LocaleHelper.translate_text("工作流动作(1/3)动作A 执行完成", locale="en-US")
|
||||
== "Workflow action (1/3) 动作A completed"
|
||||
)
|
||||
|
||||
|
||||
def test_scheduler_progress_texts_have_english_translations():
|
||||
"""定时任务相关进度文案应有英文翻译,避免前端切英文后回退中文。"""
|
||||
untranslated = []
|
||||
progress_paths = [
|
||||
Path("app/scheduler.py"),
|
||||
Path("app/chain/torrents.py"),
|
||||
Path("app/chain/mediaserver.py"),
|
||||
Path("app/chain/site.py"),
|
||||
Path("app/chain/recommend.py"),
|
||||
Path("app/chain/transfer.py"),
|
||||
Path("app/chain/subscribe.py"),
|
||||
Path("app/chain/workflow.py"),
|
||||
]
|
||||
for path in progress_paths:
|
||||
tree = ast.parse(path.read_text())
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call) or not _is_progress_text_call(node):
|
||||
continue
|
||||
for keyword in node.keywords:
|
||||
if keyword.arg != "text":
|
||||
continue
|
||||
for message in _sample_progress_text_expression(keyword.value):
|
||||
translated = LocaleHelper.translate_text(message, locale="en-US")
|
||||
if translated == message:
|
||||
untranslated.append(f"{path}:{keyword.value.lineno}:{message}")
|
||||
|
||||
assert untranslated == []
|
||||
|
||||
|
||||
def test_api_endpoint_literal_messages_have_english_translations():
|
||||
"""接口直接返回的中文 message 应有英文翻译,避免前端切英文后回退中文。"""
|
||||
untranslated = []
|
||||
|
||||
Reference in New Issue
Block a user