i18n: localize runtime notification delivery

This commit is contained in:
VAIO73
2026-09-17 11:59:48 +02:00
parent da7bf37c07
commit dc283bed9a
12 changed files with 247 additions and 81 deletions
@@ -202,12 +202,19 @@ const AI_PROVIDERS = [
const NOTIFICATION_LANGUAGES = [ const NOTIFICATION_LANGUAGES = [
{ value: "en", label: "English" }, { value: "en", label: "English" },
{ value: "de", label: "Deutsch" },
{ value: "es", label: "Español" },
{ value: "fr", label: "Français" },
{ value: "it", label: "Italiano" },
{ value: "pt", label: "Português" },
{ value: "sk", label: "Slovenčina" }, { value: "sk", label: "Slovenčina" },
{ value: "sv", label: "Svenska" },
] ]
function normalizeNotificationLanguage(notificationLanguage?: string, legacyAiLanguage?: string): string { function normalizeNotificationLanguage(notificationLanguage?: string, legacyAiLanguage?: string): string {
if (notificationLanguage === "en" || notificationLanguage === "sk") return notificationLanguage const runtimeLanguages = new Set(NOTIFICATION_LANGUAGES.map(language => language.value))
if (legacyAiLanguage === "en" || legacyAiLanguage === "sk") return legacyAiLanguage if (notificationLanguage && runtimeLanguages.has(notificationLanguage)) return notificationLanguage
if (legacyAiLanguage && runtimeLanguages.has(legacyAiLanguage)) return legacyAiLanguage
return "en" return "en"
} }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+6 -3
View File
@@ -164,9 +164,12 @@ cp "$SCRIPT_DIR/ai_context_enrichment.py" "$APP_DIR/usr/bin/" 2>/dev/null || ech
cp "$SCRIPT_DIR/startup_grace.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ startup_grace.py not found" cp "$SCRIPT_DIR/startup_grace.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ startup_grace.py not found"
cp "$SCRIPT_DIR/flask_notification_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_notification_routes.py not found" cp "$SCRIPT_DIR/flask_notification_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_notification_routes.py not found"
# Backend notification rendering reuses the canonical Monitor catalogs. # Backend notification rendering reuses the canonical Monitor catalogs.
mkdir -p "$APP_DIR/usr/share/proxmenux/messages/en" "$APP_DIR/usr/share/proxmenux/messages/sk" # Keep every supported locale in the bundle so the runtime selector and its
cp "$APPIMAGE_ROOT/messages/en/common.json" "$APP_DIR/usr/share/proxmenux/messages/en/common.json" # fallback behavior exactly match the Monitor UI.
cp "$APPIMAGE_ROOT/messages/sk/common.json" "$APP_DIR/usr/share/proxmenux/messages/sk/common.json" for locale in en de es fr it pt sk sv; do
mkdir -p "$APP_DIR/usr/share/proxmenux/messages/$locale"
cp "$APPIMAGE_ROOT/messages/$locale/common.json" "$APP_DIR/usr/share/proxmenux/messages/$locale/common.json"
done
cp "$SCRIPT_DIR/oci_manager.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ oci_manager.py not found" cp "$SCRIPT_DIR/oci_manager.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ oci_manager.py not found"
cp "$SCRIPT_DIR/flask_oci_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_oci_routes.py not found" cp "$SCRIPT_DIR/flask_oci_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_oci_routes.py not found"
cp "$SCRIPT_DIR/flask_audit_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_audit_routes.py not found" cp "$SCRIPT_DIR/flask_audit_routes.py" "$APP_DIR/usr/bin/" 2>/dev/null || echo "⚠️ flask_audit_routes.py not found"
+110 -47
View File
@@ -41,7 +41,7 @@ if BASE_DIR not in sys.path:
from notification_channels import create_channel, CHANNEL_TYPES from notification_channels import create_channel, CHANNEL_TYPES
from notification_templates import ( from notification_templates import (
render_template, format_with_ai, format_with_ai_full, enrich_with_emojis, TEMPLATES, render_template, format_with_ai, format_with_ai_full, enrich_with_emojis, TEMPLATES,
EVENT_GROUPS, get_event_types_by_group, get_default_enabled_events EVENT_GROUPS, get_event_types_by_group, get_default_enabled_events, runtime_message,
) )
from notification_events import ( from notification_events import (
JournalWatcher, TaskWatcher, PollingCollector, NotificationEvent, JournalWatcher, TaskWatcher, PollingCollector, NotificationEvent,
@@ -61,6 +61,17 @@ except ImportError:
DB_PATH = Path('/usr/local/share/proxmenux/health_monitor.db') DB_PATH = Path('/usr/local/share/proxmenux/health_monitor.db')
SETTINGS_PREFIX = 'notification.' SETTINGS_PREFIX = 'notification.'
ENCRYPTION_KEY_FILE = Path('/usr/local/share/proxmenux/.notification_key') ENCRYPTION_KEY_FILE = Path('/usr/local/share/proxmenux/.notification_key')
RUNTIME_NOTIFICATION_LANGUAGES = ('en', 'de', 'es', 'fr', 'it', 'pt', 'sk', 'sv')
ALLOWED_AI_LANGUAGES = (
'en', 'sk', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'sv', 'no',
'ja', 'zh', 'ko', 'pl', 'nl', 'tr', 'ar',
)
_AI_BYPASS_EVENTS = frozenset({'backup_complete', 'backup_fail'})
def _should_bypass_ai(event_type: str) -> bool:
"""Keep complete backup inventories deterministic on every send path."""
return event_type in _AI_BYPASS_EVENTS
# Keys that contain sensitive data and should be encrypted # Keys that contain sensitive data and should be encrypted
SENSITIVE_KEYS = { SENSITIVE_KEYS = {
@@ -934,6 +945,21 @@ class NotificationManager:
or self._config.get('ai_model', '') or self._config.get('ai_model', '')
) )
def _notification_language(self) -> str:
"""Return a bundled runtime locale, with a safe English fallback.
Older installations used the AI language for every notification. Keep
that value as a migration fallback only; notification text is now
independent of whether AI enhancement is enabled.
"""
selected = str(self._config.get('notification_language', '')).strip().lower()
if selected in RUNTIME_NOTIFICATION_LANGUAGES:
return selected
legacy = str(self._config.get('ai_language', '')).strip().lower()
if legacy in RUNTIME_NOTIFICATION_LANGUAGES:
return legacy
return 'en'
def _build_ai_config(self) -> Dict[str, Any]: def _build_ai_config(self) -> Dict[str, Any]:
"""Build the shared AI config passed to notification rewriters.""" """Build the shared AI config passed to notification rewriters."""
ai_provider = self._config.get('ai_provider', 'groq') ai_provider = self._config.get('ai_provider', 'groq')
@@ -1232,9 +1258,13 @@ class NotificationManager:
severity = event.severity severity = event.severity
event.data['severity'] = severity event.data['severity'] = severity
rendered = render_template(event.event_type, event.data) notification_language = self._notification_language()
rendered = render_template(
event.event_type, event.data, language=notification_language,
)
enriched_data = dict(event.data) enriched_data = dict(event.data)
enriched_data['_notification_language'] = notification_language
enriched_data['_rendered_fields'] = rendered.get('fields', []) enriched_data['_rendered_fields'] = rendered.get('fields', [])
enriched_data['_body_html'] = rendered.get('body_html', '') enriched_data['_body_html'] = rendered.get('body_html', '')
enriched_data['_event_type'] = event.event_type enriched_data['_event_type'] = event.event_type
@@ -1380,7 +1410,7 @@ class NotificationManager:
# neither be trusted to avoid repeating all guest rows nor to # neither be trusted to avoid repeating all guest rows nor to
# preserve every value, so these two events bypass AI entirely. # preserve every value, so these two events bypass AI entirely.
ai_result = None ai_result = None
if event_type not in {'backup_complete', 'backup_fail'}: if not _should_bypass_ai(event_type):
enriched_context = enrich_context_for_ai( enriched_context = enrich_context_for_ai(
title=ch_title, title=ch_title,
body=ch_body, body=ch_body,
@@ -1623,8 +1653,10 @@ class NotificationManager:
(issue #233). (issue #233).
""" """
host = _resolve_display_hostname(self._config) host = _resolve_display_hostname(self._config)
summary_title = ( language = self._notification_language()
f"{host}: 24h summary ({now.strftime('%Y-%m-%d %H:%M')})" summary_title = runtime_message(
'digest.title', language, hostname=host,
timestamp=now.strftime('%Y-%m-%d %H:%M'),
) )
try: try:
@@ -1642,7 +1674,7 @@ class NotificationManager:
print(f"[NotificationManager] digest read failed for {ch_name}: {e}") print(f"[NotificationManager] digest read failed for {ch_name}: {e}")
self._record_history( self._record_history(
'digest', ch_name, summary_title, 'digest', ch_name, summary_title,
f'digest read failed: {e}', 'INFO', runtime_message('digest.readFailed', language, error=e), 'INFO',
False, str(e), 'digest_scheduler', False, str(e), 'digest_scheduler',
) )
self._stats['total_errors'] += 1 self._stats['total_errors'] += 1
@@ -1660,7 +1692,7 @@ class NotificationManager:
# just nothing INFO non-exempt to summarize. # just nothing INFO non-exempt to summarize.
self._record_history( self._record_history(
'digest', ch_name, summary_title, 'digest', ch_name, summary_title,
'No INFO events buffered for this digest window.', runtime_message('digest.empty', language),
'INFO', True, '', 'digest_scheduler', 'INFO', True, '', 'digest_scheduler',
) )
return return
@@ -1670,7 +1702,8 @@ class NotificationManager:
result: dict = {'success': False, 'error': ''} result: dict = {'success': False, 'error': ''}
try: try:
result = channel.send(summary_title, summary_body, severity='INFO', result = channel.send(summary_title, summary_body, severity='INFO',
data={'_digest': True, '_count': len(rows)}) or result data={'_digest': True, '_count': len(rows),
'_notification_language': language}) or result
except Exception as e: except Exception as e:
print(f"[NotificationManager] digest send failed for " print(f"[NotificationManager] digest send failed for "
f"{ch_name}: {e}") f"{ch_name}: {e}")
@@ -1719,20 +1752,19 @@ class NotificationManager:
label = group or 'other' label = group or 'other'
groups.setdefault(label, []).append((ts, ev_type, title)) groups.setdefault(label, []).append((ts, ev_type, title))
lines = [f"{len(rows)} INFO events grouped by category:\n"] language = self._notification_language()
lines = [runtime_message('digest.lead', language, count=len(rows))]
for group, items in groups.items(): for group, items in groups.items():
lines.append(f"{group.title()}: {len(items)}") group_label = runtime_message(f'digest.groups.{group}', language) or group.title()
lines.append(f"{group_label}: {len(items)}")
for ts, ev_type, title in items[:8]: for ts, ev_type, title in items[:8]:
hhmm = datetime.fromtimestamp(ts).strftime('%H:%M') hhmm = datetime.fromtimestamp(ts).strftime('%H:%M')
short_title = title.split(': ', 1)[-1] if ': ' in title else title short_title = title.split(': ', 1)[-1] if ': ' in title else title
lines.append(f" • {hhmm} {short_title}") lines.append(f" • {hhmm} {short_title}")
if len(items) > 8: if len(items) > 8:
lines.append(f" • … and {len(items) - 8} more") lines.append(runtime_message('digest.more', language, count=len(items) - 8))
lines.append('') lines.append('')
lines.append( lines.append(runtime_message('digest.footer', language))
'(Critical/Warning events arrived at the time they happened, '
'not in this digest.)'
)
return '\n'.join(lines).rstrip() + '\n' return '\n'.join(lines).rstrip() + '\n'
# ─── Quiet Hours buffer + flush ──────────────────────────── # ─── Quiet Hours buffer + flush ────────────────────────────
@@ -1862,8 +1894,9 @@ class NotificationManager:
return return
host = _resolve_display_hostname(self._config) host = _resolve_display_hostname(self._config)
summary_title = ( language = self._notification_language()
f"{host}: {len(rows)} events buffered during Quiet Hours" summary_title = runtime_message(
'digest.quietTitle', language, hostname=host, count=len(rows),
) )
summary_body = self._compose_digest_body(rows) summary_body = self._compose_digest_body(rows)
@@ -1871,7 +1904,8 @@ class NotificationManager:
try: try:
result = channel.send( result = channel.send(
summary_title, summary_body, severity='INFO', summary_title, summary_body, severity='INFO',
data={'_quiet_hours_summary': True, '_count': len(rows)}, data={'_quiet_hours_summary': True, '_count': len(rows),
'_notification_language': language},
) or result ) or result
except Exception as e: except Exception as e:
print(f"[NotificationManager] quiet send failed for " print(f"[NotificationManager] quiet send failed for "
@@ -2341,12 +2375,20 @@ class NotificationManager:
'skipped': True, 'skipped': True,
} }
runtime_data = dict(data or {})
runtime_data.setdefault('_notification_language', self._notification_language())
# Render template if available # Render template if available
if event_type in TEMPLATES and not message: if event_type in TEMPLATES and not message:
rendered = render_template(event_type, data or {}) rendered = render_template(
event_type, runtime_data,
language=runtime_data['_notification_language'],
)
title = title or rendered['title'] title = title or rendered['title']
message = rendered['body'] message = rendered['body']
severity = severity or rendered['severity'] severity = severity or rendered['severity']
data = runtime_data
# AI config for enhancement # AI config for enhancement
ai_config = self._build_ai_config() ai_config = self._build_ai_config()
@@ -2369,13 +2411,16 @@ class NotificationManager:
# Pass channel_type so AI knows whether to append original (email only) # Pass channel_type so AI knows whether to append original (email only)
channel_ai_config = {**ai_config, 'channel_type': ch_name} channel_ai_config = {**ai_config, 'channel_type': ch_name}
ai_result = format_with_ai_full( if _should_bypass_ai(event_type):
title, message, severity, channel_ai_config, ch_title, ch_message = title, message
detail_level=detail_level, else:
use_emojis=use_rich_format ai_result = format_with_ai_full(
) title, message, severity, channel_ai_config,
ch_title = ai_result.get('title', title) detail_level=detail_level,
ch_message = ai_result.get('body', message) use_emojis=use_rich_format
)
ch_title = ai_result.get('title', title)
ch_message = ai_result.get('body', message)
result = channel.send(ch_title, ch_message, severity, data) result = channel.send(ch_title, ch_message, severity, data)
results[ch_name] = result results[ch_name] = result
@@ -2408,6 +2453,32 @@ class NotificationManager:
return self.send_notification( return self.send_notification(
'custom', severity, title, message, source=source 'custom', severity, title, message, source=source
) )
def _build_test_message(self, use_rich_format: bool, ai_enabled: bool,
ai_info: str) -> tuple:
"""Build the channel test payload in the selected notification language."""
language = self._notification_language()
icon_key = 'test.iconsEnabled' if use_rich_format else 'test.iconsDisabled'
ai_key = 'test.aiEnabled' if ai_enabled else 'test.aiDisabled'
icon_status = runtime_message(icon_key, language)
ai_status = runtime_message(ai_key, language, info=ai_info)
if use_rich_format:
icon_status = f'✅ {icon_status}'
ai_status = f'✅ {ai_status}' if ai_enabled else f'❌ {ai_status}'
body = '\n\n'.join([
runtime_message('test.welcome', language),
runtime_message('test.verify', language),
'\n'.join([
runtime_message('test.configuration', language),
icon_status,
ai_status,
]),
runtime_message('test.alerts', language),
])
return (
runtime_message('test.title', language), body,
runtime_message('test.photoCaption', language),
)
def test_channel(self, channel_name: str = 'all') -> Dict[str, Any]: def test_channel(self, channel_name: str = 'all') -> Dict[str, Any]:
"""Test one or all configured channels with AI enhancement.""" """Test one or all configured channels with AI enhancement."""
@@ -2453,7 +2524,6 @@ class NotificationManager:
# ProxMenux logo for welcome message # ProxMenux logo for welcome message
logo_url = 'https://proxmenux.com/telegram.png' logo_url = 'https://proxmenux.com/telegram.png'
logo_caption = 'You can use this image as the profile photo for your notification bot.'
for ch_name, channel in targets.items(): for ch_name, channel in targets.items():
try: try:
@@ -2464,25 +2534,8 @@ class NotificationManager:
rich_key = f'{ch_name}.rich_format' rich_key = f'{ch_name}.rich_format'
use_rich_format = self._config.get(rich_key, 'false') == 'true' use_rich_format = self._config.get(rich_key, 'false') == 'true'
# Build status indicators for icons and AI, adapted to channel format base_title, base_message, logo_caption = self._build_test_message(
if use_rich_format: use_rich_format, ai_enabled, ai_info,
icon_status = '✅ Icons: enabled'
ai_status = f'✅ AI: enabled ({ai_info})' if ai_enabled else '❌ AI: disabled'
else:
icon_status = 'Icons: disabled'
ai_status = f'AI: enabled ({ai_info})' if ai_enabled else 'AI: disabled'
# Base test message — shows current channel config
# NOTE: narrative lines are intentionally unlabeled so the AI
# does not prepend "Message:" or other spurious field labels.
base_title = 'ProxMenux Test'
base_message = (
'Welcome to ProxMenux Monitor!\n\n'
'This is a test message to verify your notification channel is working correctly.\n\n'
'Channel configuration:\n'
f'{icon_status}\n'
f'{ai_status}\n\n'
'You will receive alerts about system events, health status changes, and security incidents.'
) )
# Apply AI enhancement (translates to configured language) # Apply AI enhancement (translates to configured language)
@@ -2497,7 +2550,11 @@ class NotificationManager:
enhanced_message = ai_result.get('body', base_message) enhanced_message = ai_result.get('body', base_message)
# Send message # Send message
send_result = channel.send(enhanced_title, enhanced_message, 'INFO') send_result = channel.send(
enhanced_title, enhanced_message, 'INFO',
data={'_notification_language': self._notification_language(),
'_event_type': 'test', '_group': 'other'},
)
success = send_result.get('success', False) success = send_result.get('success', False)
error = send_result.get('error', '') error = send_result.get('error', '')
@@ -2870,6 +2927,7 @@ class NotificationManager:
'ai_api_keys': ai_api_keys, 'ai_api_keys': ai_api_keys,
'ai_models': ai_models, 'ai_models': ai_models,
'ai_model': self._active_ai_model(current_provider), 'ai_model': self._active_ai_model(current_provider),
'notification_language': self._notification_language(),
'ai_language': self._config.get('ai_language', 'en'), 'ai_language': self._config.get('ai_language', 'en'),
'ai_ollama_url': self._config.get('ai_ollama_url', 'http://localhost:11434'), 'ai_ollama_url': self._config.get('ai_ollama_url', 'http://localhost:11434'),
'ai_openai_base_url': self._config.get('ai_openai_base_url', ''), 'ai_openai_base_url': self._config.get('ai_openai_base_url', ''),
@@ -2949,6 +3007,11 @@ class NotificationManager:
if short_key == 'ai_language': if short_key == 'ai_language':
if str(value) not in _ALLOWED_AI_LANGUAGES: if str(value) not in _ALLOWED_AI_LANGUAGES:
raise ValueError(f"Invalid ai_language: must be one of {_ALLOWED_AI_LANGUAGES}") raise ValueError(f"Invalid ai_language: must be one of {_ALLOWED_AI_LANGUAGES}")
if short_key == 'notification_language':
if str(value) not in RUNTIME_NOTIFICATION_LANGUAGES:
raise ValueError(
f"Invalid notification_language: must be one of {RUNTIME_NOTIFICATION_LANGUAGES}"
)
# Encrypt sensitive values before storing. Skip if the value is # Encrypt sensitive values before storing. Skip if the value is
# already in either encrypted form — `encrypt_sensitive_value` # already in either encrypted form — `encrypt_sensitive_value`
@@ -29,10 +29,12 @@ def _placeholders(value):
class RuntimeCatalogTests(unittest.TestCase): class RuntimeCatalogTests(unittest.TestCase):
RUNTIME_LANGUAGES = ("en", "de", "es", "fr", "it", "pt", "sk", "sv")
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
cls.catalogs = {} cls.catalogs = {}
for language in ("en", "sk"): for language in cls.RUNTIME_LANGUAGES:
path = APPIMAGE_DIR / "messages" / language / "common.json" path = APPIMAGE_DIR / "messages" / language / "common.json"
cls.catalogs[language] = json.loads(path.read_text(encoding="utf-8"))["runtime"]["notifications"] cls.catalogs[language] = json.loads(path.read_text(encoding="utf-8"))["runtime"]["notifications"]
@@ -65,10 +67,11 @@ class RuntimeCatalogTests(unittest.TestCase):
return result return result
en = flatten(self.catalogs["en"]) en = flatten(self.catalogs["en"])
sk = flatten(self.catalogs["sk"]) for language, catalog in self.catalogs.items():
self.assertEqual(set(sk), set(en)) translated = flatten(catalog)
for key in en: self.assertEqual(set(translated), set(en), language)
self.assertEqual(_placeholders(sk[key]), _placeholders(en[key]), key) for key in en:
self.assertEqual(_placeholders(translated[key]), _placeholders(en[key]), f"{language}:{key}")
def test_notification_language_ui_keys_exist_in_both_catalogs(self): def test_notification_language_ui_keys_exist_in_both_catalogs(self):
required = { required = {
@@ -76,7 +79,7 @@ class RuntimeCatalogTests(unittest.TestCase):
"selectNotificationLanguage", "selectNotificationLanguage",
"notificationLanguageHint", "notificationLanguageHint",
} }
for language in ("en", "sk"): for language in self.RUNTIME_LANGUAGES:
path = APPIMAGE_DIR / "messages" / language / "common.json" path = APPIMAGE_DIR / "messages" / language / "common.json"
common = json.loads(path.read_text(encoding="utf-8")) common = json.loads(path.read_text(encoding="utf-8"))
ui = common["settings"]["notifications"]["ui"] ui = common["settings"]["notifications"]["ui"]
@@ -231,7 +234,7 @@ class RuntimeCatalogTests(unittest.TestCase):
) )
self.assertIn("Systémové zdroje", html) self.assertIn("Systémové zdroje", html)
self.assertIn("UPOZORNENIE", html) self.assertIn("UPOZORNENIE", html)
self.assertIn("Hostiteľ:", html) self.assertIn("Server:", html)
self.assertIn("Aktuálna hodnota", html) self.assertIn("Aktuálna hodnota", html)
self.assertNotIn(">Details<", html) self.assertNotIn(">Details<", html)
self.assertNotIn("System Resources Report", html) self.assertNotIn("System Resources Report", html)
@@ -281,16 +284,16 @@ class RuntimeCatalogTests(unittest.TestCase):
self.assertEqual(manager._notification_language(), "sk") self.assertEqual(manager._notification_language(), "sk")
self.assertEqual(manager._build_ai_config()["ai_language"], "de") self.assertEqual(manager._build_ai_config()["ai_language"], "de")
def test_legacy_non_runtime_ai_language_roundtrips_as_english_runtime(self): def test_legacy_runtime_capable_ai_language_is_preserved(self):
manager = notification_manager.NotificationManager() manager = notification_manager.NotificationManager()
manager._config = {"ai_language": "de"} manager._config = {"ai_language": "de"}
self.assertEqual(manager._notification_language(), "en") self.assertEqual(manager._notification_language(), "de")
self.assertEqual(manager.get_settings()["config"]["notification_language"], "en") self.assertEqual(manager.get_settings()["config"]["notification_language"], "de")
manager._config = {"notification_language": "invalid", "ai_language": "sk"} manager._config = {"notification_language": "invalid", "ai_language": "sk"}
self.assertEqual(manager._notification_language(), "sk") self.assertEqual(manager._notification_language(), "sk")
manager._config = {"notification_language": "invalid", "ai_language": "de"} manager._config = {"notification_language": "invalid", "ai_language": "de"}
self.assertEqual(manager._notification_language(), "en") self.assertEqual(manager._notification_language(), "de")
with tempfile.TemporaryDirectory() as directory: with tempfile.TemporaryDirectory() as directory:
db_path = Path(directory) / "settings.db" db_path = Path(directory) / "settings.db"
@@ -420,7 +423,7 @@ class RuntimeCatalogTests(unittest.TestCase):
self.assertEqual(visible_groups - backend_groups, set()) self.assertEqual(visible_groups - backend_groups, set())
self.assertEqual(visible_groups - frontend_groups, set()) self.assertEqual(visible_groups - frontend_groups, set())
self.assertEqual(frontend_groups, backend_groups) self.assertEqual(frontend_groups, backend_groups)
for language in ("en", "sk"): for language in self.RUNTIME_LANGUAGES:
catalog = json.loads( catalog = json.loads(
(APPIMAGE_DIR / "messages" / language / "common.json").read_text(encoding="utf-8") (APPIMAGE_DIR / "messages" / language / "common.json").read_text(encoding="utf-8")
) )
@@ -432,12 +435,12 @@ class RuntimeCatalogTests(unittest.TestCase):
def test_build_bundles_runtime_catalogs(self): def test_build_bundles_runtime_catalogs(self):
build = (SCRIPTS_DIR / "build_appimage.sh").read_text(encoding="utf-8") build = (SCRIPTS_DIR / "build_appimage.sh").read_text(encoding="utf-8")
self.assertIn('messages/en/common.json', build) for language in self.RUNTIME_LANGUAGES:
self.assertIn('messages/sk/common.json', build) self.assertIn(language, build)
self.assertIn('$APP_DIR/usr/share/proxmenux/messages', build) self.assertIn('$APP_DIR/usr/share/proxmenux/messages', build)
def test_missing_event_type_names_exist_in_both_ui_catalogs(self): def test_missing_event_type_names_exist_in_both_ui_catalogs(self):
for language in ("en", "sk"): for language in self.RUNTIME_LANGUAGES:
path = APPIMAGE_DIR / "messages" / language / "common.json" path = APPIMAGE_DIR / "messages" / language / "common.json"
event_types = json.loads(path.read_text(encoding="utf-8"))["settings"]["notifications"]["eventTypes"] event_types = json.loads(path.read_text(encoding="utf-8"))["settings"]["notifications"]["eventTypes"]
self.assertIn("lxc_update_applied", event_types) self.assertIn("lxc_update_applied", event_types)