diff --git a/frontend/src/locales/en-US.json b/frontend/src/locales/en-US.json index 2e4e76d..8a1dedb 100644 --- a/frontend/src/locales/en-US.json +++ b/frontend/src/locales/en-US.json @@ -3262,7 +3262,406 @@ "prometheusdashb.sb0f653eb": "内存 Top 10", "prometheusdashb.s5e8eb212": "连接中…", "zabbixdashboard.sdb131ab4": "未找到被监控的 Zabbix 主机", - "zabbixdashboard.s5dc99f6e": "问题" + "zabbixdashboard.s5dc99f6e": "问题", + "alertChannel": { + "addBtn": "Add Channel", + "addTitle": "Add Notification Channel", + "added": "Channel added", + "configJson": "Config (JSON)", + "dialogDesc": "Configure alert notification channels (DingTalk/WeChat/Email/SMS/Webhook)", + "editTitle": "Edit Notification Channel", + "hintDingtalk": "DingTalk bot: webhook URL + secret", + "hintEmail": "Email: {\"to\":\"ops@ezops.io\"}", + "hintFeishu": "Feishu bot: {\"url\":\"https://open.feishu.cn/open-apis/bot/v2/hook/...\"}", + "hintSms": "SMS: {\"phones\":\"13800138000\"}", + "hintWebhook": "Webhook: {\"url\":\"https://...\"}", + "hintWechat": "WeCom bot: webhook URL", + "namePlaceholder": "e.g., Ops Alert Group", + "nameRequired": "Channel Name *", + "saveChanges": "Save Changes", + "saving": "Saving...", + "typeDingtalk": "DingTalk", + "typeEmail": "Email", + "typeFeishu": "Feishu", + "typeLabel": "Channel Type", + "typeSms": "SMS", + "typeWebhook": "Webhook", + "typeWechat": "WeCom", + "updated": "Channel updated" + }, + "alertChannels": { + "addChannel": "Add Channel", + "deleteConfirm": "Delete this notification channel?", + "deleteFailed": "Delete failed", + "deleteTitle": "Delete Notification Channel", + "deleted": "Channel deleted", + "disabled": "Disabled", + "empty": "No notification channels yet. Click \"Add Channel\" to create.", + "fetchFailed": "Failed to fetch notification channels", + "testFailed": "Test failed", + "testNoConfig": "Channel not configured. Please complete config first.", + "testPassed": "Test passed", + "testSent": "Test notification sent", + "typeLabel": "Type" + }, + "alertHelper": { + "alertIdFallback": "Alert #{{id}}", + "chDingtalk": "DingTalk", + "chEmail": "Email", + "chFeishu": "Feishu", + "chSms": "SMS", + "chWebhook": "Webhook", + "chWechat": "WeCom", + "sevCritical": "Critical", + "sevInfo": "Info", + "sevWarning": "Warning", + "srcHost": "Host Monitor", + "srcLog": "Log Alert", + "srcMiddleware": "Middleware Monitor", + "srcSite": "Site Probe", + "srcSnmp": "SNMP Monitor", + "statusAcknowledged": "Acknowledged", + "statusFiring": "Firing", + "statusResolved": "Resolved", + "statusSuppressed": "Suppressed" + }, + "alertRulesTab": { + "addRule": "Add Rule", + "catHost": "Host Monitoring", + "catHostDesc": "Agent CPU/Memory/Disk/Offline", + "catMiddleware": "Middleware", + "catMiddlewareDesc": "Redis / MySQL metrics", + "catSite": "Site Probe", + "catSiteDesc": "HTTP/HTTPS availability & certificate", + "catSnmp": "SNMP", + "catSnmpDesc": "Network devices / BMC", + "defaultRules": "Default Rules", + "disabled": "Disabled", + "emptyDesc": "Click \"Add Rule\" or \"Default Rules\" to create", + "emptyTitle": "No {{cat}} rules", + "loading": "Loading rules..." + }, + "contactDialog": { + "addContactBtn": "Add Contact", + "addTitle": "Add Alert Contact", + "channelPlaceholder": "Select channel type", + "channelTypeRequired": "Channel Type *", + "contactAdded": "Contact added", + "contactUpdated": "Contact updated", + "customWebhook": "Custom Webhook", + "dialogDesc": "Select notification channel and enter the Webhook URL", + "editTitle": "Edit Alert Contact", + "emailLabel": "Email", + "feishu": "Feishu", + "namePlaceholder": "e.g., John Doe", + "nameRequired": "Name *", + "phoneLabel": "Phone", + "saveChanges": "Save Changes", + "saving": "Saving...", + "webhookRequired": "Webhook URL *" + }, + "contactGroup": { + "addBtn": "Add Contact Group", + "addTitle": "Add Alert Contact Group", + "added": "Contact group added", + "dialogDesc": "Configure alert contact group and select members", + "editTitle": "Edit Alert Contact Group", + "namePlaceholder": "e.g., Ops On-Call Team", + "nameRequired": "Group Name *", + "noContacts": "No contacts. Please add in \"Alert Contacts\" tab first.", + "notes": "Notes", + "notesPlaceholder": "e.g., Mon-Fri 9:00-21:00", + "saveChanges": "Save Changes", + "saving": "Saving...", + "selectContacts": "Select Contacts", + "selectedCount": "{{count}} contacts selected", + "updated": "Contact group updated" + }, + "hostDashboard": { + "agentOnline": "Agent Online", + "alert": "Alert", + "avgCpu": "Avg CPU", + "avgMemory": "Avg Memory", + "colAgentStatus": "Agent Status", + "colAgentVersion": "Agent Version", + "colHostId": "Hostname/ID", + "colIp": "IP Address", + "colRegion": "Region", + "cpuTop5": "CPU Usage Top 5", + "hostTotal": "Total Hosts", + "listTitle": "Host Monitoring List", + "memTop5": "Memory Usage Top 5", + "monitor": "Monitor", + "noData": "No host data", + "noDataDesc": "Please add a server first or wait for Agent data", + "perPage": "Per Page", + "realtime": "Realtime", + "searchPlaceholder": "Search hostname/IP...", + "threshold": "Threshold: 60% / 85%", + "totalCount": "items, {{count}} total", + "viewAlertRules": "View Alert Rules", + "viewMonitorChart": "View Monitor Charts" + }, + "logPanel": { + "apiNotFound": "API not found", + "authFailed": "Authentication failed", + "chartCountLabel": "Count", + "chartCountSuffix": "log entries", + "chartTimePrefix": "Time:", + "connFailed": "Connection failed", + "copied": "Copied", + "copiedDesc": "{{count}} log entries on current page", + "copyFailed": "Copy failed", + "copyFailedDesc": "Please use export or expand individual entries", + "custom": "Custom", + "defaultSuffix": " (default)", + "emptyHint": "Enter a query and click \"Query\" to view results", + "exported": "Exported", + "exportedDesc": "{{count}} log entries", + "history": "Query History", + "last5min": "Last 5 minutes", + "limit": "Limit", + "liveTailTitle": "Live Tail - refreshes last 5 min every 3 seconds", + "loadMoreFailed": "Load more failed", + "logResults": { + "addLabelFilter": "Add to query", + "collapseAll": "Collapse All", + "commonLabels": "Common Labels", + "content": "Content", + "contextTitle": "Context", + "copiedFullLog": "Full log copied", + "copyAll": "Copy All", + "copyFailedExpandDesc": "Please expand and manually select to copy", + "copyFullLog": "Copy Full Log", + "copyLine": "Copy", + "copyLineDesc": "Copy this line", + "excludeLabelFilter": "Exclude this value", + "expandAll": "Expand All", + "fullLog": "Full Log", + "hideLabels": "Hide Labels", + "jsonFormat": "JSON Format", + "labels": "Labels", + "level": "Level", + "lineNo": "Line No.", + "loading": "Loading...", + "noLabels": "No Labels", + "noLogs": "No Logs", + "noResults": "No log data", + "raw": "Raw", + "showLabels": "Show Labels", + "source": "Source", + "time": "Time", + "viewContext": "View Context" + }, + "noDatasource": "No {{name}} datasource available", + "noDatasourceHint": "No {{name}} datasource. Please add one in \"Log Library\" first.", + "noMatch": "No matching log entries found", + "noMatchDesc": "Query executed ({{ms}}ms), no results match current filters", + "query": "Query", + "queryFailed": "Query failed", + "requeries": "Retry Query", + "saveAs": "Save As", + "saveAsAlertItem": "Alert", + "saveAsQueryItem": "Query", + "savedQueries": "Saved Queries", + "share": "Share", + "shareCopied": "Share link copied", + "shareCopiedDesc": "Recipients open the link to auto-execute the query", + "shareCopyFailed": "Copy failed", + "shareLinkTitle": "Copy Share Link", + "tabCluster": "Log Clusters", + "tabLogs": "Raw Logs", + "tabStats": "Statistics", + "truncate": "Truncate Preview", + "truncateTitle": "Collapsed rows show only first 500 characters", + "viewField": "Field View", + "viewRaw": "Raw Logs", + "viewTable": "Table View", + "wrapLines": "Wrap Lines", + "wrapLinesTitle": "Auto-wrap long log entries" + }, + "metricCatalog": { + "colCrit": "Critical Threshold", + "colDesc": "Description", + "colMax": "Max", + "colMetric": "Metric", + "colType": "Type", + "colUnit": "Unit", + "colWarn": "Warning Threshold", + "desc": "Collectible metrics for Host / Middleware / SNMP / Site Probe, with default alert thresholds" + }, + "monitorRule": { + "catMiddleware": "Middleware", + "catSite": "Site Probe", + "dialogDesc": "Configure threshold alerts based on monitoring metrics", + "editRuleTitle": "Edit {{cat}} Alert Rule", + "enableRule": "Enable Rule", + "metricLabel": "Metric", + "nameMetricRequired": "Please fill in rule name and metric", + "namePlaceholder": "e.g., Redis connection count too high", + "newRuleTitle": "New {{cat}} Alert Rule", + "opEq": "= Equal to", + "opGt": "> Greater than", + "opGte": ">= Greater or equal", + "opLt": "< Less than", + "opLte": "<= Less or equal", + "ruleCreated": "Rule created", + "ruleUpdated": "Rule updated", + "thresholdLabel": "Threshold" + }, + "notifyPolicy": { + "cardFooter": "Card Footer", + "catHost": "Host Monitoring", + "catHostDesc": "Agent CPU/Memory/Disk/Offline", + "catMiddleware": "Middleware Monitoring", + "catMiddlewareDesc": "Redis/MySQL metrics", + "catSite": "Site Probe", + "catSiteDesc": "HTTP/TCP/DNS probe failures and latency", + "catSnmp": "SNMP Monitoring", + "catSnmpDesc": "H3C/Huawei/Cisco/Ruijie/Dell/Inspur", + "disable": "Disable", + "extraContacts": "Extra Contacts", + "fallbackHint": "If unconfigured, falls back to global Feishu channel in \"Notification Channels\".", + "loadFailed": "Failed to load notification policies", + "loading": "Loading notification policies...", + "msgTemplateTitle": "Message Template", + "noContacts": "No contacts", + "noGroups": "No contact groups", + "noTargetHint": "Please create targets in \"Alert Contacts\" and \"Contact Groups\" first.", + "notifyGroups": "Notify Groups", + "platformName": "Platform Signature", + "preview": "Preview", + "saveAll": "Save All", + "saved": "Notification config saved", + "selectedSummary": "{{groups}} groups + {{contacts}} contacts selected", + "testFailed": "Test failed. Check policy and contacts.", + "testSent": "Test notification sent", + "titleTemplate": "Title Template", + "topDesc": "Configure default notification targets by monitoring type. Templates apply to Feishu card titles and footer.", + "unsaved": "Unsaved" + }, + "promDs": { + "caCertPlaceholder": "PEM format CA certificate content...", + "configDesc": "Prometheus datasource config (similar to Grafana)", + "createSuccess": "Datasource created", + "namePlaceholder": "Prometheus datasource name", + "nameRequired": "Please enter a datasource name", + "noHeaders": "No custom headers", + "opFailed": "Operation failed", + "password": "Password", + "queryParamsPlaceholder": "e.g.: {\"timeout\": \"30s\"}", + "saveTest": "Save & Test", + "second": "seconds", + "testFailed": "Test failed", + "updateSuccess": "Datasource updated", + "urlInputRequired": "Please enter a URL first", + "urlRequired": "Please enter a Prometheus URL", + "username": "Username" + }, + "silenceDialog": { + "createTitle": "Create Silence Rule", + "dailyEnd": "Daily End", + "dailyStart": "Daily Start", + "descPlaceholder": "Optional notes", + "dialogDesc": "Alerts matching silence rules will not trigger notifications during silence period", + "editTitle": "Edit Silence Rule", + "effectiveWeekdays": "Effective Weekdays", + "endTime": "End Time", + "endTimeRequired": "Please set an end time", + "fri": "Fri", + "hintDaily": "Silence alerts during specified hours daily. Good for off-hours do-not-disturb.", + "hintOnetime": "Matching alerts silenced within the time window. Resumes automatically after window ends.", + "hintWeekly": "Silence only on selected weekdays during specified hours. Good for weekend maintenance.", + "mon": "Mon", + "namePlaceholder": "e.g., Maintenance Window Silence", + "nameRequired": "Please enter a rule name", + "ruleNameRequired": "Rule Name *", + "sat": "Sat", + "saving": "Saving...", + "silenceCreated": "Silence rule created", + "silenceType": "Silence Type", + "silenceUpdated": "Silence rule updated", + "startTime": "Start Time", + "sun": "Sun", + "thu": "Thu", + "tue": "Tue", + "typeDaily": "Daily Recurring", + "typeOnetime": "One-time", + "typeWeekly": "Weekly Recurring", + "wed": "Wed", + "weekdayRequired": "Weekly silence requires at least one weekday" + }, + "silenceRules": { + "active": "Active", + "addRule": "Add Silence Rule", + "createdAt": "Created {{time}}", + "dailyPrefix": "Daily", + "dayFri": "F", + "dayMon": "M", + "daySat": "S", + "daySun": "S", + "dayThu": "T", + "dayTue": "T", + "dayWed": "W", + "deleteConfirm": "Delete \"{{name}}\"?", + "deleteConfirmRestore": "Delete \"{{name}}\"? Alerts will resume after deletion.", + "deleteFailed": "Delete failed", + "deleteTitle": "Delete Silence Rule", + "deleted": "Deleted", + "disabled": "Disabled", + "disabledBadge": "Disabled", + "empty": "No silence rules. Alerts sent per notification policy.", + "emptyHint": "Click \"Add Silence Rule\" to create a maintenance window", + "emptyShort": "No silence rules", + "enabled": "Enabled", + "fetchFailed": "Failed to fetch silence rules", + "subtitle": "Maintenance windows / Do-not-disturb", + "title": "Silence Rules", + "totalCount": "{{count}} rules total", + "typeDaily": "Daily", + "typeOnetime": "One-time", + "typeWeekly": "Weekly", + "weekPrefix": "Wk" + }, + "traceMgr": { + "allServices": "All Services", + "avgLatency": "Avg Latency", + "colServiceOperation": "Service / Operation", + "day7": "7 days", + "errorRate": "Error Rate", + "errorTrend": "Error Rate Trend", + "errorTrendChart": "Error rate trend chart (shown after chart library integration)", + "fetchDetailFailed": "Failed to fetch trace detail", + "fetchServicesFailed": "Failed to fetch service data", + "fetchTracesFailed": "Failed to fetch trace data", + "hour1": "1 hour", + "hour24": "24 hours", + "hour6": "6 hours", + "inProgress": "In Progress", + "latencyDist": "Latency Distribution", + "latencyDistChart": "Latency distribution chart (shown after chart library integration)", + "loadingTraces": "Loading trace data...", + "logs": "Logs", + "min15": "15 minutes", + "noTraces": "No trace data", + "p95Latency": "P95 Latency", + "p99Latency": "P99 Latency", + "requestCount": "Requests", + "searchPlaceholder": "Search Trace ID, operation name...", + "serviceCount": "Services", + "serviceName": "Service Name", + "servicePerfTitle": "Service Performance Overview", + "spanCount": "Spans", + "spanDetailTitle": "Span Detail", + "spanName": "Span Name", + "subtitle": "Distributed trace query and service performance analysis", + "tabMetrics": "Performance Metrics", + "tabServices": "Service Topology", + "tabTraces": "Traces", + "totalDuration": "Total Duration", + "totalRequests": "Total Requests", + "traceDetailTitle": "Trace Detail" + } }, "task": { "cronJob": { @@ -6140,4 +6539,4 @@ "assistantts.s16f94bee": "流式响应不可用", "assistantts.sfeeaa489": "LLM 流式响应错误" } -} \ No newline at end of file +} diff --git a/scripts/i18n-verify-keys.py b/scripts/i18n-verify-keys.py new file mode 100644 index 0000000..c88803e --- /dev/null +++ b/scripts/i18n-verify-keys.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +""" +i18n-verify-keys.py — Detect missing/extra translation keys between zh-CN and en-US. + +Usage: + python3 scripts/i18n-verify-keys.py # Report only + python3 scripts/i18n-verify-keys.py --fix # Copy zh-CN values as placeholder + +Exit code: 0 if keys are in sync, 1 if there are missing keys. +""" +import json +import sys +import os +import argparse + +def flatten(d, prefix=''): + items = {} + for k, v in d.items(): + full = f'{prefix}.{k}' if prefix else k + if isinstance(v, dict): + items.update(flatten(v, full)) + else: + items[full] = v + return items + +def set_nested(d, key_path, value): + parts = key_path.split('.') + current = d + for part in parts[:-1]: + if part not in current or not isinstance(current[part], dict): + current[part] = {} + current = current[part] + current[parts[-1]] = value + +def main(): + parser = argparse.ArgumentParser(description='Verify i18n key parity between zh-CN and en-US') + parser.add_argument('--fix', action='store_true', help='Copy zh-CN values as placeholder for missing en-US keys') + args = parser.parse_args() + + base = os.path.join(os.path.dirname(__file__), '..', 'frontend', 'src', 'locales') + zh_path = os.path.join(base, 'zh-CN.json') + en_path = os.path.join(base, 'en-US.json') + + zh = json.load(open(zh_path)) + en = json.load(open(en_path)) + + zh_flat = flatten(zh) + en_flat = flatten(en) + + missing_en = sorted(set(zh_flat) - set(en_flat)) + extra_en = sorted(set(en_flat) - set(zh_flat)) + + print(f'zh-CN keys: {len(zh_flat)}') + print(f'en-US keys: {len(en_flat)}') + print(f'Missing in en-US: {len(missing_en)}') + print(f'Extra in en-US: {len(extra_en)}') + + if missing_en: + # Group by namespace + ns = {} + for k in missing_en: + prefix = k.split('.')[0] + '.' + k.split('.')[1] if '.' in k else k + ns[prefix] = ns.get(prefix, 0) + 1 + print('\nMissing by sub-namespace:') + for k, v in sorted(ns.items()): + print(f' {k}.* : {v} keys') + + print('\nFirst 20 missing keys:') + for k in missing_en[:20]: + print(f' - {k} = {zh_flat[k]}') + + if extra_en: + print('\nExtra en-US keys (not in zh-CN):') + for k in extra_en[:10]: + print(f' + {k}') + + if args.fix and missing_en: + for key in missing_en: + set_nested(en, key, zh_flat[key]) + with open(en_path, 'w', encoding='utf-8') as f: + json.dump(en, f, ensure_ascii=False, indent=2) + f.write('\n') + print(f'\n[fix] Copied {len(missing_en)} zh-CN values as en-US placeholders.') + print('[fix] NOTE: Replace with proper English translations!') + + if missing_en or extra_en: + sys.exit(1) + else: + print('\n✅ zh-CN and en-US keys are in sync.') + sys.exit(0) + +if __name__ == '__main__': + main()