{
  "common": {
    "ok": "OK",
    "cancel": "Cancel",
    "save": "Save",
    "delete": "Delete",
    "edit": "Edit",
    "back": "Back",
    "done": "Done",
    "close": "Close",
    "retry": "Retry",
    "copy": "Copy",
    "enabled": "Enabled",
    "refresh": "Refresh",
    "clear": "Clear"
  },
  "auth": {
    "signInTitle": "Sign in",
    "changeServer": "Change",
    "username": "Username",
    "password": "Password",
    "signIn": "Sign in",
    "signingIn": "Signing in…",
    "subtitle": "Use your operator credentials.",
    "errorRequired": "Username and password are required",
    "errorGeneric": "Login failed: {error}",
    "errorFallback": "Login failed"
  },
  "nav": {
    "sessions": "Sessions",
    "memory": "Memory",
    "notes": "Notes",
    "more": "More",
    "activity": "Activity",
    "providers": "Providers",
    "channels": "Channels",
    "integrations": "Integrations",
    "plugins": "Plugins",
    "backups": "Backups",
    "settings": "Settings",
    "workspace": "Workspace",
    "updateAvailable": "Update available"
  },
  "web": {
    "brand": "opendray",
    "loading": "Loading…",
    "topbar": {
      "expandSidebar": "Expand sidebar",
      "collapseSidebar": "Collapse sidebar",
      "search": "Search",
      "openPalette": "Open command palette",
      "theme": "Theme",
      "themeLabel": "Theme: {mode}",
      "appearance": "Appearance",
      "themeLight": "Light",
      "themeDark": "Dark",
      "themeSystem": "System",
      "language": "Language",
      "languageEnglish": "English",
      "languageChinese": "中文",
      "languageSpanish": "Español",
      "signedInAs": "Signed in as",
      "tokenExpires": "Token expires",
      "signOut": "Sign out"
    },
    "sessions": {
      "list": {
        "title": "Sessions",
        "countSeparator": "·",
        "newAria": "Spawn new session",
        "newTooltip": "New session",
        "loading": "Loading…",
        "emptyTitle": "No sessions yet.",
        "emptyHint": "Press {kbd} to spawn.",
        "endedHeader": "Ended ({count})",
        "clearAll": "Clear all",
        "confirmClearAll": "Remove all {count} ended sessions?",
        "confirmTerminate": "Terminate and remove {name}?",
        "childPromoted": " {count} child task session will be promoted to top-level.",
        "childPromotedPlural": " {count} child task sessions will be promoted to top-level.",
        "footer": "{live} live · {ended} ended",
        "row": {
          "deleteAria": "Delete session",
          "titleRemoveHistory": "Remove from history",
          "titleTerminate": "Terminate and remove",
          "titleRemove": "Remove",
          "claudeAccountTitle": "Claude account: {label}"
        },
        "deleteFailedToast": "Delete failed"
      },
      "tabs": {
        "closeAria": "Close tab and remove session",
        "closeTitle": "Close tab and remove session"
      },
      "page": {
        "removedToast": "Session removed",
        "removeFailedToast": "Remove failed",
        "stoppedToast": "Session stopped",
        "stopFailedToast": "Stop failed",
        "restartedToast": "Session restarted",
        "restartFailedToast": "Restart failed",
        "confirmCloseTabTitle": "Stop and remove \"{name}\"?",
        "confirmCloseTabDescription": "The CLI process will be terminated and the row deleted.",
        "confirmCloseTabConfirm": "Stop and remove",
        "confirmRemoveTitle": "Remove {name}?",
        "confirmRemoveTitleFallback": "Remove session?",
        "confirmRemoveDescription": "This deletes the row.",
        "confirmRemoveConfirm": "Remove"
      },
      "empty": {
        "title": "No session open",
        "hint": "Pick a session from the list, or spawn a new one. Keyboard: {kbdN} new, {kbdW} close, {kbdRange} switch.",
        "spawn": "Spawn session"
      },
      "header": {
        "loadingSession": "Loading session…",
        "showList": "Show session list",
        "hideList": "Hide session list",
        "showInspector": "Show inspector",
        "hideInspector": "Hide inspector",
        "attachImage": "Attach image",
        "attachImageTooltip": "Attach image (or paste / drop into terminal)",
        "copyOutput": "Copy output",
        "copyOutputTooltip": "Copy the terminal output (selection if any, otherwise everything)",
        "restart": "Restart",
        "restarting": "Restarting…",
        "remove": "Remove",
        "removing": "Removing…",
        "stop": "Stop",
        "stopping": "Stopping…",
        "pid": "pid {pid}"
      },
      "terminal": {
        "uploadingToast": "Uploading image…",
        "uploadedToast": "Image attached",
        "uploadFailedToast": "Upload failed",
        "uploadInvalidTypeToast": "Only image files can be attached",
        "dropToAttach": "Drop image to attach",
        "copyButton": "Copy",
        "copyAllTooltip": "Copy terminal output to clipboard (selection if any, otherwise everything)",
        "copySelection": "Copy",
        "copySelectionTooltip": "Copy selected text",
        "copiedToast": "Copied to clipboard",
        "copyEmptyToast": "Nothing to copy yet",
        "copyFailedToast": "Couldn't copy to clipboard",
        "urls": {
          "tooltip": "Open the latest link detected in this session",
          "tapToOpenLatest": "Tap to open the latest link (most recent OAuth URL)",
          "openListTooltip": "Show all links",
          "buttonLabel": "{count} link",
          "buttonLabel_plural": "{count} links",
          "dialogTitle": "Detected links",
          "dialogDesc": "URLs printed in this session's output, newest first. Tap Open to launch in your default browser — works even when the URL is line-wrapped in the terminal.",
          "openButton": "Open",
          "copyButton": "Copy",
          "copiedToast": "URL copied",
          "copyFailedToast": "Couldn't copy — long-press the URL and copy manually",
          "noneHint": "No links detected yet."
        }
      },
      "spawn": {
        "title": "Spawn session",
        "description": "Start a CLI session under a registered provider.",
        "provider": "Provider",
        "claudeAccount": "Claude account",
        "loadingAccounts": "Loading accounts…",
        "noAccounts": "No Claude accounts found. Spawn this session and run <1>claude login</1> in the terminal — credentials land in <3>~/.claude</3> on the gateway and show up automatically next time.",
        "default": "Default",
        "defaultTooltip": "Use system keychain / env",
        "tokenEmptyBadge": "·empty",
        "tokenMissingTooltip": "No token set — set token in Providers panel first",
        "multiAccountHint": "Multiple accounts configured — pick one for this session.",
        "cwd": "Working directory",
        "cwdPlaceholder": "/Users/you/projects/foo",
        "browse": "Browse",
        "nameLabel": "Name (optional)",
        "namePlaceholder": "claude in pet-tracker",
        "argsLabel": "CLI args (one per line)",
        "bypassClaude": "Bypass permission prompts",
        "bypassCodex": "Bypass approvals & sandbox (--dangerously-bypass-approvals-and-sandbox)",
        "bypassGemini": "YOLO mode (--yolo)",
        "bypassOnHint": "This session will run with elevated autonomy.",
        "bypassOffHint": "Off — confirmations and prompts behave normally.",
        "errorPickProvider": "Pick a provider.",
        "errorCwdRequired": "cwd is required.",
        "cancel": "Cancel",
        "submit": "Spawn",
        "submitting": "Spawning…",
        "spawnedToast": "Session spawned",
        "spawnedDescription": "{provider} · pid {pid}",
        "pidFallback": "—"
      },
      "accountSwitcher": {
        "tooltip": "Switch Claude account (restarts the CLI process)",
        "currentDefault": "default",
        "menuTitle": "Switch Claude account",
        "defaultName": "Default",
        "defaultSubtitle": "CLI's system keychain / env",
        "tokenEmpty": "·empty",
        "confirmSwitch": "Switching account will restart the Claude CLI. The conversation history is preserved (transcript is migrated and --resume replays it under the new account), but any in-flight tool execution or unsent input will be lost. Continue?",
        "switchedToast": "Account switched",
        "switchedDescription": "Now using @{account} · pid {pid}",
        "switchedDefault": "default",
        "switchFailedToast": "Switch failed"
      },
      "inspector": {
        "tabs": {
          "files": "Files",
          "git": "Git",
          "search": "Search",
          "tasks": "Tasks",
          "history": "History",
          "notes": "Notes",
          "memory": "Memory"
        }
      },
      "ended": {
        "bufferUnavailable": "[buffer unavailable]",
        "readOnlyBanner": "[session ended — read-only buffer]"
      },
      "fileBrowser": {
        "title": "Choose working directory",
        "description": "Browse the gateway host's filesystem and pick a folder.",
        "parent": "Parent directory",
        "home": "Home directory",
        "refresh": "Refresh",
        "pathPlaceholder": "/Users/you/projects",
        "loading": "Loading…",
        "empty": "Empty directory.",
        "newFolder": "New folder",
        "newFolderPlaceholder": "new-folder-name",
        "create": "Create",
        "cancel": "Cancel",
        "useThisFolder": "Use this folder",
        "createdToast": "Directory created",
        "mkdirFailedToast": "Mkdir failed",
        "homeFailedToast": "Failed to read home"
      }
    },
    "memory": {
      "title": "Memory",
      "subtitle": "Browse, search and edit memories agents have stored via the opendray-memory MCP server.",
      "navProject": "Project",
      "navArchived": "Archived",
      "navWorkers": "Workers",
      "navConfiguration": "Configuration →"
    },
    "journalStale": {
      "title": "Prune stale entries",
      "subtitle": "(older than {days} days, no pending conflicts)",
      "daysLabel": "Older than (days):",
      "loading": "Scanning…",
      "empty": "Nothing stale to prune.",
      "selectAll": "Select all",
      "deselectAll": "Deselect all",
      "deleteSelected": "Delete ({count})",
      "deleted_one": "{count} entry deleted",
      "deleted_other": "{count} entries deleted"
    },
    "conflicts": {
      "title": "Cross-layer conflicts",
      "subtitle": "Contradictions the daily detector found between facts, plan, goal, and journal entries.",
      "loading": "Loading conflicts…",
      "empty": "No pending conflicts. Click \"Detect now\" to run an on-demand sweep.",
      "pickCwd": "Pick a project to see its conflicts.",
      "detectNow": "Detect now",
      "detected": "{count} new conflict(s) found",
      "accept": "Accept",
      "dismiss": "Dismiss",
      "accepted": "Conflict accepted — remember to apply the fix",
      "dismissed": "Conflict dismissed",
      "deletedFact": "Fact deleted and conflict accepted",
      "quickActions": "Fix:",
      "deleteFact": "Delete fact",
      "deleteFactSide": "Delete {side}: {ref}",
      "confirmDelete": {
        "title": "Delete fact {side}?",
        "description": "This permanently removes the fact and accepts the conflict. The other side stays as the surviving claim.",
        "targetLabel": "Will delete (side {side}):",
        "keepLabel": "Will keep (side {side}):",
        "nonFactOther": "({layer} entry — open the corresponding tab to inspect)",
        "evidenceLabel": "Detector evidence:",
        "loading": "Loading fact text…",
        "loadError": "Failed to load fact text. Inspect on the Memory page.",
        "cancel": "Cancel",
        "confirm": "Delete {side}"
      },
      "openLayer": {
        "plan": "Open plan editor",
        "goal": "Open goal editor"
      },
      "severity": {
        "low": "low",
        "medium": "medium",
        "high": "high"
      }
    },
    "memoryHealth": {
      "title": "Memory health — last {days} days",
      "subtitle": "Aggregate signals across both memory subsystems for this project.",
      "loading": "Loading health snapshot…",
      "errorLoading": "Failed to load health snapshot.",
      "pickCwd": "Pick a project to see its memory health.",
      "newFacts": "New facts",
      "newFactsHint": "{total} stored in total",
      "captureFires": "Capture fires",
      "captureFiresHint": "{stored} stored · {deduped} deduped",
      "newJournal": "Journal entries",
      "newJournalHint": "{total} in total",
      "planAge": "Plan last updated",
      "planAgeHint": "{count} plan-drift proposal(s) pending",
      "planAgeHintNone": "No plan-drift proposals pending",
      "goalAge": "Goal last updated",
      "pending": "Pending proposals",
      "pendingHint": "oldest {days}d old",
      "topHit": "Top hit · {hits} retrievals",
      "zeroHit": "{count} facts older than 7d with zero retrievals — candidates for cleanup.",
      "never": "never",
      "today": "today",
      "daysAgo_one": "{count} day ago",
      "daysAgo_other": "{count} days ago"
    },
    "memoryConfig": {
      "title": "Memory configuration",
      "subtitle": "All memory-related knobs in one place: HTTP providers, per-task worker routing, capture triggers, spawn-time injection, and audit costs.",
      "sections": {
        "providers": "Providers",
        "workers": "Workers",
        "rules": "Capture rules",
        "profiles": "Injection profiles",
        "costs": "Token cost"
      },
      "sectionHints": {
        "providers": "Registered HTTP endpoints (Ollama / LM Studio / Anthropic / OpenAI / Integration) that any task can dispatch to.",
        "workers": "For each touchpoint pick HTTP provider (cheap, local) or headless Claude / Gemini Agent (higher quality, costs CLI tokens).",
        "rules": "When the capture engine fires per session (after N messages / on idle / K characters / manual). Rules without a pinned provider follow the Capture worker setting above.",
        "profiles": "How prior memories get injected into the agent's system prompt at session spawn (recency, relevance, hybrid, or off).",
        "costs": "Aggregate spend reconstructed from memory_summarizer_calls. Local providers (Ollama, LM Studio, Integration) are free; cloud providers show real-world cost."
      },
      "moveBanner": {
        "title": "Memory configuration has moved",
        "body": "All memory-related settings (providers / capture rules / injection profiles / cost) now live alongside Workers in one page so related knobs sit together.",
        "openButton": "Open Memory configuration →"
      }
    },
    "memoryWorkers": {
      "title": "Memory workers",
      "loading": "Loading worker config…",
      "errorTitle": "Endpoint not reachable.",
      "errorDescription": "The /api/v1/memory/workers routes are new in M25 — the opendray binary may need a restart to mount them and run migration 0029.",
      "intro": "Each memory-system LLM touchpoint can be served independently by the local <1>summarizer</1> endpoint (LM Studio / OpenAI-compat) or by spawning a headless <3>Claude / Gemini agent</3> in <5>--print</5> mode. High-quality narrative tasks (gitactivity, transcript) benefit from agent workers; high-frequency tasks (gatekeeper) stay on the local endpoint by design.",
      "enabledBadge": "enabled",
      "disabledBadge": "disabled",
      "summarizerOnlyBadge": "summarizer-only",
      "callsCount": "{count} calls · 24h",
      "avgMs": "avg {ms}ms",
      "errorsCount": "{count} errors",
      "workerLabel": "Worker",
      "summarizerHttp": "Summarizer (HTTP)",
      "agentCliPrint": "Agent (CLI --print)",
      "summarizerProviderLabel": "Summarizer provider",
      "registryDefault": "Registry default",
      "cliLabel": "CLI",
      "selectPlaceholder": "Select",
      "cliClaude": "Claude",
      "cliGemini": "Gemini",
      "claudeAccountLabel": "Claude account",
      "claudeAccountDefault": "Default",
      "agentWarning": "Agent mode spawns a headless CLI per call. Latency rises from <1>~1s</1> (summarizer) to <3>~5-15s</3>; cost shifts from CPU to your Claude/Gemini quota.",
      "enabledCheckbox": "Enabled",
      "testButton": "Test",
      "saveButton": "Save",
      "recentCalls": "Recent calls ({count})",
      "tableWhen": "when",
      "tableWorker": "worker",
      "tableMs": "ms",
      "tableOk": "ok",
      "savedToast": "{label} updated",
      "saveFailedToast": "Save failed",
      "testOkToast": "{label} OK — {ms}ms",
      "testFailedToast": "{label} failed",
      "testCallFailedToast": "Test call failed",
      "unknownError": "unknown error",
      "tasks": {
        "gatekeeper": {
          "label": "Gatekeeper",
          "description": "Pre-write filter on every memory_store. High frequency (<500ms target) — summarizer-only."
        },
        "cleaner": {
          "label": "Cleaner librarian",
          "description": "Periodic LLM librarian. Judges aged memories as keep / stale / duplicate."
        },
        "gitactivity": {
          "label": "Git activity summariser",
          "description": "git log → 2-3 paragraph narrative every 24h. Naturally fits an agent worker."
        },
        "transcript": {
          "label": "Session transcript summariser",
          "description": "Session-end \"what did the agent do\" summary. Naturally fits an agent worker."
        },
        "plan_drift": {
          "label": "Plan drift detector",
          "description": "After each session ends, checks whether the project plan needs updating and files a proposal. Fits an agent worker for richer reasoning."
        },
        "conflict_detector": {
          "label": "Cross-layer conflict detector",
          "description": "Daily scan that finds contradictions between facts / plan / goal / journal. Higher-quality model = fewer false positives."
        },
        "capture": {
          "label": "Capture engine",
          "description": "Per-trigger fact extraction from session transcripts. Agent mode gives noticeably better facts on long sessions; summarizer mode is cheap and local."
        }
      }
    },
    "archived": {
      "loading": "Loading…",
      "emptyTitle": "Nothing archived",
      "emptyDescription": "No archived memories across any project. The auto-cleaner soft-archives stale and duplicate facts here (restorable for 30 days) — nothing has been removed yet.",
      "title": "Archived memories",
      "subtitle": "Memories the auto-cleaner and lifecycle pass soft-archived across every project. They are excluded from recall but restorable until the 30-day grace window purges them. Restore any false positive below.",
      "globalScope": "(global)",
      "openProject": "Open project",
      "archivedAtPrefix": "Archived",
      "restoreButton": "Restore",
      "restoredToast": "Restored",
      "restoreFailedToast": "Restore failed"
    },
    "project": {
      "picker": {
        "title": "Pick a project",
        "subtitle": "Project memory is scoped by working directory. Pick one to manage its goal, plan, journal, and cleanup queue.",
        "pathPlaceholder": "/path/to/your/project",
        "browse": "Browse",
        "browseTooltip": "Browse the gateway host's filesystem",
        "open": "Open",
        "recentLabel": "Recent projects (from stored memory):",
        "orphanTooltip": "Looks like a truncated scope_key (old mirror import bug). May have no project docs.",
        "orphanBadge": "orphan"
      },
      "noCwd": "Pick a project to manage its memory.",
      "header": {
        "docsCount_one": "{count} doc",
        "docsCount_other": "{count} docs",
        "journalEntries_one": "{count} journal entry",
        "journalEntries_other": "{count} journal entries",
        "pendingProposals_one": "{count} pending proposal",
        "pendingProposals_other": "{count} pending proposals",
        "archivedCount": "{count} archived"
      },
      "tabs": {
        "health": "Health",
        "goal": "Goal",
        "plan": "Plan",
        "tech": "Tech",
        "activity": "Activity",
        "journal": "Journal",
        "inbox": "Inbox",
        "conflicts": "Conflicts",
        "archived": "Archived"
      },
      "docLabel": {
        "goal": "Goal",
        "plan": "Plan",
        "tech_stack": "Tech stack",
        "recent_activity": "Recent activity"
      },
      "editor": {
        "updatedBy": "Updated by",
        "noDocSet": "No {label} set yet.",
        "save": "Save",
        "saveFailedToast": "Save failed",
        "savedToast": "{label} saved",
        "goalPlaceholder": "What are we building? One paragraph. Read by every agent on spawn.",
        "planPlaceholder": "Active plan — what we are doing right now and what is next. Updated as work progresses."
      },
      "readonly": {
        "tech_stack": {
          "label": "Tech stack & structure",
          "empty": "Run a Claude session in this project — scanner refreshes on every spawn."
        },
        "recent_activity": {
          "label": "Recent activity (git → LLM)",
          "empty": "The git activity summariser runs every 24h; check back after the next scheduler tick."
        },
        "noneCaptured": "No {label} captured yet.",
        "generatedBy": "Generated by",
        "lastRefresh": "last refresh"
      },
      "journal": {
        "loading": "Loading…",
        "empty": "No journal entries yet. Each session-end appends one automatically."
      },
      "inbox": {
        "loading": "Loading…",
        "emptyTitle": "Inbox empty.",
        "emptyHint": "Agents file proposals here via `project_goal_set` / `project_plan_set` MCP tools.",
        "approvedToast": "{label} updated",
        "approveFailedToast": "Approve failed",
        "rejectedToast": "Rejected",
        "rejectFailedToast": "Reject failed",
        "sessionPrefix": "ses",
        "warning": "Approve will REPLACE the current {label} entirely.",
        "warningSuffix": "Review the diff below; this isn't a merge.",
        "current": "Current",
        "proposed": "Proposed",
        "emptyBody": "(empty)",
        "approve": "Approve",
        "reject": "Reject",
        "confirmDialogTitle": "Replace {label}?",
        "confirmDialogDescription": "The current {label} will be overwritten with the proposed content. This cannot be undone via this UI (you can manually edit it back).",
        "confirmCancel": "Cancel",
        "confirmReplace": "Confirm replace"
      },
      "archived": {
        "hint": "Memories the auto-cleaner soft-archived for this project. They're excluded from recall but restorable until the 30-day grace window purges them.",
        "empty": "Nothing archived for this project. The cleaner soft-archives stale and duplicate facts here automatically — none yet.",
        "archivedAtPrefix": "Archived",
        "restoreButton": "Restore",
        "restoredToast": "Restored",
        "restoreFailedToast": "Restore failed"
      },
      "reset": {
        "button": "Reset",
        "dialogTitle": "Reset project memory?",
        "dialogDescription": "Deletes all stored project context for this cwd. This cannot be undone.",
        "alwaysDeleted": "Always deleted: goal, plan, proposals, journal, cleanup decisions.",
        "alsoDeleteScannerLabel": "Also delete scanner docs",
        "alsoDeleteScannerSuffix": "(tech_stack + recent_activity).",
        "alsoDeleteScannerHint": "Auto-rebuild on next spawn anyway — leaving unchecked is usually fine.",
        "alsoDeleteMemoriesLabel": "Also delete pgvector memories",
        "alsoDeleteMemoriesSuffix": "for this scope_key.",
        "alsoDeleteMemoriesHint": "Long-term facts the agent stored (user preferences, project facts). Cannot be recovered.",
        "cancel": "Cancel",
        "deleteForever": "Delete forever",
        "successToast": "Reset: deleted {summary}",
        "summary": {
          "docs_one": "{count} doc",
          "docs_other": "{count} docs",
          "journal": "{count} journal",
          "proposals_one": "{count} proposal",
          "proposals_other": "{count} proposals",
          "cleanup": "{count} cleanup",
          "memories": "{count} memories"
        },
        "failedToast": "Reset failed"
      }
    },
    "memoryInspector": {
      "status": {
        "label": "Active embedder",
        "unavailable": "unavailable",
        "probing": "probing…",
        "dimensions": "{dim}-dim · {state}",
        "enabled": "enabled",
        "disabled": "disabled",
        "floorNoModel": "Keyword (BM25) retrieval only — no embedding model configured. Add a dense [memory.http] endpoint in Settings to enable semantic memory.",
        "denseConfiguredPendingRestart": "Configured {model} (dense) — restart the gateway to activate semantic memory and re-embed existing memories.",
        "denseUnreachableFloor": "Configured {model} (dense) but the endpoint is unreachable — using the keyword floor until it responds (auto-upgrades on restart).",
        "denseDegraded": "Dense embedder active but its endpoint is unreachable right now — existing vectors are preserved; new writes and similarity search pause until it responds."
      },
      "scope": {
        "label": "Scope",
        "scopeKey": "Scope key",
        "scopeKeyIgnored": "(ignored for global)",
        "scopeKeyCwd": "(cwd of the project)",
        "placeholderProject": "/path/to/project (cwd)",
        "syncMd": "Sync .md",
        "syncTooltip": "Re-ingest Claude's <cwd>/.claude/memory/*.md files into pgvector",
        "values": {
          "project": "project",
          "global": "global"
        }
      },
      "search": {
        "placeholder": "Semantic search query (Enter to run; empty = browse)",
        "run": "Search",
        "clear": "Clear",
        "failedToast": "Search failed"
      },
      "records": {
        "noMemories": "No memories yet",
        "matches_one": "{count} match",
        "matches_other": "{count} matches",
        "memories_one": "{count} memory",
        "memories_other": "{count} memories",
        "scopeGlobalSuffix": " (global)",
        "scopeInSuffix": " in {scope}: ",
        "addButton": "Add memory",
        "addTooltip": "Manually create a memory in this scope",
        "deleteAll": "Delete all",
        "deleteAllTooltip": "Delete every memory under this scope/scope_key",
        "loading": "Loading…",
        "enterScopeKeyHint": "Enter a scope key to browse memories.",
        "noMatchesForQuery": "No matches for \"{query}\"",
        "noMemoriesInScope": "No memories in this scope yet."
      },
      "row": {
        "simBadge": "sim {value}",
        "rankBadge": "rank {value}",
        "rankTooltip": "effective {effective} = sim {similarity} × age {age} ({days}d) × hits {hits} × conf {confidence}",
        "hits_one": "{count} hit",
        "hits_other": "{count} hits",
        "lastHitTooltip": "Last hit {relative}",
        "editPlaceholder": "Memory text — Cmd/Ctrl+Enter to save · Esc to cancel",
        "saveTooltip": "Save (Cmd/Ctrl+Enter)",
        "cancelTooltip": "Cancel (Esc)",
        "editTooltip": "Edit this memory",
        "deleteTooltip": "Delete this memory",
        "emptyError": "Memory text cannot be empty",
        "deleteConfirm": "Delete memory {id}? This is permanent."
      },
      "toasts": {
        "deleted": "Memory deleted",
        "deleteFailed": "Delete failed",
        "bulkDeleted_one": "Deleted {count} memory from this scope",
        "bulkDeleted_other": "Deleted {count} memories from this scope",
        "bulkDeleteFailed": "Bulk delete failed",
        "created": "Memory created",
        "createFailed": "Create failed",
        "updated": "Memory updated",
        "updateFailed": "Update failed",
        "migrated": "Migrated {reembed}/{examined} memories to {to}",
        "migrationFailed": "Migration failed",
        "syncIngested_one": "Ingested {count} new memory file",
        "syncIngested_other": "Ingested {count} new memory files",
        "syncEmpty": "No new .md files to sync",
        "syncEmptyDescription": "Already in sync, or no Claude memory dir for this cwd.",
        "syncFailed": "Sync failed"
      },
      "bulkDelete": {
        "title": "Delete every memory in this scope?",
        "description": "This is a single SQL operation — all memories under the specified scope are removed atomically. Memories that were ingested via the Claude mirror reappear on the next <1>Sync .md</1> run; everything else is gone for good.",
        "scope": "Scope",
        "scopeKey": "Scope key",
        "currentlyVisible": "Currently visible",
        "items_one": "{count} memory item",
        "items_other": "{count} memory items",
        "cancel": "Cancel",
        "deleteAll": "Delete all"
      },
      "addMem": {
        "title": "Add memory",
        "description": "Manually create a memory. Agents create these automatically via the <1>memory_store</1> MCP tool — this form is for cases where the operator wants to seed a fact without going through an agent.",
        "textLabel": "Text",
        "textPlaceholder": "Plain prose. The embedder turns this into a vector at store time; agents will retrieve it via memory_search.",
        "cancel": "Cancel",
        "create": "Create"
      },
      "picker": {
        "button": "Pick",
        "buttonTooltip": "Pick from saved scope keys or active sessions",
        "loading": "Loading…",
        "empty": "No saved keys or active sessions for {scope}.",
        "savedHeader": "Saved memories",
        "activeHeader": "Active sessions"
      },
      "migrationBanner": {
        "headline_one": "{count} memory won't appear in searches",
        "headline_other": "{count} memories won't appear in searches",
        "subtitle": "{summary} — current embedder is <1>{current}</1>. pgvector partitions its similarity index by embedder, so older entries are silent until reembedded.",
        "summaryItem": "{count} on {name}",
        "migrateButton": "Migrate"
      },
      "reembed": {
        "title": "Reembed memories",
        "description": "Recompute vectors for memories stored under a different embedder so they become searchable again.",
        "targetEmbedder": "Target embedder",
        "onName": "on",
        "totalToReembed": "Total to reembed",
        "explainer": "Each memory's text gets re-sent to the current embedder; the new vector replaces the old one in place. ID, scope, scope_key, metadata and timestamps are preserved. Search results take effect immediately — no restart needed.",
        "reportExamined": "Examined",
        "reportReembedded": "Reembedded",
        "reportFailed": "Failed",
        "reportFrom": "From",
        "errors_one": "{count} error",
        "errors_other": "{count} errors",
        "done": "Done",
        "cancel": "Cancel",
        "reembedding": "Reembedding…",
        "reembedTotal": "Reembed {total}"
      }
    },
    "notes": {
      "title": "Notes",
      "header": {
        "outline": "Outline",
        "showOutline": "Show outline",
        "hideOutline": "Hide outline",
        "today": "Today",
        "todayTooltip": "Open or create today's daily note",
        "new": "New"
      },
      "left": {
        "tree": "Tree",
        "tags": "Tags",
        "filterNotes": "Filter notes…",
        "filterTags": "Filter tags…",
        "filteredBy": "filtered by",
        "clearTagTooltip": "Clear tag filter",
        "expandAll": "Expand all",
        "expandAllTooltip": "Expand every folder",
        "collapseAll": "Collapse all",
        "collapseAllTooltip": "Collapse every folder",
        "loading": "Loading…",
        "footer": "{visible} / {total} notes"
      },
      "tags": {
        "emptyVault": "No tags in vault yet.",
        "noMatches": "No matches for \"{query}\"."
      },
      "tree": {
        "empty": "Vault is empty."
      },
      "outline": {
        "label": "Outline",
        "empty": "No headings in this note. Add <1>## Title</1> lines to populate the outline."
      },
      "newNote": {
        "prompt": "New note path (vault-relative, must end .md)",
        "defaultPath": "library/notes-{date}.md",
        "errorMustEndMd": "Path must end in .md",
        "createdToast": "Note created",
        "createFailedToast": "Create failed"
      },
      "empty": {
        "title": "No note selected",
        "hint": "Pick a note from the tree on the left, jump straight to today's daily log, or create a fresh one. AI-written project docs live under <1>projects/</1>; your personal scratchpads under <3>personal/</3>.",
        "today": "Today's daily note",
        "new": "New note"
      },
      "picker": {
        "browseAria": "Browse folders",
        "matches_one": "{count} match",
        "matches_other": "{count} matches",
        "foldersInVault": "{count} folders in vault",
        "noMatch": "No existing folder matches. Save anyway to use <1>{value}</1> (lazy-created on first write)."
      },
      "vaultSync": {
        "title": "Vault sync",
        "description": "Commit, pull, and push the notes vault as a git repository. Authentication uses your gateway host's git credentials (SSH agent / credential helper).",
        "reading": "Reading vault state…",
        "init": {
          "title": "Vault is not a git repo yet",
          "body": "Initialising will run <1>git init -b main</1> in your vault root and add a sane <3>.gitignore</3>. After that you can commit your notes and configure a remote (GitHub / Gitea / GitLab) for cross-machine sync.",
          "button": "Initialise vault as git repo",
          "initToast": "Vault initialised as git repo",
          "initFailedToast": "Init failed"
        },
        "branch": {
          "clean": "clean",
          "staged": "{count} staged",
          "modified": "{count} modified",
          "untracked": "{count} untracked"
        },
        "action": {
          "pull": "Pull",
          "push": "Push",
          "pullTitleNoRemote": "Configure a remote first",
          "pullTitleHasUpstream": "git pull --rebase --autostash",
          "pullTitleNoUpstream": "Pulls origin's HEAD; sets up tracking implicitly",
          "pushTitleNoRemote": "Configure a remote first",
          "pushTitleHasUpstream": "git push -u origin HEAD",
          "pushTitleNoUpstream": "First push — will set upstream to origin/HEAD",
          "noRemote": "No remote configured — pull/push disabled",
          "noUpstream": "No upstream tracking yet — first push will set it.",
          "pulledToast": "Pulled",
          "pullFailedToast": "Pull failed",
          "pushedToast": "Pushed",
          "pushFailedToast": "Push failed"
        },
        "commit": {
          "title": "Commit",
          "placeholder": "Notes: {date} (default)",
          "commitAll": "Commit all",
          "hint": "Stages every change (<1>git add .</1>) then commits with this message. Empty message defaults to a timestamped subject.",
          "committedToast": "Committed {hash}",
          "commitFailedToast": "Commit failed"
        },
        "fileList": {
          "title": "Working tree · {count}",
          "moreSuffix": "+{count} more"
        },
        "remote": {
          "title": "Remote (origin)",
          "cancel": "Cancel",
          "change": "Change",
          "configure": "Configure",
          "empty": "No remote set. Add an HTTPS or SSH URL (e.g. <1>git@github.com:you/notes.git</1> or <3>https://gitea.example.com/you/notes.git</3>) to enable push / pull.",
          "urlLabel": "URL (HTTPS or SSH)",
          "urlPlaceholder": "git@host:owner/notes.git",
          "save": "Save",
          "savedToast": "Remote saved",
          "saveFailedToast": "Set remote failed"
        },
        "history": {
          "title": "Recent commits",
          "loading": "Loading…",
          "empty": "No commits yet."
        },
        "conflict": {
          "kinds": {
            "rebase": "rebase",
            "merge": "merge",
            "cherryPick": "cherry-pick",
            "operation": "operation"
          },
          "headline": "Vault has a paused {kind} with unresolved conflicts",
          "explainer": "Pull, push and commit are blocked until the {kind} finishes. You can either <1>abort</1> (restore the working tree to its state before the {kind} — keeps your local commits, drops the remote ones) or <3>force reset to remote</3> (discard ALL local commits + uncommitted changes; vault becomes an exact mirror of origin).",
          "conflictedHeader": "Conflicted files · {count}",
          "abort": "Abort {kind}",
          "abortTitle": "git {kind} --abort",
          "forceReset": "Force reset to remote",
          "forceResetTitle": "git fetch && git reset --hard origin/<branch> && git clean -fd",
          "forceResetConfirm": "DESTRUCTIVE: this will\n  • abort the in-progress {kind}\n  • run git fetch origin\n  • reset --hard to origin/<branch>\n  • clean -fd (drop untracked files)\n\nAny local commits not pushed to origin AND any uncommitted edits will be PERMANENTLY LOST.\n\nContinue?",
          "abortedToast": "Aborted {kind}",
          "abortedDescription": "Working tree restored to pre-operation state.",
          "abortFailedToast": "Abort failed",
          "resetToast": "Reset to {branch}",
          "resetDescription": "Local changes discarded; vault matches remote.",
          "resetFailedToast": "Reset failed"
        },
        "auth": {
          "title": "Authentication",
          "httpsTokenOk": "Will use the token stored for <1>{host}</1> in Plugins → Git hosts. ✓",
          "httpsTokenMissing": "HTTPS remote on <1>{host}</1> with no opendray token configured. Push / pull will likely fail for private repos until you add one.",
          "ssh": "SSH remote on <1>{host}</1>. Auth uses the gateway host's <3>~/.ssh/</3> (ssh-agent, identity file, host config). Verify with <5>ssh -T git@{host}</5> from the host shell.",
          "configureTokenLink": "→ Configure git host token"
        },
        "autoSync": {
          "loading": "Loading auto-sync settings…",
          "title": "Auto-sync",
          "on": "on",
          "runNow": "Run now",
          "runNowTooltip": "Wake the sync loop now (skips the wait, then runs whichever steps are due)",
          "configure": "Configure",
          "hide": "Hide",
          "enabled": "Enabled",
          "enabledTooltipNoRemote": "Configure a remote first to enable auto-sync",
          "noRemoteHint": "No remote — push/pull will be skipped.",
          "commitEvery": "Commit every",
          "commitEveryExamples": "Examples: <1>30s</1>, <3>10m</3>, <5>2h</5>. Min 30s.",
          "pullEvery": "Pull every",
          "pullEveryHint": "Only used when Pull is enabled.",
          "pushAfterCommit": "Push after commit",
          "pullPeriodically": "Pull periodically",
          "commitTemplateLabel": "Commit message template",
          "commitTemplatePlaceholder": "Auto-sync: {date}  (default if empty)",
          "saveSettings": "Save settings",
          "discard": "Discard",
          "lastCommit": "last commit",
          "lastPush": "last push",
          "lastPull": "last pull",
          "never": "never",
          "savedToast": "Auto-sync settings saved",
          "saveFailedToast": "Save failed",
          "triggeredToast": "Auto-sync triggered",
          "runFailedToast": "Run failed"
        }
      },
      "syncBadge": {
        "loading": "Loading…",
        "syncLabel": "Sync",
        "initLabel": "Init",
        "initTooltip": "Vault is not a git repo yet",
        "conflictLabel": "Conflict",
        "conflictTooltip": "Vault has unresolved conflicts — click to recover",
        "syncFallback": "sync",
        "tooltip": "branch {branch} · {files} changes · {ahead} ahead · {behind} behind",
        "tooltipAutoOn": " · auto-sync on",
        "tooltipLastError": " · last error: {error}",
        "branchPlaceholder": "—"
      }
    },
    "activity": {
      "title": "Activity",
      "subtitle": "Per-call audit of API requests made by registered integrations. Includes both inbound calls (a third-party app calling opendray with its API key) and outbound proxied calls (admin → opendray proxy → integration). Calls made directly by this admin UI are not recorded.",
      "refresh": "Refresh",
      "refreshTooltip": "Refresh",
      "filters": {
        "integration": "Integration",
        "direction": "Direction",
        "status": "Status",
        "allIntegrations": "All integrations",
        "all": "All",
        "inbound": "Inbound",
        "outbound": "Outbound",
        "allStatuses": "All statuses",
        "status2": "2xx success",
        "status3": "3xx redirect",
        "status4": "4xx client error",
        "status5": "5xx server error"
      },
      "callsCount_one": "{count} call",
      "callsCount_other": "{count} calls",
      "loading": "Loading…",
      "table": {
        "time": "Time",
        "integration": "Integration",
        "directionTitle": "Direction",
        "method": "Method",
        "path": "Path",
        "status": "Status",
        "duration": "Duration",
        "inboundAria": "inbound",
        "outboundAria": "outbound"
      },
      "empty": {
        "filtered": "No calls match these filters.",
        "title": "No API calls recorded yet",
        "description": "When a third-party app calls opendray with its integration API key, every request is logged here.",
        "stepWithIntegrations": "Use an existing integration's API key in your third-party app",
        "stepRegister": "Register an integration in Integrations → New",
        "stepCallEndpoint": "Call any endpoint, e.g. <1>POST /api/v1/sessions</1>",
        "stepAppears": "Calls appear here within seconds",
        "footnote": "Calls you make from this admin UI are not logged — only integration-attributed traffic is recorded."
      },
      "events": {
        "loading": "Loading events…",
        "empty": "No events yet.",
        "emptyFiltered": "No matching events.",
        "loadOlder": "Load older events",
        "today": "Today",
        "yesterday": "Yesterday"
      }
    },
    "providers": {
      "list": {
        "title": "Providers",
        "loading": "Loading…",
        "disabledBadge": "disabled",
        "noneSelected": "No provider selected."
      },
      "detail": {
        "enabled": "Enabled",
        "disabled": "Disabled",
        "toggleAria": "Toggle {name}",
        "configuration": "Configuration",
        "noConfig": "This provider has no user-configurable fields.",
        "executable": "executable:",
        "manifestHash": "manifest_hash:",
        "reset": "Reset",
        "save": "Save changes",
        "saving": "Saving…",
        "savedToast": "Provider config saved",
        "saveFailedToast": "Save failed",
        "toggleFailedToast": "Toggle failed",
        "caps": {
          "resume": "resume",
          "stream": "stream",
          "images": "images",
          "mcp": "mcp"
        },
        "notInstalled": "not installed",
        "updateAvailable": "update available → {version}",
        "upToDate": "up to date",
        "update": "Update to {version}",
        "updating": "Updating…",
        "updatedToast": "Updated {from} → {to}",
        "alreadyLatestToast": "Already up to date",
        "updateFailedToast": "Update failed",
        "updateUnavailable": "In-app update not available here"
      },
      "configForm": {
        "selectPlaceholder": "Select…",
        "defaultOption": "(default)",
        "switchOn": "On",
        "switchOff": "Off",
        "showSecret": "Show secret",
        "hideSecret": "Hide secret"
      },
      "claudeAccounts": {
        "title": "Claude accounts",
        "importLocal": "Import local",
        "importLocalTooltip": "Scan ~/.claude-accounts/ on the gateway host and register any new directories. The button is gateway-host only.",
        "importedNothingToast": "Nothing to import — accounts already in sync.",
        "importedToast_one": "Imported {count} account from ~/.claude-accounts",
        "importedToast_other": "Imported {count} accounts from ~/.claude-accounts",
        "importFailedToast": "Import failed",
        "addingTitle": "Adding a new account.",
        "addingBodyPrefix": "Run on the gateway host:",
        "addingBodySuffix": "opendray's filesystem watcher will register the new directory automatically, or click <1>Import local</1> to scan immediately.",
        "architectureLink": "Architecture & full guide →",
        "loading": "Loading…",
        "empty": "No Claude accounts yet. Easiest path: open Sessions, spawn a Claude session, and run <1>claude login</1> in the terminal — your OAuth credentials land in <3>~/.claude</3> on the gateway and show up here automatically. Power users juggling multiple identities can use the shell workflow above instead.",
        "noTokenYet": "no token yet",
        "configDir": "config_dir:",
        "tokenPath": "token_path:",
        "toggleFailedToast": "Toggle failed",
        "removeConfirm": "Remove account \"{name}\"?",
        "removedToast": "Account removed",
        "removeFailedToast": "Remove failed",
        "toggleAria": "Toggle {name}",
        "removeAria": "Remove {name}",
        "identityAcceptedToast": "New identity recorded",
        "identityAcceptFailedToast": "Could not accept identity"
      },
      "models": {
        "title": "Models",
        "help": "Models offered for this provider. The default is passed to every session via the model flag; sessions can still override it.",
        "empty": "No models configured yet.",
        "add": "Add",
        "addPlaceholder": "model id (e.g. sonnet)",
        "suggested": "Suggested ({count})",
        "default": "default",
        "makeDefault": "set default",
        "setDefault": "Use as the default model",
        "remove": "Remove {model}"
      }
    },
    "channels": {
      "title": "Channels",
      "subtitle": "Bidirectional messaging integrations. Every enabled, unmuted channel receives session notifications.",
      "newButton": "New channel",
      "loading": "Loading…",
      "empty": {
        "title": "No channels yet",
        "description": "Bundled kinds: Telegram · Slack · Discord · Feishu · DingTalk · WeCom. Pick one and paste credentials, or use <1>bridge</1> for a custom platform via WebSocket."
      },
      "card": {
        "running": "running",
        "starting": "starting…",
        "disabled": "disabled",
        "muted": "muted",
        "tokenLabel": "token:",
        "chatIdLabel": "chat_id:",
        "channelIdLabel": "channel_id:",
        "webhookLabel": "webhook:",
        "copyWebhookTooltip": "Copy webhook URL",
        "webhookCopiedToast": "Webhook URL copied",
        "setup": "Setup",
        "setupTooltip": "Show adapter connection details + sample code",
        "test": "Test",
        "testNotRunningTooltip": "Channel must be running",
        "testBridgeTooltip": "Bridge channels cannot be tested from the admin — connect an adapter first",
        "editAria": "Edit channel",
        "editTooltip": "Edit channel config",
        "deleteAria": "Delete channel",
        "muteAria": "Mute or unmute channel",
        "muteTooltip": "Mute notifications (two-way chat keeps working)",
        "unmuteTooltip": "Unmute notifications",
        "bridgeSuffix": "(bridge)"
      },
      "toasts": {
        "testSent": "Test message sent",
        "testFailed": "Test failed",
        "deleteConfirm": "Delete channel {id}?",
        "deleted": "Channel deleted",
        "created": "Channel created",
        "updated": "Channel updated",
        "muted": "Channel muted",
        "unmuted": "Channel unmuted"
      },
      "dialog": {
        "editTitle": "Edit channel",
        "createTitle": "Register channel",
        "descriptionBridge": "External adapter (Python/Node/...) connects via WebSocket and presents this token.",
        "descriptionDefault": "Configure messaging integration.",
        "kindLabel": "Kind",
        "kindImmutable": "(immutable — delete and recreate to change kind)",
        "enabledLabel": "Enabled",
        "enabledBridgeHint": " (accept adapter connections immediately)",
        "enabledWebhookHint": " (start receiving webhooks immediately)",
        "enabledDefaultHint": " (start immediately)",
        "cancel": "Cancel",
        "save": "Save",
        "saving": "Saving…",
        "create": "Create",
        "creating": "Creating…",
        "unknownKind": "Unknown kind: {kind}",
        "nameRequired": "name is required",
        "tokenRequired": "token is required",
        "topicIdsNumeric": "Topic IDs must be numeric (got {value})",
        "fieldRequired": "{label} is required",
        "cooldownInvalid": "Cooldown must be a non-negative number of seconds",
        "snippetCapInvalid": "Snippet cap must be a non-negative number"
      },
      "notifications": {
        "sectionTitle": "Session notifications",
        "repeatPolicyLabel": "Repeat policy",
        "cooldownLabel": "Cooldown duration",
        "onceReplyHint": "Replying with non-command text in this chat resets the suppression — opendray forwards your reply to the session's stdin and re-arms the notifier.",
        "terminalSnippetLabel": "Terminal snippet",
        "embedSnippetLabel": "Embed the recent terminal screen in idle notifications",
        "snippetExplainer": "When enabled, the idle card includes a code-block snippet of what the user would see in the live web terminal — Claude TUI chrome (status spinner, \"bypass permissions\" hint, separator lines) is filtered out automatically.",
        "modes": {
          "onceLabel": "Once per session (recommended)",
          "onceHint": "Fire once when a session goes idle, then stay silent until either the session ends or you reply via this channel.",
          "cooldownLabel": "Time-window cooldown",
          "cooldownHint": "Suppress repeats for the same (session, event) within the chosen window.",
          "everyLabel": "Every event (noisy)",
          "everyHint": "No suppression. Use only for low-frequency channels."
        },
        "cooldowns": {
          "60": "1 minute",
          "300": "5 minutes",
          "900": "15 minutes",
          "1800": "30 minutes",
          "3600": "1 hour"
        },
        "snippetCaps": {
          "0": "No cap — chunk into multiple messages (default)",
          "1000": "1000 chars (terse)",
          "3000": "3000 chars",
          "6000": "6000 chars",
          "12000": "12000 chars"
        }
      },
      "bridge": {
        "nameLabel": "Bridge name",
        "namePlaceholder": "wechat / discord-custom / whatsapp...",
        "nameHint": "Human label for the adapter. Shown in the channels list.",
        "tokenLabel": "Adapter token",
        "regenerateTooltip": "Regenerate",
        "copyTooltip": "Copy",
        "tokenCopiedToast": "Token copied",
        "tokenHint": "Adapter authenticates by sending this in the WS register frame (or as <1>X-Bridge-Token</1> header).",
        "capsLabel": "Accept capabilities (optional whitelist)",
        "capsHint": "Empty = accept whatever the adapter declares. Selected = only allow these capabilities even if the adapter offers more.",
        "afterCreate": "After <1>Create</1>, the adapter setup dialog opens automatically with the WebSocket URL and copy-pasteable Python / Node / wscat starter code."
      },
      "setup": {
        "title": "Adapter setup — {name}",
        "description": "Run an adapter (any language) that connects to opendray over WebSocket using these credentials. opendray will route session notifications and slash-command actions through it.",
        "wsUrlLabel": "WebSocket URL",
        "tokenLabel": "Adapter token",
        "authInfo": "<1>Auth:</1> send the token as <3>X-Bridge-Token</3> header, <5>?token=</5> query param, or <7>Authorization: Bearer …</7>. The first WS frame must be <9>{frame}</9>. Full spec: <11>docs/bridge-protocol.md</11> in the repo.",
        "pythonInstall": "Install: <1>pip install websockets</1>. Run: <3>python adapter.py</3>.",
        "nodeInstall": "Install: <1>npm i ws</1>. Run: <3>node adapter.mjs</3>.",
        "wscatInstall": "Install: <1>npm i -g wscat</1>. Once connected, paste the JSON line shown above to register, then send further frames manually.",
        "close": "Close",
        "copyHide": "Hide",
        "copyShow": "Show",
        "copyLabelToast": "{label} copied",
        "copyCode": "Copy",
        "copied": "Copied",
        "codeCopiedToast": "Code copied"
      }
    },
    "integrations": {
      "title": "Integrations",
      "subtitle": "External apps that consume opendray. Reverse-proxy through <1>/api/v1/proxy/&lt;prefix&gt;/…</1> and subscribe to events via the WS endpoint.",
      "register": "Register",
      "loading": "Loading…",
      "tabs": {
        "registered": "Registered",
        "console": "Reverse proxy"
      },
      "empty": {
        "title": "No integrations yet",
        "description": "Register an external app to give it a scoped API key. Its code stays out of this repo.",
        "register": "Register integration"
      },
      "groupSystem": "System (managed by opendray)",
      "groupOperator": "Operator-registered",
      "card": {
        "managedBadge": "managed",
        "managedTooltip": "opendray manages this integration. Editing or rotating its key would orphan running sessions whose mcp.json holds the previous bearer.",
        "consumerBadge": "consumer",
        "consumerTooltip": "Consumer-only integration — no HTTP service to probe",
        "disabledBadge": "disabled",
        "consumerOnlyHint": "Consumes opendray's API. No reverse proxy mounted.",
        "lastProbed": "last probed {relative}",
        "rotated": "rotated {relative}",
        "managedReadOnly": "read-only — opendray bakes its key into every spawn's mcp.json",
        "managedReadOnlyTooltip": "opendray manages this row. To reset: delete ~/.opendray/memory.key and restart, or delete this row directly via SQL — it'll be re-bootstrapped at next startup.",
        "editAria": "Edit integration",
        "editTooltip": "Edit scopes / base URL / version",
        "rotateKey": "Rotate key",
        "deleteAria": "Delete integration",
        "rotateConfirm": "Rotate the API key for \"{name}\"? The current key will stop working immediately.",
        "deleteConfirm": "Delete integration {name}?",
        "removedToast": "Integration removed"
      },
      "register_dialog": {
        "title": "Register integration",
        "description": "Issues a one-time API key. Copy it before closing — opendray never displays the plaintext again.",
        "nameLabel": "Name",
        "namePlaceholder": "PetTracker",
        "modeHint": "Leave the next two fields blank for a <1>consumer-only</1> integration (third-party app that calls opendray's API but doesn't expose its own service). Fill both for a <3>reverse-proxy</3> integration.",
        "baseUrlLabel": "Base URL",
        "optionalSuffix": "(optional)",
        "baseUrlPlaceholder": "http://192.168.1.10:8080",
        "routePrefixLabel": "Route prefix",
        "routePrefixPlaceholder": "pet-tracker",
        "routePrefixHint": "Reachable at <1>/api/v1/proxy/{prefix}/*</1>.",
        "routePrefixPlaceholderToken": "<prefix>",
        "versionLabel": "Version (optional)",
        "versionPlaceholder": "0.1.0",
        "scopesLabel": "Scopes",
        "scopesIntro": "Pick the API surface this integration is allowed to call. Each toggle maps to a Bearer-token claim — opendray rejects requests that touch endpoints outside the granted set.",
        "errorNameRequired": "Name is required.",
        "errorBothOrNeither": "base_url and route_prefix go together. Set both for a reverse-proxy integration, leave both blank for a consumer-only integration.",
        "cancel": "Cancel",
        "submit": "Register",
        "submitting": "Registering…"
      },
      "reveal": {
        "titleIssued": "API key issued",
        "titleRotated": "API key rotated",
        "description": "This is the only time the plaintext key will be shown. Copy it now and update every consumer app — the previous key (if any) no longer authenticates.",
        "discardAria": "Discard new key",
        "discardTooltip": "Discard the new key (rotation already happened — old key is gone too)",
        "discardConfirm": "Discard the new key? Rotation has already invalidated the old key — discarding means you have NO working key for this integration until you rotate again.",
        "copy": "Copy",
        "copied": "Copied",
        "updateHint": "<1>Update every consumer app with this new key.</1> The previous key has been invalidated server-side and will return <3>401 unauthorized</3> on the next request.",
        "acknowledge": "I have copied the key and will update my consumer apps. I understand opendray will not display it again.",
        "discard": "Discard",
        "done": "Done"
      },
      "edit_dialog": {
        "title": "Edit integration · {name}",
        "description": "Change scopes, version, or base URL. Name and route prefix are immutable — delete + re-register if you need to change those.",
        "nameLabel": "Name",
        "routePrefixLabel": "Route prefix",
        "consumerOnlyLabel": "(consumer-only)",
        "baseUrlLabel": "Base URL",
        "baseUrlConsumerSuffix": "(consumer-only — leave blank)",
        "baseUrlProxySuffix": "(reverse-proxy target)",
        "baseUrlConsumerPlaceholder": "(blank — this integration consumes opendray's API)",
        "baseUrlProxyPlaceholder": "http://127.0.0.1:8080",
        "consumerHint": "This is a consumer-only integration. Changing base URL here would also require a route prefix; do that with delete + re-register.",
        "versionLabel": "Version",
        "versionPlaceholder": "0.1.0",
        "scopesLabel": "Scopes",
        "scopesIntro": "Trim or widen the API surface this integration's API key authorises. Live tokens are unaffected — the new scope set takes effect on the next request.",
        "errorModeSwitch": "Switching between consumer-only and reverse-proxy mode requires deleting the integration and re-registering — name and route_prefix can't change in place.",
        "updatedToast": "Integration updated",
        "cancel": "Cancel",
        "save": "Save changes"
      },
      "proxy": {
        "emptyTitle": "No integrations registered",
        "emptyDescription": "Register an integration first; the console proxies through /api/v1/proxy/{prefix}/* using the admin token.",
        "targetLabel": "Target",
        "selectPlaceholder": "Select integration…",
        "baseLabel": "base:",
        "history": "History",
        "historyEmpty": "no past requests for this integration",
        "send": "Send",
        "sending": "Sending…",
        "extraHeadersLabel": "Extra headers (one per line, Name: Value)",
        "bodyLabel": "Body",
        "headers": "Headers",
        "body": "Body",
        "emptyBody": "(empty)",
        "requestFailed": "request failed",
        "stubText": "Send a request to see the upstream response.",
        "stubInjects": "opendray injects <1>X-Integration-ID</1> and strips your <3>Authorization</3> header.",
        "prefixPlaceholder": "<prefix>"
      }
    },
    "plugins": {
      "title": "Inspector plugins",
      "subtitle": "Configure data sources surfaced in the right-hand Inspector panel when a session is open. Each plugin is admin-only and shared across all sessions. Click a section header to collapse it.",
      "common": {
        "loading": "Loading…",
        "cancel": "Cancel",
        "edit": "Edit",
        "add": "Add",
        "save": "Save",
        "create": "Create"
      },
      "mcp": {
        "title": "MCP servers",
        "description": "Model Context Protocol servers injected into every spawn (claude / codex). Vault entries live at <1>~/.opendray/vault/mcp/&lt;id&gt;/mcp.json</1>; secrets (referenced as <3>${KEY}</3> in env / headers) come from the <5>MCP secrets</5> section below.",
        "newServer": "New server",
        "empty": "No MCP servers yet. Add one to expose extra tools to your agent sessions.",
        "columns": {
          "name": "Name",
          "transport": "Transport",
          "spec": "Spec",
          "enabled": "Enabled"
        },
        "noUrl": "no url",
        "noCommand": "no command",
        "deleteConfirm": "Delete MCP server \"{id}\"?",
        "removedToast": "MCP server removed",
        "deleteFailedToast": "Delete failed",
        "toggleFailedToast": "Toggle failed",
        "codexUnsupportedBadge": "Codex: unsupported",
        "codexUnsupportedTooltip": "The codex CLI supports stdio transport only. This server will be skipped for codex sessions; claude and gemini will still use it.",
        "editor": {
          "createTitle": "New MCP server",
          "editTitle": "Edit MCP: {id}",
          "description": "JSON shape: <1>command</1>+<3>args</3>+<5>env</5> for stdio (default), or <7>transport</7> +<9> url</9>+<11>headers</11> for sse / http. Reference secrets as <13>${API_KEY}</13> — they get substituted at spawn time from the secrets file.",
          "idLabel": "ID",
          "idPlaceholder": "filesystem",
          "idHint": "Lowercase / digits / dash / underscore. Becomes both the directory name and the default <1>name</1>.",
          "bodyLabel": "mcp.json",
          "invalidJson": "Invalid JSON: {error}",
          "createdToast": "MCP server created",
          "savedToast": "MCP server saved",
          "createFailedToast": "Create failed",
          "saveFailedToast": "Save failed",
          "transportLabel": "Transport",
          "transportHint": "Switching transport replaces the JSON template with a starter shape appropriate for the new transport.",
          "transportStdio": "stdio (local subprocess)",
          "transportSse": "sse (remote server)",
          "transportHttp": "http (remote server)"
        },
        "test": {
          "button": "Test",
          "title": "Validate this MCP server from the daemon",
          "connected": "connected · {count} tools",
          "reachable": "reachable",
          "failed": "test failed"
        }
      },
      "mcpSecrets": {
        "title": "MCP secrets",
        "encryptedBadge": "encrypted",
        "plaintextBadge": "plaintext",
        "encryptedTooltip": "AES-GCM encrypted on disk; key stored in OS keychain",
        "plaintextTooltip": "OS keychain unavailable — file is plaintext on disk. Check the gateway log.",
        "description": "Values referenced from <1>${KEY}</1> placeholders in any <3>mcp.json</3> get substituted at spawn time. <5>Saved values are never returned over the API</5> — you can overwrite or delete them but not read them back.",
        "descriptionStored": " Stored at <1>{path}</1>.",
        "addSecret": "Add secret",
        "empty": "No secrets stored. Add one to start referencing it as <1>${KEY}</1> in your MCP server configs.",
        "columns": {
          "key": "Key",
          "value": "Value"
        },
        "editTooltip": "Overwrite the stored value",
        "deleteConfirm": "Delete secret \"{key}\"? Any mcp.json that references ${key} will fall back to the literal placeholder until you set a new value.",
        "removedToast": "Secret removed",
        "deleteFailedToast": "Delete failed",
        "editor": {
          "addTitle": "Add secret",
          "updateTitle": "Update {key}",
          "addDescription": "Stored encrypted on disk if the OS keychain is available. Reference it from any mcp.json env / headers / args / url with ${KEY}.",
          "editDescription": "Enter the new value to overwrite. The previous value cannot be recovered.",
          "keyLabel": "Key",
          "keyPlaceholder": "BRAVE_API_KEY",
          "keyPattern": "Must match <1>[A-Za-z_][A-Za-z0-9_]*</1>",
          "keyCollision": "Already exists — use Edit instead, or pick a different name.",
          "valueLabel": "Value",
          "valueHint": "Hidden as you type. Saved value is never returned over the API.",
          "addedToast": "Secret added",
          "updatedToast": "Secret updated",
          "saveFailedToast": "Save failed"
        }
      },
      "skills": {
        "title": "Agent skills",
        "description": "Reusable capabilities injected into Claude sessions as a Tier 1 index — the agent loads full SKILL.md on demand via <1>opendray skill describe &lt;id&gt;</1>. Built-ins ship in the binary but can be <3>customized</3> — your edits land at <5>~/.opendray/vault/skills/&lt;id&gt;/SKILL.md</5> and override the embedded version. Use Reset to revert.",
        "newSkill": "New skill",
        "empty": "No skills found.",
        "columns": {
          "id": "ID",
          "description": "Description",
          "source": "Source"
        },
        "noDescription": "no description",
        "builtinBadge": "builtin",
        "builtinTooltip": "Embedded in the opendray binary — click Customize to override in your vault",
        "vaultBadge": "vault",
        "overridesBuiltin": "overrides builtin",
        "overridesBuiltinTooltip": "This vault skill overrides the built-in version of the same id",
        "customize": "Customize",
        "customizeTooltip": "Open the SKILL.md and save changes as a vault override",
        "editTooltip": "Edit this vault skill",
        "resetTooltip": "Delete vault override and fall back to the built-in version",
        "reset": "Reset",
        "resetConfirm": "Reset \"{id}\" to the built-in version? This deletes your vault SKILL.md and falls back to the embedded copy.",
        "deleteConfirm": "Delete skill \"{id}\" from your vault? This removes the SKILL.md file.",
        "removedToast": "Skill removed",
        "deleteFailedToast": "Delete failed",
        "dropHint": "Or drop a SKILL.md here to install it.",
        "dropToInstall": "Drop SKILL.md to install",
        "uploading": "Installing skill…",
        "uploadedToast": "Skill \"{id}\" installed",
        "uploadFailedToast": "Skill upload failed",
        "uploadInvalidTypeToast": "Only SKILL.md files can be installed by drop",
        "editor": {
          "createTitle": "New skill",
          "customizeTitle": "Customize built-in: {id}",
          "editTitle": "Edit skill: {id}",
          "customizeDescription": "You're viewing a built-in skill embedded in opendray. Saving will create a vault override at the same id — your edits live under ~/.opendray/vault/skills/<id>/SKILL.md and shadow the built-in until you Reset.",
          "editDescription": "SKILL.md format — frontmatter with name + description, then markdown instructions. The description appears in the agent's Tier 1 index.",
          "idLabel": "ID",
          "idPlaceholder": "my-helper",
          "idHint": "Lowercase / digits / dash / underscore. Becomes the directory name under <1>~/.opendray/vault/skills/&lt;id&gt;/</1>.",
          "bodyLabel": "SKILL.md",
          "createdToast": "Skill created",
          "savedToast": "Skill saved",
          "savedOverrideToast": "Saved as vault override",
          "createFailedToast": "Create failed",
          "saveFailedToast": "Save failed",
          "saveAsOverride": "Save as vault override"
        }
      },
      "customTasks": {
        "title": "Custom tasks",
        "description": "Click-to-run shortcuts surfaced in the Tasks tab. Leave cwd blank for global tasks visible in every session, or pin to an absolute path to scope.",
        "addTask": "Add task",
        "empty": "No custom tasks yet.",
        "columns": {
          "name": "Name",
          "command": "Command",
          "scope": "Scope"
        },
        "globalScope": "global",
        "deleteConfirm": "Delete custom task \"{name}\"?",
        "removedToast": "Task removed",
        "deleteFailedToast": "Delete failed",
        "dialog": {
          "addTitle": "Add custom task",
          "editTitle": "Edit {name}",
          "description": "The command is sent verbatim into the session's terminal. Same as typing it at the prompt and pressing Enter.",
          "nameLabel": "Name",
          "namePlaceholder": "dev",
          "commandLabel": "Command",
          "commandPlaceholder": "docker compose up --build",
          "descLabel": "Description (optional)",
          "descPlaceholder": "Boots dev infra and tails logs",
          "cwdLabel": "Cwd scope (optional)",
          "cwdPlaceholder": "/Users/me/projects/foo  (blank = global)",
          "cwdHint": "Blank = visible in every session. Otherwise the task only shows when the session's cwd matches this absolute path.",
          "addedToast": "Task added",
          "updatedToast": "Task updated",
          "addFailedToast": "Add failed",
          "updateFailedToast": "Update failed"
        }
      },
      "gitHosts": {
        "title": "Git hosts",
        "description": "One token per host — used by the Git tab to fetch pull requests <1>and by the Notes vault sync</1> when its remote uses HTTPS to a private repo on the same host. GitHub.com, self-hosted GitHub Enterprise, Gitea, and GitLab are supported.",
        "addHost": "Add host",
        "empty": "No git hosts configured.\nAdd one to enable the PR list in the inspector's Git tab.",
        "columns": {
          "host": "Host",
          "kind": "Kind",
          "token": "Token",
          "enabled": "Enabled"
        },
        "statusEnabled": "enabled",
        "statusDisabled": "disabled",
        "deleteConfirm": "Remove git host {host}? PR queries against this host will stop working.",
        "removedToast": "Git host removed",
        "deleteFailedToast": "Delete failed",
        "dialog": {
          "addTitle": "Add git host",
          "editTitle": "Edit {host}",
          "description": "Token is stored on the gateway. Used only for read-only API calls (list PRs, etc.).",
          "kindLabel": "Kind",
          "kindGitHub": "GitHub",
          "kindGitea": "Gitea",
          "kindGitLab": "GitLab",
          "hostLabel": "Host",
          "hostPlaceholder": "github.com",
          "displayNameLabel": "Display name (optional)",
          "displayNamePlaceholder": "Personal",
          "tokenLabel": "Token",
          "newTokenLabel": "New token (leave blank to keep)",
          "tokenPlaceholder": "ghp_… / gho_… / glpat-…",
          "tokenPlaceholderEdit": "…",
          "tokenHint": "GitHub: PAT with <1>repo</1> scope. Gitea: token with <3>read:repository</3>. GitLab: PAT with <5>read_api</5>.",
          "enabledLabel": "Enabled",
          "addedToast": "Git host added",
          "updatedToast": "Git host updated",
          "addFailedToast": "Add failed",
          "updateFailedToast": "Update failed"
        }
      }
    },
    "backups": {
      "title": "Backups",
      "subtitle": "Encrypted PostgreSQL dumps written to a pluggable target. Configure schedules + retention, or trigger one-off backups for a quick safety net.",
      "exportData": "Export data",
      "loading": "Loading…",
      "loadStatusFailedToast": "Failed to load backup status",
      "tabs": {
        "backups": "Backups",
        "schedules": "Schedules",
        "targets": "Targets"
      },
      "inventory": {
        "title": "What's in a backup?",
        "summary": "{rows} rows across {tables} tables",
        "description": "Each backup is a <1>pg_dump --format=custom</1> of every table below, plus <3>manifest.json</3> and (optionally) <5>config.toml</5>. Counts are live; the bundle captures whatever's there at backup time.",
        "loadFailedToast": "Failed to load inventory",
        "rowsLabel": "rows"
      },
      "restart": {
        "title": "Restart opendray to activate backups",
        "description": "Your passphrase is saved. The gateway only loads it at startup, so the feature stays off until you bounce the process.",
        "keyFile": "Key file:",
        "configuredVia": "Configured via:",
        "envVar": "OPENDRAY_BACKUP_KEY env var",
        "checkAgain": "Check again"
      },
      "setup": {
        "title": "Set up backups",
        "description": "Choose a master passphrase. opendray uses it to encrypt every backup blob. <1>Lose it and your backups become unrecoverable</1>, so save it in a password manager (Vaultwarden, 1Password, …) before continuing.",
        "generate": "Generate",
        "pasteOwn": "Paste my own",
        "generateTitle": "256-bit random key",
        "generateHint": "Server generates a cryptographically random passphrase and shows it once. You must copy it before continuing — there is no recovery path.",
        "pasteLabel": "Your passphrase",
        "pastePlaceholder": "At least 20 characters",
        "pasteHint": "Recommended: 40+ characters from a password manager.",
        "savesTo": "Saves to:",
        "saving": "Saving…",
        "generateAndSave": "Generate and save",
        "save": "Save"
      },
      "generated": {
        "title": "Save this passphrase NOW",
        "description": "This is shown <1>once</1>. It will not be retrievable from opendray or anywhere else. Copy it into a password manager before continuing.",
        "copy": "Copy",
        "copiedToast": "Passphrase copied to clipboard",
        "copyFailedToast": "Copy failed — select and copy manually",
        "savedTo": "Saved to:",
        "ack": "I have saved this passphrase to my password manager",
        "continue": "Continue"
      },
      "status": {
        "keyFingerprint": "Key fingerprint:",
        "pgDump": "pg_dump:",
        "pgDumpUnavailable": "unavailable",
        "pgDumpHint": "Backups can't run until pg_dump is on PATH (or its absolute path is set in <1>backup.pg_dump_path</1>). Install <3>postgresql-client</3> matching your server's major version and restart."
      },
      "backupsTab": {
        "backupNow": "Backup now",
        "triggering": "Triggering…",
        "includeConfig": "include config.toml",
        "restoreFromFile": "Restore from file",
        "refresh": "Refresh",
        "queuedToast": "Backup queued",
        "triggerFailedToast": "Trigger failed",
        "listFailedToast": "Failed to list backups",
        "deleteConfirm": "Delete backup {id}? The blob is removed from its target.",
        "deletedToast": "Backup deleted",
        "deleteFailedToast": "Delete failed",
        "empty": "No backups yet. Click \"Backup now\" above to take the first one.",
        "columns": {
          "id": "ID",
          "target": "Target",
          "status": "Status",
          "started": "Started",
          "size": "Size",
          "actions": "Actions"
        },
        "downloadTooltip": "Download",
        "deleteTooltip": "Delete"
      },
      "restore": {
        "title": "Restore from backup bundle",
        "bundleLabel": "Encrypted bundle (.tar.gz.enc)",
        "targetDsnLabel": "Target database DSN",
        "targetDsnHint": "(blank = opendray's own DB — DANGEROUS)",
        "targetDsnPlaceholder": "postgres://user:pass@host:5432/dbname",
        "cleanLabel": "--clean --if-exists (drop existing schema first; required when restoring over a populated DB)",
        "auditNoteLabel": "Audit note (optional)",
        "auditNotePlaceholder": "Reason for restore — appears in slog",
        "ownDbWarning": "You're restoring into <1>opendray's own database</1>. With \"--clean\" enabled this drops every table and replays the backup verbatim — irreversible. Type <3>I understand</3> to proceed.",
        "confirmPlaceholder": "I understand",
        "confirmSentinel": "I understand",
        "pgRestoreOutput": "pg_restore output (last 8 KiB)",
        "noPgRestoreOutput": "(no pg_restore output)",
        "pickFileToast": "Pick a bundle file first",
        "succeededToast": "Restore succeeded",
        "replayedDescription": "{bytes} replayed from manifest {id}",
        "failedToast": "Restore failed",
        "restoring": "Restoring…",
        "restore": "Restore"
      },
      "schedulesTab": {
        "description": "Recurring backups. The scheduler polls every 30s and runs the oldest due schedule.",
        "newSchedule": "New schedule",
        "loadFailedToast": "Failed to load schedules",
        "deleteConfirm": "Delete schedule {id}?",
        "deletedToast": "Schedule deleted",
        "deleteFailedToast": "Delete failed",
        "toggleFailedToast": "Toggle failed",
        "empty": "No schedules. Add one to take automatic recurring backups.",
        "columns": {
          "id": "ID",
          "target": "Target",
          "interval": "Interval",
          "keep": "Keep",
          "nextRun": "Next run",
          "enabled": "Enabled",
          "actions": "Actions"
        },
        "keepCount": "{count} backups",
        "deleteTooltip": "Delete"
      },
      "newSchedule": {
        "title": "New backup schedule",
        "targetLabel": "Target",
        "everyHoursLabel": "Every (hours)",
        "keepLastNLabel": "Keep last N",
        "enableImmediately": "Enable immediately",
        "createdToast": "Schedule created",
        "createFailedToast": "Create failed",
        "creating": "Creating…",
        "create": "Create"
      },
      "targetsTab": {
        "description": "Storage destinations. v1 supports <1>local</1> (disk on the opendray host) and <3>smb</3> (any SMB / CIFS share, e.g. UNAS or Synology).",
        "newTarget": "New target",
        "listFailedToast": "Failed to list targets",
        "deleteConfirm": "Delete target {id}? Schedules referencing it will block the delete.",
        "deletedToast": "Target deleted",
        "deleteFailedToast": "Delete failed",
        "connectionOkToast": "Connection OK",
        "connectionFailedToast": "Connection failed",
        "testFailedToast": "Test failed",
        "columns": {
          "id": "ID",
          "kind": "Kind",
          "config": "Config",
          "enabled": "Enabled",
          "actions": "Actions"
        },
        "on": "on",
        "off": "off",
        "test": "Test",
        "testing": "Testing…",
        "deleteTooltip": "Delete"
      },
      "targetEditor": {
        "title": "New backup target",
        "kindPicker": "Where do you want to back up to?",
        "idLabel": "ID (optional)",
        "idPlaceholder": "auto-generated if blank, e.g. tgt_xxx",
        "createdToast": "Target created",
        "createFailedToast": "Create failed",
        "creating": "Creating…",
        "create": "Create target",
        "enableImmediately": "Enable immediately (otherwise saved as disabled — useful for \"configure now, turn on later\")",
        "local": {
          "rootLabel": "Root directory",
          "rootHint": "Empty = cfg.backup.local_dir (~/.opendray/backups)",
          "rootPlaceholder": "~/backups/opendray  or  /mnt/external-hdd/opendray"
        },
        "smb": {
          "hostLabel": "Host",
          "hostPlaceholder": "192.168.1.20",
          "portLabel": "Port",
          "shareLabel": "Share",
          "shareHint": "Top-level share name on the SMB server",
          "sharePlaceholder": "Claude_Workspace",
          "userLabel": "User",
          "passwordLabel": "Password",
          "pathPrefixLabel": "Path prefix",
          "pathPrefixHint": "Sub-folder under the share root (optional)",
          "pathPrefixPlaceholder": "opendray/backups"
        },
        "s3": {
          "endpointLabel": "Endpoint",
          "endpointHint": "Host (no protocol). AWS: s3.amazonaws.com · R2: <accountid>.r2.cloudflarestorage.com · MinIO: minio.local:9000",
          "endpointPlaceholder": "s3.amazonaws.com",
          "regionLabel": "Region",
          "regionHint": "AWS only; R2 use 'auto'",
          "regionPlaceholder": "us-east-1 / auto",
          "bucketLabel": "Bucket",
          "bucketPlaceholder": "opendray-backups",
          "accessKeyLabel": "Access key",
          "secretKeyLabel": "Secret key",
          "secretKeyHint": "Stored AES-256-GCM encrypted; never echoed back",
          "pathPrefixLabel": "Path prefix",
          "pathPrefixHint": "Object-key prefix (optional)",
          "pathPrefixPlaceholder": "opendray/backups",
          "useHttps": "Use HTTPS",
          "pathStyle": "Path-style addressing (legacy / MinIO)"
        },
        "webdav": {
          "baseUrlLabel": "Base URL",
          "baseUrlHint": "Full URL including any path. Examples: https://cloud.example.com/remote.php/dav/files/me/ (Nextcloud), https://nas.local:5006/ (Synology), https://dav.jianguoyun.com/dav/ (Jianguoyun / 坚果云)",
          "baseUrlPlaceholder": "https://cloud.example.com/remote.php/dav/files/<user>/",
          "userLabel": "User",
          "passwordLabel": "Password",
          "pathPrefixLabel": "Path prefix",
          "pathPrefixHint": "Sub-folder under the base URL (optional)",
          "pathPrefixPlaceholder": "opendray/backups"
        },
        "sftp": {
          "hostLabel": "Host",
          "hostPlaceholder": "vps.example.com",
          "portLabel": "Port",
          "userLabel": "User",
          "passwordLabel": "Password",
          "passwordHint": "Either password OR private key required. If both, password is treated as the key passphrase.",
          "privateKeyLabel": "Private key (PEM)",
          "privateKeyHint": "Paste contents of an OpenSSH/PEM private key (e.g. ~/.ssh/id_ed25519). Leave blank for password-only auth.",
          "privateKeyPlaceholder": "-----BEGIN OPENSSH PRIVATE KEY-----...",
          "hostKeyLabel": "Host key (pinning)",
          "hostKeyHint": "OpenSSH-style server public key (run `ssh-keyscan host` to obtain). Leave blank to disable pinning (NOT recommended outside LAN).",
          "hostKeyPlaceholder": "ssh-ed25519 AAAA...",
          "pathPrefixLabel": "Path prefix",
          "pathPrefixHint": "Absolute or relative to user home (optional)",
          "pathPrefixPlaceholder": "/var/backups/opendray  or  opendray-backups"
        },
        "rclone": {
          "rcloneHint": "Requires the <1>rclone</1> CLI installed on the opendray host. First configure your remote with <3>rclone config</3>, then use the remote name below. opendray invokes <5>rclone rcat / cat / lsd</5> under the hood.",
          "remoteLabel": "Remote name",
          "remoteHint": "Name from `rclone config` (no colon). Example: gdrive, onedrive, dropbox-personal, baidu-pan",
          "remotePlaceholder": "gdrive",
          "pathPrefixLabel": "Path prefix",
          "pathPrefixHint": "Sub-folder under the remote root (optional)",
          "pathPrefixPlaceholder": "opendray/backups",
          "binaryPathLabel": "Binary path",
          "binaryPathHint": "Override `which rclone`. Empty uses PATH lookup.",
          "binaryPathPlaceholder": "/opt/homebrew/bin/rclone",
          "configPathLabel": "Config path",
          "configPathHint": "Override --config (default ~/.config/rclone/rclone.conf or ~/.rclone.conf)",
          "configPathPlaceholder": "leave blank for rclone default"
        }
      }
    },
    "serverSettings": {
      "sections": {
        "general": {
          "title": "General",
          "desc": "Listen address, operator account, token TTL."
        },
        "logging": {
          "title": "Logging",
          "desc": "Verbosity, format, and live tail."
        },
        "sessions": {
          "title": "Sessions",
          "desc": "Idle detection thresholds."
        },
        "vault": {
          "title": "Vault",
          "desc": "Notes, skills, and git-versioned root."
        },
        "mcp": {
          "title": "MCP registry",
          "desc": "Server registry + secrets."
        },
        "memory": {
          "title": "Memory",
          "desc": "Cross-CLI persistent memory subsystem."
        },
        "backup": {
          "title": "Backup",
          "desc": "Encrypted DB backups, restore, and admin data exports."
        },
        "claude": {
          "title": "Storage · Claude",
          "desc": "Where Claude transcripts live on disk."
        },
        "codex": {
          "title": "Storage · Codex",
          "desc": "Codex sessions root."
        },
        "gemini": {
          "title": "Storage · Gemini",
          "desc": "Gemini per-project tmp + projects.json."
        }
      },
      "loading": "Loading server settings…",
      "loadFailed": "Failed to load: {message}",
      "noConfigFlag": "opendray was started without a -config flag. Settings are loaded from environment variables only and cannot be edited here.",
      "resetButton": "Reset",
      "resetButtonTitle": "Discard unsaved changes in this section",
      "resetConfirm": "Reset \"{section}\" to last-saved values?",
      "badgeRestartRequired": "restart required",
      "badgeUnsaved": "unsaved",
      "saveButton": "Save changes",
      "saveToastTitle": "Settings saved",
      "saveToastDesc": "Click Restart to apply.",
      "saveErrorTitle": "Save failed",
      "dangerousConfirm": "You changed listen address / admin user / admin password. After restart you may need to re-authenticate or use the new address. Continue?",
      "unsavedHint": "You have unsaved changes",
      "savedHint": "All changes saved",
      "searchPlaceholder": "Filter fields…",
      "restart": {
        "button": "Restart server",
        "buttonTitle": "Self-exec the gateway process",
        "dirtyConfirm": "You have unsaved changes. Restart will use the LAST SAVED config. Continue?",
        "confirm": "Restart the opendray gateway? All open terminal sessions will reconnect automatically.",
        "overlay": "Restarting server…",
        "waiting": "Waiting for /health · {tick}s",
        "timedOutTitle": "Restart timed out",
        "timedOutDesc": "Health endpoint never came back. Check server logs.",
        "successToast": "Server restarted"
      },
      "formGroups": {
        "network": "Network",
        "operatorAccount": "Operator account",
        "memoryConfiguration": "Configuration",
        "memoryHttp": "HTTP backend (used when backend=http)",
        "memoryLocal": "Local ONNX (used when backend=local)",
        "backupStatus": "Status",
        "backupWhere": "Where backups go",
        "backupSchedules": "Schedules",
        "backupWhatsInside": "What's in a backup?"
      },
      "fields": {
        "listenAddress": {
          "label": "Listen address",
          "hint": "The host:port the HTTP server binds to. Example: 0.0.0.0:8770."
        },
        "username": {
          "label": "Username",
          "hint": "Login name used in the sign-in form. Changing this forces a re-login on the next request."
        },
        "password": {
          "label": "Password",
          "hint": "Leave blank to keep the current password. Sending a value overwrites it.",
          "hideTitle": "Hide",
          "revealTitle": "Reveal"
        },
        "tokenTTL": {
          "label": "Token TTL",
          "hint": "Bearer-token lifetime as a Go duration, e.g. \"24h\", \"30m\". Empty = never expire."
        },
        "logLevel": {
          "label": "Log level",
          "hint": "Lines below this level are dropped."
        },
        "logFormat": {
          "label": "Format",
          "hint": "\"text\" is human-readable; \"json\" is machine-parsable."
        },
        "logFile": {
          "label": "Log file",
          "hint": "Optional file path. Auto-rotates at 10 MB, keeps 5 backups. Empty = stderr only."
        },
        "idleThreshold": {
          "label": "Idle threshold",
          "hint": "A session is silent for this long before session.idle fires. Empty = 30s."
        },
        "idlePollInterval": {
          "label": "Idle poll interval",
          "hint": "How often the idle detector wakes up. Lower = lower latency, more wakeups. Empty = 5s."
        },
        "vaultRoot": {
          "label": "Vault root",
          "hint": "Top-level directory for notes, skills, and MCP registry."
        },
        "notesDirectory": {
          "label": "Notes directory",
          "hint": "Override notes location. Defaults to <vault root>/notes."
        },
        "skillsDirectory": {
          "label": "Skills directory",
          "hint": "Override skills location. Defaults to <vault root>/skills."
        },
        "gitRoot": {
          "label": "Git root",
          "hint": "Working tree the Vault Sync feature commits to."
        },
        "personalPrefix": {
          "label": "Personal prefix",
          "hint": "Folder name used for personal notes when auto-deriving paths. Default \"personal\"."
        },
        "projectsPrefix": {
          "label": "Projects prefix",
          "hint": "Folder name used for project notes. Default \"projects\"."
        },
        "registryRoot": {
          "label": "Registry root",
          "hint": "Directory holding MCP server JSON definitions. Defaults to <vault>/mcp."
        },
        "secretsFile": {
          "label": "Secrets file",
          "hint": "key=value file substituted into MCP server commands at spawn time."
        },
        "memoryBackend": {
          "label": "Embedder backend",
          "hint": "\"auto\" / \"bm25\" use the cgo-free pure-Go keyword path. \"http\" calls any OpenAI-compatible /v1/embeddings (ollama / OpenAI / LocalAI). \"local\" runs an ONNX sentence-transformer in-process — requires a binary built with `-tags local_onnx`."
        },
        "memoryStore": {
          "label": "Store",
          "hint": "\"pgvector\" reuses opendray's existing PG with the vector extension; only option in v1."
        },
        "memoryTopK": {
          "label": "Default top-K",
          "hint": "How many hits memory_search returns when the agent doesn't specify. Empty = 5."
        },
        "memoryThreshold": {
          "label": "Similarity threshold",
          "hint": "Hits below this score are dropped. Empty = 0.1 (permissive — BM25 sparse vectors rarely break 0.5)."
        },
        "memoryScope": {
          "label": "Default scope",
          "hint": "What memory_store uses when the agent doesn't specify. \"project\" (recommended) groups by cwd; \"global\" shares across cwds."
        },
        "memoryBaseUrl": {
          "label": "Base URL",
          "hint": "e.g. \"http://localhost:11434/v1\" for ollama, \"https://api.openai.com/v1\" for OpenAI."
        },
        "memoryModel": {
          "label": "Model",
          "hint": "e.g. \"nomic-embed-text\" for ollama, \"text-embedding-3-small\" for OpenAI."
        },
        "memoryApiKey": {
          "label": "API key",
          "hint": "Empty for ollama / local servers. Required for OpenAI / Voyage / hosted services."
        },
        "memoryLocalModel": {
          "label": "Model name",
          "hint": "Cosmetic — appears in logs / Inspector. e.g. \"bge-m3\", \"bge-small-en-v1.5\"."
        },
        "memoryLibraryPath": {
          "label": "Library path",
          "hint": "Directory holding libonnxruntime.dylib (macOS) / libonnxruntime.so (Linux). After `brew install onnxruntime`, that's /opt/homebrew/opt/onnxruntime/lib."
        },
        "memoryModelPath": {
          "label": "Model path",
          "hint": "Absolute path to the .onnx weights. Download from HuggingFace, e.g. Xenova/bge-m3 or Xenova/bge-small-en-v1.5."
        },
        "memoryTokenizerPath": {
          "label": "Tokenizer path",
          "hint": "Absolute path to tokenizer.json (HuggingFace standard format) — usually right next to the model."
        },
        "memoryMaxSeqLen": {
          "label": "Max sequence length",
          "hint": "Tokens beyond this are truncated. bge-m3 default is 512. Empty = 512."
        },
        "claudeHistoryRoots": {
          "label": "History roots",
          "hint": "Directories scanned for Claude per-project JSONL transcripts. Empty = scan ~/.claude/projects + every ~/.claude-accounts/*/projects."
        },
        "claudeAccountsDir": {
          "label": "Accounts directory",
          "hint": "Root used for opendray-managed Claude account ConfigDirs. Default ~/.claude-accounts."
        },
        "codexSessionsRoot": {
          "label": "Sessions root",
          "hint": "Directory walked for Codex rollout JSONL files. Default ~/.codex/sessions."
        },
        "geminiTmpRoot": {
          "label": "Tmp directory",
          "hint": "Root holding Gemini per-project tmp folders. Default ~/.gemini/tmp."
        },
        "geminiProjectsFile": {
          "label": "projects.json",
          "hint": "Path to Gemini's cwd→short-name mapping file. Default ~/.gemini/projects.json."
        },
        "backupLocalDir": {
          "label": "Local backup directory",
          "hint": "Default root for the auto-created `local` target. Empty = ~/.opendray/backups. Restart required."
        },
        "backupExportDir": {
          "label": "Export directory",
          "hint": "Where one-shot export zips are staged on disk. Empty = ~/.opendray/exports. Bundles auto-expire after 24h. Restart required."
        },
        "backupPgDumpPath": {
          "label": "pg_dump path",
          "hint": "Absolute path to pg_dump. Major version must be ≥ the server's. Empty = first pg_dump on PATH."
        },
        "backupPgRestorePath": {
          "label": "pg_restore path",
          "hint": "Absolute path to pg_restore for the /backups/restore flow. Same major-version rule."
        }
      },
      "liveTail": {
        "heading": "Live tail",
        "description": "In-memory ring buffer (last ~2,000 records). Resets on restart."
      },
      "memoryInspectorCard": {
        "heading": "Inspector",
        "description": "Browse, search and edit stored memories on the dedicated page.",
        "openButton": "Open Memory →"
      },
      "localOnnxBanner": "Requires the binary to be compiled with <1>-tags local_onnx</1>. The standard build returns a clear stub error when this backend is selected. See <3>Memory → Local ONNX</3> tutorial for setup steps.",
      "stringList": {
        "noneDefault": "(none — using built-in defaults)",
        "addPath": "Add path",
        "removeTitle": "Remove"
      },
      "httpHelpers": {
        "autoDetected": "Auto-detected at startup",
        "modelCount": "{count} model(s) — click to use",
        "presets": "Presets:",
        "testConnection": "Test connection",
        "presetTip": {
          "ollama": "Local ollama daemon",
          "lmStudio": "LM Studio local server",
          "openai": "OpenAI cloud (needs API key)"
        }
      },
      "probe": {
        "unreachable": "✗ unreachable: {error}",
        "connectionFailed": "connection failed",
        "reachable": "✓ reachable {detected}· {total} model(s) total · {embedding} embedding",
        "modelMissing": "⚠ Configured model {model} isn't in the list. Pick one of the embedding models below or fix the name.",
        "embeddingModelsLabel": "embedding models:",
        "moreModels": "+{count} more",
        "noEmbeddingFound": "⚠ No model name contains \"embed\". The endpoint might not have an embedding model loaded — check your local server.",
        "configuredTitle": "Currently configured",
        "applyTitle": "Click to apply"
      },
      "backup": {
        "featureDisabledTitle": "Feature disabled",
        "featureDisabledHint": "Set <1>OPENDRAY_BACKUP_ENABLED=1</1> + <3>OPENDRAY_BACKUP_KEY=&lt;passphrase&gt;</3> in opendray's environment, then restart. The master passphrase is env-only — it never touches config.toml.",
        "statusRowLabel": "Status",
        "enabledHealthy": "enabled · healthy",
        "enabledDegraded": "enabled · degraded",
        "keyFingerprintLabel": "Key fingerprint",
        "keyFingerprintHint": "record in Vaultwarden — losing it locks all prior backups",
        "pgDumpLabel": "pg_dump",
        "pgDumpUnavailable": "unavailable",
        "pgRestoreLabel": "pg_restore",
        "pgRestoreNotResolved": "(not resolved)",
        "openBackups": "Open Backups page →",
        "openExport": "Open Export / Import →",
        "whereDesc": "Each target is one place a backup blob can be written. opendray supports <1>local disk</1>, <3>SMB/CIFS</3> (Windows / NAS), <5>S3-compatible</5> (AWS, R2, B2, MinIO, Alibaba Cloud OSS, Tencent Cloud COS, ...), <7>WebDAV</7> (Nextcloud, Synology, Jianguoyun), <9>SFTP</9>, plus an <11>rclone</11> passthrough that taps into 70+ extra backends (Google Drive, OneDrive, Dropbox, Baidu Pan, Aliyun Drive, ...).",
        "loading": "Loading…",
        "noTargets": "No targets yet. Add one to start backing up.",
        "addTarget": "Add target",
        "noSchedulesHint": "No recurring schedules. Add one on <1>/backups → Schedules</1> to take backups automatically.",
        "scheduleHeaders": {
          "schedule": "Schedule",
          "target": "Target",
          "cadence": "Cadence",
          "keep": "Keep",
          "state": "State"
        },
        "every": "every {interval}",
        "backupsKeep": "{count} backups",
        "stateEnabled": "enabled",
        "statePaused": "paused",
        "manageSchedules": "Manage on /backups → Schedules →",
        "whatsInsideDesc": "Each backup is a <1>pg_dump --format=custom</1> of every opendray table (sessions, integrations, memories, audit_log, etc.) plus a <3>manifest.json</3> and (optionally) the live <5>config.toml</5>. Open the \"What's in a backup?\" panel on the <7>Backups page</7> to see the live inventory with row counts.",
        "advancedToggle": "Advanced (paths & client binaries) — restart required"
      },
      "targetRow": {
        "on": "on",
        "off": "off",
        "test": "Test",
        "testing": "Testing…",
        "delete": "Delete",
        "connectionOk": "{id}: connection OK",
        "connectionFailedTitle": "Connection failed",
        "testFailedTitle": "Test failed",
        "deleteConfirm": "Delete target \"{id}\"? Schedules referencing it will block the delete.",
        "deleteSuccess": "Target deleted",
        "deleteFailedTitle": "Delete failed",
        "unknownError": "Unknown error"
      }
    },
    "settings": {
      "title": "Settings",
      "subtitle": "Workspace, account, and gateway config.",
      "groups": {
        "workspace": "Workspace",
        "server": "Server",
        "system": "System"
      },
      "items": {
        "appearance": "Appearance",
        "font": "Font size",
        "account": "Account",
        "status": "Status",
        "about": "About"
      },
      "health": {
        "connecting": "connecting…",
        "dbOk": "db ok",
        "dbDown": "db down"
      },
      "breadcrumb": {
        "server": "Server"
      },
      "appearance": {
        "title": "Appearance",
        "description": "Choose how opendray looks.",
        "options": {
          "light": "Light",
          "lightDesc": "Always light",
          "dark": "Dark",
          "darkDesc": "Always dark",
          "system": "System",
          "systemDesc": "Follow the OS setting"
        }
      },
      "font": {
        "title": "Font size",
        "description": "Scales the entire interface. Persisted per browser.",
        "options": {
          "compact": "Compact",
          "default": "Default",
          "comfy": "Comfy",
          "large": "Large"
        }
      },
      "account": {
        "title": "Account",
        "description": "Operator and current bearer token.",
        "username": "Username",
        "tokenExpires": "Token expires",
        "changeCredentials": "Change credentials"
      },
      "changeCredentials": {
        "title": "Change credentials",
        "description": "Verify your current password, then pick new credentials. All other signed-in sessions will be revoked.",
        "currentPassword": "Current password",
        "newUsername": "New username",
        "newPassword": "New password",
        "newPasswordHint": "At least 8 characters.",
        "confirm": "Confirm new password",
        "errorTooShort": "New password must be at least 8 characters.",
        "errorMismatch": "New password and confirmation don't match.",
        "errorWrongPassword": "Current password is wrong.",
        "cancel": "Cancel",
        "update": "Update",
        "saving": "Saving…"
      },
      "system": {
        "title": "System status",
        "description": "Live status from the gateway's /health endpoint.",
        "status": "Status",
        "version": "Version",
        "uptime": "Uptime",
        "database": "Database",
        "reachable": "reachable",
        "unreachable": "unreachable"
      },
      "about": {
        "title": "About",
        "description": "opendray v2 — the multiplexer + integration gateway for AI agent CLIs. Source under Apache 2.0.",
        "version": "Version",
        "commit": "Commit",
        "updateAvailable": "Update available: {version}",
        "releaseNotes": "Release notes ↗",
        "updateNow": "Update now",
        "upgradingShort": "Upgrading…",
        "confirmRestart": "This restarts the service; running sessions reconnect.",
        "confirmUpgrade": "Upgrade & restart",
        "upgrading": "Upgrading to {version}…",
        "upgraded": "Updated to {version}.",
        "upgradeSlow": "Upgrade is taking a while — check the service logs if it doesn't come back.",
        "guidedHint": "In-app upgrade isn't available here. Run on the server:",
        "checkFailed": "Couldn't check for updates (offline or rate-limited).",
        "upToDate": "You're on the latest release.",
        "checkUpdates": "Check for updates",
        "checking": "Checking…",
        "reinstall": "Re-install"
      }
    },
    "logViewer": {
      "filterPlaceholder": "Filter…",
      "debugTooltip": "Debug count",
      "infoTooltip": "Info count",
      "warnTooltip": "Warn count",
      "errorTooltip": "Error count",
      "streaming": "Streaming",
      "disconnected": "Disconnected",
      "live": "live",
      "offline": "offline",
      "pauseTooltip": "Pause auto-scroll",
      "resumeTooltip": "Resume auto-scroll",
      "clearTooltip": "Clear local view (server ring untouched)",
      "downloadTooltip": "Download full ring as .log file",
      "emptyWaiting": "Waiting for log records…",
      "emptyFiltered": "No records match \"{query}\""
    },
    "pathInput": {
      "testButton": "Test",
      "testTooltip": "Resolve and check this path",
      "notFound": "not found ·",
      "childrenSuffix": "children",
      "expectedDirectory": "· expected directory"
    },
    "memoryAmbient": {
      "header": {
        "title": "Ambient memory — auto-capture & inject",
        "body": "opendray polls every live agent session every 10 seconds, extracts durable facts via a configurable LLM, and dedups before storing them in the shared memory pool. Configure which LLM does the extraction (Provider), when extraction fires (Capture rule), and what — if anything — gets prepended to the agent's system prompt at spawn (Injection profile)."
      },
      "loading": "Loading…",
      "providers": {
        "title": "Summarizer providers",
        "addButton": "Add provider",
        "intro": "At least one enabled provider is required for capture to actually fire. Local options (Ollama, LM Studio, Integration) keep your transcripts off external networks.",
        "empty": "No providers configured yet.",
        "row": {
          "defaultBadge": "★ default",
          "makeDefault": "Make default",
          "test": "Test",
          "testing": "Testing…",
          "delete": "Delete",
          "testOk": "{name}: connection OK",
          "testFailedToast": "Test failed",
          "deleteConfirm": "Delete provider \"{name}\"?",
          "deletedToast": "Provider deleted",
          "deleteFailedToast": "Delete failed",
          "updateFailedToast": "Update failed",
          "madeDefaultToast": "{name} is now the default"
        },
        "dialog": {
          "title": "Add summarizer provider",
          "kindLabel": "Kind",
          "nameLabel": "Name",
          "namePlaceholder": "e.g. lmstudio-qwen",
          "modelLabel": "Model",
          "baseUrlLabel": "Base URL",
          "integrationNote": "Integration providers resolve their base URL from a registered integration. Configure that under Integrations first; advanced wiring (extra_config) is DB-only in this release.",
          "apiKeyLabel": "API key",
          "apiKeyHint": "Stored encrypted (AES-GCM with the backup master passphrase). Never echoed back; only the fingerprint is shown after save.",
          "makeDefaultLabel": "Make this the default provider",
          "create": "Create",
          "nameRequiredToast": "Name is required",
          "createdToast": "Provider {name} created",
          "createFailedToast": "Create failed"
        }
      },
      "rules": {
        "title": "Capture rules",
        "addButton": "Add rule",
        "intro": "Each rule says \"when this trigger fires, summarize new transcript messages and store the durable facts.\" Per-session rules override the global default. v1 ships 4 trigger kinds.",
        "empty": "No capture rules yet. Add one to enable auto-capture.",
        "row": {
          "globalDefault": "global default",
          "scopeLabel": "scope:",
          "dedupLabel": "dedup:",
          "runNow": "Run now",
          "running": "Running…",
          "delete": "Delete",
          "firedToast": "Rule fired across {sessions} session(s)",
          "runNowFailedToast": "Run-now failed",
          "deleteConfirm": "Delete rule \"{name}\"?",
          "deletedToast": "Rule deleted",
          "deleteFailedToast": "Delete failed",
          "summary": {
            "afterMessages": "every {n} messages",
            "onIdle": "idle ≥ {seconds}s",
            "kChars": "≥ {k} chars",
            "manual": "manual only"
          }
        },
        "dialog": {
          "title": "Add capture rule",
          "nameLabel": "Name",
          "triggerLabel": "Trigger",
          "nLabel": "N (messages)",
          "idleLabel": "Idle seconds",
          "kLabel": "K (characters)",
          "scopeLabel": "Target scope",
          "scopeProject": "project (recommended)",
          "scopeGlobal": "global",
          "dedupLabel": "Dedup threshold (0.0 – 1.0)",
          "dedupHint": "Higher = stricter de-duplication. 0.85 is the recommended sweet spot.",
          "create": "Create",
          "nameRequiredToast": "Name is required",
          "createdToast": "Rule {name} created",
          "createFailedToast": "Create failed"
        }
      },
      "profiles": {
        "title": "Injection profiles",
        "addButton": "Add profile",
        "intro": "At spawn time opendray prepends a markdown banner of recent project memories to the agent's system prompt — IF a profile is configured. Without a profile, the model still uses memory_search on demand.",
        "empty": "No injection profile. Memories are not auto-injected at spawn — model still uses memory_search.",
        "row": {
          "globalDefault": "global default",
          "delete": "Delete",
          "deleteConfirm": "Delete this injection profile?",
          "deletedToast": "Profile deleted",
          "deleteFailedToast": "Delete failed"
        },
        "dialog": {
          "title": "Add injection profile",
          "strategyLabel": "Strategy",
          "kLabel": "K (top memories to inject)",
          "hint": "One profile per session_id (or global default). Per-session profiles can be added later via API; UI currently only manages the global default.",
          "create": "Create",
          "createdToast": "Profile created",
          "createFailedToast": "Create failed"
        }
      },
      "cost": {
        "title": "Token cost (all-time)",
        "intro": "Per-provider summary aggregated from <1>memory_summarizer_calls</1>. Local providers (Ollama, LM Studio, Integration) are priced as $0 — operator owns hardware cost.",
        "empty": "No enabled providers — no cost data.",
        "columns": {
          "provider": "Provider",
          "calls": "Calls",
          "inTokens": "In tokens",
          "outTokens": "Out tokens",
          "usdEst": "USD est."
        }
      }
    },
    "noteEditor": {
      "loading": "Loading…",
      "source": "Source",
      "preview": "Preview",
      "tagTitle": "tag #{tag}",
      "emptyNote": "Empty note. Switch to Source to start writing.",
      "saveFailedToast": "Save failed",
      "status": {
        "saveFailed": "save failed",
        "saving": "saving…",
        "unsaved": "unsaved",
        "newNote": "new note",
        "saved": "saved"
      }
    },
    "export": {
      "title": "Export data",
      "subtitle": "Take a one-shot zip bundle of selected logical entities. Bundles are kept on the server for 24 hours, then automatically reaped.",
      "backToBackups": "← Backups",
      "sections": {
        "export": "Export",
        "import": "Import"
      },
      "form": {
        "scope": "Scope",
        "memories": "Memories",
        "memoriesHint": "Cross-CLI persistent memory rows (text + scope + metadata). Embedding vectors are omitted; importer re-embeds.",
        "integrations": "Integrations",
        "customTasks": "Custom tasks",
        "customTasksHint": "Operator-defined tasks shown in the Inspector's Tasks tab.",
        "integrationOptions": {
          "none": "None",
          "noneHint": "Skip the integrations table entirely.",
          "metadata": "Metadata only (recommended)",
          "metadataHint": "ID, name, route prefix, scopes — no API key material.",
          "plaintext": "Include plaintext API keys",
          "plaintextHint": "v1 bcrypt-only: no recoverable plaintext exists. Manifest documents this; nothing leaks."
        },
        "confirmWarning": "Type <1>I understand</1> to confirm. opendray currently stores only bcrypt hashes — selecting plaintext does NOT export any plaintext (the feature is reserved for a future release that keeps plaintext caches).",
        "confirmPlaceholder": "I understand",
        "confirmSentinel": "i understand",
        "footnote": "Audit logs and session transcripts are out of scope — covered by /backups (operator dump) instead.",
        "building": "Building…",
        "create": "Create export",
        "readyToast": "Export ready",
        "readyDescription": "{bytes} bytes",
        "failedToast": "Export failed"
      },
      "history": {
        "loading": "Loading…",
        "empty": "No exports yet. Use the form above to create one.",
        "title": "History",
        "columns": {
          "id": "ID",
          "status": "Status",
          "scope": "Scope",
          "size": "Size",
          "expires": "Expires",
          "actions": "Actions"
        },
        "download": "Download",
        "deleteTooltip": "Delete",
        "listFailedToast": "Failed to list exports",
        "downloadFailedToast": "Download failed",
        "noTokenToast": "No download token (expired?)",
        "deleteConfirm": "Delete export {id}?",
        "deletedToast": "Export deleted",
        "deleteFailedToast": "Delete failed",
        "scopeEmpty": "(empty)"
      },
      "import": {
        "intro": "Replay an export bundle (zip) into the live database. Conflicts (matching id, or unique route_prefix for integrations) are <1>skipped</1> by default. Memories are tagged <3>embedder=imported_v1</3> and need a re-embed pass before search returns them; trigger re-embed under <5>Memory → Maintenance</5>. Integrations are imported with <7>enabled=false</7> and a non-bcrypt placeholder key — operator must rotate before use.",
        "memoryLink": "Memory → Maintenance",
        "bundleLabel": "Bundle (.zip)",
        "memoriesLabel": "Memories",
        "integrationsLabel": "Integrations (metadata only — keys never imported)",
        "customTasksLabel": "Custom tasks",
        "importing": "Importing…",
        "importBundle": "Import bundle",
        "pickFileToast": "Pick a bundle file first",
        "doneToast": "Import done",
        "finishedWithErrors": "Import finished with errors",
        "failedToast": "Import failed",
        "summaryCard": {
          "memories": "Memories",
          "integrations": "Integrations",
          "customTasks": "Custom tasks",
          "created": "created",
          "skipped": "skipped",
          "failed": "failed"
        }
      },
      "imports": {
        "loading": "Loading…",
        "empty": "No imports yet.",
        "title": "History",
        "columns": {
          "id": "ID",
          "status": "Status",
          "source": "Source",
          "counts": "Counts",
          "when": "When"
        },
        "noneCounts": "(none)",
        "listFailedToast": "Failed to list imports"
      }
    }
  },
  "more": {
    "title": "More",
    "identity": {
      "signedInAs": "Signed in as",
      "server": "Server",
      "tokenExpires": "Token expires"
    },
    "sections": {
      "gateway": "Gateway",
      "memory": "Memory",
      "system": "System"
    },
    "items": {
      "integrations": {
        "title": "Integrations",
        "subtitle": "API callers — recent activity & error rates"
      },
      "channels": {
        "title": "Channels",
        "subtitle": "Notification destinations"
      },
      "providers": {
        "title": "Providers",
        "subtitle": "Claude / Codex / Gemini CLI status"
      },
      "mcp": {
        "title": "MCP",
        "subtitle": "Model Context Protocol servers & secrets"
      },
      "skills": {
        "title": "Skills",
        "subtitle": "Agent SKILL.md library (built-in + vault)"
      },
      "gitHosts": {
        "title": "Git hosts",
        "subtitle": "PAT credentials for GitHub / GitLab / etc."
      },
      "customTasks": {
        "title": "Custom tasks",
        "subtitle": "Slash commands shown in the session task picker"
      },
      "projectMemory": {
        "title": "Project goal / plan / journal",
        "subtitle": "Per-cwd memory layers 2-4 + agent proposals"
      },
      "archived": {
        "title": "Archived memories",
        "subtitle": "Restore memories the auto-cleaner soft-archived (30-day grace)"
      },
      "backups": {
        "title": "Backups",
        "subtitle": "Latest backup status & run-now"
      },
      "dataExport": {
        "title": "Data export & import",
        "subtitle": "User-level data bundles (memories / integrations / custom tasks)"
      },
      "settings": {
        "title": "Settings",
        "subtitle": "Language, appearance, account"
      },
      "about": {
        "title": "About",
        "subtitle": "Build version & server info"
      }
    },
    "signOut": "Sign out"
  },
  "sessions": {
    "title": "Sessions",
    "refresh": "Refresh",
    "actions": "Actions",
    "spawn": "Spawn",
    "filters": {
      "all": "All",
      "running": "Running",
      "idle": "Idle",
      "ended": "Ended"
    },
    "card": {
      "startedRelative": "{provider} · started {when}"
    },
    "empty": {
      "titleAll": "No sessions yet",
      "titleFiltered": "No sessions match the \"{filter}\" filter.",
      "subtitleAll": "Tap the Spawn button to create one.",
      "subtitleFiltered": "Try a different filter or pull to refresh."
    },
    "errorTitle": "Failed to load sessions",
    "relative": {
      "secondsAgo": "{n}s ago",
      "minutesAgo": "{n}m ago",
      "hoursAgo": "{n}h ago",
      "daysAgo": "{n}d ago"
    },
    "detail": {
      "fallbackTitle": "Session",
      "refreshMetadata": "Refresh metadata",
      "inspector": "Inspector (Files / Git / Tasks / History / Notes)",
      "projectMemory": "Project memory (goal / plan / journal / inbox)",
      "actions": "Actions",
      "started": "started {when}",
      "startedEnded": "started {started}  ·  ended {ended}",
      "idPrefix": "id: {id}",
      "errorTitle": "Failed to load session",
      "accountSwitcher": {
        "tooltip": "Switch Claude account",
        "sheetTitle": "Switch Claude account",
        "current": "Currently: {account}",
        "defaultName": "Default (system credential)",
        "defaultSubtitle": "Use the CLI's own login, no specific account",
        "defaultShort": "default",
        "tokenEmpty": "no token",
        "confirmTitle": "Switch account?",
        "confirmBody": "This restarts the CLI under the new account — the current in-CLI conversation context is lost (the session tab is kept).",
        "confirmAction": "Switch",
        "cancel": "Cancel",
        "switchedSnack": "Switched to {account}",
        "switchFailed": "Switch failed: {error}",
        "noneHint": "No Claude accounts configured. Add them in More → Providers → Claude."
      }
    },
    "terminal": {
      "snackbar": {
        "imagePickerFailed": "Image picker failed: {error}",
        "uploadingImage": "Uploading image…",
        "imageAttached": "Image attached: {path}",
        "uploadFailed": "Upload failed ({status}): {message}",
        "uploadFailedGeneric": "Upload failed: {error}"
      },
      "imageSource": {
        "photoLibrary": "Photo library",
        "takePhoto": "Take a photo"
      },
      "keyboard": {
        "copyBuffer": "Copy buffer",
        "paste": "Paste",
        "attachImage": "Attach image",
        "enter": "Enter"
      },
      "connection": {
        "connecting": "Connecting…",
        "connected": "Connected",
        "reconnecting": "Reconnecting…",
        "reconnectingWithError": "Reconnecting ({error})…",
        "disconnected": "Disconnected",
        "disconnectedWithError": "Disconnected ({error})",
        "ended": "Session ended"
      }
    },
    "action": {
      "stop": "Stop",
      "stopping": "Stopping…",
      "stopDescription": "Send SIGTERM, retain history",
      "restart": "Restart",
      "restarting": "Restarting…",
      "restartDescription": "Re-spawn the CLI process",
      "delete": "Delete",
      "deleteDescription": "Remove the session and its history",
      "deleteConfirm": "Delete this session permanently? Its ring buffer and history will be gone.",
      "errors": {
        "stop": "Stop failed: {error}",
        "start": "Restart failed: {error}",
        "delete": "Delete failed: {error}"
      }
    },
    "dirPicker": {
      "parent": "Parent",
      "newFolder": "New folder",
      "useThisFolder": "Use this folder",
      "loading": "Loading…",
      "empty": "No subfolders here.\nPick this folder, or create a new one.",
      "createdSnack": "Created {path}",
      "mkdirFailedSnack": "mkdir failed: {error}",
      "dialog": {
        "title": "New folder",
        "hint": "Folder name",
        "create": "Create"
      }
    },
    "inspector": {
      "shell": {
        "title": "Inspector",
        "loadError": "Failed to load session: {error}",
        "tabs": {
          "files": "Files",
          "git": "Git",
          "tasks": "Tasks",
          "history": "History",
          "notes": "Notes"
        }
      },
      "shared": {
        "refresh": "Refresh",
        "inserted": "Inserted: {text}",
        "insertFailedApi": "Insert failed ({status}): {message}",
        "insertFailedGeneric": "Insert failed: {error}",
        "insertFailedShort": "Insert failed: {error}"
      },
      "history": {
        "insertIntoTerminal": "Insert into terminal",
        "searchHint": "Search prompts…"
      },
      "files": {
        "insertAtRef": "Insert as @reference",
        "insertPath": "Insert path",
        "insertPathSubtitle": "Pastes the absolute path verbatim",
        "readContent": "Read content",
        "readContentSubtitle": "Up to 256 KiB plain text",
        "readFailedApi": "Read failed ({status}): {message}",
        "readFailedGeneric": "Read failed: {error}",
        "parent": "Parent",
        "backToCwd": "Back to session cwd"
      },
      "git": {
        "insertAtRef": "Insert as @reference",
        "insertPath": "Insert path",
        "showDiff": "Show diff",
        "diffFailedApi": "Diff failed ({status}): {message}",
        "diffFailedGeneric": "Diff failed: {error}",
        "insertHash": "Insert hash",
        "showFullPatch": "Show full patch",
        "showFailedApi": "Show failed ({status}): {message}",
        "showFailedGeneric": "Show failed: {error}",
        "tabStatus": "Status",
        "tabLog": "Log"
      },
      "tasks": {
        "runCommand": "Run command",
        "runCommandSubtitle": "Runs in a new shell session and switches to it",
        "filterHint": "Filter tasks…",
        "noMatch": "No tasks match \"{query}\"",
        "emptyTitle": "No tasks in this folder",
        "emptyHint": "Looking for package.json, Makefile, Taskfile, justfile, Cargo.toml, go.mod, pyproject.toml, or shell scripts"
      },
      "notes": {
        "insertedAt": "Inserted: @{path}",
        "myNotes": "My notes",
        "projectDocs": "Project docs",
        "insertAtRefTooltip": "Insert as @reference",
        "insertAtRefShort": "Insert @reference",
        "draftHint": "# {project}\n\nThoughts, todos, context for the agent…",
        "createFailed": "Create failed: {error}",
        "saveFailed": "Save failed: {error}",
        "changeLocationTooltip": "Change project docs location",
        "filenameHint": "filename (e.g. spec or design.md)",
        "create": "Create",
        "filterHint": "Filter…",
        "locationDialogTitle": "Project docs location",
        "loadFailedApi": "Load failed: {error}",
        "loadFailedGeneric": "Load failed: {error}",
        "saveFailedApi": "Save failed: {error}",
        "saveFailedGeneric": "Save failed: {error}",
        "insertFailedApi": "Insert failed: {error}",
        "insertFailedGeneric": "Insert failed: {error}",
        "createFailedApi": "Create failed: {error}",
        "createFailedGeneric": "Create failed: {error}",
        "personalHint": "Personal scratchpad — auto-saves as you type. AI agents do not write here.",
        "projectDocsHint": "Architecture / spec / decisions / plan / retros — typically authored or maintained by an agent.",
        "mappingCleared": "Mapping cleared — using default",
        "mappedTo": "Mapped to {path}",
        "cancelTooltip": "Cancel",
        "newDocTooltip": "New doc",
        "noProjectMapping": "Could not resolve a project mapping for this session. Check that the gateway has a notes vault configured and that the session cwd is set.",
        "emptyProjectDocs": "No project docs yet. Tap + to create one, or let an AI agent generate from a prompt.",
        "emptyFilterMatch": "No matches for \"{query}\".",
        "locationDialogHelp": "Pin this session's cwd to a specific folder under your notes vault. Leave blank to reset.",
        "sessionCwd": "Session cwd",
        "projectDocsPath": "Vault-relative project docs path",
        "locationStoredHint": "Stored in <vault>/.opendray-projects.json — git-syncs with the rest of the vault.",
        "pinnedHint": "Pinned to {path}/ (overrides {defaultPath}). AI agents author docs here too.",
        "noProjectMapping2": "(no project mapping)",
        "clearOverride": "Clear override",
        "save": "Save"
      }
    },
    "spawnSheet": {
      "title": "New session",
      "errorRequired": "Provider and working directory are required",
      "errorGeneric": "Failed to spawn session: {error}",
      "cancel": "Cancel",
      "spawn": "Spawn",
      "providerLabel": "Provider",
      "disabledSuffix": " (disabled)",
      "cwdLabel": "Working directory",
      "cwdHint": "/Users/you/projects/foo",
      "cwdHelper": "Absolute path on the gateway host.",
      "browse": "Browse",
      "nameLabel": "Name (optional)",
      "nameHint": "e.g. backend-refactor",
      "argsLabel": "Extra args (optional)",
      "argsHint": "--continue --verbose",
      "argsHelper": "Whitespace-separated; blank uses the provider's defaults.",
      "bypass": {
        "labelClaude": "Bypass permissions",
        "labelCodex": "Bypass approvals & sandbox",
        "labelGemini": "YOLO mode",
        "subtitleOn": "This session will run with elevated autonomy.",
        "subtitleOff": "Off — confirmations and prompts behave normally."
      },
      "noProviders": {
        "title": "No providers configured",
        "message": "The gateway has no CLI providers enabled. Configure one under Providers (web admin) or [providers] in config.toml, then tap Reload.",
        "reload": "Reload"
      },
      "providerLoadError": {
        "title": "Could not load providers",
        "networkError": "Network error",
        "serverPrefix": "Server {code}",
        "format": "{prefix}: {message}"
      },
      "claudeAccount": {
        "label": "Claude account",
        "helperMulti": "Multiple accounts configured — pick one for this session.",
        "helperSingle": "Pick a configured account or use the default (env / system).",
        "default": "Default (env / system)",
        "disabledSuffix": " (disabled)",
        "noTokenSuffix": " (no token)",
        "noneHint": "No Claude accounts configured — the gateway will use the system ANTHROPIC_API_KEY. Add accounts under Settings → Accounts on the web admin.",
        "errorHint": "Could not load Claude accounts ({error}). The session will spawn with the gateway default."
      }
    }
  },
  "mcp": {
    "title": "MCP",
    "newServer": "New server",
    "addSecret": "Add secret",
    "editConfig": "Edit config",
    "viewRawConfig": "View raw config",
    "copyId": "Copy id",
    "copiedSnack": "Copied {id}",
    "deleteServerTitle": "Delete MCP server?",
    "deleteSecretTitle": "Delete secret?",
    "errorPrefix": {
      "delete": "Delete failed",
      "add": "Add failed",
      "update": "Update failed",
      "toggle": "Toggle failed"
    },
    "errorWithMessage": "{prefix}: {error}",
    "editor": {
      "nameHint": "my-mcp-server",
      "jsonHint": "JSON config — name, transport: stdio, command, args…",
      "descriptionPlaceholder": "Optional one-liner",
      "validateJsonObject": "Body must be a JSON object",
      "validateJsonInvalid": "Invalid JSON: {error}",
      "appBarEdit": "Edit MCP server",
      "appBarNew": "New MCP server",
      "idLockedHint": "Locked in edit mode — delete + recreate to change.",
      "jsonLabel": "Server JSON",
      "jsonSchemaHelp": "Schema: transport must be stdio, http or sse. For stdio set command + args. For http/sse set url + headers. Use \\$secret:KEY to reference vault secrets.",
      "idLabel": "id (URL segment, lowercase alphanumeric / dash / underscore)",
      "idRequired": "id is required",
      "saving": "Saving…",
      "save": "Save",
      "create": "Create"
    },
    "secret": {
      "keyLabel": "Key",
      "keyHint": "GITHUB_TOKEN, OPENAI_KEY, …",
      "valueLabel": "Value",
      "keyRequired": "Key is required.",
      "keyInvalid": "Key must match [A-Za-z_][A-Za-z0-9_]* — same rules as a shell env var.",
      "valueRequired": "Value is required.",
      "replaceTitle": "Replace secret value",
      "addTitle": "Add secret",
      "saveButton": "Save",
      "addButton": "Add",
      "helpRules": "Shell-env-var rules: starts with a letter or _, then letters / digits / _ only.",
      "replaceHint": "Paste new value (the previous one is wiped)",
      "addHint": "Paste secret value",
      "addedSnack": "Secret {key} added.",
      "updatedSnack": "Secret {key} updated.",
      "deletedSnack": "Deleted {key}.",
      "deleteBody": "Removes the value from the encrypted vault. Any MCP server that references it will fail until restored."
    },
    "popup": {
      "editConfigSubtitle": "Full JSON editor — vault-backed servers only",
      "viewRawSubtitle": "Read-only inspector for the server JSON",
      "deleteLabel": "Delete"
    },
    "kv": {
      "transport": "Transport",
      "description": "Description",
      "command": "Command",
      "args": "Args",
      "headers": "Headers"
    },
    "deleteServerBody": "Removes the vault directory for {id}. Sessions that reference this server stop being able to spawn it.",
    "deleteServerSnack": "Deleted {id}.",
    "serversCount": "Servers ({count})",
    "secretsCount": "Secrets ({count})",
    "emptyServers": "No MCP servers registered. Tap \"New server\" to add one.",
    "emptySecrets": "No secrets stored. Add one to feed sensitive env / headers into MCP servers without putting them in the JSON.",
    "noVaultFileYet": "No vault file yet — added secrets create it.",
    "tapToReplaceHint": "Tap to replace · long-press / trash to delete",
    "failedToLoad": "Failed to load MCP state",
    "serverCreatedSnack": "MCP server created.",
    "serverUpdatedSnack": "MCP server updated.",
    "envHeading": "Env",
    "encryptionAes": "AES-GCM encrypted (key in OS keychain)",
    "encryptionPlaintext": "PLAINTEXT — keychain unavailable",
    "toggleEnabledSnack": "{name} enabled.",
    "toggleDisabledSnack": "{name} disabled."
  },
  "providers": {
    "title": "Providers",
    "configSaved": "Provider config updated.",
    "saveFailedApi": "Save failed: {error}",
    "saveFailedGeneric": "Save failed: {error}",
    "reload": "Reload",
    "errorPrefix": {
      "toggle": "Toggle failed",
      "rename": "Rename failed",
      "delete": "Delete failed"
    },
    "errorWithMessage": "{prefix}: {error}",
    "updateCheck": {
      "sectionTitle": "CLI version",
      "checking": "Checking for updates…",
      "checkFailed": "Couldn't check for updates",
      "notInstalled": "Not installed on the gateway host",
      "installed": "Installed: {version}",
      "upToDate": "Up to date",
      "updateAvailable": "Update available: {version}",
      "latest": "latest {version}",
      "updateButton": "Update CLI",
      "updating": "Updating…",
      "updatedSnack": "Updated to {version}.",
      "noChangeSnack": "Already on the latest version.",
      "updateFailed": "Update failed: {error}",
      "notAvailableHere": "In-app update isn't available on this host: {reason}",
      "activeSessionsWarning": "{n} active session(s) are using this CLI — updating won't interrupt them, but they keep the old version until restarted."
    },
    "accounts": {
      "rename": "Rename",
      "renameTitle": "Rename {name}",
      "displayNameLabel": "Display name",
      "displayNameHint": "Work account",
      "deleteTitle": "Delete account?",
      "importFailedApi": "Import failed: {error}",
      "importFailedGeneric": "Import failed: {error}",
      "enable": "Enable",
      "disable": "Disable",
      "deleteLabel": "Delete",
      "deleteBody": "Removes the account and its stored OAuth token. Sessions already using this account stay running but reauth will fail.",
      "deletedSnack": "Deleted {name}.",
      "importSyncedSnack": "Already in sync — gateway has no new accounts.",
      "importedSnackOne": "Imported {n} account.",
      "importedSnackOther": "Imported {n} accounts.",
      "importing": "Syncing…",
      "importLocal": "Import local",
      "addHint": "Adding a new account is gateway-host only.",
      "addBody": "The new directory shows up here automatically. See the docs for OAuth flow steps.",
      "loadFailed": "Failed to load accounts: {error}",
      "intro": "Sessions spawned with the Claude provider pick from these accounts (or fall back to env).",
      "enabledSnack": "{name} enabled.",
      "disabledSnack": "{name} disabled.",
      "renamedSnack": "Renamed to {name}.",
      "activeSessions": "{count} active",
      "usedAgo": "used {when}",
      "identityChanged": "Identity changed",
      "identityWas": "was {email}",
      "acceptIdentity": "Accept",
      "identityAcceptedSnack": "Identity change accepted",
      "identityAcceptFailed": "Accept failed"
    },
    "configFallbackTitle": "Provider config",
    "saving": "Saving…",
    "save": "Save",
    "configLoadFailed": "Failed to load provider",
    "argsHelper": "Whitespace-separated CLI args.",
    "listEmptyHeadline": "No providers loaded.",
    "listEmptyBody": "The gateway resolves providers from its plugin directory at startup. Check the logs if you expect one.",
    "listLoadFailed": "Failed to load providers",
    "cliSectionHeader": "CLI providers",
    "enabledSnack": "{name} enabled.",
    "disabledSnack": "{name} disabled."
  },
  "integrations": {
    "title": "Integrations",
    "register": "Register",
    "registerDialogTitle": "Register integration",
    "edit": "Edit",
    "editTitle": "Edit {name}",
    "enabledLabel": "Enabled",
    "iSavedIt": "I've saved it",
    "apiKeyForName": "API key for {name}",
    "apiKeySubtitleRegister": "Hand this to the integration so it can authenticate against /api/v1/{routePrefix}/…",
    "copiedRequestId": "Copied request_id {id}",
    "updateOk": "Integration updated.",
    "registerFailedApi": "Register failed: {error}",
    "registerFailedGeneric": "Register failed: {error}",
    "updateFailedApi": "Update failed: {error}",
    "updateFailedGeneric": "Update failed: {error}",
    "deleteTitle": "Delete integration?",
    "deletedSnack": "Deleted {name}.",
    "deleteFailedApi": "Delete failed: {error}",
    "deleteFailedGeneric": "Delete failed: {error}",
    "rotateKey": "Rotate key",
    "rotateConfirmTitle": "Rotate API key?",
    "rotate": "Rotate",
    "newApiKeyTitle": "New API key for {name}",
    "newApiKeySubtitle": "Hand this to the integration. The previous key has just been invalidated.",
    "rotateFailedApi": "Rotate failed: {error}",
    "rotateFailedGeneric": "Rotate failed: {error}",
    "deleteBody": "Removes the registration and revokes the API key. In-flight requests using the old key will start failing.",
    "rotateBody": "Generates a new API key for {name} and immediately invalidates the old one.",
    "appBarFallback": "Integration",
    "tooltipMore": "More",
    "tooltipReadOnly": "System integration — read-only",
    "kvRoutePrefix": "Route prefix",
    "kvBaseUrl": "Base URL",
    "kvScopes": "Scopes",
    "kvVersion": "Version",
    "kvLastHealthPing": "Last health ping",
    "kvCreated": "Created",
    "kvKeyRotated": "Key rotated",
    "detailLoadFailed": "Failed to load integration: {error}",
    "callsLoadFailed": "Failed to load calls",
    "noMatchingCalls": "No matching calls in the log yet.",
    "directionAll": "All",
    "directionInbound": "Inbound",
    "directionOutbound": "Outbound",
    "form": {
      "validateRequired": "Name, base URL, and route prefix are required.",
      "fieldName": "Name",
      "fieldNameHint": "My Bot",
      "fieldBaseUrl": "Base URL",
      "fieldRoutePrefix": "Route prefix",
      "routePrefixHelper": "Reachable as /api/v1/<prefix>/...",
      "fieldScopes": "Scopes (optional)",
      "scopesHelper": "Comma-separated. Empty = server defaults.",
      "fieldVersion": "Version (optional)",
      "validateBaseUrl": "Base URL is required.",
      "editFieldScopes": "Scopes",
      "editScopesHelper": "Comma-separated.",
      "editFieldVersion": "Version",
      "apiKeyWarn": "You won't see this key again.",
      "copyCopied": "Copied",
      "copyCopy": "Copy"
    },
    "emptyState": "Register from the web admin: Integrations → New.",
    "sectionRegistered": "Registered",
    "sectionSystem": "System",
    "listLoadFailed": "Failed to load integrations"
  },
  "memoryWorkers": {
    "title": "Memory workers",
    "savedSnack": "{label} saved",
    "saveFailed": "Save failed: {error}",
    "testFailed": "Test call failed: {error}",
    "workerLabel": "Worker",
    "summarizerHttp": "Summarizer (HTTP)",
    "agentCliPrint": "Agent (CLI --print)",
    "cliLabel": "CLI",
    "cliClaude": "Claude",
    "cliGemini": "Gemini",
    "claudeAccountLabel": "Claude account",
    "claudeAccountDefault": "Default",
    "test": "Test",
    "intro": "Each memory-system LLM touchpoint can be served independently by the local summarizer endpoint (LM Studio / OpenAI-compat) or by spawning a headless Claude / Gemini agent in --print mode. High-quality narrative tasks (gitactivity, transcript) benefit from agent workers; high-frequency tasks (gatekeeper) stay on the local endpoint by design.",
    "errorTitle": "Endpoint not reachable",
    "errorDetail": "The /api/v1/memory/workers routes are new in M25 — the opendray binary may need a restart to mount them and run migration 0029.",
    "summarizerOnlyBadge": "summarizer-only",
    "summarizerProviderLabel": "Summarizer provider",
    "registryDefault": "Registry default",
    "agentWarning": "Agent mode spawns a headless CLI per call. Latency ~5-15s (vs ~1s summarizer); cost shifts from CPU to your Claude/Gemini quota.",
    "noCalls24h": "No calls in last 24h.",
    "testOkSnack": "{label} OK — {duration}ms",
    "testFailedReturnedSnack": "{label} failed: {error}",
    "unknownError": "unknown",
    "tasks": {
      "gatekeeper": {
        "label": "Gatekeeper",
        "description": "Pre-write filter on every memory_store. High frequency (<500ms target) — summarizer-only."
      },
      "cleaner": {
        "label": "Cleaner librarian",
        "description": "Periodic LLM librarian. Judges aged memories as keep / stale / duplicate."
      },
      "gitactivity": {
        "label": "Git activity summariser",
        "description": "git log → 2-3 paragraph narrative every 24h. Naturally fits an agent worker."
      },
      "transcript": {
        "label": "Session transcript summariser",
        "description": "Session-end 'what did the agent do' summary. Naturally fits an agent worker."
      },
      "planDrift": {
        "label": "Plan drift detector",
        "description": "After each session ends, checks whether the project plan needs updating and files a proposal. Fits an agent worker for richer reasoning."
      },
      "conflictDetector": {
        "label": "Cross-layer conflict detector",
        "description": "Daily scan that finds contradictions between facts / plan / goal / journal. Higher-quality model = fewer false positives."
      },
      "capture": {
        "label": "Capture engine",
        "description": "Per-trigger fact extraction from session transcripts. Agent mode gives noticeably better facts on long sessions; summarizer mode is cheap and local."
      }
    }
  },
  "memoryArchived": {
    "title": "Archived memories",
    "loadFailed": "Failed to load: {error}",
    "restoreFailed": "Restore failed: {error}",
    "emptyTitle": "Nothing archived",
    "emptyBody": "No archived memories across any project. The auto-cleaner soft-archives stale and duplicate facts here (restorable for 30 days) — nothing has been removed yet.",
    "globalScope": "(global)",
    "countBadge": "{count} archived",
    "restore": "Restore"
  },
  "project": {
    "title": "Project",
    "pickFirst": "Pick a project first.",
    "health": {
      "title": "Memory health — last {days} days",
      "subtitle": "Aggregate signals across both memory subsystems for this project.",
      "newFacts": "New facts",
      "newFactsHint": "{total} stored in total",
      "captureFires": "Capture fires",
      "captureFiresHint": "{stored} stored · {deduped} deduped",
      "newJournal": "Journal entries",
      "newJournalHint": "{total} in total",
      "planAge": "Plan last updated",
      "planAgeHint": "{count} plan-drift proposal(s) pending",
      "planAgeHintNone": "No plan-drift proposals pending",
      "goalAge": "Goal last updated",
      "pending": "Pending proposals",
      "pendingHint": "oldest {days}d old",
      "topHit": "Top hit · {hits} retrievals",
      "zeroHit": "{count} facts older than 7d with zero retrievals — candidates for cleanup.",
      "never": "never",
      "today": "today",
      "daysAgo": "{count}d ago"
    },
    "conflicts": {
      "subtitle": "Contradictions the daily detector found between facts, plan, goal, and journal entries.",
      "empty": "No pending conflicts. Tap Detect now for an on-demand sweep.",
      "detectNow": "Detect now",
      "detected": "{count} new conflict(s) found",
      "accept": "Accept",
      "dismiss": "Dismiss",
      "deleteFact": "Delete fact {side}",
      "deleteConfirmTitle": "Delete fact {side}?",
      "deleteConfirmBody": "This permanently removes the fact and accepts the conflict. The other side stays as the surviving claim.",
      "deleteWillDelete": "Will delete (side {side}):",
      "deleteWillKeep": "Will keep (side {side}):",
      "deleteNonFactOther": "({layer} entry — open the corresponding tab to inspect)",
      "deleteLoading": "Loading fact text…",
      "deleteFactLabel": "Delete {side}",
      "deletedFact": "Fact deleted and conflict accepted",
      "openPlanEditor": "Open plan editor",
      "openGoalEditor": "Open goal editor",
      "severity": {
        "low": "low",
        "medium": "medium",
        "high": "high"
      }
    },
    "journalPrune": {
      "title": "Prune stale journal entries",
      "subtitle": "Older than {days} days, no pending conflicts.",
      "daysLabel": "Older than (days):",
      "empty": "Nothing stale to prune.",
      "selectAll": "Select all",
      "deselectAll": "Deselect all",
      "deleteSelected": "Delete ({count})",
      "deleted": "{count} entry/entries deleted"
    },
    "loadFailed": "Failed to load: {error}",
    "projectsLoadFailed": "Failed to load projects: {error}",
    "projectLabel": "Project",
    "resetTooltip": "Reset project memory",
    "append": "Append",
    "appendDialogTitle": "Append journal entry",
    "titleFieldLabel": "Title (optional)",
    "contentFieldLabel": "Content (markdown)",
    "appendFailed": "Failed: {error}",
    "approveFailed": "Approve failed: {error}",
    "rejectFailed": "Reject failed: {error}",
    "resetConfirmTitle": "Reset project memory?",
    "alsoDeleteScanner": "Also delete scanner docs",
    "alsoDeletePgvector": "Also delete pgvector memories",
    "deleteForever": "Delete forever",
    "resetDoneSnack": "Reset: {parts}",
    "resetFailed": "Reset failed: {error}",
    "docSavedSnack": "{kind} saved",
    "docSaveFailed": "Save failed: {error}",
    "docHintTemplate": "Write the {kind} as markdown…",
    "deleteEntryTooltip": "Delete entry",
    "agentReason": "Agent reason",
    "reject": "Reject",
    "approve": "Approve",
    "replaceConfirmTitle": "Replace current {kind}?",
    "replaceKind": "Replace {kind}",
    "archived": {
      "emptyTitle": "Nothing archived",
      "emptyBody": "No archived memories for this project. The auto-cleaner soft-archives stale and duplicate facts here automatically — none yet.",
      "restoreFailed": "Restore failed: {error}",
      "restore": "Restore"
    }
  },
  "backups": {
    "title": "Backups",
    "runConfirmTitle": "Run backup now?",
    "runConfirmBody": "Triggers a fresh dump against the local target. The job runs server-side; this list will refresh as it progresses.",
    "run": "Run",
    "runNow": "Run now",
    "queueing": "Queueing…",
    "queuedSnack": "Backup queued ({id}). Watching for progress…",
    "runFailedApi": "Run failed: {error}",
    "runFailedGeneric": "Run failed: {error}",
    "rowSucceededSnack": "Backup succeeded ({bytes}).",
    "rowFailedSnack": "Backup failed: {error}",
    "unknownError": "unknown error",
    "detailTitle": "Backup detail",
    "deleteTitle": "Delete backup?",
    "deleteBody": "Removes the blob from {target} and marks the row deleted in the index.",
    "deletedSnack": "Deleted {id}.",
    "deleteFailedApi": "Delete failed: {error}",
    "deleteFailedGeneric": "Delete failed: {error}",
    "menuSchedules": "Schedules",
    "menuTargets": "Targets",
    "kv": {
      "status": "Status",
      "target": "Target",
      "triggeredBy": "Triggered by",
      "started": "Started",
      "finished": "Finished",
      "size": "Size",
      "encrypted": "Encrypted",
      "targetPath": "Target path",
      "error": "Error",
      "yes": "yes",
      "no": "no"
    },
    "emptyMissingDeps": {
      "headline": "Backups can't run yet",
      "body": "Install postgresql-client and restart opendray."
    },
    "emptyNoTargets": {
      "headline": "No backup targets configured",
      "body": "Open the More menu → Targets to add a destination (local / S3 / SMB / SFTP / WebDAV / rclone). Then come back and tap \"Run now\"."
    },
    "emptyNoBackups": {
      "headline": "No backups yet",
      "body": "Tap \"Run now\" to take a fresh snapshot, or open Schedules to set up recurring runs."
    },
    "restartToActivate": "Restart opendray to activate backups",
    "passphraseSaved": "Your passphrase is saved. The gateway only loads it at startup, so changes only take effect after a restart.",
    "keyFileLabel": "Key file",
    "configuredViaLabel": "Configured via",
    "wizard": {
      "title": "Set up backups",
      "intro": "Choose a master passphrase. opendray uses it to encrypt every backup blob with AES-256-GCM. Lose the passphrase and you lose the data — there is no recovery.",
      "saving": "Saving…",
      "generateAndSave": "Generate and save",
      "savePassphrase": "Save passphrase",
      "generateHint": "Server generates a cryptographically random passphrase, you copy it to a password manager, then commit.",
      "helperRecommended": "Recommended: 40+ chars from a password manager",
      "saveNowHeader": "Save this passphrase NOW",
      "saveNowBody": "This is shown ONCE. It will not be retrievable from opendray afterwards."
    },
    "statusReady": "Backups ready",
    "statusCannot": "Backups cannot run",
    "overviewTargets": "Targets",
    "overviewSchedules": "Schedules",
    "overviewBackups": "Backups",
    "failedToLoad": "Failed to load backups",
    "envVarConfigured": "OPENDRAY_BACKUP_KEY env var",
    "savedConfirmCheckbox": "I have saved this passphrase to my password manager",
    "pgDumpMissing": "pg_dump is not on PATH. Install postgresql-client and restart opendray.",
    "encryption": {
      "checkAgain": "Check again",
      "generate": "Generate",
      "paste": "Paste",
      "random256bit": "256-bit random key",
      "passphraseLabel": "Your passphrase",
      "passphraseHint": "At least 20 characters",
      "passphraseCopied": "Passphrase copied to clipboard"
    },
    "restoreFromFile": "Restore from file",
    "restore": {
      "title": "Restore from bundle",
      "subtitle": "Replay an encrypted .tar.gz.enc bundle into a Postgres database. The bundle is uploaded from this phone — pick a file produced by a prior backup.",
      "bundleLabel": "Bundle file (.tar.gz.enc)",
      "pickFile": "Pick file",
      "fileSelected": "{name} · {size}",
      "noFile": "No file selected",
      "targetDsnLabel": "Target Postgres DSN",
      "targetDsnHint": "Leave empty to restore into opendray's own DB.",
      "targetDsnPlaceholder": "postgres://user:pass@host:5432/dbname",
      "cleanLabel": "pg_restore --clean --if-exists",
      "cleanHint": "Drops existing objects before recreating them.",
      "auditNoteLabel": "Audit note (optional)",
      "auditNotePlaceholder": "e.g. recovering from #INC-481",
      "ownDbWarning": "Restoring into opendray's OWN database will rewrite the rows this gateway is currently serving. Type \"I understand\" to confirm.",
      "confirmPlaceholder": "Type \"I understand\"",
      "confirmSentinel": "I understand",
      "restoring": "Restoring…",
      "restore": "Restore",
      "succeededTitle": "Restore succeeded",
      "succeededBody": "Replayed {bytes} from backup {id}.",
      "failedTitle": "Restore failed",
      "pickFileToast": "Pick a bundle file first.",
      "outputTitle": "pg_restore output",
      "noPgRestoreOutput": "(empty — restore completed silently)",
      "manifestTitle": "Manifest",
      "manifestBackupId": "Backup ID",
      "manifestVersion": "Manifest version",
      "manifestCreatedAt": "Created",
      "manifestPgVersion": "pg_version",
      "manifestOpendrayVersion": "opendray version",
      "fingerprint": "Key fingerprint",
      "fingerprintOk": "matched",
      "fingerprintMismatch": "MISMATCH",
      "encryptionAlgo": "Encryption",
      "bytesRead": "Bytes read",
      "targetDsnUsed": "Target DSN",
      "targetDsnSelfLabel": "(opendray's own DB)",
      "done": "Done"
    },
    "inventory": {
      "title": "What's in a backup",
      "summary": "{rows} rows · {tables} tables",
      "description": "Live row counts from opendray's Postgres database. Backups capture every row below; binary artifacts on disk are not included.",
      "rowsLabel": "rows",
      "loadFailedToast": "Failed to load inventory",
      "loading": "Loading…",
      "tap": "Tap to expand"
    }
  },
  "backupTargets": {
    "title": "Backup targets",
    "newTarget": "New target",
    "testConnection": "Test connection",
    "editConfig": "Edit config",
    "viewRawConfig": "View raw config",
    "configDialogTitle": "{kind} config",
    "deleteTitle": "Delete target?",
    "errorWithMessage": "{prefix}: {error}"
  },
  "backupSchedules": {
    "title": "Backup schedules",
    "newButton": "New",
    "deleteTitle": "Delete schedule?",
    "targetLabel": "Target",
    "intervalLabel": "Interval",
    "retentionLabel": "Retention (keep N most recent)",
    "errorWithMessage": "{prefix}: {error}",
    "noTargets": "No backup targets configured. Add one from the web admin or the Targets screen.",
    "okMsgCreate": "Schedule created.",
    "okMsgUpdate": "Schedule updated.",
    "okMsgDelete": "Schedule deleted.",
    "errorPrefixCreate": "Create failed",
    "errorPrefixUpdate": "Update failed",
    "errorPrefixDelete": "Delete failed",
    "deleteBody": "Removes the recurring spec for target {targetId}. Existing backup blobs are not touched.",
    "emptyList": "No schedules yet.\nTap \"New\" to create one.",
    "validatePickTarget": "Pick a target.",
    "validateInterval": "Interval must be > 0.",
    "formTitleEdit": "Edit schedule",
    "formTitleNew": "New schedule",
    "saveButtonEdit": "Save",
    "saveButtonNew": "Create",
    "targetFixedHint": "Target is fixed once created.",
    "enabledOn": "Scheduler will run this on cadence.",
    "enabledOff": "Paused — no automatic runs until re-enabled.",
    "loadFailedTitle": "Failed to load schedules",
    "pausedBadge": "paused",
    "everyInterval": "every {interval}",
    "keepRetention": "· keep {n}",
    "nextRun": "· next {when}",
    "lastRun": "· last {when}"
  },
  "backupTargetEditor": {
    "useHttps": "Use HTTPS",
    "pathStyle": "Path-style addressing",
    "pathStyleSubtitle": "Legacy / MinIO",
    "kinds": {
      "local": {
        "label": "Local disk",
        "description": "Folder on the machine running opendray"
      },
      "smb": {
        "label": "SMB share",
        "description": "Windows shares + most home NAS appliances"
      },
      "webdav": {
        "label": "WebDAV",
        "description": "Self-hosted clouds + file-sharing services"
      },
      "sftp": {
        "label": "SFTP",
        "description": "Any SSH-accessible server"
      },
      "s3": {
        "label": "S3 / compatible",
        "description": "Amazon S3 + S3-compatible buckets (MinIO, R2, B2)"
      },
      "rclone": {
        "label": "rclone (any)",
        "description": "OneDrive, Google Drive, Dropbox via the rclone CLI"
      }
    },
    "formTitleEdit": "Edit target",
    "formTitleNew": "New backup target",
    "idHintAuto": "Auto: {prefix}-1",
    "idHelper": "Lower-case letters, digits, dashes. Defaults to the next available slot.",
    "enabledOn": "Scheduled and ad-hoc backups can target this.",
    "enabledOff": "Server will refuse to write backups here.",
    "saving": "Saving…",
    "create": "Create",
    "rootDirLabel": "Root directory",
    "rootDirHint": "Empty = cfg.backup.local_dir (~/.opendray/backups)",
    "hostLabel": "Host",
    "portLabel": "Port",
    "shareLabel": "Share",
    "shareHint": "Top-level share name",
    "shareSampleHint": "Claude_Workspace",
    "userLabel": "User",
    "passwordLabel": "Password",
    "passwordHintKeepCurrent": "Leave blank to keep current",
    "passwordHintKeep": "Leave blank to keep",
    "pathPrefixLabel": "Path prefix",
    "pathPrefixHintShareRoot": "Sub-folder under the share root (optional)",
    "pathPrefixHintBaseUrl": "Sub-folder under the base URL (optional)",
    "pathPrefixHintObjectKey": "Object-key prefix (optional)",
    "pathPrefixHintSshFolder": "Absolute or relative to user home (optional)",
    "pathPrefixHintRemoteRoot": "Sub-folder under the remote root (optional)",
    "endpointLabel": "Endpoint",
    "regionLabel": "Region",
    "bucketLabel": "Bucket",
    "accessKeyLabel": "Access key",
    "secretKeyLabel": "Secret key",
    "secretKeyHintEdit": "Leave blank to keep current. Stored AES-256-GCM encrypted.",
    "secretKeyHintNew": "Stored AES-256-GCM encrypted; never echoed back.",
    "baseUrlLabel": "Base URL",
    "baseUrlHint": "Full URL including path. Nextcloud: https://cloud.example/remote.php/dav/files/<user>",
    "sftpPasswordHintEdit": "Leave blank to keep. If both password + private key are present, the private key wins.",
    "sftpPasswordHintNew": "Either password OR private key. If both, password becomes a fallback only.",
    "privateKeyLabel": "Private key (PEM)",
    "privateKeyHintEdit": "Leave blank to keep. Paste OpenSSH/PEM contents.",
    "privateKeyHintNew": "Paste the contents of an OpenSSH/PEM private key. Multi-line input — keep the BEGIN/END markers.",
    "hostKeyLabel": "Host key (pinning)",
    "hostKeyHint": "OpenSSH-style server public key. `ssh-keyscan <host>` to obtain. Blank = no pinning (NOT recommended outside LAN).",
    "rcloneNote": "Requires the rclone CLI on the opendray host. First run `rclone config` once interactively to authenticate cloud accounts.",
    "rcloneRemoteLabel": "Remote name",
    "rcloneRemoteHint": "Name from `rclone config` (no colon).",
    "rcloneBinaryLabel": "Binary path",
    "rcloneBinaryHint": "Override `which rclone`. Empty = PATH lookup.",
    "rcloneConfigLabel": "Config path",
    "rcloneConfigHint": "Override --config. Empty = rclone default."
  },
  "githosts": {
    "title": "Git hosts",
    "addHost": "Add host",
    "deleteTitle": "Delete git host?",
    "errorWithMessage": "{prefix}: {error}",
    "errorPrefix": {
      "toggle": "Toggle failed",
      "delete": "Delete failed"
    },
    "form": {
      "kindLabel": "Kind",
      "hostLabel": "Host",
      "nameLabel": "Name",
      "nameHint": "work-github, personal-gitlab, …",
      "kinds": {
        "github": "GitHub",
        "gitlab": "GitLab",
        "bitbucket": "Bitbucket",
        "gitea": "Gitea",
        "custom": "Custom"
      },
      "validateHost": "Host is required.",
      "validateName": "Name is required.",
      "snackAdded": "Host added.",
      "snackUpdated": "Host updated.",
      "saveFailedApi": "Save failed: {error}",
      "saveFailedGeneric": "Save failed: {error}",
      "saving": "Saving…",
      "save": "Save",
      "add": "Add",
      "nameHelper": "Display name shown in PR lists.",
      "tokenLabelKeep": "Token (leave blank to keep existing)",
      "tokenLabel": "Token",
      "tokenHintKeep": "Leave blank to keep existing.",
      "tokenHintNew": "Paste the personal access token.",
      "enabledHelper": "Available to sessions for PR / remote lookups.",
      "validateTokenRequired": "Token is required when adding a host.",
      "appBarEdit": "Edit {name}",
      "appBarNew": "Add git host",
      "tokenPreviewHint": "Current preview: {preview}",
      "tokenPreviewNone": "(none)",
      "pausedSubtitle": "Paused — sessions skip this host."
    },
    "deleteBody": "Removes the credential. Sessions trying to list PRs from {host} will fall back to the unauthenticated API.",
    "deletedSnack": "Deleted {name}.",
    "enabledSnack": "{name} enabled.",
    "disabledSnack": "{name} disabled.",
    "emptyList": "No git hosts configured.\n\nAdd a credential so the gateway can list pull requests across your repos.",
    "failedToLoad": "Failed to load git hosts"
  },
  "channels": {
    "title": "Channels",
    "new": "New",
    "sendTest": "Send test message",
    "editConfig": "Edit config",
    "editNotifications": "Edit notifications",
    "viewRawConfig": "View raw config",
    "copyChannelId": "Copy channel id",
    "copiedSnack": "Copied {id}",
    "createdSnack": "Created {kind} channel.",
    "createFailedApi": "Create failed: {error}",
    "createFailedGeneric": "Create failed: {error}",
    "deleteTitle": "Delete channel?",
    "configDialog": {
      "title": "{kind} config"
    },
    "webhookDialog": {
      "title": "{kind} webhook URL",
      "copiedSnack": "Copied webhook URL."
    },
    "errorWithMessage": "{prefix}: {error}",
    "notifications": {
      "title": "Notification preferences",
      "repeatPolicy": "Repeat policy",
      "cooldownWindow": "Cooldown window",
      "includeSnippet": "Include terminal snippet",
      "snippetLengthCap": "Snippet length cap",
      "snippetHelper": "Embeds the recent terminal tail in each notification.",
      "snippetNoCap": "no cap",
      "snippetChars": "{n} chars",
      "updatedSnack": "Notification preferences updated.",
      "modes": {
        "onceLabel": "Once per session",
        "onceDescription": "Fire once when idle, stay silent until reply or end.",
        "cooldownLabel": "Time-window cooldown",
        "cooldownDescription": "Suppress repeats within the chosen window.",
        "everyLabel": "Every event (noisy)",
        "everyDescription": "No suppression — only for low-frequency channels."
      }
    },
    "popup": {
      "enable": "Enable",
      "disable": "Disable",
      "mute": "Mute",
      "unmute": "Unmute",
      "deleteLabel": "Delete"
    },
    "badges": {
      "running": "running",
      "starting": "starting…",
      "disabled": "disabled",
      "muted": "muted"
    },
    "capsLabel": "· caps: {list}",
    "bridgeWebOnly": "Bridge channels stay web-only",
    "bridgeEmptyAdd": "Add one from the web admin: Channels → New.",
    "deleteBody": "Stops the channel and removes its configuration. In-flight notifications addressed to it will be dropped silently.",
    "snacks": {
      "testDispatched": "Test message dispatched.",
      "channelEnabled": "Channel enabled.",
      "channelDisabled": "Channel disabled.",
      "channelMuted": "Channel muted.",
      "channelUnmuted": "Channel unmuted.",
      "configUpdated": "Channel config updated.",
      "channelDeleted": "Channel deleted."
    },
    "errorPrefix": {
      "test": "Test failed",
      "toggle": "Toggle failed",
      "muteToggle": "Mute toggle failed",
      "update": "Update failed",
      "delete": "Delete failed"
    },
    "failedToLoad": "Failed to load channels",
    "kinds": {
      "telegram": {
        "description": "Bot via @BotFather. opendray long-polls getUpdates and sends via REST. Buttons + reply_to_message work natively.",
        "botTokenLabel": "Bot token",
        "botTokenHint": "From @BotFather. Stored in channel config; admin-only API.",
        "chatIdLabel": "Default chat ID",
        "chatIdPlaceholder": "42 (optional — used when no ReplyCtx)",
        "ownerUserIdsLabel": "Owner Telegram user ID(s)",
        "ownerUserIdsPlaceholder": "123456789 (comma-separated for more than one)",
        "ownerUserIdsHint": "Only these numeric Telegram user IDs may drive sessions, run commands, or tap buttons; everyone else is ignored. Leave blank to allow anyone (not recommended for two-way chat). Get yours by DMing @userinfobot.",
        "chatEnabledLabel": "Two-way chat (route messages into the session)",
        "chatEnabledHint": "When on, your messages are typed into the selected session and the agent replies here. Turn off for notifications only.",
        "chatTypingLabel": "Show “typing…” while the agent works",
        "chatTypingHint": "Displays a typing indicator until the agent’s reply settles.",
        "replyMaxCharsLabel": "Max reply length (characters)",
        "replyMaxCharsPlaceholder": "3500 (blank = 3500, 0 = unlimited)",
        "replyMaxCharsHint": "Caps how much of the agent’s reply is sent before it’s trimmed with a “…(truncated)” note. Blank uses the 3500 default (~one message); set 0 to send the whole reply, split across multiple messages."
      },
      "slack": {
        "description": "Socket Mode — no public webhook needed. Requires a bot OAuth token (xoxb-) and an app-level token (xapp-) with connections:write.",
        "botTokenLabel": "Bot token (xoxb-…)",
        "botTokenHint": "OAuth & Permissions → Bot User OAuth Token. Needs chat:write.",
        "appTokenLabel": "App-level token (xapp-…)",
        "appTokenHint": "Settings → Basic Information → App-Level Tokens. Scope: connections:write.",
        "channelIdLabel": "Default channel ID",
        "channelIdPlaceholder": "C0123ABC456 (optional)"
      },
      "discord": {
        "description": "Bot via Discord Developer Portal with MESSAGE CONTENT INTENT enabled. Connects to Gateway WS — no public URL required.",
        "botTokenLabel": "Bot token",
        "botTokenPlaceholder": "Bot token from Discord Developer Portal",
        "botTokenHint": "Application → Bot → Reset Token. Invite bot with send_messages + embed_links.",
        "channelIdLabel": "Default channel ID",
        "channelIdPlaceholder": "123456789012345678 (optional)"
      },
      "feishu": {
        "description": "App-level credentials. Uses event subscription webhook for inbound. Public webhook URL is generated below — paste it into the Feishu dev console.",
        "afterCreateHint": "Open the webhook URL from the channel card and paste it into Feishu Open Platform → Event Subscriptions → Request URL.",
        "appIdLabel": "App ID",
        "appSecretLabel": "App secret",
        "appSecretPlaceholder": "Application credential secret",
        "verificationTokenLabel": "Verification token",
        "verificationTokenHint": "From Event Subscriptions → Verification Token. When set, opendray rejects webhooks with a different token.",
        "chatIdLabel": "Default chat ID (oc_…)",
        "chatIdPlaceholder": "oc_xxxxxxxxxx (optional)"
      },
      "dingtalk": {
        "description": "Custom group robot. Outbound only. Group chat → Robots → Add → Sign mode → copy webhook + secret.",
        "webhookUrlLabel": "Webhook URL",
        "secretLabel": "Sign secret",
        "secretHint": "When the robot is set to \"Sign\" security mode, copy the secret here. opendray adds the timestamp + sign params automatically."
      },
      "wecom": {
        "description": "Group robot webhook. Outbound only (text + markdown). Group settings → Group robots → Add → copy webhook URL.",
        "webhookKeyLabel": "Webhook key",
        "webhookKeyPlaceholder": "The \"key=\" query value",
        "webhookKeyHint": "Or paste the whole webhook URL into the field below — either is enough.",
        "webhookUrlLabel": "Or full webhook URL"
      }
    }
  },
  "onboarding": {
    "gatewayLabel": "Gateway URL",
    "gatewayHint": "https://opendray.example.com",
    "continue": "Continue"
  },
  "skills": {
    "title": "Skills",
    "newSkill": "New skill",
    "customizingBuiltin": "Customizing built-in {id}",
    "idLabel": "Id (slug)",
    "idHint": "e.g. tdd-guide",
    "bodyLabel": "Body (markdown)",
    "loadFailedApi": "Load failed: {error}",
    "loadFailedGeneric": "Load failed: {error}",
    "idRequired": "Id is required.",
    "bodyRequired": "Body cannot be empty.",
    "snackCreated": "Skill created.",
    "snackOverride": "Saved as vault override.",
    "snackUpdated": "Skill updated.",
    "saveFailedApi": "Save failed: {error}",
    "saveFailedGeneric": "Save failed: {error}",
    "resetTitle": "Reset to built-in?",
    "deleteTitle": "Delete skill?",
    "resetBody": "Removes the vault override for {id}. Sessions will fall back to the built-in body.",
    "resetButton": "Reset",
    "resetSnack": "Reset {id} to built-in.",
    "deletedSnack": "Deleted {id}.",
    "deleteFailedApi": "Delete failed: {error}",
    "deleteFailedGeneric": "Delete failed: {error}",
    "deleteBody": "Removes {id} from the vault. Sessions that reference it will fail until restored.",
    "newSkillTitle": "New skill",
    "customizeTitle": "Customize {id}",
    "editTitle": "Edit {id}",
    "resetTooltip": "Reset to built-in",
    "deleteTooltip": "Delete",
    "saving": "Saving…",
    "saveOverride": "Save override",
    "overrideBanner": "Saving creates a vault override with the same id. Sessions will use this body instead of the built-in until you reset.",
    "idHelper": "Lowercase letters / digits / dash. Locked once created.",
    "emptyList": "No skills configured. The gateway ships with built-ins (planner, code-reviewer, etc.).",
    "failedToLoad": "Failed to load skills"
  },
  "customTasks": {
    "title": "Custom tasks",
    "newTask": "New task",
    "deleteTitle": "Delete task?",
    "deletedSnack": "Deleted {name}.",
    "deleteFailedApi": "Delete failed: {error}",
    "deleteFailedGeneric": "Delete failed: {error}",
    "popupEdit": "Edit",
    "popupDelete": "Delete",
    "nameHint": "e.g. backend-tests",
    "commandHint": "/run pnpm test --filter backend",
    "descriptionHint": "One-liner shown under the task name.",
    "scopeGlobal": "Global",
    "scopeProject": "Project",
    "cwdHint": "/Users/you/projects/backend",
    "snackCreated": "Task created.",
    "snackUpdated": "Task updated.",
    "deleteBody": "Removes the task from the catalogue. Sessions that already inserted it stay unaffected.",
    "introBanner": "Define your own slash commands. They appear in the session task picker alongside the built-ins.",
    "validateNameRequired": "Name is required",
    "validateCommandRequired": "Command is required",
    "validateProjectCwd": "Project-scoped tasks need an absolute cwd path",
    "appBarEdit": "Edit custom task",
    "appBarNew": "New custom task",
    "fieldName": "Name",
    "nameHelper": "Shown in the inspector's task picker.",
    "fieldCommand": "Command",
    "commandHelper": "The text inserted into the session when picked. Can be a CLI command or a Claude slash command.",
    "fieldDescription": "Description (optional)",
    "fieldScope": "Scope",
    "globalScopeHint": "Visible from every session, regardless of cwd.",
    "projectScopeHint": "Visible only when a session's cwd matches the path below.",
    "fieldProjectCwd": "Project cwd",
    "cwdHelper": "Absolute path. Sessions spawned with this exact cwd will see the task.",
    "saving": "Saving…",
    "save": "Save",
    "create": "Create",
    "failedToLoad": "Failed to load custom tasks"
  },
  "notesPage": {
    "title": "Notes",
    "newButton": "New",
    "newNoteDialogTitle": "New note",
    "searchHint": "Search across the whole vault…",
    "up": "Up",
    "copyPath": "Copy path",
    "open": "Open",
    "copiedSnack": "Copied {path}",
    "deleteTitle": "Delete note?",
    "deletedSnack": "Deleted {path}",
    "deleteFailedApi": "Delete failed: {error}",
    "deleteFailedGeneric": "Delete failed: {error}",
    "createFailedApi": "Create failed: {error}",
    "createFailedGeneric": "Create failed: {error}",
    "pathLabel": "Vault-relative path",
    "pathHint": "personal/scratch.md",
    "create": "Create",
    "popupDelete": "Delete",
    "deleteBody": "This is irreversible. Vault git sync will remove the file on the gateway host too.",
    "emptyFilterMatch": "No notes match \"{query}\".",
    "emptyVault": "Vault is empty. Tap + to create your first note.",
    "emptyFolder": "Folder \"{path}\" is empty.",
    "validatePath": "Path is required",
    "validatePathDots": "Path cannot contain \"..\"",
    "pathHelper": "Auto-appends .md if missing.",
    "editor": {
      "markdownHint": "Markdown…",
      "saving": "Saving…",
      "autosave": "Auto-saves as you type",
      "loadFailedApi": "Load failed: {error}",
      "loadFailedGeneric": "Load failed: {error}",
      "saveFailedApi": "Save failed: {error}",
      "saveFailedGeneric": "Save failed: {error}",
      "savedAt": "Saved {time}"
    }
  },
  "dataExport": {
    "title": "Data export & import",
    "subtitle": "User-level bundles for migration or verification — separate from /backups (disaster recovery).",
    "sections": {
      "export": "Export",
      "import": "Import"
    },
    "form": {
      "scope": "Scope",
      "memories": "Memories",
      "memoriesHint": "All persisted memories + their embeddings.",
      "integrations": "Integrations",
      "integrationOptions": {
        "none": "Skip",
        "noneHint": "Don't include the /integrations registry.",
        "metadata": "Metadata only (default)",
        "metadataHint": "Per-integration name + endpoint, no API keys.",
        "plaintext": "Plaintext keys",
        "plaintextHint": "DANGEROUS: includes raw API tokens. v1 stores only bcrypt hashes, so this is effectively a no-op today; surface anyway."
      },
      "confirmWarning": "Plaintext key export contains decryptable secrets. Type \"I understand\" to confirm.",
      "confirmPlaceholder": "Type \"I understand\"",
      "confirmSentinel": "I understand",
      "customTasks": "Custom tasks",
      "customTasksHint": "Per-user task definitions (cron schedules + script bodies).",
      "footnote": "Bundles expire 7 days after creation. Download link is single-use.",
      "create": "Create bundle",
      "building": "Building…",
      "readyToast": "Bundle ready",
      "readyDescription": "{bytes} bytes — download from the history below.",
      "failedToast": "Bundle creation failed: {error}"
    },
    "history": {
      "title": "Export history",
      "loading": "Loading…",
      "empty": "No exports yet.",
      "listFailedToast": "Failed to load exports: {error}",
      "downloadFailedToast": "Failed to fetch download token: {error}",
      "noTokenToast": "This export has no usable download token (already consumed or expired).",
      "deletedToast": "Export deleted.",
      "deleteFailedToast": "Failed to delete export: {error}",
      "deleteConfirmTitle": "Delete export?",
      "deleteConfirmBody": "Removes the bundle and revokes the download token. {id}",
      "download": "Download",
      "delete": "Delete",
      "downloadCopiedToast": "Download URL copied to clipboard. Paste into a browser to fetch (single-use).",
      "columns": {
        "scope": "Scope",
        "size": "Size",
        "expires": "Expires",
        "actions": "Actions"
      },
      "scopeEmpty": "(empty)",
      "scopeMemories": "memories",
      "scopeIntegrations": "integrations({mode})",
      "scopeCustomTasks": "custom_tasks"
    },
    "import": {
      "intro": "Replays a bundle previously produced by Export. Only the entities you tick below are imported; everything else in the bundle is ignored.",
      "bundleLabel": "Bundle file (.zip)",
      "pickFile": "Pick file",
      "fileSelected": "{name} · {size}",
      "noFile": "No file selected",
      "memoriesLabel": "Memories",
      "integrationsLabel": "Integrations",
      "customTasksLabel": "Custom tasks",
      "importBundle": "Import bundle",
      "importing": "Importing…",
      "pickFileToast": "Pick a bundle file first.",
      "doneToast": "Import done",
      "finishedWithErrors": "Import finished with errors",
      "failedToast": "Import failed: {error}",
      "summaryCard": {
        "memories": "Memories",
        "integrations": "Integrations",
        "customTasks": "Custom tasks",
        "created": "created",
        "skipped": "skipped",
        "failed": "failed"
      }
    },
    "imports": {
      "title": "Import history",
      "loading": "Loading…",
      "empty": "No imports yet.",
      "listFailedToast": "Failed to load imports: {error}",
      "noneCounts": "(no counts)",
      "sourceUnknown": "(unknown source)",
      "columns": {
        "id": "ID",
        "status": "Status",
        "source": "Source",
        "counts": "Counts",
        "when": "When"
      }
    },
    "relative": {
      "inSeconds": "in {n}s",
      "inMinutes": "in {n}m",
      "inHours": "in {n}h",
      "inDays": "in {n}d",
      "secondsAgo": "{n}s ago",
      "minutesAgo": "{n}m ago",
      "hoursAgo": "{n}h ago"
    },
    "status": {
      "pending": "pending",
      "running": "running",
      "ready": "ready",
      "failed": "failed",
      "expired": "expired",
      "succeeded": "succeeded"
    }
  },
  "memory": {
    "status": {
      "label": "Active embedder",
      "dimensions": "{dim}-dim · {state}",
      "enabled": "enabled",
      "disabled": "disabled",
      "floorNoModel": "Keyword (BM25) retrieval only — no embedding model configured. Configure a dense endpoint in Settings to enable semantic memory.",
      "denseConfiguredPendingRestart": "Configured {model} (dense) — restart the gateway to activate semantic memory and re-embed existing memories.",
      "denseUnreachableFloor": "Configured {model} (dense) but the endpoint is unreachable — using the keyword floor until it responds (auto-upgrades on restart).",
      "denseDegraded": "Dense embedder active but its endpoint is unreachable right now — existing vectors are preserved; new writes and similarity search pause until it responds."
    },
    "title": "Memory",
    "more": "More",
    "workers": "Memory workers",
    "rank": {
      "title": "Rank breakdown",
      "effective": "Effective score: {value}",
      "similarity": "Cosine similarity",
      "ageMultiplier": "Age multiplier ({days}d old)",
      "hitMultiplier": "Hit-count multiplier ({hits} hits)",
      "confidenceMultiplier": "Confidence multiplier",
      "formula": "effective = similarity × age × hits × confidence",
      "close": "Close"
    },
    "new": "New",
    "searchHint": "Search…",
    "projectLabel": "Project",
    "filterHint": "Filter by name or path…",
    "copied": "Copied",
    "copyTooltip": "Copy text",
    "deleteAllConfirm": {
      "title": "Delete every memory in this scope?",
      "deleteAll": "Delete all"
    },
    "deletedSnackOne": "Deleted {n} memory item",
    "deletedSnackOther": "Deleted {n} memory items",
    "bulkDeleteFailedApi": "Bulk delete failed: {error}",
    "bulkDeleteFailedGeneric": "Bulk delete failed: {error}",
    "deleteOne": {
      "title": "Delete memory?",
      "body": "This cannot be undone."
    },
    "scope": {
      "project": "Project",
      "global": "Global"
    },
    "create": {
      "textLabel": "Text",
      "scopeKeyLabel": "Scope key (project cwd)",
      "scopeKeyHint": "/Users/you/projects/foo",
      "submit": "Create"
    }
  },
  "about": {
    "title": "About",
    "loading": "Loading…",
    "sections": {
      "app": "App",
      "server": "Server",
      "gateway": "Gateway"
    },
    "fields": {
      "app": "App",
      "version": "Version",
      "versionFormat": "{version} (build {build})",
      "package": "Package",
      "url": "URL",
      "signedInAs": "Signed in as",
      "tokenExpires": "Token expires"
    },
    "copied": "Copied {label}",
    "copyTooltip": "Copy",
    "copyLabels": {
      "version": "version",
      "serverUrl": "server URL"
    },
    "tagline": "opendray mobile — multi-CLI gateway control.\nSource: github.com/Opendray/opendray",
    "gateway": {
      "version": "Version",
      "commit": "Commit",
      "checking": "Checking for updates…",
      "upToDate": "Up to date",
      "updateAvailable": "Update available: {version}",
      "releaseNotes": "Release notes",
      "checkFailed": "Update check unavailable"
    }
  },
  "settings": {
    "title": "Settings",
    "language": {
      "section": "Language",
      "system": "System",
      "systemSubtitle": "Follow your phone's language setting",
      "english": "English",
      "chinese": "中文",
      "spanish": "Español"
    },
    "appearance": {
      "section": "Appearance",
      "system": "System",
      "systemSubtitle": "Follow your phone's appearance setting",
      "light": "Light",
      "lightSubtitle": "Always use the light palette",
      "dark": "Dark",
      "darkSubtitle": "Always use the dark palette"
    },
    "account": {
      "section": "Account",
      "changeCredentials": "Change credentials",
      "changeCredentialsSubtitle": "Username and password"
    },
    "gateway": {
      "section": "Gateway",
      "serverSettings": "Server settings",
      "serverSettingsSubtitle": "Listen address, logging, vault, memory, storage paths…",
      "liveLogs": "Live logs",
      "liveLogsSubtitle": "Tail the gateway log buffer — same source as the web admin"
    },
    "changeCredentials": {
      "title": "Change credentials",
      "explanation": "Verify your current password, then pick new credentials. All other signed-in sessions will be revoked.",
      "currentPassword": "Current password",
      "newUsername": "New username",
      "newPassword": "New password",
      "confirmPassword": "Confirm new password",
      "validatorRequired": "Required",
      "passwordHelper": "At least 8 characters",
      "passwordTooShort": "Must be at least 8 characters",
      "passwordMismatch": "Doesn't match the new password",
      "updatedSnack": "Credentials updated.",
      "wrongCurrent": "Current password is wrong.",
      "saving": "Saving…",
      "update": "Update"
    },
    "logViewer": {
      "title": "Live logs",
      "reconnect": "Reconnect",
      "copyBuffer": "Copy buffer",
      "clearLocal": "Clear local view",
      "copiedSnack": "Copied buffer to clipboard",
      "filterHint": "Filter substring…",
      "levels": {
        "all": "All",
        "debug": "Debug",
        "info": "Info",
        "warn": "Warn",
        "error": "Error"
      }
    },
    "serverSettings": {
      "title": "Server settings",
      "reloadTooltip": "Reload from server",
      "restartTooltip": "Restart gateway",
      "restartConfirmTitle": "Restart opendray?",
      "restartConfirmBody": "The gateway will exec itself. The mobile app may briefly lose connection.",
      "restart": "Restart",
      "restartQueuedSnack": "Restart requested. Pull-to-refresh in a moment.",
      "restartFailedApi": "Restart failed: {error}",
      "restartFailedGeneric": "Restart failed: {error}",
      "loadedFrom": "Loaded from: {path}",
      "restartHint": "Most sections need a gateway restart to take effect. The restart button is in the AppBar.",
      "savedNeedsRestart": "Saved. Restart the gateway to apply.",
      "savedSimple": "Saved.",
      "changesNeedRestart": "Changes to this section need a gateway restart.",
      "loadFailed": "Failed to load server settings",
      "sections": {
        "general": "General",
        "logging": "Logging",
        "sessions": "Sessions",
        "vault": "Vault",
        "mcpRegistry": "MCP registry",
        "memory": "Memory",
        "backup": "Backup",
        "storageClaude": "Storage · Claude",
        "storageCodex": "Storage · Codex",
        "storageGemini": "Storage · Gemini"
      },
      "sectionDescriptions": {
        "general": "Listen address, operator account, token TTL.",
        "logging": "Verbosity, format, and on-disk log path.",
        "sessions": "Idle detection thresholds.",
        "vault": "Notes, skills, and git-versioned root.",
        "mcpRegistry": "Vault paths for MCP servers + secrets file.",
        "memory": "Cross-CLI persistent memory subsystem.",
        "backup": "Encrypted DB backups + admin data exports. Passphrase lives in the keyfile (Settings → Backups).",
        "storageClaude": "Where Claude transcripts live on disk.",
        "storageCodex": "Codex sessions root.",
        "storageGemini": "Per-project tmp + projects.json paths."
      },
      "fields": {
        "listenAddress": "Listen address",
        "adminUser": "Admin user",
        "adminUserHelper": "Effective when no keyfile or env var is set. Otherwise see Settings → Account.",
        "adminPassword": "Admin password",
        "adminPasswordHelper": "Send blank to preserve. For ongoing rotations use Settings → Account (keyfile-backed, no restart).",
        "tokenTtlWeb": "Token TTL (web)",
        "tokenTtlHelper": "Go duration string, e.g. 24h, 30m.",
        "level": "Level",
        "format": "Format",
        "filePath": "File path",
        "filePathHelper": "Empty = stdout only.",
        "idleThreshold": "Idle threshold",
        "idleThresholdHelper": "Quiet period before a session is flagged idle. Go duration.",
        "idleCheckInterval": "Idle check interval",
        "idleCheckHelper": "How often the idle reaper runs.",
        "root": "Root",
        "rootHelper": "Parent of notes / skills / git_root sub-paths.",
        "notesPath": "Notes path",
        "skillsPath": "Skills path",
        "gitRoot": "Git root",
        "personalPrefix": "Personal prefix",
        "projectsPrefix": "Projects prefix",
        "registryRoot": "Registry root",
        "secretsFile": "Secrets file",
        "backend": "Backend",
        "store": "Store",
        "defaultTopK": "Default top-k",
        "similarityThreshold": "Similarity threshold",
        "defaultScope": "Default scope",
        "preserveHelper": "Blank to preserve current.",
        "localModelName": "Local model name",
        "localLibraryPath": "Local library path",
        "localModelPath": "Local model path",
        "localTokenizerPath": "Local tokenizer path",
        "localMaxSeqLen": "Local max seq len",
        "backupEnabled": "Enabled",
        "backupEnabledHelper": "Even with this on, the backup subsystem stays off until OPENDRAY_BACKUP_KEY or the keyfile is configured.",
        "backupLocalDir": "Local dir",
        "backupExportDir": "Export dir",
        "pathHelper": "Empty = resolve from PATH at startup.",
        "accountsDir": "Accounts dir",
        "accountsHelper": "Parent of per-account .claude/ subdirs. Empty = ~/.claude-accounts.",
        "sessionsRoot": "Sessions root",
        "sessionsRootHelper": "Empty = ~/.codex/sessions.",
        "listenHelper": "host:port the gateway binds to. Restart required.",
        "secretsHelper": "AES-256-GCM encrypted secrets vault.",
        "backendHelper": "auto picks the best available; local needs ONNX.",
        "similarityHelper": "0.0–1.0; results under this are filtered out.",
        "defaultFallback": "Default: {value}",
        "httpBaseUrl": "HTTP base URL",
        "httpModel": "HTTP model",
        "httpApiKey": "HTTP api key",
        "httpDimensions": "HTTP dimensions",
        "pgDumpPath": "pg_dump path",
        "pgRestorePath": "pg_restore path",
        "tmpRoot": "tmp root",
        "projectsJson": "projects.json"
      },
      "validateInteger": "\"{field}\" must be an integer",
      "validateNumber": "\"{field}\" must be a number"
    }
  }
}
