mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-07 05:56:40 +00:00
fix: preserve notification icons across locales
This commit is contained in:
@@ -2111,6 +2111,7 @@ FIELD_EMOJI = {
|
|||||||
'hostname': '\U0001F4BB', # laptop
|
'hostname': '\U0001F4BB', # laptop
|
||||||
'vmid': '\U0001F194', # ID button
|
'vmid': '\U0001F194', # ID button
|
||||||
'vmname': '\U0001F3F7\uFE0F', # label
|
'vmname': '\U0001F3F7\uFE0F', # label
|
||||||
|
'ct_name': '\U0001F4E6', # package / container
|
||||||
'device': '\U0001F4BD', # disk
|
'device': '\U0001F4BD', # disk
|
||||||
'mount': '\U0001F4C2', # open folder
|
'mount': '\U0001F4C2', # open folder
|
||||||
'source_ip': '\U0001F310', # globe
|
'source_ip': '\U0001F310', # globe
|
||||||
@@ -2141,6 +2142,82 @@ FIELD_EMOJI = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_TEMPLATE_FIELD_RE = re.compile(r'\{([a-zA-Z_][a-zA-Z0-9_]*)[^}]*\}')
|
||||||
|
|
||||||
|
|
||||||
|
def _localized_template_labels(event_type: str, language: str) -> Dict[str, List[str]]:
|
||||||
|
"""Return the visible labels paired with template fields in one locale.
|
||||||
|
|
||||||
|
The old emoji pass compared rendered English words such as ``Duration``
|
||||||
|
and ``Total updates``. That necessarily stops matching after a template
|
||||||
|
is translated. The template itself still knows which field each label
|
||||||
|
describes, so derive the visible wording from that localized template.
|
||||||
|
"""
|
||||||
|
requested = (language or 'en').split('-', 1)[0].lower()
|
||||||
|
key = f'templates.{event_type}.body'
|
||||||
|
# Use the raw catalog entry rather than runtime_message(): the latter
|
||||||
|
# deliberately formats unknown placeholders away, while this helper needs
|
||||||
|
# to inspect those placeholders to associate each label with its field.
|
||||||
|
body = (
|
||||||
|
_catalog_value(_load_runtime_catalog(requested), key)
|
||||||
|
or _catalog_value(_load_runtime_catalog('en'), key)
|
||||||
|
)
|
||||||
|
if not body:
|
||||||
|
body = TEMPLATES.get(event_type, {}).get('body', '')
|
||||||
|
|
||||||
|
labels: Dict[str, List[str]] = {}
|
||||||
|
lines = body.splitlines()
|
||||||
|
for index, line in enumerate(lines):
|
||||||
|
matches = list(_TEMPLATE_FIELD_RE.finditer(line))
|
||||||
|
for match in matches:
|
||||||
|
label = line[:match.start()].strip().rstrip(':').strip()
|
||||||
|
if label:
|
||||||
|
labels.setdefault(match.group(1), []).append(label)
|
||||||
|
|
||||||
|
# A heading on its own line (for example "Important packages:")
|
||||||
|
# labels the variable rendered on the following line.
|
||||||
|
if line.strip().endswith(':') and index + 1 < len(lines):
|
||||||
|
next_matches = list(_TEMPLATE_FIELD_RE.finditer(lines[index + 1]))
|
||||||
|
if len(next_matches) == 1:
|
||||||
|
label = line.strip().rstrip(':').strip()
|
||||||
|
if label:
|
||||||
|
labels.setdefault(next_matches[0].group(1), []).append(label)
|
||||||
|
return labels
|
||||||
|
|
||||||
|
|
||||||
|
def _lxc_update_label_icons(language: str) -> Dict[str, str]:
|
||||||
|
"""Return localized LXC-update detail prefixes with stable icons."""
|
||||||
|
values = {
|
||||||
|
'before': '0', 'after': '0', 'items': 'item', 'targets': 'target',
|
||||||
|
'reason': 'reason', 'value': 'value', 'error': 'error', 'duration': '0s',
|
||||||
|
}
|
||||||
|
definitions = (
|
||||||
|
('lxcUpdate.sourceLabel', '🧭'),
|
||||||
|
('lxcUpdate.targets', '🎯'),
|
||||||
|
('lxcUpdate.osPending', '📦'),
|
||||||
|
('lxcUpdate.osUnverified', '📦'),
|
||||||
|
('lxcUpdate.applications', '🧩'),
|
||||||
|
('lxcUpdate.applicationsUnverified', '🧩'),
|
||||||
|
('lxcUpdate.dockerEngineChange', '🐳'),
|
||||||
|
('lxcUpdate.dockerEngineVerified', '🐳'),
|
||||||
|
('lxcUpdate.dockerEngineUnverified', '🐳'),
|
||||||
|
('lxcUpdate.dockerImagesPending', '🐳'),
|
||||||
|
('lxcUpdate.dockerImagesChanged', '🐳'),
|
||||||
|
('lxcUpdate.deferredTargets', '⏳'),
|
||||||
|
('lxcUpdate.reason', '📝'),
|
||||||
|
('lxcUpdate.restartRequired', '🔄'),
|
||||||
|
('lxcUpdate.verificationPending', '⏳'),
|
||||||
|
('lxcUpdate.verificationWarning', '⚠️'),
|
||||||
|
('lxcUpdate.duration', '⏱️'),
|
||||||
|
)
|
||||||
|
result = {}
|
||||||
|
for key, icon in definitions:
|
||||||
|
rendered = runtime_message(key, language, **values).strip()
|
||||||
|
if rendered:
|
||||||
|
result[rendered.split(':', 1)[0].strip()] = icon
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def enrich_with_emojis(event_type: str, title: str, body: str,
|
def enrich_with_emojis(event_type: str, title: str, body: str,
|
||||||
data: Dict[str, Any]) -> tuple:
|
data: Dict[str, Any]) -> tuple:
|
||||||
"""Replace the plain title/body with emoji-enriched versions.
|
"""Replace the plain title/body with emoji-enriched versions.
|
||||||
@@ -2201,6 +2278,13 @@ def enrich_with_emojis(event_type: str, title: str, body: str,
|
|||||||
preprocessed = re.sub(r'^\n+', '', preprocessed)
|
preprocessed = re.sub(r'^\n+', '', preprocessed)
|
||||||
preprocessed = preprocessed.strip()
|
preprocessed = preprocessed.strip()
|
||||||
|
|
||||||
|
language = str(data.get('_notification_language') or 'en')
|
||||||
|
localized_labels = _localized_template_labels(event_type, language)
|
||||||
|
lxc_update_labels = (
|
||||||
|
_lxc_update_label_icons(language)
|
||||||
|
if event_type == 'lxc_update_applied' else {}
|
||||||
|
)
|
||||||
|
|
||||||
# ── Extended emoji mappings for health/disk messages ──
|
# ── Extended emoji mappings for health/disk messages ──
|
||||||
HEALTH_EMOJI_MAP = {
|
HEALTH_EMOJI_MAP = {
|
||||||
# Disk patterns
|
# Disk patterns
|
||||||
@@ -2246,6 +2330,30 @@ def enrich_with_emojis(event_type: str, title: str, body: str,
|
|||||||
|
|
||||||
if health_enriched:
|
if health_enriched:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# LXC update outcomes are assembled from structured details rather
|
||||||
|
# than one static template line. Their translated labels come from
|
||||||
|
# the runtime catalog, so keep the mapping semantic rather than
|
||||||
|
# comparing an English translation.
|
||||||
|
matched_lxc_label = next(
|
||||||
|
(label for label in lxc_update_labels
|
||||||
|
if stripped.lower().startswith(label.lower())),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if matched_lxc_label:
|
||||||
|
icon = lxc_update_labels[matched_lxc_label]
|
||||||
|
if not stripped.startswith(icon):
|
||||||
|
enriched_lines.append(f'{icon} {stripped}')
|
||||||
|
else:
|
||||||
|
enriched_lines.append(stripped)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Docker's engine inventory is generated by the detector, not the
|
||||||
|
# translated template. "Docker Engine" is its product name and stays
|
||||||
|
# stable across locales, so it is safe to decorate directly.
|
||||||
|
if event_type == 'docker_stack_update_available' and stripped.startswith('• Docker Engine:'):
|
||||||
|
enriched_lines.append(f'🐳 {stripped}')
|
||||||
|
continue
|
||||||
|
|
||||||
# Try to match "FieldName: value" patterns
|
# Try to match "FieldName: value" patterns
|
||||||
enriched = False
|
enriched = False
|
||||||
@@ -2270,6 +2378,7 @@ def enrich_with_emojis(event_type: str, title: str, body: str,
|
|||||||
}
|
}
|
||||||
if field_key in _LABEL_MAP:
|
if field_key in _LABEL_MAP:
|
||||||
label_variants.append(_LABEL_MAP[field_key])
|
label_variants.append(_LABEL_MAP[field_key])
|
||||||
|
label_variants.extend(localized_labels.get(field_key, []))
|
||||||
|
|
||||||
for label in label_variants:
|
for label in label_variants:
|
||||||
if stripped.lower().startswith(label.lower() + ':'):
|
if stripped.lower().startswith(label.lower() + ':'):
|
||||||
|
|||||||
@@ -154,6 +154,70 @@ class RuntimeCatalogTests(unittest.TestCase):
|
|||||||
self.assertNotIn("Source:", rendered["body"])
|
self.assertNotIn("Source:", rendered["body"])
|
||||||
self.assertNotIn("succeeded", rendered["title"])
|
self.assertNotIn("succeeded", rendered["title"])
|
||||||
|
|
||||||
|
_title, enriched_body = notification_templates.enrich_with_emojis(
|
||||||
|
"lxc_update_applied", rendered["title"], rendered["body"],
|
||||||
|
{**data, "_notification_language": "sk", "severity": "INFO"},
|
||||||
|
)
|
||||||
|
self.assertIn("🧭 Zdroj: Manuálne", enriched_body)
|
||||||
|
self.assertIn("🎯 Ciele: iVentoy", enriched_body)
|
||||||
|
self.assertIn("🧩 Aplikácie: iVentoy: 1.0.42 → 1.0.43", enriched_body)
|
||||||
|
self.assertIn("🔄 Vyžaduje sa reštart: nie", enriched_body)
|
||||||
|
self.assertIn("⏱️ Trvanie: 16s", enriched_body)
|
||||||
|
|
||||||
|
for language in self.RUNTIME_LANGUAGES:
|
||||||
|
rendered_locale = notification_templates.render_template(
|
||||||
|
"lxc_update_applied", data, language=language,
|
||||||
|
)
|
||||||
|
_title, body_locale = notification_templates.enrich_with_emojis(
|
||||||
|
"lxc_update_applied", rendered_locale["title"], rendered_locale["body"],
|
||||||
|
{**data, "_notification_language": language, "severity": "INFO"},
|
||||||
|
)
|
||||||
|
self.assertIn("🧭", body_locale, language)
|
||||||
|
self.assertIn("⏱️", body_locale, language)
|
||||||
|
|
||||||
|
def test_update_summary_body_icons_follow_the_selected_language(self):
|
||||||
|
data = {
|
||||||
|
"hostname": "pve01", "total_count": "2", "security_count": "0",
|
||||||
|
"pve_count": "1", "kernel_count": "0", "important_list": "none",
|
||||||
|
"severity": "INFO", "_notification_language": "sk",
|
||||||
|
}
|
||||||
|
rendered = notification_templates.render_template("update_summary", data, language="sk")
|
||||||
|
_title, enriched_body = notification_templates.enrich_with_emojis(
|
||||||
|
"update_summary", rendered["title"], rendered["body"], data,
|
||||||
|
)
|
||||||
|
self.assertIn("📦 Aktualizácie spolu: 2", enriched_body)
|
||||||
|
self.assertIn("🛡️ Bezpečnostné aktualizácie: 0", enriched_body)
|
||||||
|
self.assertIn("⚙️ Aktualizácie jadra: 0", enriched_body)
|
||||||
|
self.assertIn("📋 Dôležité balíky:", enriched_body)
|
||||||
|
|
||||||
|
for language in self.RUNTIME_LANGUAGES:
|
||||||
|
locale_data = {**data, "_notification_language": language}
|
||||||
|
rendered_locale = notification_templates.render_template(
|
||||||
|
"update_summary", locale_data, language=language,
|
||||||
|
)
|
||||||
|
_title, body_locale = notification_templates.enrich_with_emojis(
|
||||||
|
"update_summary", rendered_locale["title"], rendered_locale["body"], locale_data,
|
||||||
|
)
|
||||||
|
total_label = notification_templates._localized_template_labels(
|
||||||
|
"update_summary", language,
|
||||||
|
)["total_count"][0]
|
||||||
|
self.assertIn(f"📦 {total_label}: 2", body_locale, language)
|
||||||
|
|
||||||
|
def test_docker_update_body_icons_preserve_localized_container_label(self):
|
||||||
|
data = {
|
||||||
|
"hostname": "pve01", "vmid": "210", "ct_name": "repopulse-labs-test",
|
||||||
|
"count": "1", "details": "• Docker Engine: 29.8.0 → 29.8.1",
|
||||||
|
"severity": "INFO", "_notification_language": "sk",
|
||||||
|
}
|
||||||
|
rendered = notification_templates.render_template(
|
||||||
|
"docker_stack_update_available", data, language="sk",
|
||||||
|
)
|
||||||
|
_title, enriched_body = notification_templates.enrich_with_emojis(
|
||||||
|
"docker_stack_update_available", rendered["title"], rendered["body"], data,
|
||||||
|
)
|
||||||
|
self.assertIn("📦 Kontajner repopulse-labs-test (CT 210) má 1 aktualizácií Docker:", enriched_body)
|
||||||
|
self.assertIn("🐳 • Docker Engine: 29.8.0 → 29.8.1", enriched_body)
|
||||||
|
|
||||||
def test_missing_slovak_key_falls_back_to_english(self):
|
def test_missing_slovak_key_falls_back_to_english(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
root = Path(directory)
|
root = Path(directory)
|
||||||
|
|||||||
Reference in New Issue
Block a user