support scheduler progress i18n (#6069)

This commit is contained in:
jxxghp
2026-07-07 06:04:45 +08:00
committed by GitHub
parent 827ed8330c
commit 8245124e82
4 changed files with 246 additions and 1 deletions

View File

@@ -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]]:

View File

@@ -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"

View File

@@ -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} 不存在"

View File

@@ -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 = []