Kaynağa Gözat

i18n: complete all 35 locale packs (all keys covered; existing translations preserved, missing keys fall back to English template)

COBCO IT 3 gün önce
ebeveyn
işleme
7508dda4c0
33 değiştirilmiş dosya ile 6127 ekleme ve 22 silme
  1. 193 1
      web-modules/WecomCenter/locale/bg_BG.php
  2. 193 1
      web-modules/WecomCenter/locale/ca_ES.php
  3. 193 1
      web-modules/WecomCenter/locale/cs_CZ.php
  4. 193 1
      web-modules/WecomCenter/locale/da_DK.php
  5. 171 0
      web-modules/WecomCenter/locale/de_DE.php
  6. 193 1
      web-modules/WecomCenter/locale/el_GR.php
  7. 193 1
      web-modules/WecomCenter/locale/en_GB.php
  8. 171 0
      web-modules/WecomCenter/locale/es_ES.php
  9. 193 1
      web-modules/WecomCenter/locale/fa_IR.php
  10. 193 1
      web-modules/WecomCenter/locale/fi_FI.php
  11. 171 0
      web-modules/WecomCenter/locale/fr_FR.php
  12. 193 1
      web-modules/WecomCenter/locale/he_IL.php
  13. 193 1
      web-modules/WecomCenter/locale/hu_HU.php
  14. 193 1
      web-modules/WecomCenter/locale/id_ID.php
  15. 171 0
      web-modules/WecomCenter/locale/it_IT.php
  16. 171 0
      web-modules/WecomCenter/locale/ja_JP.php
  17. 193 1
      web-modules/WecomCenter/locale/ka_GE.php
  18. 171 0
      web-modules/WecomCenter/locale/ko_KR.php
  19. 193 1
      web-modules/WecomCenter/locale/lt_LT.php
  20. 193 1
      web-modules/WecomCenter/locale/lv_LV.php
  21. 193 1
      web-modules/WecomCenter/locale/nb_NO.php
  22. 193 1
      web-modules/WecomCenter/locale/nl_NL.php
  23. 171 0
      web-modules/WecomCenter/locale/pl_PL.php
  24. 171 0
      web-modules/WecomCenter/locale/pt_BR.php
  25. 193 1
      web-modules/WecomCenter/locale/ro_RO.php
  26. 171 0
      web-modules/WecomCenter/locale/ru_RU.php
  27. 193 1
      web-modules/WecomCenter/locale/sk_SK.php
  28. 193 1
      web-modules/WecomCenter/locale/sv_SE.php
  29. 171 0
      web-modules/WecomCenter/locale/tr_TR.php
  30. 193 1
      web-modules/WecomCenter/locale/uk_UA.php
  31. 193 1
      web-modules/WecomCenter/locale/uz_UZ.php
  32. 193 1
      web-modules/WecomCenter/locale/vi_VN.php
  33. 171 0
      web-modules/WecomCenter/locale/zh_TW.php

+ 193 - 1
web-modules/WecomCenter/locale/bg_BG.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - bg_BG (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 193 - 1
web-modules/WecomCenter/locale/ca_ES.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - ca_ES (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 193 - 1
web-modules/WecomCenter/locale/cs_CZ.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - cs_CZ (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 193 - 1
web-modules/WecomCenter/locale/da_DK.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - da_DK (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 171 - 0
web-modules/WecomCenter/locale/de_DE.php

@@ -32,4 +32,175 @@ return [
 	'tab.templates' => 'Vorlagenimport',
 	'tpl.choose' => 'Mitgelieferte Vorlage',
 	'tpl.none' => 'Keine Vorlage gefunden',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'map.choose_host' => '-- select host --',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
 ];

+ 193 - 1
web-modules/WecomCenter/locale/el_GR.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - el_GR (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 193 - 1
web-modules/WecomCenter/locale/en_GB.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - en_GB (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 171 - 0
web-modules/WecomCenter/locale/es_ES.php

@@ -32,4 +32,175 @@ return [
 	'tab.templates' => 'Importar plantilla',
 	'tpl.choose' => 'Plantilla integrada',
 	'tpl.none' => 'No hay plantilla integrada',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'map.choose_host' => '-- select host --',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
 ];

+ 193 - 1
web-modules/WecomCenter/locale/fa_IR.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - fa_IR (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 193 - 1
web-modules/WecomCenter/locale/fi_FI.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - fi_FI (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 171 - 0
web-modules/WecomCenter/locale/fr_FR.php

@@ -32,4 +32,175 @@ return [
 	'tab.templates' => 'Import de modèles',
 	'tpl.choose' => 'Modèle intégré',
 	'tpl.none' => 'Aucun modèle intégré',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'map.choose_host' => '-- select host --',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
 ];

+ 193 - 1
web-modules/WecomCenter/locale/he_IL.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - he_IL (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 193 - 1
web-modules/WecomCenter/locale/hu_HU.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - hu_HU (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 193 - 1
web-modules/WecomCenter/locale/id_ID.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - id_ID (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 171 - 0
web-modules/WecomCenter/locale/it_IT.php

@@ -32,4 +32,175 @@ return [
 	'tab.templates' => 'Importa template',
 	'tpl.choose' => 'Template integrato',
 	'tpl.none' => 'Nessun template integrato',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'map.choose_host' => '-- select host --',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
 ];

+ 171 - 0
web-modules/WecomCenter/locale/ja_JP.php

@@ -32,4 +32,175 @@ return [
 	'tab.templates' => 'テンプレートのインポート',
 	'tpl.choose' => '内蔵テンプレート',
 	'tpl.none' => '内蔵テンプレートなし',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'map.choose_host' => '-- select host --',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
 ];

+ 193 - 1
web-modules/WecomCenter/locale/ka_GE.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - ka_GE (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 171 - 0
web-modules/WecomCenter/locale/ko_KR.php

@@ -32,4 +32,175 @@ return [
 	'tab.templates' => '템플릿 가져오기',
 	'tpl.choose' => '내장 템플릿',
 	'tpl.none' => '내장 템플릿 없음',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'map.choose_host' => '-- select host --',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
 ];

+ 193 - 1
web-modules/WecomCenter/locale/lt_LT.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - lt_LT (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 193 - 1
web-modules/WecomCenter/locale/lv_LV.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - lv_LV (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 193 - 1
web-modules/WecomCenter/locale/nb_NO.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - nb_NO (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 193 - 1
web-modules/WecomCenter/locale/nl_NL.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - nl_NL (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 171 - 0
web-modules/WecomCenter/locale/pl_PL.php

@@ -32,4 +32,175 @@ return [
 	'tab.templates' => 'Import szablonu',
 	'tpl.choose' => 'Wbudowany szablon',
 	'tpl.none' => 'Brak wbudowanego szablonu',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'map.choose_host' => '-- select host --',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
 ];

+ 171 - 0
web-modules/WecomCenter/locale/pt_BR.php

@@ -32,4 +32,175 @@ return [
 	'tab.templates' => 'Importar modelo',
 	'tpl.choose' => 'Modelo integrado',
 	'tpl.none' => 'Nenhum modelo integrado',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'map.choose_host' => '-- select host --',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
 ];

+ 193 - 1
web-modules/WecomCenter/locale/ro_RO.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - ro_RO (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 171 - 0
web-modules/WecomCenter/locale/ru_RU.php

@@ -32,4 +32,175 @@ return [
 	'tab.templates' => 'Импорт шаблонов',
 	'tpl.choose' => 'Встроенный шаблон',
 	'tpl.none' => 'Встроенных шаблонов нет',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'map.choose_host' => '-- select host --',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
 ];

+ 193 - 1
web-modules/WecomCenter/locale/sk_SK.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - sk_SK (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 193 - 1
web-modules/WecomCenter/locale/sv_SE.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - sv_SE (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 171 - 0
web-modules/WecomCenter/locale/tr_TR.php

@@ -32,4 +32,175 @@ return [
 	'tab.templates' => 'Şablon İçe Aktar',
 	'tpl.choose' => 'Yerleşik şablon',
 	'tpl.none' => 'Yerleşik şablon yok',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'map.choose_host' => '-- select host --',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
 ];

+ 193 - 1
web-modules/WecomCenter/locale/uk_UA.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - uk_UA (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 193 - 1
web-modules/WecomCenter/locale/uz_UZ.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - uz_UZ (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 193 - 1
web-modules/WecomCenter/locale/vi_VN.php

@@ -1,3 +1,195 @@
 <?php
 // WeCom Center - vi_VN (not translated yet, falls back to English)
-return [];
+return [	'menu.title' => 'ROMAN Custom Center',
+	'page.title' => 'ROMAN Custom Center',
+	'tab.alert' => 'Alert',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.alerting_on' => 'Alerting enabled',
+	'lbl.severity' => 'Severities that will notify',
+	'lbl.key' => 'Robot key (global macro {$WECOM_KEY})',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.save' => 'Save',
+	'btn.test' => 'Send test',
+	'btn.import' => 'Import template',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'sev.1' => 'Not classified',
+	'sev.2' => 'Information',
+	'sev.3' => 'Warning',
+	'sev.4' => 'Average',
+	'sev.5' => 'High',
+	'sev.6' => 'Disaster',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'tpl.choose' => 'Bundled template',
+	'tpl.none' => 'No bundled template found',
+	'map.choose_host' => '-- select host --',
+	'map.host' => 'Marker host',
+	'map.lat' => 'Latitude',
+	'map.lon' => 'Longitude',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.saved' => 'Alert settings saved.',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
+];

+ 171 - 0
web-modules/WecomCenter/locale/zh_TW.php

@@ -32,4 +32,175 @@ return [
 	'tab.templates' => '範本匯入',
 	'tpl.choose' => '內建範本',
 	'tpl.none' => '找不到內建範本',
+	'tab.opt' => 'Optimization',
+	'tab.maint' => 'Maintenance',
+	'tab.l2' => 'L2 Topology',
+	'sec.robot' => 'WeCom group robot',
+	'sec.strategy' => 'Alert strategy (pick one, then Apply)',
+	'sec.threshold' => 'Manual threshold (overrides the strategy)',
+	'sec.tuning' => 'Server tuning generator',
+	'sec.template' => 'Template import',
+	'sec.map' => 'Map marker',
+	'sec.triggers' => 'Per-trigger alerting switch',
+	'sec.class_sev' => 'Bulk severity by class',
+	'lbl.mediatype' => 'Alert media type',
+	'lbl.strategy_hint' => 'Every strategy pins the "must alert" classes to the highest severity (Disaster) so they always fire, and sets a threshold for the rest. Apply is safe and repeatable.',
+	'lbl.threshold_hint' => 'The strategy sets this automatically. Change it here only for a one-off adjustment.',
+	'lbl.tuning_hint' => 'This only GENERATES the settings and command — nothing is changed. Copy them onto the Zabbix host and recreate the server container yourself.',
+	'btn.preview' => 'Preview',
+	'btn.apply' => 'Apply',
+	'btn.generate' => 'Generate',
+	'btn.search' => 'Search',
+	'cls.device_down' => 'Device down / unreachable',
+	'cls.hw_fault' => 'Hardware fault (fan/power/temperature)',
+	'cls.restarted' => 'Device restarted',
+	'cls.core_link' => 'Core/uplink link down',
+	'cls.interface_down' => 'Access interface down',
+	'cls.perf' => 'Performance (bandwidth/error/CPU)',
+	'cls.other' => 'Other (unchanged)',
+	'cls.col.class' => 'Class',
+	'cls.col.target' => 'Severity',
+	'cls.col.count' => 'Triggers',
+	'cls.col.changes' => 'Will change',
+	'cls.total' => 'Total',
+	'preset.current' => '(current)',
+	'preset.col.name' => 'Strategy',
+	'preset.col.desc' => 'Description',
+	'preset.col.action' => 'Actions',
+	'preset.steady' => 'Steady operations',
+	'preset.steady.desc' => 'Must-alert always fires; rest only High+. Balanced for daily production.',
+	'preset.core' => 'Core essentials only',
+	'preset.core.desc' => 'Only must-alert classes fire (Disaster). Everything else muted — max noise reduction.',
+	'preset.observe' => 'Full observation',
+	'preset.observe.desc' => 'Notify from Warning up. For onboarding / troubleshooting periods.',
+	'preset.night' => 'Night quiet',
+	'preset.night.desc' => 'Disaster only. For night shifts / on-call quiet hours.',
+	'preset.balanced' => 'Core strict / access loose',
+	'preset.balanced.desc' => 'Must-alert + Average up. Slightly broader than steady.',
+	'preview.title' => 'Preview — dry run for "{1}" (nothing written)',
+	'preview.hint' => 'These are the hosted triggers that would change. Apply to actually write them.',
+	'map.choose_host' => '-- select host --',
+	'map.hint' => 'The map uses one representative point (the Zabbix server). Note: the Geomap widget needs the browser to reach the internet for base-map tiles.',
+	'tuning.choose' => 'Hardware profile',
+	'tuning.small' => 'Small (< 200 hosts)',
+	'tuning.medium' => 'Medium (~ 500 hosts)',
+	'tuning.large' => 'Large (1000+ hosts)',
+	'tuning.env' => 'Settings (paste into .env / compose environment):',
+	'tuning.cmd' => 'Then apply:',
+	'lbl.trig_search' => 'Search triggers by description (e.g. CPU, disk)',
+	'lbl.trig_list' => 'Triggers (checked = alerting enabled)',
+	'lbl.trig_hint' => 'There are {1} triggers. Type a keyword above to search (max 200 shown).',
+	'lbl.trig_edit_hint' => 'Adjust severity or toggle alerting per row, then Save. Changes apply immediately and the list refreshes.',
+	'lbl.class_sev_hint' => 'Pick a class of triggers and a target severity. Apply sets every trigger in that class — template, prototypes and already-discovered triggers — then refreshes the list.',
+	'lbl.class_pick' => 'Trigger class',
+	'lbl.set_severity' => 'Set severity to',
+	'msg.robot_saved' => 'Robot key saved.',
+	'msg.tested' => 'Test message sent successfully (errcode 0).',
+	'msg.test_failed' => 'Test failed (HTTP {1}): {2}',
+	'msg.imported' => 'Template imported. Templates: {1}',
+	'msg.map_saved' => 'Map coordinates saved.',
+	'msg.no_mediatype' => 'No webhook media type found.',
+	'msg.no_url' => 'Robot key is empty (set {$WECOM_KEY}).',
+	'msg.no_template' => 'Please choose a template.',
+	'msg.no_host' => 'Please choose a host.',
+	'msg.no_template_host' => 'Huawei walk template not found on this server.',
+	'msg.no_server_host' => 'Host "Zabbix server" not found.',
+	'msg.bad_scheme' => 'Unknown strategy.',
+	'msg.unknown_op' => 'Unknown operation.',
+	'msg.no_triggers' => 'No trigger selected.',
+	'msg.triggers_saved' => 'Trigger alerting updated: {1} trigger(s).',
+	'msg.no_trig_search' => 'No triggers matched your search.',
+	'msg.bad_class_sev' => 'Choose a class and a severity (0-5).',
+	'msg.class_sev_saved' => 'Class severity updated: template triggers {1}, discovered triggers {2}. List refreshed.',
+	'msg.strategy_applied' => 'Strategy "{1}" applied — template triggers: {2}, prototypes: {3}, discovered triggers: {4}.',
+	'hero.sub' => 'WeCom group alerts, one-click alert strategy, server tuning, template migration and per-trigger control — all in one place.',
+	'lbl.template_hint' => 'Bundled templates ship with the module. Importing an existing template updates it in place; importing a new one creates it. Use the switch card below to move devices from an old template to the new one.',
+	'sec.switch' => 'Template switch (migrate devices)',
+	'lbl.switch_hint' => 'Pick the OLD template and the NEW template. Preview lists the affected devices; confirm replaces the template link on every one of them. This is the "import, then switch?" step.',
+	'switch.pick' => '-- select template --',
+	'switch.src' => 'From template (old)',
+	'switch.dst' => 'To template (new)',
+	'switch.none' => 'No device currently uses "{1}". Nothing to switch.',
+	'switch.result' => '{1} device(s) on "{2}" will be moved to "{3}".',
+	'btn.confirm' => 'Confirm switch',
+	'msg.need_both' => 'Please choose both source and target template.',
+	'msg.same_tpl' => 'Source and target template must differ.',
+	'msg.switch_applied' => 'Switched {1} device(s) to the new template.',
+	'sec.init' => 'Environment initialisation',
+	'lbl.init_hint' => 'On a brand-new Zabbix click this once — it creates the WeCom webhook media type, the robot user with its media, the trigger action and the {$WECOM_KEY} macro. Existing objects are reused (safe to re-run as a self-check).',
+	'lbl.init_key' => 'Group-robot key (Webhook key)',
+	'btn.init' => 'Initialise / self-check',
+	'init.title' => 'First-time setup',
+	'init.sub' => 'This Zabbix has no WeCom media type yet. Paste your group-robot key and initialise — the media type, robot user, trigger action and macro will all be created automatically, then alerts start flowing to the group.',
+	'msg.init_done' => 'Initialisation complete: {1}.',
+	'msg.init_mediatype_new' => 'media type created',
+	'msg.init_mediatype_ok' => 'media type reused',
+	'msg.init_user_new' => 'robot user created',
+	'msg.init_user_ok' => 'robot user reused',
+	'msg.init_action_new' => 'trigger action created',
+	'msg.init_action_ok' => 'trigger action exists',
+	'msg.init_key_set' => 'key saved',
+	'menu.l2' => 'L2 Topology',
+	'sec.l2_collect' => 'L2 collection',
+	'sec.l2_display' => 'L2 display',
+	'sec.l2_colors' => 'Link speed colours',
+	'sec.l2_latency' => 'Latency thresholds',
+	'sec.l2_group' => 'Grouping rules',
+	'lbl.l2_templates' => 'Collect hosts using these template(s)',
+	'lbl.l2_templates_hint' => 'Only hosts linked to the selected templates are collected. Recommended/default: Huawei VRP by SNMP (walk).',
+	'lbl.l2_refresh' => 'Display auto-refresh (seconds)',
+	'lbl.l2_full' => 'Full collection interval (minutes)',
+	'lbl.l2_alarm' => 'Alarm-only refresh (minutes)',
+	'lbl.l2_root' => 'Outermost frame label',
+	'lbl.l2_other' => 'Fallback group label',
+	'lbl.l2_dark' => 'Open in dark mode by default',
+	'lbl.color_100' => '100M',
+	'lbl.color_1000' => '1G',
+	'lbl.color_10000' => '10G',
+	'lbl.color_20000' => '20G+',
+	'lbl.color_unknown' => 'Unknown',
+	'lbl.color_down' => 'Down',
+	'lbl.lat_warn' => 'Orange above (ms)',
+	'lbl.lat_bad' => 'Red above (ms)',
+	'lbl.rule_match' => 'Match',
+	'lbl.rule_pattern' => 'Pattern',
+	'lbl.rule_label' => 'Frame label',
+	'lbl.rule_big' => 'Big',
+	'lbl.rule_hint' => 'First matching rule wins; unmatched devices go to the fallback group. For regex rules the label may use $1..$9 back-references.',
+	'btn.add_rule' => 'Add rule',
+	'msg.l2_saved' => 'L2 topology settings saved.',
+	'l2.title' => 'L2 Physical Topology',
+	'l2.search_ph' => 'Search IP / MAC / name…',
+	'l2.show_term' => 'Show terminals',
+	'l2.only_off' => 'Only offline',
+	'l2.auto_refresh' => 'Auto refresh',
+	'l2.min' => 'min',
+	'l2.refresh_now' => 'Refresh now',
+	'l2.fit' => 'Fit to window',
+	'l2.alarms' => 'Alarms',
+	'l2.offline' => 'Offline terminals',
+	'l2.time_live' => 'Live',
+	'l2.time_btn' => 'Go',
+	'l2.time_label' => 'Snapshot time',
+	'l2.dark' => 'Dark mode',
+	'l2.no_data' => 'No snapshot yet — the collector has not run.',
+	'l2.offline_empty' => 'No offline terminals.',
+	'l2.alarm_empty' => 'No active alarms.',
+	'l2.updated' => 'Updated',
+	'l2.pause' => 'Pause auto-refresh',
+	'l2.resume' => 'Resume auto-refresh',
+	'l2.legend_speed' => 'Link speed',
+	'l2.legend_tier' => 'Tier',
+	'l2.legend_lat' => 'Latency',
+	'l2.core' => 'Core',
+	'l2.dist' => 'Distribution',
+	'l2.access' => 'Access',
+	'l2.lat_norm' => 'Normal',
+	'l2.term_on' => 'Online',
+	'l2.term_off' => 'Offline',
+	'l2.zoom_in' => 'Zoom in',
+	'l2.zoom_out' => 'Zoom out',
+	'l2.snapshot' => 'Snapshot',
+	'l2.showing' => 'Showing',
+	'l2.live_tag' => 'LIVE',
 ];