Skip to content

Commit b409264

Browse files
authored
Merge pull request #514 from QuantStrategyLab/feat/operational-alert-i18n
feat: localize operational alert notifications
2 parents e689fc4 + d52d6ab commit b409264

3 files changed

Lines changed: 243 additions & 0 deletions

File tree

src/quant_platform_kit/common/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313
resolve_strategy_display_name,
1414
translator_uses_zh,
1515
)
16+
from .operational_notification_localization import (
17+
format_operational_alert,
18+
format_operational_heartbeat_status,
19+
localize_operational_activity,
20+
operational_notification_text,
21+
resolve_operational_notification_locale,
22+
)
1623
from .artifacts import (
1724
RELEASE_MANIFEST_TYPE,
1825
FEATURE_SNAPSHOT_MANIFEST_TYPE,
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"""Locale-aware rendering helpers for human-facing operational alerts.
2+
3+
Runtime and workflow logs deliberately remain structured and machine-friendly.
4+
This module is only for the short messages delivered to an operator through
5+
Telegram, email, or another notification channel.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from collections.abc import Mapping, Sequence
11+
12+
13+
OperationalLocale = str
14+
15+
16+
_TEXTS: dict[str, dict[str, str]] = {
17+
"zh": {
18+
"runtime_guard_title": "[运行守卫] {name}",
19+
"execution_report_heartbeat_title": "[执行回执心跳] {name}",
20+
"runtime_workflow_heartbeat_title": "[运行工作流心跳] {name}",
21+
"project": "项目:{value}",
22+
"lookback_minutes": "检查范围:过去 {value} 分钟",
23+
"lookback_hours": "检查范围:过去 {value} 小时",
24+
"issues": "问题:",
25+
"technical_details": "技术详情(原文):",
26+
"recent_reports": "最近检查的回执(原文):",
27+
"latest_runtime_run": "最近一次运行:",
28+
"workflow": "工作流:{value}",
29+
"status_normal": "状态:正常",
30+
"runtime_guard_service_configuration_error": "服务配置读取失败",
31+
"runtime_guard_cloud_run_log_query_failed": "Cloud Run 日志查询失败:{service}",
32+
"runtime_guard_cloud_run_failure_logs": "{count} 条 Cloud Run 失败日志:{service}",
33+
"runtime_guard_no_successful_request": "过去 {lookback_minutes} 分钟内未发现成功请求:{service}",
34+
"runtime_guard_scheduler_failure_logs": "发现 {count} 条 Cloud Scheduler 失败日志",
35+
"runtime_guard_scheduler_log_query_failed": "Cloud Scheduler 日志查询失败",
36+
"heartbeat_scheduler_policy_error": "运行目标调度策略读取失败",
37+
"heartbeat_list_failed": "执行回执列表读取失败",
38+
"heartbeat_no_recent_report": "过去 {lookback_hours} 小时内没有新的执行回执",
39+
"heartbeat_missing_acceptable_report": "缺少可接受的执行回执:{targets}",
40+
"heartbeat_no_acceptable_report": "最近 {count} 份执行回执均不符合要求",
41+
"heartbeat_runtime_target_disabled": "运行目标已停用;未提交订单。",
42+
"heartbeat_no_scheduled_window_due": "当前没有应执行的交易窗口;未提交订单。",
43+
"heartbeat_no_scheduler_run_due": "当前没有应执行的调度任务;未提交订单。",
44+
"heartbeat_accepted_report": "已收到合格执行回执:{detail}",
45+
"activity_no_trade": "无交易",
46+
"activity_rebalance_recorded": "已记录调仓操作",
47+
"workflow_heartbeat_latest_failed": "最近一次运行未成功完成(结论:{conclusion})",
48+
"workflow_heartbeat_no_success": "GitHub Actions 查询未返回成功的运行记录",
49+
"workflow_heartbeat_missing_dispatches": "已连续 {count} 个预期周期未发现运行(阈值:{threshold})",
50+
},
51+
"en": {
52+
"runtime_guard_title": "[Runtime Guard] {name}",
53+
"execution_report_heartbeat_title": "[Execution Report Heartbeat] {name}",
54+
"runtime_workflow_heartbeat_title": "[Runtime Workflow Heartbeat] {name}",
55+
"project": "Project: {value}",
56+
"lookback_minutes": "Lookback: {value} minutes",
57+
"lookback_hours": "Lookback: {value} hours",
58+
"issues": "Issues:",
59+
"technical_details": "Technical details (original):",
60+
"recent_reports": "Recent reports (original):",
61+
"latest_runtime_run": "Latest runtime run:",
62+
"workflow": "Workflow: {value}",
63+
"status_normal": "Status: normal",
64+
"runtime_guard_service_configuration_error": "Service configuration could not be read",
65+
"runtime_guard_cloud_run_log_query_failed": "Cloud Run log query failed: {service}",
66+
"runtime_guard_cloud_run_failure_logs": "{count} Cloud Run failure log(s): {service}",
67+
"runtime_guard_no_successful_request": "No successful request for {service} in the last {lookback_minutes} minutes",
68+
"runtime_guard_scheduler_failure_logs": "{count} Cloud Scheduler failure log(s) found",
69+
"runtime_guard_scheduler_log_query_failed": "Cloud Scheduler log query failed",
70+
"heartbeat_scheduler_policy_error": "Runtime-target scheduler policy could not be read",
71+
"heartbeat_list_failed": "Execution-report listing failed",
72+
"heartbeat_no_recent_report": "No new execution report in the last {lookback_hours} hours",
73+
"heartbeat_missing_acceptable_report": "Missing acceptable execution report: {targets}",
74+
"heartbeat_no_acceptable_report": "None of the most recent {count} execution reports was acceptable",
75+
"heartbeat_runtime_target_disabled": "Runtime target is disabled; no order was submitted.",
76+
"heartbeat_no_scheduled_window_due": "No scheduled trading window was due; no order was submitted.",
77+
"heartbeat_no_scheduler_run_due": "No scheduler-backed run was due; no order was submitted.",
78+
"heartbeat_accepted_report": "An acceptable execution report was received: {detail}",
79+
"activity_no_trade": "no trade",
80+
"activity_rebalance_recorded": "rebalance action recorded",
81+
"workflow_heartbeat_latest_failed": "The latest runtime run did not complete successfully (conclusion: {conclusion})",
82+
"workflow_heartbeat_no_success": "The GitHub Actions query returned no successful runtime run",
83+
"workflow_heartbeat_missing_dispatches": "No runtime dispatch was found for {count} expected interval(s) (threshold: {threshold})",
84+
},
85+
}
86+
87+
88+
def resolve_operational_notification_locale(value: object | None) -> OperationalLocale:
89+
"""Normalize the operator's configured notification locale.
90+
91+
Chinese locale variants intentionally share the concise ``zh`` templates;
92+
unknown values fall back to English to preserve the previous behavior.
93+
"""
94+
95+
return "zh" if str(value or "").strip().lower().replace("_", "-").startswith("zh") else "en"
96+
97+
98+
def operational_notification_text(
99+
locale: object | None,
100+
key: str,
101+
/,
102+
**values: object,
103+
) -> str:
104+
"""Render one stable operational-message key in the requested locale."""
105+
106+
normalized = resolve_operational_notification_locale(locale)
107+
template = _TEXTS[normalized].get(key) or _TEXTS["en"].get(key) or key
108+
return template.format(**values)
109+
110+
111+
def localize_operational_activity(locale: object | None, detail: object) -> str:
112+
"""Translate known activity labels while keeping diagnostic values intact."""
113+
114+
value = str(detail or "").strip()
115+
known = {
116+
"no trade": "activity_no_trade",
117+
"rebalance action recorded": "activity_rebalance_recorded",
118+
}
119+
key = known.get(value.lower())
120+
return operational_notification_text(locale, key) if key else value
121+
122+
123+
def format_operational_alert(
124+
*,
125+
locale: object | None,
126+
alert_type: str,
127+
name: object,
128+
context: Mapping[str, object] | None = None,
129+
issues: Sequence[str] = (),
130+
recent_reports: Sequence[str] = (),
131+
technical_details: Sequence[str] = (),
132+
latest_runtime_run: Sequence[str] = (),
133+
workflow_url: object | None = None,
134+
) -> str:
135+
"""Build a concise localized operator notification.
136+
137+
``recent_reports`` and ``technical_details`` are expressly labelled as
138+
original diagnostic text. They may contain broker or cloud-provider
139+
wording and therefore must not be presented as localized summaries.
140+
"""
141+
142+
title_key = f"{str(alert_type).strip()}_title"
143+
lines = [operational_notification_text(locale, title_key, name=str(name or "runtime"))]
144+
for key, value in (context or {}).items():
145+
lines.append(operational_notification_text(locale, str(key), value=value))
146+
if issues:
147+
lines.extend([operational_notification_text(locale, "issues"), *(f"- {item}" for item in issues)])
148+
if recent_reports:
149+
lines.extend(
150+
[
151+
operational_notification_text(locale, "recent_reports"),
152+
*(str(item) for item in recent_reports),
153+
]
154+
)
155+
if latest_runtime_run:
156+
lines.extend(
157+
[
158+
operational_notification_text(locale, "latest_runtime_run"),
159+
*(str(item) for item in latest_runtime_run),
160+
]
161+
)
162+
if technical_details:
163+
lines.extend(
164+
[
165+
operational_notification_text(locale, "technical_details"),
166+
*(str(item) for item in technical_details),
167+
]
168+
)
169+
if workflow_url:
170+
lines.append(operational_notification_text(locale, "workflow", value=str(workflow_url)))
171+
return "\n".join(line for line in lines if line)
172+
173+
174+
def format_operational_heartbeat_status(
175+
*,
176+
locale: object | None,
177+
name: object,
178+
detail: object,
179+
) -> str:
180+
"""Render the opt-in normal execution-heartbeat summary."""
181+
182+
return "\n".join(
183+
(
184+
operational_notification_text(
185+
locale,
186+
"execution_report_heartbeat_title",
187+
name=str(name or "runtime"),
188+
),
189+
operational_notification_text(locale, "status_normal"),
190+
str(detail or ""),
191+
)
192+
)

tests/test_notification_localization.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
merge_strategy_plugin_i18n,
1111
translator_uses_zh,
1212
)
13+
from quant_platform_kit.common.operational_notification_localization import (
14+
format_operational_alert,
15+
format_operational_heartbeat_status,
16+
localize_operational_activity,
17+
operational_notification_text,
18+
resolve_operational_notification_locale,
19+
)
1320

1421

1522
def _translator_factory(no_trades_text: str):
@@ -109,6 +116,43 @@ def test_merge_strategy_plugin_i18n_can_prefer_shared_keys(self):
109116
self.assertEqual(merged["zh"]["strategy_plugin_name_taco_rebound_shadow"], "TACO 反弹观察通知")
110117
self.assertEqual(merged["en"]["strategy_plugin_route_watch"], "watch")
111118

119+
def test_operational_alert_renderer_keeps_technical_detail_separate_from_zh_summary(self):
120+
message = format_operational_alert(
121+
locale="zh-CN",
122+
alert_type="runtime_guard",
123+
name="LongBridge SG",
124+
context={"project": "longbridgequant", "lookback_minutes": 180},
125+
issues=[
126+
operational_notification_text(
127+
"zh",
128+
"runtime_guard_cloud_run_log_query_failed",
129+
service="longbridge-quant-sg-service",
130+
)
131+
],
132+
technical_details=["HttpError: INTERNAL"],
133+
workflow_url="https://example.test/run/1",
134+
)
135+
136+
self.assertIn("[运行守卫] LongBridge SG", message)
137+
self.assertIn("问题:", message)
138+
self.assertIn("Cloud Run 日志查询失败", message)
139+
self.assertIn("技术详情(原文):", message)
140+
self.assertIn("HttpError: INTERNAL", message)
141+
self.assertIn("工作流:https://example.test/run/1", message)
142+
143+
def test_operational_locale_and_normal_heartbeat_are_bilingual(self):
144+
self.assertEqual(resolve_operational_notification_locale("zh_TW"), "zh")
145+
self.assertEqual(resolve_operational_notification_locale("fr"), "en")
146+
self.assertEqual(localize_operational_activity("zh", "no trade"), "无交易")
147+
self.assertEqual(
148+
format_operational_heartbeat_status(
149+
locale="zh",
150+
name="LongBridge SG",
151+
detail=operational_notification_text("zh", "heartbeat_runtime_target_disabled"),
152+
),
153+
"[执行回执心跳] LongBridge SG\n状态:正常\n运行目标已停用;未提交订单。",
154+
)
155+
112156

113157
if __name__ == "__main__":
114158
unittest.main()

0 commit comments

Comments
 (0)