const en = { common: { help: "Help", refresh: "Refresh", refreshing: "Refreshing…", query: "Search", clearFilters: "Clear filters", prevPage: "Previous", nextPage: "Next", perPage: "{{n}}/page", pagerMeta: "{{total}} items · page {{page}}/{{pages}}", jumpPage: "Go to page", pageSize: "Page size", exportCsv: "Export CSV", exporting: "Exporting…", exportOk: "Exported {{count}} rows", exportTruncated: "Exported first {{count}} of {{total}} rows (truncated)", exportFailed: "Export failed", opFailed: "Operation failed", empty: "-", cancel: "Cancel", copy: "Copy", copied: "Copied", }, workbench: { title: "Workbench", modules: "Modules", sections: "Sections", moduleCount: "{{count}} modules", backToWorkbench: "Back to Workbench", openInNewTab: "Open in new tab", openModule: "Open or switch to this module tab", monitoring: "Integration", operations: "Operations", system: "System", welcomeEyebrow: "Workbench", welcomeTitle: "Welcome back, {{name}}", welcomeSub: "Monitor sync, topology, and ops tasks from one place.", today: "Today · {{date}}", kpi: { uptime: "API uptime", health: "System health", tasks: "Active tasks", healthOk: "OK", healthWarn: "CHECK", healthUnknown: "N/A", }, statusTitle: "Runtime status", hostHealth: "HOST", platformCapacity: "Platform capacity", channels: "CHANNELS", fleet: "FLEET", fleetNe: "NE", fleetNeUnit: "units", fleetNeKnown: "Known", fleetNePass: "Pass", fleetNeFail: "Fail", fleetNeTesting: "Testing", fleetNeUnknown: "Untested", fleetNeNoTag: "No tag", fleetNeTags: "Top tags", fleetTopo: "Topology", fleetTopoNodes: "nodes", fleetTopoEdges: "edges", fleetTopoOther: "Other regions", fleetTopoUnassigned: "Unassigned", fleetTopoNoRegion: "No regions", fleetTopoDiscover: "Last discover", fleetTopoUpdated: "Summary updated", fleetSched: "Schedulers", fleetSchedConfig: "Config sync", fleetSchedLldp: "LLDP", fleetSchedPort: "Port traffic", fleetSchedNeCollect: "NE collect", fleetSchedFabric: "Fabric reconcile", fleetSchedOn: "ON", fleetSchedOff: "OFF", fleetSchedMode: "Mode", fleetSchedHeartbeat: "Heartbeat", fleetSchedFresh: "Fresh", fleetSchedStale: "Stale", sidePanel: "Console side panel", consoleId: "OPS CONSOLE", refreshedAt: "Updated {{time}}", taskRail: "ACTIVE TASKS", taskRailAll: "ALL", taskRailEmpty: "No active tasks", taskTotal: "POOL {{count}}", announceTitle: "Announcements", recentTitle: "Recent operations", recentMore: "View all", quickTitle: "Quick actions", emptyRecent: "No recent audit entries.", gauge: { cpu: "CPU", mem: "Memory", storage: "Storage", storageUsed: "Used", storageUnavailable: "Unavailable", cli: "CLI budget", db: "DB pool", webcrt: "WebCRT", tasks: "Active tasks", low: "Low load", normal: "Normal", high: "Busy", }, quick: { tasks: "Task overview", logs: "Audit logs", users: "User admin", apiKeys: "API keys", sessions: "Sessions", }, announce: { a1: "Dark workbench shell is live — other modules follow the same palette.", a1Date: "2026-08-04", a2: "LLDP collect supports pause / resume / stop; interrupted jobs resume after restart.", a2Date: "2026-08-04", a3: "Config sync and LLDP KPI boards refresh while jobs are running.", a3Date: "2026-08-03", }, recentCols: { action: "Action", module: "Module", time: "Time", user: "User", }, cards: { umeSync: "UME Sync", network: "Network Management", managedNe: "NE Management", collect: "Batch Collect", webcrt: "WebCRT", topology: "Topology", portTrafficWall: "Traffic wall", users: "Users", audit: "Audit", apiKeys: "API Keys", sessions: "Login sessions", }, }, network: { title: "Network Management", nav: { detail: "Network details", tasks: "Monitoring tasks", cutoverCompare: "Cutover compare", topologyMgmt: "Topology management", devices: "Device list", topology: "Topology", lldpLinks: "LLDP", topoClassify: "Classify", alarms: "Alarms", configs: "Configurations", webcrt: "WebCRT", collectTasks: "Collection tasks", configSync: "Config sync", portTraffic: "Port traffic", portTrafficWall: "Traffic wall", bizState: "Business monitor", bizCompare: "Business compare", bizMigration: "Cutover monitor", compareTemplates: "Compare templates", monitorTemplates: "Monitor templates", }, collapseNav: "Collapse sidebar", expandNav: "Expand sidebar", placeholder: { title: "Coming soon", body: "This capability is not available yet.", }, }, bizState: { title: "Business monitor", create: "Create task", createHint: "Pick any valid NE (managed / UME); search supported", created: "Task created", createFailed: "Create failed", empty: "No business-state tasks yet.", pickNe: "Select NE…", neKeywordPh: "Name / IP / ID", allSource: "All sources", selectedNe: "Selected", neEmpty: "No matching NEs.", colSource: "Source", colVendor: "Vendor", colConnect: "Connect", start: "Start schedule", pause: "Pause", statusPaused: "Paused", scheduleEnabled: "Enable schedule", scheduleOn: "Scheduled", scheduleOff: "Manual", scheduleHint: "When checked, collect on interval; otherwise only Collect now.", collectNow: "Collect now", delete: "Delete", confirmDelete: "Delete this task and all batches?", deleted: "Deleted", started: "Schedule enabled", paused: "Schedule paused (manual only)", collecting: "Collecting", detail: "Details", interval: "Interval", intervalUnitDays: "days", intervalUnitHours: "hours", intervalUnitSeconds: "seconds", retention: "Retention days", retentionHint: "Batches older than this are purged; baselines and compare/cutover refs are kept.", dailyKeepEnabled: "Daily keep policy", dailyKeepCount: "Keep per day", dailyKeepHint: "When on: keep only the newest N batches per past day; extras purge next day (off by default).", markBaseline: "Mark baseline", unmarkBaseline: "Unmark baseline", baseline: "Baseline", protected: "Protected", deleteBatch: "Delete", bulkDelete: "Bulk delete", confirmDeleteBatch: "Delete this batch? Protected batches cannot be deleted.", confirmBulkDelete: "Delete {{count}} selected batches? Protected ones are skipped.", batchDeleted: "Batch deleted", bulkDeleted: "Deleted {{deleted}}, skipped {{skipped}}", purgeNow: "Purge now", purgeOk: "Purge done: dropped {{dropped}}", selectAll: "Select deletable", colProtect: "Protect", saveSchedule: "Save schedule", scheduleSaved: "Schedule saved", colInterval: "Interval", profiles: "Monitor items", enable: "On", command: "Command template", params: "Bindings", discoverVrf: "Discover VRFs", discoverBind: "Discover / Bind", saveBindings: "Save bindings", cancel: "Close", bindTitle: "Select bindings", unbound: "Not bound", bindHintRequired: "Bind params before collect", discoverLoading: "Discovering…", discoverEmpty: "No candidates discovered", selectAllVrfs: "Select all", deselectAllVrfs: "Deselect all", batches: "Batches", viewBatch: "Open", export: "Export", noBatches: "No batches yet", batchDetail: "Batch detail", batchWorkbook: "Batch workbook", batchId: "Batch ID", copyBatchId: "Copy batch ID", sheetCommands: "Commands", sheetLldp: "LLDP neighbors", sheetVrfRoute: "VRF route summary", sheetIsis: "ISIS adjacency", sheetIfaceBrief: "Interface brief", sheetArp: "ARP", sheetNd6: "ND6 cache", sheetBgpPeer: "BGP peers", sheetConfigIface: "Interface intent", sheetConfigVrf: "VRF intent", sheetConfigBgp: "BGP intent", sheetFilterPh: "Filter this sheet…", sheetEmpty: "No rows in this sheet", sheetLoading: "Loading…", collectCommand: "Collect command", viewRawLog: "Raw log", rawLogTitle: "Raw collect log", rawLogEmpty: "No raw output", filterColumn: "Column", filterAllCols: "All columns", listFilterPh: "Filter NE / IP / vendor / status", purposeFilter: "Task purpose", purposeAll: "All purposes", purposePortrait: "Portrait", purposeCutoverHf: "Cutover HF", colPurpose: "Purpose", colNe: "NE", colStatus: "Status", colLast: "Last collect", colTime: "Time", colRows: "Rows", colActions: "Actions", colCommand: "Command", colMessage: "Message", pickTask: "Open details to manage a task", noNeedBind: "This item needs no parameter bindings", discoverFailed: "Discover failed", bindingsSaved: "Bindings saved", }, bizCompare: { title: "Business compare", templates: "Compare templates", newTemplate: "New template", editTemplate: "Edit template", saveTemplate: "Save template", templateSaved: "Template saved", templateDeleted: "Template deleted", confirmDeleteTemplate: "Delete this compare template?", confirmDeleteJob: "Delete this compare job and all runs?", confirmDeleteRun: "Delete this compare run? Job config is kept.", deleteJob: "Delete job", deleteRun: "Delete run", runDeleted: "Compare run deleted", emptyTemplates: "No templates yet. Create one or import a JSON file.", exportTemplate: "Export", importTemplate: "Import", templateExported: "Template exported", templateImported: "Template imported", templateImportInvalid: "Invalid file: need name and metrics", templateImportHint: "Import/export JSON to share templates privately (no job/batch data)", templateHint: "Pick metrics, then set Key / interface / compare / display. Compare modes: exact, abs delta, or relative %. Display can include context-only columns. Empty compare = presence-only.", stepPickMetrics: "① Pick metrics", stepConfigRules: "② Configure compare rules", pickMetricsHint: "Check metrics to add. Then configure each sheet below.", pickMetricsHintShort: "Click to add/remove; switch sheets in the left list below.", showMetrics: "Show metric picker", hideMetrics: "Hide metric picker", noMetricsYet: "Select at least one metric first", selectedCount: "{{n}} selected", tabFields: "Field rules", tabFilters: "Row filters", fieldsHintShort: "Key aligns rows; Compare fields take part in value diff; empty compare = presence-only.", tagCompareN: "{{n}} cmp", colFields: "Fields", keyFields: "Key", ifaceFields: "Interface", compareFields: "Compare", compareMode: "Mode", tolerance: "Tolerance", displayField: "Display", keyAlwaysDisplay: "Key columns are always shown", modeEq: "Exact equal", modeNumeric: "Abs delta", modePercent: "Relative %", normalizeField: "Norm", ignoreFields: "Ignore", field: "Field", note: "Note", keyRequired: "Select at least one key field", keyIsIdentity: "Key aligns rows; not used as a value-compare field", metricsRequired: "Add at least one metric sheet", noMetricFields: "No fields for this metric", rowFilters: "Row filter rules", rowFiltersHint: "Filter rows before compare. Conditions in a group are AND; groups are OR. Selecting ARP seeds editable default rules you can change.", rowFiltersHintShort: "AND in group · OR across groups · empty = all rows", rowFiltersEmpty: "No filters → compare all rows.", filterClear: "Clear", filterAddOr: "Add group (OR)", filterAddAnd: "Add condition (AND)", filterAddOrShort: "+ OR group", filterAddAndShort: "+ AND", filterRemoveGroup: "Remove group", filterGroupN: "Group {{n}}", filterOr: "OR", filterAnd: "AND", filterField: "Field", filterOp: "Op", filterValue: "Value", normalizeNone: "None", normalizeLower: "lower", normalizeUpper: "upper", normalizeMac: "MAC", normalizeEmpty: "empty→blank", templateFilterPh: "Filter template / metric", jobFilterPh: "Filter job / template / status", addMetric: "Add metric", pickMetric: "Pick metric…", addSheet: "Add to template", removeSheet: "Remove sheet", sheetIdRequired: "sheet_id is required (unique per split)", duplicateSheetId: "sheet_id must be unique", sheetTitle: "Sheet title", sheetId: "sheet_id", splitSheet: "Split / duplicate", colSheets: "Sheets", sheetsUnit: "sheets", presenceShort: "presence", modeFields: "Mode: compare selected field values", modeFieldsShort: "fields", modePresence: "Mode: presence-only (entry set match)", mapping: "Port mapping (optional)", newMapping: "New mapping…", mapName: "Mapping name", saveMapping: "Save mapping", validateMapping: "Validate", validateNeed: "Select mapping and before/after batches first", mapHint: "One pair per line: before_if,after_if (comma / tab / pipe). Example:\ngei-0/1/1,gei-0/1/2\nxgei-0/1/0,xgei-0/2/0\n# comments start with #. Not slash-grouped A,B/C,D.", mappingSaved: "Mapping saved", mappingOptionalHint: "Optional. Empty = ignore local port renames (match on non-interface keys). Fill to apply port map. Main-port pairs also cover .subinterfaces (suffix kept).", ifaceNormalize: "Interface normalize", ifaceNormalizeHint: "Template rules, one per line: alias,canonical. Applied before port mapping. Example:\nGE,gei\nSG,smartgroup\nXGE,xgei", createJob: "Create compare job", defaultJobName: "Cutover compare", jobName: "Name", template: "Template", mode: "Mode", modeManual: "Manual batches", modeAuto: "Auto (pin before; latest after batch)", beforeTask: "Before task", beforeBatch: "Before batch", afterTask: "After task", afterBatch: "After batch", sideBefore: "Before", sideAfter: "After", sidesVs: "vs", sidesTitle: "Compare sides", pick: "Select…", sameAsBefore: "Same as before / select…", autoHint: "auto: new after-task batches trigger compare", createCompare: "Create job", createSteps: "Create steps", stepBasics: "Basics", stepSheets: "Sheets", stepBatches: "Batches", stepMapping: "Port mapping", next: "Next", back: "Back", enabledSheets: "Compare sheets", enabledSheetsHint: "Toggle sheets; all on by default. Off sheets are skipped.", noTemplateSheets: "This template has no sheets", needJobName: "Enter a job name", needTemplate: "Select a compare template", needSheets: "Enable at least one sheet", needBeforeBatch: "Select before task and batch", needAfterBatch: "Select after batch", runNow: "Run now", saveJob: "Save config", jobSaved: "Job config saved", jobDeleted: "Compare job deleted", jobList: "Compare jobs", detail: "Details", edit: "Edit", delete: "Delete", tabConfig: "Job config", tabResult: "Result", runs: "Run history", noRuns: "No runs yet — run a compare first", result: "Result", pickBatchRun: "Select compare run", pickRun: "Select run…", runCount: "{{n}} runs", resultEmpty: "No matching diff rows", resultFilterPh: "Filter key / values…", kindAll: "All", kindDiff: "Fail", kindAdded: "Added", kindRemoved: "Missing only", kindChanged: "Mismatch only", kindUnchanged: "Pass", kindAddedShort: "Added", kindRemovedShort: "Missing", kindChangedShort: "Mismatch", kindUnchangedShort: "Pass", kindFail: "Fail", kindPass: "Pass", kindSuccess: "Pass", successCount: "Pass", passRateShort: "Pass %", verdictPass: "Pass", verdictFail: "Fail", failCount: "Failed rows", missCount: "Missing", extraCount: "Extra", mismatchCount: "Mismatch", matchCount: "Match", failFields: "Failed fields", failFieldsEmpty: "—", failWholeRow: "Whole row", pairBefore: "pre", pairAfter: "post", sheetAllPass: "All match", filterFailField: "Failed compare field", filterFailFieldAll: "Any failed field", resultEmpty: "No matching rows", resultFilterPh: "Filter identity / values…", diffCount: "{{n}} failed", passRate: "Pass rate", passOk: "All passed", passWarn: "Has failures", runOverview: "Verdict", sheetCurrent: "Current check", diffCountLabel: "Failed rows", diffRate: "Fail rate", beforeCount: "Before rows", afterCount: "After rows", matchedRows: "Matched", totalRows: "Total rows", sheetCount: "Sheets", sheetNavTitle: "Checks", sheetNavHint: "{{n}} checks · click to switch", collapseSheetNav: "Collapse checks", expandSheetNav: "Expand checks", sheetFailCount: "{{n}} failed", topChangedFields: "Top failed fields", exportTables: "Download tables", exportOk: "Compare tables exported", fullscreen: "Fullscreen board", exitFullscreen: "Exit fullscreen", emptyJobs: "No compare jobs yet", created: "Compare job created", ran: "Compare finished", colName: "Name", colMode: "Mode", colStatus: "Status", colKind: "Verdict", colResult: "Result", colDetail: "Detail", colFailFields: "Failed fields", colActions: "Actions", colKey: "Identity", colChange: "Changes", }, bizMigration: { title: "Cutover monitor", hint: "Separate from business compare: baseline drift, expect set, dual-device verdict. Reuses collect tasks and port mappings.", hintPort: "Metric focus: port status (interface_brief / admin·phy·prot). Baseline → pick ports → evaluate board.", hintShort: "Monitor template sets rules; bind collect + dual baselines → expect → evaluate board.", create: "Create task", createHint: "Except portrait baselines, the full cutover flow runs on this page; create auto-spawns HF biz-monitor tasks on schedule.", createHintShort: "Pick old/new NEs → monitor template & metrics → interval/window → baselines → port map → create. HF tasks are auto-created.", pickOldNe: "Old NE", pickNewNe: "New NE", pickNe: "Pick NE", pickNeHint: "Click to pick a device", hideNePicker: "Hide list", nePairReady: "Both NEs selected — continue, or click a card to re-pick.", needNePair: "Enter a name and select old/new NEs", needProjectName: "Enter a task name", needMonitorTemplate: "Select a monitor template", needMetrics: "Select at least one metric", noPortraitBaseline: "No portrait task/batches for this NE yet — run portrait collect in business monitor first (baselines can be set later).", baselineOptionalHint: "Baselines can be set later in detail; pick from portrait batches when available.", metricInterval: "Per-metric interval (sec)", metricIntervalOptional: "Per-metric interval (optional)", metricIntervalHint: "Leave blank to use the global interval; a filled value creates a separate HF task.", scheduleHint: "Enter the window in your local time; stored as UTC. Scheduler pauses HF outside the window.", sectionSchedule: "Collect interval & window", sectionBaseline: "Baselines & mapping", sectionDevices: "Old / new NEs", sectionMetrics: "Collect metrics", createSteps: "Create steps", stepDevices: "Devices", stepTemplate: "Template", stepSchedule: "Schedule", stepBaseline: "Baselines", next: "Next", back: "Back", colHf: "HF", hfReady: "Ready", hfMissing: "Missing", showCreateMore: "More: template / mapping / baselines", hideCreateMore: "Hide more", setupHintShort: "Portrait tasks feed baselines; HF tasks feed cutover collect. Configure template, metrics, window and dual baselines here.", createProject: "Create task", projectName: "Task name", projectNamePh: "e.g. PE-A / PE-B cutover-1", sectionName: "1. Task name", sectionTasks: "2. Bind collect tasks (required)", sectionTasksHint: "Pick the old-device and new-device business-monitor tasks.", sectionOptional: "3. Monitor template, mapping & baselines (optional)", sectionOptionalHint: "Skip for now if needed; set later under Baseline. Defaults to the system monitor template when omitted.", sectionPair: "Bound collect tasks", sectionPairHint: "Collect tasks are fixed at create time; adjust monitor template, mapping and baselines here.", sectionBaselineHint: "Both old and new baselines are required; old lists expect objects, new feeds dual-side verdict.", optionalNone: "None", monitorTemplate: "Monitor template", monitorTemplateHint: "Chooses which metrics to compare and dual-side success semantics. Defaults to the system monitor template.", compareTemplate: "Compare template", compareTemplateBound: "Bound compare template: {{name}}", portMappingHint: "With a mapping, iface metrics compare only mapped ports (they must appear after cutover). Same-name ports outside the map are not a migration; old down + new up is an anomaly. No mapping means same-name compare. BGP/PW still use the batch expect set.", baselineNeedTask: "Select the matching collect task first", baselinePickHint: "Choose from that task’s collect batches", oldTask: "Old-device portrait task", newTask: "New-device portrait task", portMapping: "Port mapping", oldBaseline: "Old baseline batch", newBaseline: "New baseline batch", needProjectFields: "Enter a name and pick old/new portrait tasks", needExpectPorts: "Select at least one expected object for this batch", needBaselineFirst: "Save an old baseline batch first to list expect objects", needBothBaselines: "Save both old and new baseline batches before evaluating", errOldBaselineRequired: "Old baseline batch is required", errNewBaselineRequired: "New baseline batch is required", needBizStateTasks: "No portrait business-monitor tasks yet. Create one first:", projectCreated: "Task created", projects: "Tasks", empty: "No cutover tasks yet.", emptyProjects: "No cutover tasks yet", listFilterPh: "Filter name / NE / IP / status", kpiProjects: "Tasks", kpiActive: "Active", detail: "Detail", cancel: "Close", colName: "Task", colPair: "Old → New", colBaseline: "Baseline", colCreated: "Created", colActions: "Actions", baselineSet: "Set", baselineUnset: "Unset", statusActive: "Active", statusDone: "Done", statusPaused: "Paused", statusPending: "Pending", statusReview: "Review", tabSetup: "Baseline", tabBatches: "Batches", tabBoard: "Board", saveBaseline: "Save", baselineSaved: "Baseline saved", pickExpectPorts: "Expect", portFilterPh: "Filter", emptyBaselinePorts: "No baseline data", batchLabel: "Batch label", batchId: "Batch ID", copyBatchId: "Copy batch ID", defaultBatchLabel: "Night N", expectPorts: "Expected ports (old side)", expectPortsPh: "Comma or newline separated", createBatch: "New", batchCreated: "Batch created", batches: "Batches", startBatch: "Start", finishBatch: "Finish", evaluate: "Evaluate", evaluated: "Done", statusUpdated: "Status updated", board: "Board", metricPort: "Port status", progress: "Progress", anomaly: "Anomaly", onlyExpect: "Expect only", onlyExpectHint: "Shows all drift by default; check to filter to this batch's expect set", pinCurrentBatches: "Current collect batches", pinOldBatch: "Old current", pinNewBatch: "New current", pinLatest: "Latest", runHistory: "Runs", runLatest: "Latest", colRuleHit: "Rule hit", colCommand: "Collect command", mappingBound: "Mapping saved and bound to this project", allMetrics: "All metrics", colMetric: "Metric", colKey: "Object", colPort: "Old port", openRedChip: "Red {{n}}", metaPortrait: "Portrait", metaHf: "HF", metaBaseline: "Baseline", baselineOk: "set", baselineMissing: "missing", needBaselineFirst: "Set baseline first", enableHighfreq: "Enable HF", highfreqReady: "HF ready", collectNow: "Collect now", collectTriggered: "Collect triggered", openBizState: "Biz monitor", hfNotBound: "Unbound", projectCollectMetrics: "Metrics", hfInterval: "Interval(s)", hfStart: "Start", hfEnd: "End", markProjectDone: "Done / stop HF", confirmProjectDone: "Complete project and pause all HF?", projectMarkedDone: "Done; HF paused", deleteProject: "Delete", confirmDeleteProject: "Delete project? HF will pause and batches removed.", projectDeleted: "Deleted", acceptPassedShort: "Pass", acceptFailedShort: "Fail", colMapped: "Mapped new", colDesc: "Description", colOldPort: "Old port", colNewPort: "New port", colOldStatus: "Old admin/phy/prot", colNewStatus: "New admin/phy/prot", colOld: "Old", colNew: "New", colVerdict: "Verdict", emptyDiffs: "No rows yet — run Evaluate", emptyRed: "No red tickets", verdictMigrated: "Migrated", verdictMigrating: "Migrating", verdictLost: "Lost", verdictAnomaly: "Anomaly", verdictNotInvolved: "Not involved", verdictUnexpected: "Unexpected new", verdictOk: "OK", highfreqHint: "", boundTasks: "Bound tasks", oldPortraitTask: "Old portrait", newPortraitTask: "New portrait", oldHfTask: "Old HF", newHfTask: "New HF", projectCollectHint: "", hfWindowInactive: "HF window inactive or project done", baselinePortraitOnly: "", collectSkipped: "skipped", collectSkippedBanner: "Skipped: {{metrics}}", currentMissing: "no current", currentMissingBanner: "No current: {{metrics}}", highfreqReadyInactive: "HF bound (window inactive, no collect)", collectPartial: "Partial fail (ok {{ok}} / fail {{fail}})", errMetricNeedsBindings: "Metric \"{{metric}}\" needs bindings ({{params}}); not supported for cutover HF — remove it", errNoProfileForMetric: "Metric \"{{metric}}\" has no collect profile for this vendor", acceptTitle: "Acceptance", acceptPassed: "Passed", acceptFailed: "Failed ({{n}} red)", newBaselineMissingBanner: "Missing new baseline: {{metrics}}", acceptCarryHint: "May continue with red", openRedHint: "Red {{n}}", redTitle: "Red tickets", redOpen: "open", resolveRed: "Resolve", redResolved: "Resolved", batchCreatedWithRed: "Batch created ({{n}} red)", colStatus: "Status", verdictUnfinished: "Unfinished", }, bizMonitorTpl: { title: "Monitor templates", hint: "Compare templates define HOW to align/diff; monitor templates define WHEN dual-device migrate succeeds — one sheet tab per compare sheet.", create: "New monitor template", edit: "Edit", save: "Save", cancel: "Close", created: "Created", saved: "Saved", deleted: "Deleted", confirmDelete: "Delete this monitor template?", delete: "Delete", empty: "No monitor templates yet.", listFilterPh: "Filter name / compare template / note", kpiTotal: "Templates", colName: "Name", namePh: "e.g. Status cutover monitor", colCompare: "Compare template", colCollect: "HF collect metrics", colNote: "Note", colActions: "Actions", needName: "Name is required", defaultsInvalid: "Invalid defaults JSON", overridesInvalid: "Invalid sheet_overrides JSON", exportTemplate: "Export", importTemplate: "Import", templateExported: "Template exported", templateImported: "Template imported", templateImportInvalid: "Invalid import file: need name and compare_template_id (or compare_template_name)", templateImportHint: "Import/export JSON to share monitor templates (no job/batch data)", templateImportCompareMissing: "Matching compare template not found — import or create it first", formHint: "Pick a compare template → same sheet tabs appear → set success conditions per sheet.", compareHint: "Owns which sheets and Key/compare fields; here you only set dual verdict rules.", pickCompare: "Pick compare template…", openCompare: "Manage compare templates", collectAll: "All sheets", collectPh: "interface_brief, bgp_peer (empty = all)", collectHint: "HF collect metrics; empty / “All sheets” uses every compare-template sheet.", collectTitle: "High-freq collect subset", defaults: "defaults (JSON)", overrides: "sheet_overrides (JSON)", overridesHint: "Optional per-metric status_fields / success patterns.", globalPolicy: "Global policy", outOfExpect: "Out-of-expect objects", outOfExpectHint: "How to treat drift outside the batch expect set.", ooeStrict: "Strict (red anomaly)", ooeWarn: "Warn (yellow only, no red tickets)", ooeIgnore: "Ignore (always not involved)", sheetRules: "Per-sheet migrate rules", applyAllPresets: "Apply recommended presets to all", applyAllPresetsShort: "All presets", sheetsUnit: " sheets", sheetHowReadonly: "Compare template (read-only)", iface: "Interface", compare: "Compare", presenceOnly: "presence only", editInCompare: "Edit HOW", presetAuto: "Recommended", presetSkip: "Skip sheet", skipDual: "Skip dual for this sheet", skipDualHint: "Skipped in cutover evaluate (use a compare job for counters).", statusFields: "Fields used for up/down", statusFieldsHint: "Ports: admin/phy/prot; BGP: state; ARP: leave empty (presence only).", statusOptionalHint: "With no status fields, only presence (gone/added) is used.", downValues: "Down values", upValues: "Up values", noFields: "No fields (pick metrics on the compare template first)", successPatterns: "Success rules (old OR-groups × new OR-groups)", successPatternsShort: "Success", successPatternsHint: "Presence is whether the row exists. Value change binds a field and a strategy. AND inside a group, OR between groups.", successPatternsHintShort: "AND in group · OR across groups · old+new both match ⇒ success", successPatternsHintFree: "Each side has two condition kinds: presence (no field) or value change (must bind a field). Inside a group everything must match (AND). Between groups, any group is enough (OR).", anomalyPatternsShort: "Anomaly", anomalyPatternsHint: "Same shape as success. Match ⇒ red. Check “don’t care” on one side to watch only the other (e.g. new-side gone/down). Not-yet-success is still migrating, not anomaly.", anomalyPatternsHintShort: "Success first; then anomaly. Don’t-care watches one side. Mid-state ≠ anomaly.", successRow: "Success {{n}}", anomalyRow: "Anomaly {{n}}", removeRow: "Remove", oldSideAny: "Old", newSideAny: "New", groupLogicHint: "AND inside a group, OR between groups", sideDontCare: "Don’t care this side", sideDontCareHint: "Judged by the other side only.", groupN: "Group {{n}}", addAnd: "Add to this group (AND)", addOr: "Add another group (OR)", addAndShort: "+ AND", addOrShort: "+ OR group", addSuccessRow: "+ Success rule", addAnomalyRow: "+ Anomaly rule", modeAny: "Match any", modeAll: "Match all", addCond: "Add condition", pickField: "Pick field…", condValue: "Value", opEmpty: "empty", opNotEmpty: "not empty", opChanged: "changed vs baseline", opUnchanged: "same as baseline", op_eq: "equals", op_ne: "not equals", op_in: "in", op_not_in: "not in", op_empty: "empty", op_not_empty: "not empty", op_changed: "changed vs baseline", op_unchanged: "same as baseline", and: "and", groupPresence: "Presence", groupValue: "Value change", groupStatus: "Status class", groupField: "Field condition", tokRemoved: "Gone", tokAdded: "Added", tokUnchanged: "Unchanged", tokChanged: "Changed", tokUp: "up", tokDown: "down", tokOther: "other", or: "or", tagSkip: "skip", tagRules: "{{n}}", tagEmpty: "empty", needCompareFirst: "Pick a compare template that has sheets first.", showMore: "More: note / HF collect / JSON", hideMore: "Hide more", showJson: "Advanced: edit JSON", hideJson: "Back to visual editor", }, portTraffic: { title: "Port traffic", create: "Monitor device", backList: "Back to list", created: "Device monitoring added", createFailed: "Create failed", updated: "Saved", interfacesSaved: "Interfaces updated", started: "Started", paused: "Paused", stopped: "Stopped", statusRunning: "Running", statusPaused: "Paused", statusStopped: "Stopped", statusDraft: "Draft", collecting: "Collecting", deleted: "Deleted", confirmDelete: "Delete this device monitoring and its samples?", empty: "No devices monitored yet. Pick a NE and interfaces to start.", keywordPh: "Name / IP / note / ID", allStatus: "All statuses", exportDevicesName: "port-traffic-devices", exportEventsName: "port-traffic-events", wall: "Ops wall", wallTitle: "Port traffic wall", toDevices: "Manage devices", wallEmpty: "No monitored devices yet. Add a device and pick interfaces first.", fullscreen: "Fullscreen", exitFullscreen: "Exit fullscreen", board: "Board", boardPick: "Select board", boardPickHint: "Select or create a traffic wall board.", boardEmpty: "No boards yet. Create one for cutover or duty monitoring.", boardDefaultName: "Untitled board", boardName: "Name", boardCols: "Columns", boardCreate: "New board", boardEdit: "Edit layout", boardSave: "Save", boardCancel: "Cancel", boardSaved: "Board saved", boardCreated: "Board created", boardDeleted: "Board deleted", boardConfirmDelete: "Delete this board and its panels?", boardUnsavedLeave: "You have unsaved changes. Leave anyway?", boardOpenTab: "Open in new tab", boardOpen: "Open wall", boardListHint: "Open a board in its own browser tab; launching Network from the workbench will not refresh open walls.", boardPanelCount: "Panels", boardUpdated: "Updated", boardClosedHint: "You can close this tab, or reopen from Network → Traffic wall list.", boardAddPanel: "Add panel", boardNoPanels: "No panels yet. Edit the board and add interfaces.", boardPickDevice: "Select device", boardPanelSettings: "Panel settings", boardPanelTitle: "Title (optional)", boardColSpan: "Column span", boardPanelStale: "Interface missing or inactive. Please reselect.", boardRetarget: "Reselect port", edit: "Edit", save: "Save", start: "Start", pause: "Pause", stop: "Stop", delete: "Delete", wizStep1: "Device", wizStep2: "Interfaces", wizStep3: "Params", step1: "Params", step2: "NEs", step3: "Interfaces", step4: "Confirm", prev: "Back", next: "Next", fieldTitle: "Task name", titlePh: "e.g. Uplink group", titleRequired: "Task name is required", portsRequired: "Select at least one interface", interval: "Interval (sec)", retention: "Retention (days)", retentionHintWizard: "Use ≥8 days for week-over-week compare", concurrency: "Concurrency", note: "Note", notePh: "Optional", startNow: "Start after create", neKeywordPh: "Name / IP / vendor", deepLinkHint: "Opened from topology: NE {{ne}}{{ifname}}. Confirm selection, then continue.", deepLinkIfname: ", interface {{ifname}}", selectedNe: "{{count}} NE(s) selected", selectedOneNe: "Selected", selectedPorts: "{{count}} interface(s) selected", pickNeFirst: "Select one NE first", discover: "Discover interfaces", discoverDetail: "Discover detail", discoverCommand: "Command", discoverRaw: "Raw log", discoverOk: "Discover succeeded", discoverFail: "Discover failed", confirmCreate: "Start monitoring", wallTask: "Task", wallPort: "Interface", wallDevice: "Device", wallDeviceLabel: "Device", pickPort: "Select interface", range: "Range", compare: "Compare", compareOff: "Off", compareDay: "Yesterday (same TOD)", compareWeek: "Last week (same weekday)", compareShift: "Previous window", compareCustom: "Custom offset", offsetHours: "Offset (hours)", aheadHours: "Extend past now (hours)", aheadHoursHint: "Push the chart end past now. With period compare enabled, the overlay can show trend after the current clock; 0 ends at now.", mapBaselineHint: "Mapped baseline can be combined with period compare; turn on Yesterday yourself when you need that contrast.", baseline: "Baseline", baselineAt: "Baseline time", baselineDevice: "Baseline device", baselinePort: "Baseline interface", seriesCurrentIn: "Current In", seriesCurrentOut: "Current Out", seriesBaselineIn: "Baseline In", seriesBaselineOut: "Baseline Out", mapBaselinePort: "Mapped baseline interface", mapBaselineTask: "Mapped baseline task", mapBaselineNone: "None", retentionHint: "Week compare needs retention_days ≥ 8 or the baseline may be empty.", collectNow: "Collect now", collectStarted: "Collect triggered", collectBusy: "Collect already running", targetError: "Last interface error", waitSamples: "Samples appear after the device runs; or click Collect now. Chart auto-refreshes.", waitAfterError: "Collect failed — open Logs under Traffic monitoring for details, then Collect now.", log: "Logs", logTitle: "Collect logs", logEmpty: "No log entries yet.", logDevice: "Device", logLevel: "Level", logTime: "Time", logMessage: "Message", logClose: "Close", logHasError: "Has error", rebind: "Rebind NE", rebindTitle: "Rebind network element", rebindOk: "Rebound to selected NE; collect can resume", rebindFailed: "Rebind failed", rebindHint: "Pick the correct NE and confirm. IPs may not be unique, and a matching IP does not mean the same device. History is kept.", rebindCurrent: "Current binding", rebindEmpty: "No NEs found — add the device back under inventory first.", rebindSameIp: "Same IP", rebindSameIpHint: "IP matches current monitor — still verify name and ID", rebindSubmit: "Confirm rebind", rebindConfirm: "Bind monitoring to {{ne}}? History samples are kept.", err: { managedNeNotFound: "Managed NE not found (often deleted then re-created with a new id). Use Rebind NE and pick manually.", targetErrors: "{{count}} interface(s) failed", unsupportedVendor: "Unsupported vendor / device type", invalidSource: "Invalid NE source", rebindPickFirst: "Select an NE first", rebindNeMissing: "Selected NE not found — refresh and try again", rebindSourceMismatch: "NE source does not match this monitor device", alreadyMonitored: "That NE is already monitored by another device entry", }, kpiLatest: "latest", kpiBw: "Bandwidth", latestAt: "Latest sample", hoverPoint: "Cursor", wallLiveTitle: "Live traffic", filterView: "View", filterMap: "Mapped baseline", yMode: "Y axis", yModeAuto: "Auto (incl. baseline)", yModeCurrent: "Fit current", yModeUtil: "Utilization %", kpi: { tasks: "Tasks", devices: "Devices", running: "Running", ports: "Interfaces", samples24h: "Samples 24h", }, col: { title: "Task", device: "Device", status: "Status", ports: "Interfaces", interval: "Interval", last: "Last collect", actions: "Actions", source: "Source", name: "Device", vendor: "Vendor", ifname: "Interface", bw: "BW", upDown: "Admin / Phy / Prot", admin: "Admin", phy: "Phy", prot: "Prot", description: "Description", }, }, lldpLinks: { title: "LLDP links", collectNow: "Collect now", retryFailed: "Retry failed", started: "LLDP collect started", pause: "Pause", resume: "Resume", stop: "Stop", confirmStop: "Stop this job and skip remaining NEs?", paused: "Paused", resumed: "Resumed", stopped: "Collect stopped", policyTitle: "Collect policy", policySaved: "Policy saved", enabled: "Enable scheduled collect", autoAddUnmatched: "Auto-add unmatched neighbors as placeholders on discover", autoAddUnmatchedHint: "Shared by scheduled collect and canvas discover. Off = record unmatched only, no placeholder NE. Default on.", intervalDays: "Interval (days)", interval: "Collect interval", intervalUnitDays: "days", intervalUnitHours: "hours", concurrency: "Concurrency", historyKeep: "Jobs to keep", scope: "Scope", scopeAll: "All NEs (managed + UME)", scopeSelected: "Selected NEs", savePolicy: "Save policy", selectedCount: "{{count}} selected", targetKeywordPh: "Name / IP / vendor", jobsTitle: "Collect jobs", jobDetailTitle: "Job details", expand: "Details", edgesTitle: "Fabric links", edgeKeywordPh: "Endpoint name or IP", edgeStatusAll: "All statuses", edgeStatusActive: "Active", edgeStatusMissing: "Missing", edgeSourceAll: "All sources", exportEdgesName: "lldp-fabric-edges", exportJobsName: "lldp-collect-jobs", exportItemsName: "lldp-job-items", statusAll: "All statuses", jobKeywordPh: "Job ID / error / scope", itemKeywordPh: "NE name / IP / error", itemResultAll: "All results", missCount: "Miss cycles", replacedBy: "Replaced", kpi: { nodes: "Fabric nodes", edges: "Active links", missing: "Missing", running: "In progress", idle: "Idle", last: "Last job", nextDue: "Next due", }, col: { source: "Source", name: "Device", vendor: "Vendor", trigger: "Trigger", scope: "Scope", status: "Status", progress: "Progress", edges: "Edges", missingDelta: "Missing/purged", started: "Started", ended: "Ended", neighbors: "Neighbors", error: "Error", aSide: "Side A", bSide: "Side B", aPort: "Port A", bPort: "Port B", lastSeen: "Last seen", }, jobStatus: { running: "Running", pending: "Pending", paused: "Paused", done: "Done", failed: "Failed", cancelled: "Cancelled", stopped: "Stopped", }, triggerMode: { manual: "Manual", schedule: "Scheduled", topology: "Topology canvas", retry_failed: "Retry failed", }, jobScope: { all: "All NEs", ne_ids: "Selected NEs", selected: "Selected NEs", }, itemResult: { ok: "OK", warn: "Warn", fail: "Fail", }, edgeSource: { lldp: "LLDP", manual: "Manual", stale: "Stale", }, }, configSync: { title: "Config sync", syncNow: "Sync now", retryFailed: "Retry failed", pause: "Pause", resume: "Resume", stop: "Stop", confirmStop: "Stop this cycle and cancel remaining NE tasks?", started: "Sync cycle started", paused: "Paused", resumed: "Resumed", stopped: "Sync cycle stopped", policyTitle: "Sync policy", policySaved: "Policy saved", enabled: "Enable scheduled sync", intervalDays: "Interval (days)", concurrency: "Concurrency", historyKeep: "Config versions to keep", cycleKeep: "Jobs to keep", scope: "Scope", scopeAll: "All NEs", scopeSelected: "Selected NEs", savePolicy: "Save policy", selectedCount: "{{count}} selected", targetKeywordPh: "Name / IP / vendor", cyclesTitle: "Sync cycles", cyclesEmpty: "No sync cycles yet", tasksTitle: "Cycle tasks", taskKeywordPh: "Name / IP / message", keywordPh: "ID / trigger / message", statusAll: "All statuses", allStatus: "All statuses", exportCyclesName: "config-sync-cycles", exportTasksName: "config-sync-tasks", expand: "Details", collapse: "Collapse", kpi: { snapshots: "Snapshots", running: "In progress", idle: "Idle", last: "Last cycle", nextDue: "Next due", }, col: { source: "Source", name: "Device", vendor: "Vendor", trigger: "Trigger", status: "Status", progress: "Progress", started: "Started", ended: "Ended", message: "Message", actions: "Actions", }, }, networkDevices: { title: "Device list", keywordPh: "Name / IP / ID", allSource: "All sources", empty: "No network elements.", exportName: "network-devices", col: { source: "Source", name: "Name", vendor: "Vendor", deviceType: "Device type", connect: "Connect", actions: "Actions", }, }, networkConfigs: { title: "Configurations", keywordPh: "Name / IP / ID", vendorPh: "Vendor", allSource: "All sources", empty: "No synced configs yet. Run Config sync first.", view: "View", export: "Export", exportBoth: "Export all", exportOk: "Config exported", exportListName: "network-configs", close: "Close", tabSet: "Set format", tabHier: "Hierarchical", col: { name: "Device", vendor: "Vendor", source: "Source", size: "Size", collected: "Collected", }, }, layout: { title: "UME Integration", titleUme: "UME Sync", titleNetwork: "Network Management", titleManagedNe: "NE Management", titleCollect: "Batch Collect", titleWebcrt: "WebCRT", titleTopology: "Topology", titlePortTrafficWall: "Traffic wall", titleUsers: "Users", titleAudit: "Audit", titleApiKeys: "API Keys", titleSessions: "Login sessions", navUme: "UME", netxApi: "netx api", oclawBridge: "oclaw WSS", activeTasks: "Active {{count}}", activeTasksHint: "Open Audit · Task overview", language: "Language", switchLanguage: "Switch language / 切换语言", moreMenu: "More", langZh: "中文", langEn: "English", }, auth: { loginTitle: "Sign in", username: "Username", password: "Password", login: "Sign in", loggingIn: "Signing in…", loginFailed: "Login failed", logout: "Sign out", sessionsTitle: "Login sessions", sessionsHint: "Manage browser/device logins for this account. Behind a reverse proxy, IP is taken from trusted forwarded headers; use the Client column to tell browsers apart on the same public IP. Revoked sessions must sign in again.", sessionsEmpty: "No active sessions.", revokeOtherSessions: "Revoke other sessions", revokeSession: "Revoke", sessionCurrent: "Current", sessionRevoked: "Session revoked", sessionsRevokedOthers: "Revoked {{count}} other session(s)", revokeCurrentConfirm: "This is your current session; revoking it requires signing in again. Continue?", colSession: "Session", colClient: "Client", colLastSeen: "Last seen", colCreated: "Created", usersTitle: "User management", usersHint: "Only admins can create and manage local accounts.", addUser: "Add user", role: "Role", roleAdmin: "Admin", roleUser: "User", status: "Status", active: "Active", disabled: "Disabled", enable: "Enable", disable: "Disable", actions: "Actions", newPassword: "New password", resetPassword: "Reset password", userCreated: "User created", userUpdated: "User updated", auditTitle: "Audit log", auditHintAdmin: "View login and operation records for all users.", auditHintUser: "View your own operation records.", filterUsername: "Filter username", filterAction: "Filter action", colTime: "Time", colUser: "User", colAction: "Action", colMethod: "Method", colPath: "Path", colStatus: "Status", colIp: "IP", apiKeysTitle: "API Key management", apiKeysHelp: "The web UI uses login sessions, not API keys — keys are for MCP/scripts and netxops. Defaults on create: alarms:read + ne:read + ne:write + ne:exec + biz-monitor:read; refine later with Edit scopes (takes effect immediately, no need to regenerate). Secret is shown once. Admins may issue for other users; scopes cannot exceed the owner's. Unused scopes are reserved. System-wide cap: {{max}} non-revoked keys (revoke to free a slot).", tokenQuota: "Active {{active}} / {{max}}", tokenLimitReached: "API key limit reached ({{max}}). Revoke unused keys first.", tokenName: "Name", expiresIn: "Expiry", expire7d: "7 days", expire30d: "30 days", expire90d: "90 days", expire365d: "1 year", expireNever: "Never", tokenOwner: "Owner", tokenOwnerSelf: "Myself ({{user}})", createToken: "Create key", tokenCreated: "API key created", tokenUpdated: "Scopes updated", tokenRevoked: "Revoked", tokenOnceHint: "Copy and store this secret now; it will not be shown again:", copyToken: "Copy", tokenCopied: "Copied", tokenCopyFailed: "Copy failed", expiresAt: "Expires", lastUsed: "Last used", tokenStatusActive: "Active", tokenStatusExpired: "Expired", tokenStatusRevoked: "Revoked", revokeToken: "Revoke", revokeConfirm: "Revoke this API key?", editScopes: "Edit scopes", saveScopes: "Save scopes", scopesUnsetLegacy: "Unrestricted (same as owner)", scopesCol: "Scopes", scopesRequired: "Select at least one scope", scopesNoneAvailable: "This owner has no grantable scopes", scopeAlarmsRead: "alarms:read — Alarms read (netx MCP)", scopeNeRead: "ne:read — NE / topology read (MCP)", scopeNeWrite: "ne:write — NE / topology write / draw (MCP)", scopeNeExec: "ne:exec — Managed NE CLI (netx MCP)", scopeBizMonitorRead: "biz-monitor:read — Biz monitor / cutover read (netxops)", scopeSql: "sql:query — UME SQL (netx MCP)", scopeWebcrt: "webcrt:session — WebCRT (reserved MCP)", scopeAdminUsers: "admin:users — User admin (reserved MCP)", scopeOpsWrite: "ops:write — Ops write / subscriptions (reserved MCP)", forceChangeTitle: "Change initial password", forceChangeHint: "Account {{user}} is still using the default password. You must change it before continuing.", oldPassword: "Current password", confirmPassword: "Confirm new password", savePassword: "Save new password", savingPassword: "Saving…", passwordTooShort: "New password must be at least 8 characters", passwordMismatch: "New passwords do not match", passwordMustChange: "New password must differ from the default/old password", }, collect: { statusAll: "All statuses", keywordPh: "Job title / ID", exportJobsName: "collect-jobs", exportRunsName: "collect-runs", policyTitle: "Collect policy", policyHint: "Periodic schedule is off by default (one-shot / manual). Enable it to auto-create and run jobs on an interval. Keeps the last 3 finished jobs by default.", policySaved: "Policy saved", enabled: "Enable scheduled collect", interval: "Collect interval", intervalUnitDays: "days", intervalUnitHours: "hours", historyKeep: "Jobs to keep", scope: "Scope", scopeAll: "All NEs (managed + UME)", scopeSelected: "Selected NEs only", savePolicy: "Save policy", collectNow: "Collect now", selectedCount: "{{count}} selected", colSource: "Source", policyCommandsRequired: "Add commands and save the policy before enabling schedule or collecting now", policyTargetsRequired: "No eligible NEs in scope; adjust scope or select NEs", create: { title: "New collection job", hint: "Configure commands and NEs, then create the job. Click Start in the job list to run collection.", selectedTitle: "Selected NEs", selectedEmpty: "Filter below and add NEs one by one.", selectedCount: "{{count}} selected", clearSelected: "Clear selection", remove: "Remove", pickTitle: "Eligible NEs", pickHint: "All managed and UME inventory NEs are selectable (any connect status). UME uses CLI connect profile credentials.", pickEmpty: "No matching NEs.", filterKeyword: "Filter", filterKeywordPh: "Name / IP / vendor / device type", add: "Add", addBatch: "Add selected ({{count}})", added: "Added", create: "Create job", creating: "Creating…", expand: "New", collapse: "Close", meta: "{{ne}} NE(s) selected · {{cmd}} command(s)", }, eligible: { title: "Eligible NEs", hint: "All managed and UME inventory NEs are selectable (any connect status).", empty: "No NEs.", }, form: { title: "Collection setup", jobTitle: "Job title", jobTitlePh: "Auto-generated if empty", commands: "Commands (one per line, # for comments)", commandsPh: "display version\ndisplay current-configuration", commandsHint: "Commands run in order on each device; output is saved as a text log.", meta: "{{ne}} NE(s) selected · {{cmd}} command(s)", start: "Start collection", starting: "Submitting…", }, created: "Collection job created: {{id}}", started: "Collection started: {{id}}", paused: "Job paused", restarted: "Collection restarted", retryFailedDone: "Retrying failed devices", deleted: "Job deleted", nothingToRetry: "No failed devices to retry", neBusy: "Some NEs are busy in another collection job; wait for it to finish before starting", jobRunning: "This job is already running", confirmDelete: "Delete this collection job and its log files?", jobs: { title: "Collection jobs", listTitle: "Job list", col: { title: "Job", status: "Status", progress: "Progress", created: "Created", lastRun: "Last run", actions: "Actions", }, ok: "ok", fail: "fail", expand: "Details", collapse: "Hide", detailTitle: "Job details", pause: "Pause", start: "Start", starting: "Starting…", restart: "Restart", retryFailed: "Retry failed", downloadResults: "Download results", delete: "Delete", autoRefresh: "Auto-refresh every 2s while jobs are running", runsInProgress: "Some devices are still being collected…", }, kpi: { jobs: "Jobs", running: "In progress", idle: "Idle", last: "Last job", active: "Active jobs", nextDue: "Next due", }, runs: { status: "Status", message: "Message", download: "Log", downloadFile: "Download", filterStatus: "Status filter", filterDevice: "Device filter", deviceKeywordPh: "Name / IP", allStatus: "All statuses", }, }, managedNe: { title: "Network Elements", credsNotConfigured: "NETX_CREDENTIAL_SECRET_KEY is not set. Configure a Fernet key in .env and restart the API to store passwords.", add: "Add NE", importBtn: "Import", downloadTemplate: "Download template", importing: "Importing…", edit: "Edit", delete: "Delete", openTerminal: "Terminal", confirmDelete: "Delete this network element? If its Fabric node is managed-only (no UME binding), that node and its direct links will also be removed.", batchDelete: "Batch delete", batchDeleting: "Deleting…", batchDeleteConfirm: "Delete {{n}} selected network element(s)? Cannot be undone. Managed-only Fabric nodes cascade (node + direct links); nodes still bound to UME keep topology.", batchDeleteDone: "Deleted {{n}} network element(s)", batchDeleteSelectRequired: "Select network elements to delete", keywordPh: "name / IP / user / tags", allVendors: "All vendors", allConnectStatus: "All connect status", stats: { title: "Overview", total: "{{n}} NEs managed", pass: "Connected", fail: "Failed", unknown: "Untested", testing: "Testing", batchProxy: "Batch add proxy", batchTest: "Batch connect-test", tagFilter: "Filter by tag", all: "All", allTag: "All (no tag filter)", noTag: "No tags", tagStats: "Top tags", confirm: "Apply to {{n}} NE(s). Continue?", proxyDone: "Proxy applied to {{n}} NE(s)", testDone: "Connect-test submitted for {{n}} NE(s)", loadingIds: "Loading…", }, col: { name: "Name", vendor: "Vendor", deviceType: "Device type", source: "Source", ip: "Management IP", port: "Port", protocol: "Protocol", user: "Username", password: "Password", tags: "Tags", remark: "Remark", connect: "Connect", testedAt: "Tested at", actions: "Actions", }, source: { manual: "Manual", ume_sync: "UME sync", webcrt: "WebCRT", lldp: "LLDP placeholder", topology: "Topology placeholder", }, connect: { run: "Connectivity test", running: "Testing…", submitted: "Submitted tests for {{n}} device(s)", retest: "Test again", }, connectDetail: "Details", connectDetailTitle: "Connectivity test log", connectDetailEmpty: "No log yet. Run a connectivity test first; failures store full errors and hop context (passwords excluded).", umeSync: { sync: "Sync UME NEs", syncing: "Syncing…", delete: "Delete UME NEs", deleting: "Deleting…", deleteConfirm: "Delete all managed NEs created from UME sync? Cannot be undone. Fabric nodes that become fully orphaned (no UME left) are removed with their direct links.", done: "UME sync done: {{inserted}} inserted, {{updated}} updated, {{deleted}} deleted, {{total}} inventory total", deletedDone: "Deleted {{n}} UME-synced NEs", }, account: { batchAdd: "Batch add account", batchByTag: "Batch account by tag", batchTitle: "Batch add account", batchHint: "Apply the account info below to {{n}} selected NE(s).", batchByTagHint: "Apply the account info below to NEs under the selected tag; leave password blank to update only username.", batchDone: "Account updated for {{n}} NE(s)", selectRequired: "Select network elements first", applying: "Applying…", apply: "Apply to NEs", usernameOrPasswordRequired: "Enter at least a username or a password", passwordOptionalBatch: "leave blank to keep unchanged", }, help: "[Bulk import]\n" + "· Required columns: device_type, ip, username, port, protocol, name, vendor. Download the template first.\n" + "· password may be empty (required for direct login; optional for bastion-managed or batch proxy later).\n" + "· Recommended flow: import NEs first, select rows, then use Batch add proxy.\n\n" + "[Jump / bastion]\n" + "· Bastion SSH username template: {hop_user}@{target_user}@{target_ip} (bastion host is separate IP or FQDN); CLI: ssh user@target@ip@bastion-host.\n" + "· Bastion-managed: set Jump password (Vault); target password optional. Manual mode needs target password.\n" + "· Jump host accepts IP (192.0.2.10) or domain (ssh-bastion.example.com). JumpServer/CBH often use port 2222; some sites use 22.\n\n" + "[Connectivity / edit]\n" + "· NETX_CREDENTIAL_SECRET_KEY required to store passwords.\n" + "· Leave password blank on edit to keep unchanged; open Details after failed connect-test for hop context (no secrets).", importResult: { done: "Import done: {{inserted}} inserted, {{updated}} updated, {{failed}} failed row(s)", }, form: { requiredHint: "Fields marked with * are required", nameConnectHint: "After a successful connectivity test, name is replaced with the device hostname", createTitle: "Add network element", editTitle: "Edit network element", save: "Save", saving: "Saving…", cancel: "Cancel", created: "Network element created", updated: "Network element updated", deleted: "Network element deleted", passwordRequired: "Password is required", passwordOptional: "leave blank to keep unchanged", }, hop: { sectionTitle: "Jump host / proxy", type: "Jump type", enable: "Connect to target via jump host", vendor: { zte: "ZTE device (CLI jump)", huawei: "Huawei device (CLI jump)", cisco: "Cisco device (CLI jump)", linux: "Linux server (SSH tunnel)", bastion: "Bastion host (SSH protocol proxy)", }, zteHint: "Run ssh/telnet on the ZTE device to reach the target; target credentials use secondary auth.", huaweiHint: "Run telnet / stelnet (SSH) on the Huawei hop; stelnet is SSH. Target credentials use secondary auth.", ciscoHint: "Run Cisco ssh -vrf / telnet /vrf jump commands; target credentials use secondary auth.", linuxHint: "SSH to the Linux bastion, then direct-tcpip tunnel to target IP:port (ProxyJump-style).", bastionHint: "SSH with composite username via bastion protocol proxy. Hop host may be an IP or FQDN. Examples: bastion-user@target-user@198.51.100.20@192.0.2.10 or bastion-user@target-user@198.51.100.20@ssh-bastion.example.com; password is the bastion/Vault password. JumpServer/CBH often use port 2222.", targetAuthMode: "Target credentials", targetAuthBastionManaged: "Bastion-managed (target password optional)", targetAuthManual: "Manual (secondary auth after connect)", targetAuthHint: "Bastion-managed needs only bastion password; manual mode requires target NE password.", usernameTemplate: "SSH username template", templateHintBastion: "Default {hop_user}@{target_user}@{target_ip}. Bastion address (IP or FQDN) goes in Jump host. CLI example: ssh bastion-user@target-user@198.51.100.20@ssh-bastion.example.com.", host: "Jump host", hostPlaceholderBastion: "192.0.2.10 or ssh-bastion.example.com", hostHintBastion: "IP or FQDN. You can paste a full SSH destination (e.g. bastion-user@target-user@198.51.100.20@ssh-bastion.example.com); on blur it splits into jump username + host.", port: "Jump port", protocol: "Jump protocol", protocolSshStelnet: "ssh (stelnet)", username: "Jump username", password: "Jump password", vrf: "Mgmt VRF (optional)", vpnInstance: "VPN-Instance (optional)", vrfCisco: "VRF (optional, e.g. MGMT)", enterSystemView: "Huawei system-view before jump", enterSystemViewNo: "Stay in user-view (default)", enterSystemViewYes: "Enter system-view first", enterSystemViewHint: "Some Huawei hops only accept stelnet/telnet in system-view. Choose explicitly; NetX will not auto-retry.", commandTemplate: "Jump command template", templateHint: "ZTE CLI: telnet {target_ip}, telnet {target_ip} vrf {vrf}, ssh {target_ip}, ssh {target_ip} vrf {vrf}. Auto-suggested from jump protocol/VRF; same when left blank. Target credentials via secondary auth prompts.", templateHintHuawei: "Huawei CLI: telnet {target_ip}, telnet vpn-instance {vrf} {target_ip}, stelnet {target_ip}, stelnet {target_ip} -vpn-instance {vrf}. SSH protocol maps to stelnet. Auto-suggested when left blank.", templateHintCisco: "Cisco CLI: ssh {target_ip}, ssh -vrf {vrf} {target_ip}, telnet {target_ip}, telnet {target_ip} /vrf {vrf}. Auto-suggested when left blank.", badge: { zte: "ZTE hop", huawei: "Huawei hop", cisco: "Cisco hop", linux: "Linux hop", bastion: "Bastion", }, hostRequired: "Jump host is required", userRequired: "Jump username is required", passwordRequired: "Jump password is required", batchAdd: "Add proxy", batchTitle: "Batch add jump proxy", batchHint: "Apply the jump-host settings below to {{n}} selected NE(s) (overwrites existing proxy config).", batchDone: "Proxy applied to {{n}} NE(s)", selectRequired: "Select network elements first", applying: "Applying…", apply: "Apply to selected", }, }, ume: { token: { title: "UME Token", disconnected: "disconnected", connected: "connected", connectedRenew: "connected (renew required)", expiresIn: "expires_in", needsRenew: "renew / 0s", preview: "preview", refreshStatus: "Refresh status", renewLogin: "Renew / login", renewing: "Renewing…", disconnect: "Disconnect token", disconnecting: "Disconnecting…", renewOkChanged: "Token renewed and updated", renewOkSame: "Renew completed (unchanged)", disconnectOk: "UME token disconnected", renewHint: "Token missing expiry or expired. Use Renew / login or wait for background keepalive.", }, subscription: { title: "UME alarm subscription (WebSocket)", help: "Subscriptions must be created/cancelled manually. After establish (or on restart if a valid subscription exists in DB), the backend auto-connects WSS for live alarms. Pause/resume on runtime task alarms_current_ws_consumer stops/resumes WSS only (does not cancel the UME subscription).", sub: "subscription", subActive: "active", subInactive: "inactive", wssPaused: "paused", wssModeBanner: "Current alarms are maintained via WSS (mode: {{mode}}); scheduled REST full sync is paused. Use Sync current alarms below for full reconcile.", serverLostBanner: "UME server subscription is missing or expired; local state may still show active. Clear local record and re-subscribe.", establish: "Establish subscription", establishing: "Establishing…", reestablish: "Clear local & re-subscribe", reestablishing: "Re-subscribing…", cancel: "Cancel subscription", cancelling: "Cancelling…", refreshStatus: "Refresh subscription", clearLocalOnly: "Clear local only", clearing: "Clearing…", opFailed: "Subscription operation failed", confirmClearDefault: "UME alarm subscription no longer exists or has expired.\n\nClear local subscription record? You can establish again afterwards.", confirmReestablish: "Clear local record and re-establish subscription on UME. Continue?", titleLoginFirst: "Log in to UME token first", titleReestablish: "Clear stale local record and re-establish on UME", establishInvalid: "Establish did not return a valid id/uri", establishExists: "Subscription already exists (WSS will stay/connect)", establishOk: "Subscription established; WSS will connect", clearLocalOk: "Local subscription cleared; you can establish again", serverLostMsg: "UME subscription lost. Clear local record and re-subscribe?", cancelFailed: "Cancel failed: subscription may still exist on UME", cancelOkMissing: "Local cleared (UME subscription was already missing)", cancelOk: "Subscription cancelled", wsLogs: "WSS runtime log", wsActivity: "Last activity {{time}}", wsAutoRefresh: "Auto refresh", wsLogsEmpty: "No logs yet (connect WSS after establishing subscription)", }, keyAlert: { title: "AI alarm monitor", showPanel: "Details", hidePanel: "Close", help: "Exact match by notificationId, or substring match on alarm description (case-insensitive; nativeProbableCause, etc.). Optionally restrict to UME inventory ne_type values (multi-select; empty = all devices). Matched alerts are broadcast by NetX (server) to subscribed Netx Ops (DSH) clients.", hub: "DSH subscribers", hubConnected: "subscribed", hubEmpty: "no subscribers", hubSubscribers: "subscribers", hubPublished: "published", hubDeliverOk: "deliver ok", hubDeliverFail: "deliver fail", hubConnections: "Active connections", hubColId: "id", hubColUser: "user", hubColRemote: "remote", hubColClient: "client", hubColConnected: "connected", hubColLastSeen: "last seen", hubEmptyConnections: "No Netx Ops clients are connected to the alarm hub", hubPath: "path", matchType: "Match by", matchNotificationId: "notificationId", matchKeyword: "description keyword", matchValuePhNotificationId: "Enter notificationId", matchValuePhKeyword: "Enter keyword (case-insensitive)", label: "Rule description", labelPh: "Required, e.g. BGP Down", neTypesLabel: "Device types (optional, multi-select)", neTypesHint: "Leave empty to match all device types. Options come from synced UME Inventory ne_type.", neTypesEmpty: "No device types yet — sync Inventory first", neTypesAll: "All", forwardOnClear: "Push on alarm clear", forwardOnClearHelp: "Global switch for all monitor rules. By default only new/updated alarms are pushed; when enabled, clear notifications are also sent to subscribed Netx Ops clients.", add: "Add rules", adding: "Adding…", delete: "Delete", edit: "Edit", editTitle: "Edit monitor rule", save: "Save", saving: "Saving…", cancel: "Cancel", editOk: "Rule updated", pickFromAlarms: "Load alarm hints", emptyRules: "No monitor rules yet", colType: "type", colMatch: "match", colLabel: "description", colNeTypes: "device types", colForwardClear: "on clear", colPublished: "published", colAttempts: "attempts", colLast: "last push", colActions: "actions", colMonitor: "monitor", monitorOn: "on", monitorOff: "off", filterKeywordPh: "Filter match / label", filterMatchAll: "All types", filterEnabledAll: "All states", filterEnabledOn: "Monitoring on", filterEnabledOff: "Monitoring off", addOk: "Monitor rule added", deleteOk: "Deleted", confirmDelete: "Delete this monitor rule?", yes: "yes", no: "no", }, sync: { title: "UME sync", inventory: "Sync inventory", alarmsCurrent: "Sync current alarms", full: "Full sync", failed: "Sync failed", }, tasks: { currentTitle: "Running jobs", running: "running", noRunning: "No running jobs", runtimeTitle: "Background tasks", runtimeOpFailed: "Background task operation failed", noRuntime: "No background task status", intervalTitle: "Configured sleep between loop iterations (clamped at process start)", lastRunTitle: "Last finished sync for scheduled tasks; refreshed at each loop tick", deletedTitle: "Reconcile deletes: inventory = NE rows; alarms_current = current alarm rows", lastErrorCol: "last_error", runtimeError: { startup_alarm_sync_before_ws: "Starting up: syncing current alarms (WSS connects after completion)…", startup_gate_waiting: "Startup REST snapshot pending; scheduled REST and WSS on standby", wss_active_skip_rest: "WSS receiving live alarms; REST sync skipped", pulling_alarms_current: "Pulling UME current alarms…", alarms_sync_in_progress_skip: "Another current-alarm REST sync in progress; skipped", pulling_inventory: "Pulling UME inventory…", pulling_topology: "Pulling UME topology (nodes/links)…", topology_sync_busy: "Topology sync already running; skipped", ume_ws_disabled_no_base_url: "Disabled or UME_BASE_URL not configured", resumed_sync_soon: "Resumed: skipping debounce wait, sync soon", resumed_wss_reconnect: "Resumed: reconnecting WSS soon", resumed: "Resumed", keepalive_failed: "Token keepalive failed", }, wsState: { init: "Initializing", connected: "Connected", connecting: "Connecting", disconnected: "Disconnected", no_subscription: "No subscription", waiting_token: "Waiting for token", paused: "Paused", error: "Connection error", reconnecting: "Reconnecting", subscription_lost: "UME subscription lost", disabled_no_base_url: "Disabled or UME_BASE_URL not configured", }, runtimeTask: { token_keepalive: "UME token keepalive", alarms_current_auto_sync: "Current alarms auto sync", alarms_current_ws_consumer: "UME alarm WSS", inventory_auto_sync: "Inventory auto sync", topology_auto_sync: "Topology auto sync", }, }, syncStatus: { title: "Sync history", showPanel: "Details", hidePanel: "Close", empty: "No sync records", summary: "{{total}} record(s), {{running}} running", fabricGapTitle: "Topology dock is newer than Fabric / UME World", fabricGapHint: "Dock ME {{dock}}, Fabric UME {{fabric}}. Apply without pulling UME. Last status: {{status}}", applyFabric: "Apply to topology", applyingFabric: "Applying…", applyFabricOk: "Applied to topology", }, ne: { title: "Network elements", showPanel: "Details", hidePanel: "Close", keywordPh: "keyword(ne_id/ne_name/user_label/ip/host_name)", clearTitle: "Clear keyword and reset to page 1", expand: "Details", collapse: "Close", cliConnect: "CLI connect", summary: "{{total}} inventory NE(s)", }, tabs: { main: "UME sync", cli: "CLI connect", }, cli: { title: "UME CLI connection profile", showPanel: "Details", hidePanel: "Close", hint: "Unified SSH credentials and hop/bastion for UME inventory NEs. OClaw MCP can exec show commands via ume_ne_id without per-device managed_ne entries.", statusReady: "CLI profile is ready for UME NE command execution.", statusNotReady: "Set target username and hop/bastion settings (if enabled).", profileName: "Profile name", targetUsername: "Target SSH username", targetPassword: "Target SSH password", passwordKeep: "Leave blank to keep unchanged", newProfile: "New profile", deleteConfirm: "Delete this CLI profile?", deleted: "Profile deleted", saved: "CLI profile saved", connectTestTitle: "Sample NE connectivity test", sampleNePh: "Filter UME NEs (name / IP / ID)", sampleNeRequired: "Select a UME NE first", sampleNeEmpty: "No matching NE", sampleNeLoading: "Loading…", connectTestSubmitted: "Connectivity test submitted", connectTestWaiting: "Waiting for test result…", connectTestRunning: "Connectivity test in progress…", connectTestResult: "Test result", existingProfiles: "Existing profiles", default: "default", }, alarms: { title: "Current alarms", movedHint: "Current alarm query moved to Network Management →", showPanel: "Expand", hidePanel: "Collapse", keywordPh: "keyword(alarm key/cause/ne_name/host_name/ip…)", hostNamePh: "host_name (contains)", allSeverity: "All severities", clearedAll: "Cleared: all", clearedYes: "Cleared", clearedNo: "Active", clearTitle: "Clear filters and reset to page 1", filterByHost: "Filter by this host", noHostName: "No host_name (sync inventory first)", col: { time: "Time", severity: "Severity", neId: "NE ID", hostName: "Host", neType: "NE type", cause: "Cause", }, }, }, webcrt: { title: "WebCRT", deviceList: "Device list", sessionManager: "Device list", connect: "Connect", hint: "Pick an NE and open an interactive terminal in the browser. Credentials stay on the server.", hintCrt: "Click an NE on the left to open a terminal. Includes managed, Sessions, and UME; or use New Session for a quick login.", filterKeyword: "Filter", filterKeywordPh: "Filter by name / IP / tag", empty: "No matching NEs. Switch source or refine the search.", open: "Open terminal", opening: "Connecting…", opened: "Opened {{name}}", disconnect: "Disconnect", closed: "Session closed", termPlaceholder: "No terminal session open", sessionMeta: "{{name}} · {{ip}}", source: { all: "All", managed: "Managed", webcrt: "Sessions", ume: "UME", }, newSession: { title: "New Session", claimTitle: "Complete placeholder session", claimHint: "Enter a reachable host. Session name defaults to the placeholder name. On success, source becomes WebCRT.", connect: "Connect", connecting: "Connecting…", protocol: "Protocol", host: "Hostname", port: "Port", sessionName: "Session name", sessionNamePh: "Optional; defaults to IP", authTitle: "SSH Authentication", savePassword: "Save password", ipRequired: "Hostname is required", userRequired: "Username is required", passwordRequired: "Password is required", }, status: { idle: "Idle", connecting: "Connecting", connected: "Connected", error: "Error", closed: "Closed", }, phase: { creating: "Creating session", authenticating: "Authenticating", waitingPrompt: "Waiting for device prompt", hint: "Login progress streams into the terminal; slow devices may take >10s", }, tabMenu: { close: "Close", closeOthers: "Close others", closeAll: "Close all", copyIp: "Copy IP", copyIpOk: "Copied {{ip}}", copyIpEmpty: "No IP on this tab", }, treeMenu: { rename: "Rename", renameLabel: "Session name", renameSave: "Save", renameRequired: "Session name is required", renamed: "Session renamed", delete: "Delete session", deleteConfirm: "Delete session \"{{name}}\"? This cannot be undone.", deleted: "Session deleted", }, keywordHl: { title: "Keyword Highlighting", enabled: "Enabled", newKeyword: "New keyword", newKeywordPh: "Keyword or regex", regex: "Regular expression", add: "Add", colKeyword: "Keyword", colRegex: "Regex", empty: "No keywords yet", moveUp: "Move up", moveDown: "Move down", remove: "Remove", caseSensitive: "Case sensitive", color: "Text color", saved: "Keyword highlighting saved", }, actions: { reconnect: "Reconnect", recordLog: "Record log", clear: "Clear", copy: "Copy", recording: "Recording", recordingOn: "Log recording started", recordingSaved: "Log saved", copied: "Copied to clipboard", copyEmpty: "Terminal is empty", copyFailed: "Copy failed", break: "Break", breakSent: "Break sent", broadcast: "Broadcast", broadcastSent: "Sent to all sessions", postLogin: "Post-login cmds", }, favorites: "Favorites", recent: "Recent", favorite: "Toggle favorite", encoding: "Encoding", fontSize: "Font size", theme: "Theme", themeDark: "Dark", themeLight: "Light", colorScheme: "Color scheme", backgroundColor: "Background", foregroundColor: "Font color", scheme: { dark: "Deep blue (default)", blackWhite: "Black on white", whiteBlack: "White on black", greenBlack: "Green on black", amberBlack: "Amber on black", custom: "Custom", }, copyOnSelect: "Copy on select", pasteDelay: "Paste max line wait", pasteDelayOff: "No delay (bulk send)", keepalive: "SSH keepalive", keepaliveOff: "Off", keepaliveSec: "{{n}} sec", options: "Options", globalSessionOptions: "Global Session Options", sessionOptionsSave: "Save", sessionOptionsCancel: "Cancel", sessionOptionsSaved: "Global session options saved", disconnectBanner: "Session disconnected", disconnectBannerError: "Session error or connection lost", broadcastPh: "Send to all tabs…", buttonBarHint: "One per line: label|command", postLoginPrompt: "Post-login commands (one per line, this device)", postLoginSaved: "Post-login commands saved", sftp: { title: "SFTP", refresh: "Refresh", up: "Up", upload: "Upload", uploaded: "Upload complete", uploading: "Upload {{done}}/{{total}} · {{name}} {{pct}}", downloading: "Download {{done}}/{{total}} · {{name}} {{pct}}", downloaded: "Downloaded {{count}} file(s)", downloadPartial: "Downloaded {{ok}}, failed {{failed}}", uploadPartial: "Uploaded {{ok}}, failed {{failed}}", retrying: "Retrying {{name}} ({{attempt}}/{{max}})…", cancel: "Cancel", dropHint: "Drop to upload into current folder", dropEmpty: "No uploadable files found", mkdir: "New", mkdirPrompt: "New folder name", mkdirInvalid: "Invalid folder name (no / or ..)", mkdirOk: "Folder created", rename: "Rename", renamePrompt: "New name", renameInvalid: "Invalid name (no / or ..)", renamed: "Renamed", download: "Download", chmod: "chmod", chmodPrompt: "Mode (e.g. 755 or rwxr-xr-x)", chmodOk: "Permissions updated", chmodPartial: "chmod ok {{ok}}, failed {{failed}}", delete: "Delete", deleteConfirm: "Delete {{name}}?", deleteRecursiveConfirm: "{{name}} includes folders; non-empty folders will be removed recursively. Continue?", deletePartial: "Deleted {{ok}}, failed {{failed}}", deleted: "Deleted", selectedCount: "{{count}} selected", empty: "This folder is empty", listTruncated: "Folder is large; showing first {{count}} entries (limit {{max}})", dir: "dir", loading: "Loading…", enterHint: "Click to select; Ctrl/Shift multi-select; double-click to open", downloadHint: "Click to select; Ctrl/Shift multi-select; double-click to download", colName: "Name", colSize: "Size", colMtime: "Modified", colOwner: "Owner", colGroup: "Group", colMode: "Access", resizeHint: "Drag to resize SFTP / terminal", colResizeHint: "Drag to resize column", }, term: { findPh: "Find…", findPrev: "Previous", findNext: "Next", findClose: "Close find", copy: "Copy", paste: "Paste", pasteEmpty: "Clipboard is empty", pasteFailed: "Cannot read clipboard (on HTTP use the paste box, or Ctrl+V)", pasteBridgeHint: "This page is not HTTPS, so the browser blocks clipboard reads. Press Ctrl+V in this box to send text to the terminal.", pasteBridgePh: "Press Ctrl+V here…", pasteBridgeCancel: "Cancel", clear: "Clear", pasting: "Pasting {{done}}/{{total}} lines…", wsError: "[WebSocket error]", wsClosed: "[WebSocket closed code={{code}}]", sessionStatus: "[session {{state}}{{detail}}]", outputTruncated: "[output truncated: {{count}} chunk(s) dropped]", }, err: { deviceType: "Unsupported device type", ipConflictRestart: "Database still enforces unique IP; restart the netx API and retry", sessionLimit: "Session limit reached; close other terminals first", neNotFound: "Network element not found", credsIncomplete: "Incomplete username/password on this NE", authRejected: "Wrong username or password. Please try again.", cliProfile: "UME NEs require a CLI connect profile (default account / hop)", connectFailed: "Device login failed: {{detail}}", sftpHop: "SFTP does not support hop devices yet (direct SSH only)", sftpSsh: "SFTP requires SSH", sftpNeedPassword: "SFTP needs a saved SSH password (one-shot dialog passwords are not reused for files)", sftpUnsupported: "This SSH session has no SFTP channel (device may not offer it)", sftpTooLarge: "File exceeds size limit (default 512MB)", sftpChmodInvalid: "Invalid mode (use 755 or rwxr-xr-x)", sftpAborted: "Transfer cancelled", queueDropped: "Terminal output too fast; about {{count}} chunk(s) dropped — transcript may be incomplete", websocket: "WebSocket connection failed", }, }, topology: { title: "Topology", maps: "Maps", tree: "Topology tree", treeLoading: "Loading tree…", treeLoadFailed: "Failed to load topology tree", treeRetry: "Retry", graphLoading: "Loading topology…", graphLoadFailed: "Failed to load topology", graphRefreshing: "Refreshing…", canvasEmpty: "No NEs on this map yet", canvasEmptyHint: "Use Add NE, or pick a region that already has devices.", treeHint: "Top level is a Root (nav only); its unique Root Map is the canvas. Open the root map or a sub-region to edit devices.", treeSearch: "Search NEs across maps", treeSearchPh: "Name / IP", treeSearchEmpty: "No matching NEs", treeSearchNoViews: "Not on any map", treeSearchNotOnMap: "This NE is not placed on any topology map yet", locateNotOnCanvas: "This NE is missing from the world-map sample and has no world coordinates. Open a region map instead.", treeSearchViewCount: "{{count}} map(s)", treeSearchTruncated: "Showing first 30 of {{total}}", rootName: "Roots", selectRegion: "Select a root or region", openMap: "Open map", openRegion: "Open", kindPhysical: "Physical", kindCustom: "Custom", layerNodeHint: "{{count}} NE(s)", layerNodeEmpty: "Click to create & open", rootCanvasHint: "Pick a root from the left tree.", regionCanvasHint: "Open the root map to edit devices; create sub-regions on that canvas.", regionBrowseHint: "Root folder: unique root map below. Click to open.", browserTitle: "Topology folder", browserRegionSub: "{{count}} map(s)", browserRegionsSub: "{{count}} item(s)", browserEmptyMaps: "No root map under this root yet.", addNe: "Add NE", more: "More", backUp: "Back", newRegion: "New root", newRegionName: "New root", newRegionPrompt: "Root name", newSubRegion: "New sub-region", newSubRegionName: "New sub-region", newSubRegionPrompt: "Sub-region name", subRegionNotOnFlat: "The world map has no regions. Create sub-regions on World or a lower-level canvas.", worldMapNoDirectNes: "Cannot add NEs on the world map — add them on a sub-region canvas.", worldMapName: "World map", worldMapOffline: "World map is temporarily offline", rootMapName: "Root map", worldHud: "World {{total}} · close-up {{cached}} · z{{zoom}} · {{lod}}", worldEmptyCoords: "World map has no coordinates yet", worldEmptyCoordsHint: "Dock has {{dock}} NEs, but Fabric / world coords are empty. Apply without pulling UME.", worldApply: "Apply to world map", worldApplying: "Applying…", worldApplyOk: "Applied to world map", subRegionNeedParent: "Open a root map or region canvas first, then create a sub-region.", regionCreated: "Created", regionDeleted: "Deleted", deleteRegion: "Delete", deleteRegionConfirm: "Delete \"{{name}}\"? Its maps will also be deleted.", deleteUmeWorldConfirm: "Delete \"{{name}}\"? This cascades all UME regions and maps under it. A later UME topology apply may recreate it.", renameRegion: "Rename", renameRegionPrompt: "Name", regionRenamed: "Renamed", collapseRegion: "Collapse", expandRegion: "Expand", newCustom: "Custom map", newCustomName: "Custom topology", newCustomPrompt: "Custom map name", folderHint: "Creating a root auto-creates its unique root map. Add sub-regions on that canvas.", folderHintShort: "Root folder", outsidePeers: "{{count}} outside peer(s) linked but not on this map", outsidePeersView: "Off-map NEs {{count}}", outsidePeersTitle: "Outside peers", outsidePeersHint: "Linked to this map but not placed. Select and add only what you need.", outsidePeersVia: "via {{name}}", outsidePeersFilterPh: "Filter: name / IP / via", outsidePeersEmpty: "No matching outside peers", projectNeighbors: "Project neighbors", projectedNeighbors: "Projected {{count}} neighbor(s) onto the map", localBrowserOnly: "this browser only", deleteEdgeConfirm: "Remove selected link(s) from the canvas? They are deleted from Fabric only when you Save. LLDP may recreate them on the next collect.", edgeDeleted: "Queued {{count}} link delete(s) — Save to apply", discoverCancelled: "Discovery cancelled", truncatedMembership: "Membership cap reached; some neighbors were not placed. Use classify or a new map.", truncatedFrozen: "This map membership is frozen; neighbors cannot be projected. Unfreeze via classify workflow or create a new map.", truncatedNodes: "Too many view nodes; display truncated. Narrow membership or create another map.", truncatedEdges: "Too many edges; display truncated. Filter status or create another map.", truncatedGeneric: "Graph data truncated. Narrow scope or create another map.", deleteSelectionConfirm: "Remove {{nodes}} NE(s) from this map and {{edges}} link(s)? Link deletes apply on Save; NE remove from map applies on Save.", selectionDeleted: "Queued: remove {{nodes}} NE(s), delete {{edges}} link(s) — Save to apply", newMap: "New", newMapName: "New topology", rename: "Rename", renamePrompt: "Topology map name", renameHint: "Double-click to rename", renamed: "Topology map renamed", deleteMap: "Delete map", deleteMapConfirm: "Delete map \"{{name}}\"? Nodes and edges will be removed.", emptyMaps: "No roots yet. Create a root first.", palette: "NE library", paletteHint: "Select multiple to add in batch, or drag one onto the canvas", paletteDragHint: "Drag onto canvas to add", selectAllVisible: "Select all visible", addSelected: "Add selected ({{count}})", addSelectedDone: "Added {{count}} NE(s)", addingNe: "Adding…", filterPh: "Type to filter: name / IP / vendor", save: "Save", saveDirty: "Save *", unsavedConfirm: "You have unsaved changes. Discard and switch maps?", saving: "Saving…", saved: "Topology saved", saveFailed: "Save failed: {{detail}}", readOnlyBanner: "Read-only (ne:write scope required to edit)", readOnlyHint: "Read-only — no write permission", discoverTimeout: "Discovery timed out; check LLDP tasks for progress", policySyncFailed: "Failed to sync LLDP policy; local toggle still applies to canvas discovery", discover: "Discover links", discovering: "Discovering…", discoverOne: "Discover links", discovered: "Discovery done: +{{added}} updated {{updated}} missing {{stale}}", discoverFail: "Discovery failed: {{detail}}", discoverOneNeedNe: "This node is not linked to a scannable NE", selectMap: "Select or create a topology map", renameNode: "Rename", renameNodePrompt: "Node display name", sourcePort: "Local port", targetPort: "Remote port", endpointA: "Endpoint A", endpointB: "Endpoint B", port: "port", expandPhysicalLinks: "Expand physical links", scaleBundleWidth: "Thicken bundles by link count", linkMembers: "Physical members ({{count}})", canvasHint: "Left tree lists regions; add maps yourself, then open a map to edit devices with Add NE and save positions.", selected: "Selected", selectedEdge: "Selected edge", selectedCount: "{{count}} selected", toolModes: "Edit modes", toolSelect: "Select", toolPan: "Pan", toolDrag: "Drag", toolConnect: "Connect", layout: "Layout", layoutHierarchicalTb: "Hierarchical · top-bottom", layoutHierarchicalLr: "Hierarchical · left-right", layoutForce: "Force-directed", layoutGrid: "Grid", layoutRadial: "Radial", layoutSelected: "Selected only (hierarchical)", layoutNeedSelection: "Select nodes to lay out first", align: "Align", alignLeft: "Align left", alignRight: "Align right", alignTop: "Align top", alignBottom: "Align bottom", alignHCenter: "Align center X", alignVCenter: "Align center Y", alignHDistribute: "Distribute horizontally", alignVDistribute: "Distribute vertically", alignNeedSelection: "Select at least two nodes", snapGrid: "Snap to grid", autoLayoutDiscover: "Auto-layout after discover", discoverAutoAddUnmatched: "Auto-add unmatched NEs as placeholders on discover", discoverProjectNeighbors: "After discover, project neighbors of scanned NEs onto the canvas (Save to keep permanently)", undo: "Undo", redo: "Redo", discoverSelected: "Discover links ({{count}})", removeSelected: "Remove selected ({{count}})", openWebcrt: "Open terminal", openNe: "NE details", openAlarms: "View alarms", noAlarmLink: "Cannot build an alarm filter for this node", removeNode: "Remove from canvas", deletePlaceholder: "Delete placeholder", deletePlaceholderConfirm: "Permanently delete this placeholder NE (inventory + Fabric links)? This cannot be undone with canvas Undo.", deletePlaceholderDone: "Placeholder deleted", removeEdge: "Delete edge", nodeMenu: "Node", paneMenu: "Canvas", edgeMenu: "Edge", createNe: "New NE", createNeModeHint: "Create a full managed NE, or a topology placeholder (credentials can be filled later).", createNeManaged: "Managed NE", createNeManagedHint: "Same full form as NE management; placed on the canvas after create.", createNePlaceholder: "Placeholder NE", createNePlaceholderHint: "Name/IP only; topology placeholder for sketching first.", createNeTitle: "New topology placeholder NE", createNeHint: "Source is topology placeholder. Complete login via WebCRT, or promote in NE management.", createNeName: "Name", createNeNamePh: "e.g. SW-CORE-01", createNeNameRequired: "NE name is required", createNeIp: "IP (optional)", createNeIpPh: "Can fill later during setup", createNeDone: "Created placeholder {{name}}", createManagedNeDone: "Created and placed {{name}}", createManagedNePlaceFailed: "{{name}} was created in inventory, but placing on the map failed: {{error}}. Add it again from NE management or the sidebar.", createManagedNePosFailed: "{{name}} is on the map, but could not move to the click position (kept default layout coords)", createNeBusy: "Creating…", connectTest: "Connectivity test", connectTestSelected: "Connectivity test ({{count}})", connectTestNeedManaged: "Node is not linked to a managed NE; cannot run connectivity test", connectTestSkipped: "skipped {{count}} without managed link", selectionMenu: "Selection", edgeStyle: "Edge style", edgeColor: "Color", edgeLineStyle: "Line", edgeLineSolid: "Solid", edgeLineDashed: "Dashed", edgeLineDotted: "Dotted", edgeWidth: "Width", edgeStyleReset: "Reset style", edgeDefaults: "Default edge styles", edgeDefaultsReset: "Factory reset", noNeLink: "Not linked to a managed NE", completeNeFirst: "Login details are incomplete (vendor, hop/proxy, etc. belong in NE inventory). Complete the NE first, then open the terminal.", edgeManual: "Manual", edgeUme: "UME", edgeDiscovered: "LLDP", edgeStale: "Missing", fit: "Fit view", export: "Export", exportSvg: "SVG vector", exportXml: "XML data", exporting: "Exporting…", exportEmpty: "Nothing on the canvas to export", exportedSvg: "SVG exported", exportedXml: "XML exported", exportFailed: "Export failed: {{detail}}", liveSync: "Live sync", liveSyncOn: "Syncing…", liveSyncHint: "Poll the tree about every 5s and the open map every 3s (watch MCP create/draw). Off by default", fullscreen: "Fullscreen", exitFullscreen: "Exit fullscreen", display: "Display", canvasBg: "Canvas background", canvasBgColor: "Background color", canvasBgReset: "Reset default", textColors: "Text colors", textColorsReset: "Reset default", labelColor: "NE name", edgeLabelColor: "Link numbers", vendorColors: "Vendor icon colors", vendorColorsReset: "Reset default", vendorTone: { cisco: "Cisco", huawei: "Huawei", zte: "ZTE", juniper: "Juniper", nokia: "Nokia", ericsson: "Ericsson", h3c: "H3C", ruijie: "Ruijie", mikrotik: "MikroTik", gray: "Unknown / unmanaged", }, hideIp: "Hide IP", hideVendor: "Hide vendor", showPlaceholderBadge: "Show placeholder badge (TOPO/LLDP)", showConnectStatus: "Show connect-status dots", showAlarmOverlay: "Show alarm overlay", showLevelColors: "Color by classify level", levelUnmatched: "Unclassified", level: { external: "External", core: "Core", aggregation: "Aggregation", access: "Access", edge: "Edge", }, hidePorts: "Hide ports", edgeFlow: "Edge flow", collapseSidebar: "Collapse sidebar", expandSidebar: "Expand sidebar", resizeSidebar: "Drag to resize sidebar", hideAdded: "Hide added", paletteEmpty: "No matching NEs.", paletteHiddenAll: "Added NEs are hidden; uncheck to show them again.", paletteSource: "NE source", paletteManaged: "Managed", paletteUme: "UME", paletteLoading: "Loading…", discoverReport: "Discovery progress", discoverClose: "Close", discoverCancel: "Cancel discovery", discoverProgress: "Scanning {{count}} managed NE(s) on the map (serial LLDP; please wait)…", discoverProgressLive: "Scanning {{index}}/{{total}}: {{name}}", discoverSummary: "Scanned {{scanned}} · added {{added}} · updated {{updated}} · missing {{stale}} · failed {{failed}}", discoverNeOk: "neighbors {{neighbors}} · added {{added}} · updated {{updated}}", discoverNeFail: "Collect failed", discoverUnmatched: "unmatched {{count}}", discoverUnmatchedMore: "{{count}} more not listed", discoverVendorRequired: "Vendor/device type missing; skipped LLDP command", discoverParserStub: "Parser \"{{parser}}\" is still a stub; output was collected but neighbors were not parsed", discoverNoTargets: "No scannable NEs on the map. Add managed NEs, or UME NEs with a CLI connect profile.", discoverViewDetail: "Detail", discoverLinkCount: "{{count}} link(s)", discoverLinksTitle: "Matched links ({{count}})", discoverLinksEmpty: "No on-map peer links matched in this scan.", discoverUnmatchedTitle: "Unmatched neighbors ({{count}})", discoverUnmatchedEmpty: "No unmatched neighbors.", discoverColPeer: "Peer NE", discoverColRemote: "Neighbor", discoverColLocalPort: "Local port", discoverColRemotePort: "Remote port", discoverColProtocol: "Protocol", discoverColAction: "Result", discoverLinkAdded: "Added", discoverLinkUpdated: "Updated", discoverLinkKeptManual: "Kept manual", discoverLocatePeer: "Locate peer", discoverLocateNe: "Locate NE", discoverRawPreview: "Raw output preview (truncated when long; parsing uses full output)", discoverGoLldp: "Details → LLDP links", findNode: "Find", findNodePh: "Type to search: name / IP / vendor", findNoMatch: "No matching node on the canvas", findMore: "{{count}} more matches…", locateOnCanvas: "Click to locate on canvas", openPortTraffic: "Open port traffic", removeStale: "Clear missing ({{count}})", removeStaleHint: "Queue delete of red missing edges (applies on Save); scheduled collect also purges after 4 consecutive misses", staleRemoved: "Queued {{count}} missing edge delete(s) — Save to apply", connectHint: "Connect mode: drag from the NE center anchor to the target (Esc or Select to exit)", }, topoClassify: { title: "Classify", openTopo: "Topology", inventory: "NE inventory", keywordPh: "Filter by name/IP", filterLevelAll: "All levels", filterRegionAll: "All regions", filterUnmatchedOff: "No unmatched filter", kindAny: "Missing level or region", kindLevel: "Missing level", kindRegion: "Missing region", filterLinkAll: "All link states", filterLinkLinked: "Linked inventory", filterLinkOrphaned: "Detached", colLink: "Link", linkManaged: "Managed", linkUme: "UME", linkBoth: "Managed+UME", linkOrphaned: "Detached", levelExternal: "L0 external", levelCore: "L1 core", levelCoreSub: "L1.1 core′", levelAgg: "L2 aggregation", levelAggSub: "L2.1 agg′", levelAccess: "L3 access", levelAccessSub: "L3.1 deeper access", levelInvalid: "Invalid level (0–99.9, one decimal)", sourceManual: "Manual", sourceUmeSync: "UME sync", sourceLldp: "LLDP placeholder", sourceTopology: "Topology placeholder", sourceWebcrt: "WebCRT", regexTitle: "Regex find", patternPh: "Regex, e.g. ^CORE- or -EDGE-", fieldName: "Name", fieldNameIp: "Name+IP", findMatches: "Find matches", matchOk: "Matched {{count}}; selected", matchHint: "{{count}} matched", assignLevel: "Level only", assignRegion: "Region only", assignBoth: "Level + region", clearRegion: "Clear region", confirmAssign: "Confirm assign", bulkConfirm: "Write tags to {{count}} selected/matched NE(s)?", bulkOk: "Updated {{count}}", selectedCount: "{{count}} selected", selectPage: "Select page", colNe: "NE", colLevel: "Level", colRegion: "Region", colActions: "Actions", edit: "Edit", save: "Save", cancel: "Cancel", delete: "Delete", deleteSelected: "Delete nodes ({{count}})", deleteConfirm: "Delete {{count}} fabric node(s) (placements and edges)? Managed/UME inventory is not touched.", deleteOk: "Deleted {{count}} node(s)", deleteHint: "Only orphaned or LLDP/WebCRT placeholder fabric nodes can be deleted; UME-only and real managed NEs cannot", deleteBlocked: "UME-only or still linked to real managed inventory — cannot delete here", rowSaved: "Saved", loading: "Loading…", empty: "No NEs.", }, audit: { title: "Audit & ops", logsTitle: "Operation logs", colSummary: "Summary", colDetail: "Detail", showDetail: "Expand", hideDetail: "Collapse", filterBusiness: "Business", filterAll: "All", filterWebcrt: "Device terminal", filterNeExec: "Device exec", filterAuth: "Auth", filterHttp: "HTTP", summary: { connecting: "Connecting to {{device}}", loginOk: "Logged in to {{device}}", loginFail: "Login failed {{device}}: {{error}}", logout: "Disconnected {{device}}", logoutReason: "Disconnected {{device}} ({{reason}})", command: "{{device}} · cmd: {{command}}", neExec: "{{device}} · exec: {{commands}}", neExecBatch: "Batch exec {{n}} NE(s) (ok {{ok}} / fail {{fail}})", }, nav: { tasks: "Task overview", logs: "Operation logs", }, tasks: { title: "Task overview", hint: "Live view of running or pending background work. Actor prefers recent audit entries; scheduled work shows as scheduler/system.", empty: "No tasks to show right now.", open: "Open", inflight: "running {{n}}", kpiActive: "Active / in-flight", kpiTotal: "Total rows", kpiKinds: "By kind", kpiStatuses: "By status", colKind: "Kind", colTitle: "Task", colStatus: "Status", colActor: "Actor", colTrigger: "Trigger", colProgress: "Progress", colStarted: "Started", colUpdated: "Updated", colLink: "Open", kind: { port_traffic: "Port traffic", config_sync: "Config sync", ne_collect: "Batch collect", lldp_discover: "LLDP discover", ume_sync: "UME sync", ne_connect: "Connect test", ume_runtime: "UME runtime", webcrt: "WebCRT", }, trigger: { manual: "Manual", schedule: "Schedule", topology: "Topology", system: "System", retry_failed: "Retry failed", }, status: { collecting: "Collecting", running: "Running", testing: "Testing", connecting: "Logging in", ready: "Connected", detached: "Detached (grace)", pending: "Pending", paused: "Paused", idle: "Idle", error: "Error", failed: "Failed", }, }, }, }; export default en;