{
  "app": {
    "name": "Tinyforge",
    "version": "v0.1"
  },
  "layout": {
    "serviceStatus": "Service status"
  },
  "health": {
    "connected": "connected",
    "disconnected": "disconnected",
    "rawError": "Technical details",
    "retryNow": "Retry now"
  },
  "nav": {
    "dashboard": "Dashboard",
    "apps": "Apps",
    "eventTriggers": "Event Triggers",
    "logScanRules": "Log Rules",
    "metricAlertRules": "Metric Alerts",
    "sharedSecrets": "Shared Secrets",
    "triggers": "Triggers",
    "proxies": "Proxies",
    "events": "Events",
    "settings": "Settings",
    "logout": "Log out",
    "dns": "DNS Records",
    "containers": "Containers",
    "sectionObserve": "Observe",
    "sectionSystem": "System",
    "closeSidebar": "Close sidebar",
    "openSidebar": "Open sidebar",
    "quickNavTitle": "Press 'g' then a letter to jump between sections",
    "quickNavLabel": "quick-nav"
  },
  "dashboard": {
    "title": "Dashboard",
    "newApp": "New app",
    "totalWorkloads": "Total Workloads",
    "runningContainers": "Running Containers",
    "failedContainers": "Failed Containers",
    "recentWorkloads": "Recent Workloads",
    "retry": "Retry",
    "noWorkloads": "No workloads yet.",
    "noWorkloadsDesc": "Create an app and forge your first workload to get started.",
    "loadFailed": "Failed to load dashboard",
    "staleContainers": "Stale Containers",
    "unusedImagesWarning": "Unused Docker images are taking up disk space",
    "unusedImages": "unused images",
    "systemHealth": "System health",
    "daemons": "Daemons",
    "systemResources": "System resources",
    "systemResourcesSubtitle": "CPU, memory, disk, and top consumers",
    "statSubWorkloads": "workloads →",
    "statSubRunning": "running",
    "statSubNeedAttention": "need attention",
    "statSubStale": "stale →"
  },
  "resources": {
    "cpuCores": "CPU Cores",
    "memory": "Memory",
    "running": "Running",
    "dockerDisk": "Docker Disk",
    "workloadUtilization": "Workload utilization",
    "windowMinutes": "{n} minutes",
    "windowHours": "{n} hours",
    "noSamples": "No samples yet — the collector samples every {interval}s.",
    "collectionDisabled": "Stats collection is disabled. Enable it in Settings to populate this chart.",
    "diskImages": "Images",
    "diskContainers": "Containers",
    "diskVolumes": "Volumes",
    "diskBuildCache": "Build cache",
    "reclaimable": "{size} reclaimable",
    "topConsumers": "Top consumers",
    "byCpu": "by cpu",
    "byMemory": "by memory",
    "noRunning": "No running containers.",
    "instance": "instance",
    "site": "site",
    "showHistory": "Show history",
    "hideHistory": "Hide history",
    "cpuSeries": "CPU %",
    "memorySeries": "Memory %",
    "loading": "Loading…",
    "sectionTitle": "Resources",
    "showLogs": "Show logs",
    "hideLogs": "Hide logs",
    "dockerUnavailable": "Docker is unavailable. Check that the daemon is running."
  },
  "statsSettings": {
    "intervalLabel": "Stats collection interval (s)",
    "intervalHelp": "How often resource samples are collected. 0 disables collection. Range: 5–300s.",
    "retentionLabel": "Stats retention (hours)",
    "retentionHelp": "How long resource samples are kept. 0 disables collection. Range: 0–24h."
  },
  "tagPicker": {
    "registry": "Registry",
    "local": "Local"
  },
  "settings": {
    "title": "Settings",
    "general": "General",
    "integrations": "Integrations",
    "dns": "DNS",
    "maintenance": "Maintenance",
    "registries": "Registries",
    "credentials": "Credentials",
    "authentication": "Authentication",
    "backup": "Backups",
    "appearance": "Appearance",
    "groupMain": "Overview",
    "groupProxy": "Routing",
    "groupSystem": "System",
    "groupSecurity": "Security",
    "staleThreshold": "Stale threshold (days)",
    "staleThresholdHelp": "Containers inactive for longer than this will be flagged as stale.",
    "dockerCleanup": "Docker Image Cleanup",
    "dockerCleanupHelp": "Remove unused Docker images belonging to your projects. Only images not used by active instances are removed.",
    "pruneThreshold": "Warning Threshold (MB)",
    "pruneThresholdHelp": "Show dashboard warning when unused project images exceed this size. 0 = disabled.",
    "pruneImages": "Prune Unused Images",
    "pruning": "Pruning...",
    "pruneResult": "Removed {count} images, reclaimed {mb} MB",
    "pruneConfirmMessage": "This will remove unused Docker images belonging to your projects. Images used by active instances will not be affected.",
    "pruneFailed": "Failed to prune images",
    "buildCacheCleanupHelp": "Reclaim disk used by Docker build cache from image and static-site builds. Only unused cache is removed — apps redeploying still hit any cache that is still in use.",
    "pruneBuildCache": "Prune Build Cache",
    "pruningCache": "Pruning...",
    "pruneCacheResult": "Removed {count} build-cache records, reclaimed {mb} MB",
    "pruneBuildCacheConfirmMessage": "This removes unused Docker build-cache layers daemon-wide. Layers still in use are kept, so apps that redeploy soon keep their warm cache; others rebuild from scratch on their next deploy.",
    "pruneCacheFailed": "Failed to prune build cache",
    "proxyProvider": "Proxy Provider",
    "proxyProviderHelp": "Select how reverse proxy routes are managed for deployed containers.",
    "proxyNone": "None",
    "proxyNoneDesc": "No proxy — containers are accessed directly by port",
    "proxyNpm": "Nginx Proxy Manager",
    "proxyNpmDesc": "Routes managed via NPM API (configure credentials below)",
    "npm": "Nginx Proxy Manager",
    "traefik": "Traefik",
    "traefikLabelsTitle": "Docker Labels Reference",
    "traefikLabelsDesc": "These labels are automatically added to deployed containers. Shown here for reference.",
    "proxyTraefik": "Traefik",
    "proxyTraefikDesc": "Auto-discovery via Docker labels — no API calls needed",
    "proxyNoneWarning": "Switching to None does not remove existing proxy routes. You may need to clean them up manually.",
    "traefikEntrypoint": "Entrypoint",
    "traefikEntrypointHelp": "Traefik entrypoint name for HTTPS routes",
    "traefikCertResolver": "Cert Resolver",
    "traefikCertResolverHelp": "TLS certificate resolver name (e.g., letsencrypt)",
    "traefikNetwork": "Docker Network",
    "traefikNetworkHelp": "Network Traefik listens on (leave empty to use global network)",
    "traefikApiUrl": "Traefik API URL",
    "traefikApiUrlHelp": "Optional — for health checks (e.g., http://traefik:8080)"
  },
  "settingsGeneral": {
    "title": "General Settings",
    "globalConfig": "Global Configuration",
    "globalConfigDesc": "Core infrastructure: the base domain, network, and polling cadence Tinyforge uses to orchestrate containers.",
    "configureNpm": "Nginx Proxy Manager is selected.",
    "configureTraefik": "Traefik is selected.",
    "configureLink": "Configure provider",
    "domain": "Domain",
    "domainHelp": "Base domain for subdomain routing (e.g., example.com → stage-dev-app.example.com)",
    "serverIp": "Server IP (Docker Host)",
    "serverIpHelp": "IP of the machine running Docker. Used for NPM remote forwarding.",
    "publicIp": "Public IP (DNS Target)",
    "publicIpHelp": "IP for DNS A records — typically your proxy/load balancer. Falls back to Server IP if empty.",
    "dockerNetwork": "Docker Network",
    "dockerNetworkHelp": "Docker network that containers and proxy share. Must match your NPM/Traefik network.",
    "subdomainPattern": "Subdomain Pattern",
    "subdomainPatternHelp": "Pattern for auto-generated subdomains",
    "subdomainVarsTitle": "Available variables",
    "varProject": "Project name",
    "varStage": "Stage name",
    "varTag": "Image tag",
    "varPort": "Container port",
    "pollingInterval": "Polling Interval (seconds)",
    "pollingIntervalHelp": "How often to check registries for new tags (60-86400)",
    "notificationUrl": "Notification URL",
    "notificationUrlHelp": "Webhook URL for deploy notifications",
    "saveSettings": "Save Settings",
    "saving": "Saving...",
    "saved": "Settings saved successfully",
    "saveFailed": "Failed to save settings",
    "loadFailed": "Failed to load settings",
    "webhookUrl": "Webhook URL",
    "webhookDesc": "This secret URL receives image push notifications from your CI pipeline.",
    "noWebhookUrl": "No webhook URL configured",
    "copy": "Copy",
    "copied": "Webhook URL copied to clipboard",
    "regenerateUrl": "Regenerate URL",
    "regenerating": "Regenerating...",
    "regenerated": "Webhook URL regenerated",
    "regenerateFailed": "Failed to regenerate webhook URL",
    "regenerateWarning": "Warning: regenerating will invalidate the current URL. Update your CI pipelines.",
    "sslCertificate": "SSL Certificate",
    "sslCertificateHelp": "Wildcard certificate from NPM for auto-SSL on proxy hosts",
    "selectCertificate": "Select Certificate",
    "noCertificate": "None (no SSL)",
    "clearCertificate": "Clear",
    "loadingCertificates": "Loading certificates...",
    "noCertificatesFound": "No wildcard certificates found in NPM",
    "dnsConfig": "DNS Configuration",
    "wildcardDns": "Wildcard DNS is configured",
    "wildcardDnsHelp": "When enabled, all subdomains resolve to your server via a wildcard DNS rule. Disable to manage DNS records per service.",
    "dnsProvider": "DNS Provider",
    "dnsProviderHelp": "Select a DNS provider for automatic record management",
    "cloudflareApiToken": "Cloudflare API Token",
    "cloudflareApiTokenHelp": "API token with DNS edit permissions for your zone",
    "cloudflareApiTokenPlaceholder": "Enter Cloudflare API token",
    "cloudflareApiTokenConfigured": "API token is configured",
    "cloudflareZone": "Cloudflare Zone",
    "cloudflareZoneHelp": "Select the DNS zone to manage records in",
    "selectZone": "Select Zone",
    "noZone": "No zone selected",
    "loadingZones": "Loading zones...",
    "noZonesFound": "No zones found for this token",
    "testConnection": "Test Connection",
    "testingConnection": "Testing...",
    "connectionSuccess": "Connection successful",
    "connectionFailed": "Connection failed",
    "baseVolumePath": "Base Volume Path",
    "baseVolumePathHelp": "Prepended to relative volume sources (e.g., /data + my-app/uploads = /data/my-app/uploads)"
  },
  "settingsRegistries": {
    "title": "Container Registries",
    "description": "Manage your container registries for image detection.",
    "addRegistry": "Add Registry",
    "editRegistry": "Edit Registry",
    "addNewRegistry": "Add New Registry",
    "name": "Name",
    "nameHelp": "A friendly name for this registry",
    "url": "URL",
    "urlHelp": "Registry base URL",
    "type": "Type",
    "typeHelp": "Registry type for API compatibility",
    "token": "Token",
    "tokenHelpNew": "API token for authentication",
    "tokenHelpEdit": "Leave empty to keep the existing token",
    "owner": "Owner",
    "ownerHelp": "Package owners, comma-separated (e.g., alexei,my-org)",
    "save": "Save",
    "saving": "Saving...",
    "update": "Update",
    "test": "Test",
    "testing": "Testing...",
    "edit": "Edit",
    "delete": "Delete",
    "noRegistries": "No registries configured yet.",
    "addFirst": "Add your first registry",
    "registryUpdated": "Registry updated",
    "registryAdded": "Registry added",
    "registryDeleted": "Registry \"{name}\" deleted",
    "testSuccess": "Connection to \"{name}\" successful",
    "saveFailed": "Failed to save registry",
    "deleteFailed": "Failed to delete registry",
    "testFailed": "Connection test failed",
    "loadFailed": "Failed to load registries",
    "deleteTitle": "Delete registry?",
    "deleteConfirm": "Delete registry \"{name}\"? This cannot be undone.",
    "healthChecking": "Checking...",
    "healthConnected": "Connected",
    "healthUnreachable": "Unreachable"
  },
  "settingsNpm": {
    "testConnection": "Test Connection",
    "testing": "Testing...",
    "testSuccess": "NPM connection successful",
    "testFailed": "NPM connection failed",
    "saveFailedConnection": "Cannot save — connection test failed",
    "remoteMode": "Remote NPM",
    "remoteModeHelp": "Enable when NPM runs on a different machine than Docker. Forwards to Server IP with published host ports.",
    "remoteModeWarning": "Requires Server IP in General settings. Ports are auto-mapped to random host ports.",
    "accessList": "Default Access List",
    "accessListHelp": "NPM access list for HTTP authentication on proxy hosts. Can be overridden per project.",
    "noAccessList": "Global default",
    "selectAccessList": "Select an access list",
    "noAccessLists": "No access lists found in NPM",
    "accessListLoadFailed": "Failed to load access lists"
  },
  "settingsCredentials": {
    "title": "Credentials",
    "description": "Manage credentials for Nginx Proxy Manager and registry tokens. All values are encrypted at rest.",
    "npm": "Nginx Proxy Manager",
    "npmDesc": "Credentials for managing proxy hosts via NPM API",
    "configured": "Configured",
    "npmUrl": "NPM URL",
    "npmUrlHelp": "Nginx Proxy Manager API URL",
    "email": "Email",
    "emailHelp": "NPM admin email",
    "password": "Password",
    "passwordHelpNew": "NPM admin password (will be encrypted)",
    "passwordHelpEdit": "Enter the new password to replace the existing one",
    "changeCredentials": "Change Credentials",
    "save": "Save",
    "saving": "Saving...",
    "saved": "NPM credentials saved",
    "saveFailed": "Failed to save NPM credentials",
    "loadFailed": "Failed to load credentials",
    "registryTokens": "Registry Tokens",
    "registryTokensDesc": "Registry authentication tokens are managed per-registry in the",
    "registriesLink": "Registries",
    "registryTokensSuffix": "section. Each registry stores its token encrypted in the database.",
    "notSet": "Not set"
  },
  "settingsBackup": {
    "title": "Backup Management",
    "description": "Manage database backups and configure automatic backup schedules.",
    "autoBackup": "Automatic Backups",
    "autoBackupHelp": "Automatically create backups at the configured interval.",
    "interval": "Backup Interval",
    "intervalHelp": "How often to create automatic backups.",
    "intervalHours": "{hours} hours",
    "retention": "Retention Count",
    "retentionHelp": "Maximum number of backups to keep. Oldest are deleted first.",
    "backupNow": "Backup Now",
    "creatingBackup": "Creating...",
    "backupCreated": "Backup created successfully",
    "backupFailed": "Failed to create backup",
    "backupList": "Backups",
    "noBackups": "No backups yet. Create one manually or enable automatic backups.",
    "columnFilename": "Filename",
    "columnSize": "Size",
    "columnType": "Type",
    "columnDate": "Created",
    "columnActions": "Actions",
    "download": "Download",
    "delete": "Delete",
    "restore": "Restore",
    "deleteConfirm": "Are you sure you want to delete this backup?",
    "deleted": "Backup deleted",
    "deleteFailed": "Failed to delete backup",
    "restoreConfirm": "Are you sure you want to restore from this backup? This will replace the current database and restart the server. All current data will be lost.",
    "restoreWarning": "This action cannot be undone!",
    "restored": "Database restored. The server is restarting...",
    "restoreFailed": "Failed to restore backup",
    "typeManual": "Manual",
    "typeAuto": "Auto",
    "typePreDeploy": "Pre-deploy",
    "preDeploy": "Backup before every deploy",
    "preDeployHelp": "Take a Tinyforge DB snapshot at the start of every project deploy. Independent of the periodic schedule above; restorable from this list under the \"Pre-deploy\" type.",
    "save": "Save",
    "saving": "Saving...",
    "saved": "Backup settings saved",
    "saveFailed": "Failed to save backup settings"
  },
  "settingsAuth": {
    "title": "Authentication Settings",
    "description": "Configure authentication mode and manage users.",
    "authMode": "Authentication Mode",
    "local": "Local (username/password)",
    "oidc": "OIDC (SSO)",
    "oidcConfig": "OIDC Provider Configuration",
    "issuerUrl": "Issuer URL",
    "clientId": "Client ID",
    "clientSecret": "Client Secret",
    "redirectUrl": "Redirect URL",
    "saveSettings": "Save Settings",
    "saving": "Saving...",
    "saved": "Settings saved",
    "saveFailed": "Failed to save",
    "loadFailed": "Failed to load settings",
    "localUsers": "Local Users",
    "username": "Username",
    "email": "Email",
    "role": "Role",
    "created": "Created",
    "noUsers": "No users found.",
    "addUser": "Add User",
    "viewer": "Viewer",
    "admin": "Admin",
    "userCreated": "User created",
    "userDeleted": "User deleted",
    "createFailed": "Failed to create user",
    "deleteFailed": "Failed to delete user",
    "deleteConfirm": "Are you sure you want to delete this user?",
    "deleteConfirmMessage": "Delete user \"{username}\"? This cannot be undone.",
    "usernameRequired": "Username and password are required",
    "networkError": "Network error",
    "password": "Password"
  },
  "login": {
    "title": "Tinyforge",
    "subtitle": "Sign in to your account",
    "username": "Username",
    "password": "Password",
    "signIn": "Sign in",
    "signingIn": "Signing in...",
    "or": "or",
    "ssoButton": "Sign in with SSO (OIDC)",
    "loginFailed": "Login failed",
    "networkError": "Network error"
  },
  "proxies": {
    "title": "Proxy Routes",
    "description": "Active proxy routes from deployed containers and static sites.",
    "domain": "Domain",
    "project": "Project / Site",
    "stage": "Stage / Mode",
    "tag": "Tag",
    "port": "Port",
    "status": "Status",
    "source": "Source",
    "sourceContainer": "Container",
    "sourceStatic": "Static Site",
    "sourceDeno": "Deno Site",
    "filterAll": "All",
    "filterContainers": "Containers",
    "filterSites": "Sites",
    "noRoutes": "No proxy routes",
    "noRoutesDesc": "Proxy routes are created automatically when you deploy a container with proxy enabled or publish a static site.",
    "searchPlaceholder": "Search by domain, project, or tag...",
    "noMatch": "No routes match your search.",
    "loadFailed": "Failed to load proxy routes",
    "route": "route",
    "routes": "routes",
    "actions": "Actions",
    "viewTriggers": "Triggers",
    "viewTriggersTitle": "View trigger bindings for this workload"
  },
  "common": {
    "cancel": "Cancel",
    "confirm": "Confirm",
    "close": "Close",
    "toggle": "Toggle",
    "dismissNotification": "Dismiss notification",
    "delete": "Delete",
    "edit": "Edit",
    "change": "Change",
    "save": "Save",
    "retry": "Retry",
    "loading": "Loading...",
    "noData": "No data",
    "project": "Project",
    "stack": "Stack",
    "site": "Site",
    "back": "Back",
    "actions": "Actions",
    "stop": "Stop",
    "start": "Start",
    "restart": "Restart",
    "remove": "Remove",
    "instance": "instance",
    "instances": "instances",
    "next": "Next",
    "yes": "Yes",
    "no": "No",
    "saving": "Saving...",
    "refresh": "Refresh",
    "all": "All",
    "running": "Running",
    "stopped": "Stopped",
    "missing": "Missing"
  },
  "containers": {
    "eyebrowSuffix": "GLOBAL",
    "errLoad": "Failed to load containers",
    "searchPlaceholder": "Search workload, role, image, subdomain…",
    "kindFilterLabel": "Workload kind",
    "stateFilterLabel": "Container state",
    "emptyTitle": "No containers",
    "emptyDesc": "Deploy a project, stack, or site to see containers here.",
    "noMatch": "No containers match the current filters.",
    "showingN": "Showing {visible} of {total} containers",
    "col": {
      "workload": "Workload",
      "kind": "Kind",
      "role": "Role",
      "image": "Image",
      "state": "State",
      "subdomain": "Subdomain",
      "lastSeen": "Last seen"
    }
  },
  "empty": {
    "noRegistries": "No registries",
    "noRegistriesDesc": "Add a container registry to enable image detection.",
    "noUsers": "No users",
    "noUsersDesc": "Add local users to manage access."
  },
  "validation": {
    "required": "{field} is required",
    "invalidUrl": "Invalid URL format",
    "invalidDomain": "Invalid domain format",
    "invalidIp": "Invalid IP format",
    "invalidEmail": "Invalid email format",
    "invalidPort": "Port must be between 1 and 65535",
    "invalidPollingInterval": "Polling interval must be between 60 and 86400 seconds",
    "invalidProjectName": "Only lowercase letters, numbers, and hyphens allowed",
    "requiredWhenUpdating": "{field} is required when updating credentials",
    "requiredForNew": "{field} is required for new registries"
  },
  "theme": {
    "light": "Light",
    "dark": "Dark",
    "system": "System"
  },
  "entityPicker": {
    "search": "Search...",
    "noResults": "No results found"
  },
  "stale": {
    "title": "Stale Containers",
    "eyebrowSuffix": "STALE",
    "noStale": "No stale containers",
    "noStaleDesc": "All containers are healthy and running.",
    "cleanup": "Clean up",
    "cleanupAll": "Clean up all",
    "confirmCleanup": "This will stop and remove the container. Continue?",
    "confirmBulkCleanup": "This will stop and remove all stale containers. Continue?",
    "daysStale": "days stale",
    "lastAlive": "Last alive",
    "count": "Stale",
    "cleanedUp": "Container cleaned up",
    "bulkCleanedUp": "{count} containers cleaned up",
    "cleanupFailed": "Cleanup failed",
    "loadFailed": "Failed to load stale containers"
  },
  "logs": {
    "title": "Container Logs",
    "lines": "lines",
    "follow": "Follow",
    "following": "Following...",
    "loading": "Loading logs...",
    "noLogs": "No log output"
  },
  "events": {
    "title": "Event Log",
    "noEvents": "No events found",
    "noEventsDesc": "Events will appear here as they occur.",
    "loadMore": "Load more",
    "newEvents": "new events",
    "totalCount": "{count} total",
    "clearAll": "Clear All",
    "clearAllTitle": "Clear Event Log",
    "clearAllMessage": "This will permanently delete all event log entries. This cannot be undone.",
    "cleared": "Cleared {count} events",
    "clearFailed": "Failed to clear events",
    "filter": {
      "severity": "Severity",
      "source": "Source",
      "dateRange": "Date range",
      "search": "Search events...",
      "lastHour": "Last hour",
      "last24h": "Last 24 hours",
      "last7d": "Last 7 days",
      "allTime": "All time",
      "clear": "Clear filters"
    },
    "severity": {
      "info": "Info",
      "warn": "Warning",
      "error": "Error"
    },
    "source": {
      "deploy": "Deploy",
      "image": "Image",
      "compose": "Compose",
      "dockerfile": "Dockerfile",
      "static_site": "Static Site",
      "stale_scanner": "Stale Scanner",
      "stale_cleanup": "Stale Cleanup",
      "metric_alert": "Metric Alert",
      "admin": "Admin"
    },
    "metadata": "Details"
  },
  "stats": {
    "cpu": "CPU",
    "mem": "MEM",
    "unavailable": "Stats unavailable"
  },
  "systemHealth": {
    "containers": "Containers",
    "proxies": "Proxies",
    "recentErrors": "Recent Errors"
  },
  "daemons": {
    "title": "Daemons",
    "notReachable": "{provider} is not reachable.",
    "refresh": "Refresh",
    "refreshing": "Refreshing",
    "docker": "Docker Engine",
    "npm": "Nginx Proxy Manager",
    "traefik": "Traefik",
    "proxy": "Proxy",
    "online": "Online",
    "offline": "Offline",
    "notConfigured": "Not configured",
    "containers": "Containers",
    "running": "Running",
    "paused": "Paused",
    "stopped": "Stopped",
    "version": "Version",
    "apiVersion": "API Version",
    "platform": "Platform",
    "kernel": "Kernel",
    "cpu": "CPU",
    "memory": "Memory",
    "storage": "Storage Driver",
    "images": "Images",
    "latency": "Latency",
    "rootDir": "Root Dir",
    "provider": "Provider",
    "endpoint": "Endpoint",
    "proxyHosts": "Proxy Hosts",
    "managed": "managed",
    "external": "external",
    "accessLists": "Access Lists",
    "certificates": "Certificates",
    "dockerHint": "Check that the Docker daemon is running and that the socket is reachable.",
    "proxyHint": "Verify the proxy URL, credentials, and that the service is listening.",
    "noProxyDesc": "No proxy provider is configured. Tinyforge can manage routes via Nginx Proxy Manager or Traefik.",
    "configureProxy": "Configure in Settings",
    "dockerNotReachable": "Docker daemon is not reachable.",
    "dockerUnreachable": "Docker unreachable",
    "proxyUnreachable": "Proxy unreachable",
    "reachable": "reachable"
  },
  "dns": {
    "title": "DNS Records",
    "description": "View and manage DNS records created by Tinyforge.",
    "wildcardActive": "Wildcard DNS Mode Active",
    "wildcardActiveDesc": "DNS records are managed externally via wildcard DNS. Disable wildcard DNS in Settings to manage records individually.",
    "refresh": "Refresh",
    "syncNow": "Sync Now",
    "syncing": "Syncing...",
    "syncComplete": "Sync complete: {created} created, {deleted} deleted, {synced} already synced",
    "syncFailed": "DNS sync failed",
    "searchPlaceholder": "Search by FQDN...",
    "allConsumers": "All consumers",
    "managed": "Managed (instances)",
    "standalone": "Standalone proxies",
    "orphaned": "Orphaned",
    "allStatuses": "All statuses",
    "statusSynced": "Synced",
    "statusMissing": "Missing",
    "statusOrphaned": "Orphaned",
    "columnFqdn": "FQDN",
    "columnType": "Type",
    "columnValue": "Value",
    "columnConsumer": "Consumer",
    "columnStatus": "Status",
    "columnActions": "Actions",
    "noConsumer": "No consumer",
    "noRecords": "No DNS records found. Records will appear here when services are deployed.",
    "noMatchingRecords": "No records match the current filters.",
    "deleteRecord": "Delete record",
    "recordDeleted": "DNS record {fqdn} deleted",
    "deleteFailed": "Failed to delete DNS record",
    "loadFailed": "Failed to load DNS records",
    "totalRecords": "Total: {count}",
    "syncedCount": "Synced: {count}",
    "missingCount": "Missing: {count}",
    "orphanedCount": "Orphaned: {count}"
  },
  "language": {
    "en": "English",
    "ru": "Russian"
  },
  "timezone": {
    "eyebrow": "The Forge // Chronograph",
    "title": "Display timezone",
    "subtitle": "All dates across Tinyforge — event log, deploys, backups, sites — render in this zone.",
    "modeLabel": "Detection mode",
    "modeAuto": "Auto-detect",
    "modeManual": "Manual",
    "autoDetect": "Auto-detect from browser",
    "autoBadge": "Auto",
    "activeZone": "Active zone",
    "changeZone": "Change timezone",
    "clickToChange": "Click to pick a zone →",
    "pickerTitle": "Select timezone",
    "pickerPlaceholder": "Search zones — city, region, UTC offset…",
    "groupAuto": "Detection",
    "groupPopular": "Popular",
    "groupAll": "All timezones",
    "previewFull": "Full timestamp",
    "previewDate": "Date only",
    "previewHint": "Timestamps like the event log will look exactly like this."
  },
  "settingsDns": {
    "title": "DNS Configuration",
    "description": "Choose whether routes rely on a wildcard record or per-subdomain records managed by a DNS provider."
  },
  "settingsIntegrations": {
    "title": "Integrations",
    "outgoing": "Outgoing notifications",
    "outgoingDesc": "Where Tinyforge posts deploy and alert events. Paste a webhook URL (Apprise, Discord, Slack, your own handler).",
    "incoming": "Incoming webhooks",
    "incomingMovedDesc": "Inbound webhooks are now scoped per entity. Open a project or static site to view and rotate its webhook URL."
  },
  "webhookLog": {
    "title": "Recent webhook deliveries",
    "description": "The last 14 days of inbound webhook hits — outcome, signature state, and reason. Refreshes every 30 seconds.",
    "refresh": "Refresh",
    "loadFailed": "Failed to load webhook deliveries",
    "empty": "No webhook deliveries yet.",
    "colTime": "When",
    "colStatus": "Status",
    "colOutcome": "Outcome",
    "colSignature": "Signature",
    "colDetail": "Detail",
    "colSource": "Source",
    "outcome": {
      "deploy": "Deployed",
      "skip": "Skipped",
      "rejected": "Rejected",
      "not_found": "Not found",
      "bad_request": "Bad request",
      "error": "Error"
    },
    "sig": {
      "valid": "valid",
      "invalid": "invalid",
      "missing": "missing",
      "unconfigured": "off"
    }
  },
  "webhookPanel": {
    "copy": "Copy",
    "copied": "Webhook URL copied to clipboard",
    "copyFailed": "Failed to copy to clipboard",
    "noUrl": "No webhook URL configured",
    "loadFailed": "Failed to load webhook URL",
    "regenerate": "Regenerate URL",
    "regenerated": "Webhook URL regenerated",
    "regenerateFailed": "Failed to regenerate webhook URL",
    "regenerateWarning": "Regenerating invalidates the current URL. Update any CI pipeline or Git webhook that uses it.",
    "confirmRegenerate": "Replace the current URL?",
    "confirmYes": "Regenerate",
    "confirmNo": "Cancel",
    "signingTitle": "Inbound HMAC signing",
    "signingDesc": "Verify webhook payloads with an HMAC-SHA256 signature so a leaked URL alone cannot be used to forge requests. Compatible with Gitea/GitHub webhook secrets.",
    "signingActive": "Signing secret configured.",
    "signingInactive": "No signing secret — inbound requests are not authenticated beyond the URL.",
    "signingIssue": "Issue signing secret",
    "signingRotate": "Rotate signing secret",
    "signingDisable": "Disable signing",
    "signingDisableConfirm": "Disable signing",
    "signingIssued": "New signing secret issued — copy it before leaving this page",
    "signingIssueFailed": "Failed to issue signing secret",
    "signingDisabled": "Signing disabled",
    "signingDisableFailed": "Failed to disable signing",
    "signingShownOnce": "Copy this secret now — it will not be shown again.",
    "signingDismiss": "Dismiss",
    "signingHint": "Set this as the webhook secret in Gitea/GitHub/GitLab. Tinyforge expects {header} on every request.",
    "signingCopied": "Signing secret copied to clipboard",
    "requireSignature": "Require signature",
    "requireSignatureHelp": "Reject any request that lacks a valid signature. Issue a signing secret first.",
    "signingRequireFailed": "Failed to update signature requirement"
  },
  "outgoingWebhook": {
    "signingOn": "Signed",
    "signingOff": "Unsigned",
    "signingSecret": "HMAC signing secret",
    "noSecret": "No signing secret — outgoing events are not signed.",
    "reveal": "Reveal",
    "generate": "Generate",
    "copy": "Copy",
    "copied": "Signing secret copied to clipboard",
    "copyFailed": "Failed to copy to clipboard",
    "loadFailed": "Failed to load signing secret",
    "regenerate": "Regenerate",
    "regenerated": "Signing secret regenerated",
    "regenerateFailed": "Failed to regenerate signing secret",
    "confirmRegenerateTitle": "Rotate signing secret?",
    "confirmRegenerate": "The current secret is invalidated immediately. Every receiver verifying it must be updated in lock-step or it will start rejecting events.",
    "confirmDisableTitle": "Disable HMAC signing?",
    "confirmDisable": "Future events go out without the X-Hub-Signature-256 header. Receivers that require signatures will reject them.",
    "confirmYes": "Confirm",
    "confirmNo": "Cancel",
    "disable": "Disable signing",
    "disabled": "Signing disabled",
    "disableFailed": "Failed to disable signing",
    "sendTestTitle": "Send a test event",
    "sendTestHelp": "Fires a synthetic \"test\" event to the resolved URL using the current secret.",
    "sendTest": "Send test",
    "sending": "Sending…",
    "testFailed": "Failed to send test event",
    "tier": "Tier",
    "signed": "Signed",
    "unsigned": "Unsigned",
    "deliveryId": "Delivery",
    "responseBody": "Response body",
    "networkError": "Network error",
    "fallbackTo": "No URL set on this tier — events will fall through to {label}.",
    "noUrlConfigured": "No URL set. Configure one above before sending a test."
  },
  "settingsMaintenance": {
    "title": "Maintenance",
    "thresholds": "Thresholds",
    "thresholdsDesc": "Tune when Tinyforge flags stale containers and warns about unused image disk usage.",
    "dangerZone": "Danger zone"
  },
  "observability": {
    "section": "Observability",
    "manage": "manage",
    "loading": "Loading…",
    "anyEvent": "any event",
    "noUrlSet": "No URL configured",
    "configured": "CONFIGURED",
    "clear": "Clear",
    "advanced": "Advanced",
    "cancel": "Cancel",
    "save": "Save changes",
    "saving": "Saving…",
    "delete": "Delete",
    "deleting": "Deleting…",
    "refresh": "Refresh",
    "open": "Open",
    "edit": "Edit",
    "back": "Back",
    "regex": {
      "sampleLabel": "Sample line",
      "placeholder": "paste a representative log line here",
      "promptType": "type a sample to test the pattern",
      "noMatch": "NO MATCH",
      "noMatchHint": "pattern did not match this line",
      "match": "MATCH",
      "invalid": "REGEX",
      "captures": "Captures"
    }
  },
  "triggers": {
    "title": "Event triggers",
    "titleNew": "Forge a new trigger",
    "titleSingular": "Trigger",
    "lede": "Filter event-log entries (deploy events, log scanner output, future sources) and dispatch a webhook when they match. Filters AND together; empty filters mean \"match anything.\"",
    "ledeNew": "Create a filter+action rule. The dispatcher AND-composes all filter fields. Leave a field empty to skip that dimension.",
    "stat": {
      "total": "TOTAL",
      "enabled": "ENABLED",
      "disabled": "DISABLED"
    },
    "toolbar": {
      "newButton": "New trigger",
      "backToList": "Back to triggers"
    },
    "empty": {
      "heading": "No triggers yet",
      "body": "Configure a trigger to forward event-log entries to Slack, a notification bridge, or any HTTP receiver. Tinyforge signs requests with X-Hub-Signature-256 when a secret is set.",
      "cta": "Create the first trigger"
    },
    "list": {
      "name": "Name",
      "filters": "Filters",
      "action": "Action",
      "status": "Status",
      "open": "Open"
    },
    "detail": {
      "config": "Configuration",
      "configSub": "id #{id} · updated {updatedAt}",
      "dangerZone": "Danger zone",
      "dangerZoneSub": "Trigger deletion is immediate. No soft-delete.",
      "sendTest": "Send test",
      "sending": "Sending…",
      "testHttp": "HTTP {code}",
      "testSigned": "signed",
      "testOk": "OK",
      "testFail": "FAIL",
      "deleteButton": "Delete trigger",
      "deleteTitle": "Delete trigger?",
      "deleteMessage": "Trigger \"{name}\" will be removed immediately. This cannot be undone."
    },
    "form": {
      "name": "Name",
      "namePlaceholder": "e.g. Slack #alerts on deploy failure",
      "required": "REQUIRED",
      "andComposed": "AND-COMPOSED",
      "filtersLabel": "Filters",
      "actionLabel": "Action",
      "actionWebhookBadge": "WEBHOOK",
      "severityCsv": "Severity (CSV)",
      "severityPlaceholder": "warn,error",
      "sourceCsv": "Source (CSV)",
      "sourcePlaceholder": "deploy,logscan",
      "messageRegex": "Message regex (optional)",
      "messageRegexPlaceholder": "(?i)\\bpanic\\b",
      "invalidRegex": "Invalid regex — server will reject.",
      "urlLabel": "URL",
      "urlPlaceholder": "https://hooks.slack.com/services/...",
      "secretLabel": "HMAC secret (optional)",
      "secretPlaceholder": "leave blank for unsigned delivery",
      "secretHint": "Receivers verify X-Hub-Signature-256 against the raw body.",
      "secretRotateHint": "Stored encrypted at rest. The value is never returned by the API after creation — leave the placeholder untouched to preserve the existing secret, type a new value to rotate, or clear and save to remove signing.",
      "enabled": "Enabled",
      "enabledHint": "Disabled triggers stay in the table but never dispatch.",
      "submit": "Forge trigger",
      "submitting": "Forging…",
      "webhookUrl": "Webhook URL"
    },
    "status": {
      "enabled": "enabled",
      "disabled": "disabled"
    }
  },
  "metricalert": {
    "title": "Metric alert rules",
    "titleNew": "Forge a new alert",
    "titleSingular": "Alert rule",
    "lede": "Threshold checks the watcher runs against each running container's sampled CPU and memory. When a sample crosses the threshold the rule fires into event_log with the rule's severity, where event triggers pick it up and fan out to operator-configured webhooks. {enabled} of {total} enabled.",
    "ledeNew": "Pick a metric, a comparator, and a threshold. Leave the workload field empty to create a global rule that applies to every workload, or scope it to a single workload.",
    "stat": {
      "total": "TOTAL",
      "global": "GLOBAL",
      "workload": "WORKLOAD",
      "enabled": "ENABLED"
    },
    "toolbar": {
      "newButton": "New alert",
      "backToList": "Back to alerts"
    },
    "filter": {
      "all": "ALL",
      "global": "GLOBAL",
      "workload": "WORKLOAD"
    },
    "empty": {
      "heading": "No alert rules yet",
      "body": "Start with a global rule like CPU greater than 80%, then narrow per-workload by scoping a rule to a single workload.",
      "cta": "Create the first alert"
    },
    "list": {
      "name": "Name",
      "condition": "Condition",
      "scope": "Scope",
      "severity": "Severity",
      "status": "Status",
      "open": "Open"
    },
    "detail": {
      "config": "Configuration",
      "configSub": "id #{id} · scope {scope}",
      "dangerZone": "Danger zone",
      "dangerZoneSub": "Deleting an alert rule removes it immediately and stops it from firing.",
      "deleteButton": "Delete alert",
      "deleteTitle": "Delete alert rule?",
      "deleteMessage": "Rule \"{name}\" will be removed immediately and will stop firing."
    },
    "form": {
      "name": "Name",
      "namePlaceholder": "e.g. Worker CPU saturated",
      "condition": "Condition",
      "metric": "Metric",
      "comparator": "Comparator",
      "threshold": "Threshold",
      "thresholdPlaceholder": "e.g. 80",
      "thresholdHintPercent": "Percent of the limit (0–100). The rule fires when the sampled value crosses this threshold.",
      "thresholdHintBytes": "Absolute bytes (e.g. 536870912 for 512 MiB). The rule fires when sampled memory crosses this threshold.",
      "matchShape": "Match shape",
      "matchShapeOpts": "SEVERITY · COOLDOWN",
      "severity": "Severity",
      "cooldown": "Cooldown (s)",
      "cooldownHint": "Cooldown is per-rule per-container — the same rule firing on two containers stays independent. It caps how often a sustained breach re-emits to event_log.",
      "scope": "Scope",
      "scopeHint": "Workload-scoped rules apply only to that workload's containers. Leave empty to apply the rule to every workload.",
      "scopeGlobal": "Global (applies to every workload)",
      "scopePick": "Pick workload…",
      "scopePickTitle": "Pick a workload",
      "scopeClear": "Make global",
      "scopeSelected": "Workload",
      "scopeUnknown": "Unknown workload",
      "enabled": "Enabled",
      "enabledHint": "Disabled rules stay in the table but never fire.",
      "required": "REQUIRED",
      "optional": "OPTIONAL",
      "submit": "Forge alert",
      "submitting": "Forging…"
    },
    "metric": {
      "cpu_percent": "CPU %",
      "memory_percent": "Memory %",
      "memory_bytes": "Memory (bytes)"
    },
    "metricShort": {
      "cpu": "CPU",
      "memory": "Memory"
    },
    "comparator": {
      "gt": "greater than",
      "lt": "less than"
    },
    "unit": {
      "percent": "%",
      "bytes": "bytes"
    },
    "scope": {
      "global": "global",
      "workload": "workload {id}"
    },
    "status": {
      "enabled": "enabled",
      "disabled": "disabled"
    }
  },
  "sharedsecrets": {
    "eyebrow": "System",
    "title": "Shared secrets",
    "titleNew": "Forge a new secret",
    "titleSingular": "Shared secret",
    "lede": "Named environment keys the reconciler injects into matching workloads. Scope a secret globally to reach every app, or to a single app to keep it contained. Values are write-only — they are stored once and never returned. {enabled} of {total} enabled.",
    "ledeNew": "Give the secret an env key and a value. Scope it globally to apply to every app, or to a single app to keep it contained. The value is write-only — once saved it is never shown again.",
    "stat": {
      "total": "TOTAL",
      "global": "GLOBAL",
      "app": "APP",
      "enabled": "ENABLED"
    },
    "toolbar": {
      "newButton": "New secret",
      "backToList": "Back to secrets"
    },
    "filter": {
      "all": "ALL",
      "global": "GLOBAL",
      "app": "APP"
    },
    "empty": {
      "heading": "No shared secrets yet",
      "body": "Start with a global secret like DATABASE_URL, then narrow per-app by scoping a secret to a single app.",
      "cta": "Create the first secret"
    },
    "list": {
      "name": "Name",
      "scope": "Scope",
      "value": "Value",
      "encrypted": "Encrypted",
      "status": "Status",
      "open": "Open",
      "valueSet": "set",
      "valueNone": "—",
      "encOn": "encrypted",
      "encOff": "plain"
    },
    "detail": {
      "config": "Configuration",
      "configSub": "scope {scope}",
      "dangerZone": "Danger zone",
      "dangerZoneSub": "Deleting a shared secret removes it immediately and stops it from being injected.",
      "deleteButton": "Delete secret",
      "deleteTitle": "Delete shared secret?",
      "deleteMessage": "Secret \"{name}\" will be removed immediately and will stop being injected."
    },
    "form": {
      "name": "Name",
      "namePlaceholder": "e.g. DATABASE_URL",
      "nameHint": "The environment variable key injected into matching workloads.",
      "value": "Value",
      "valuePlaceholder": "Enter the secret value",
      "valuePlaceholderEdit": "Leave blank to keep the stored value",
      "valueHintNew": "The secret value. Stored write-only — once saved it is never shown again.",
      "valueHintEditSet": "A value is set. Leave blank to keep it, or enter a new value to rotate it.",
      "valueHintEditUnset": "No value is stored yet. Enter one to set it.",
      "encrypted": "Encrypted",
      "encryptedHint": "Encrypted secrets are stored sealed at rest and decrypted only when injected. Disable only for non-sensitive config values.",
      "encryptedFlipWarning": "Changing the encrypted setting requires re-entering the value. Enter a value above to save.",
      "scope": "Scope",
      "scopeGlobalOption": "Global — every app",
      "scopeAppOption": "App — a single app",
      "scopeAppEmpty": "No app selected",
      "scopePick": "Pick app…",
      "scopePickTitle": "Pick an app",
      "scopeClear": "Clear app",
      "scopeSelected": "App",
      "scopeUnknown": "Unknown app",
      "scopeHint": "Global secrets apply to every app. App-scoped secrets apply only to that app's workloads.",
      "description": "Description",
      "descriptionPlaceholder": "Optional note about what this secret is for",
      "enabled": "Enabled",
      "enabledHint": "Disabled secrets stay in the table but are never injected.",
      "required": "REQUIRED",
      "optional": "OPTIONAL",
      "submit": "Forge secret",
      "submitting": "Forging…",
      "conflict": "A shared secret with this scope and name already exists."
    },
    "scope": {
      "global": "Global",
      "app": "App · {name}"
    },
    "status": {
      "enabled": "enabled",
      "disabled": "disabled"
    }
  },
  "logscan": {
    "title": "Log scan rules",
    "titleNew": "Forge a new rule",
    "titleSingular": "Rule",
    "lede": "Regex patterns the scanner runs against every running container's log stream. Matched lines land in event_log with the rule's severity, where event triggers pick them up and fan out to operator-configured webhooks. {enabled} of {total} enabled.",
    "ledeNew": "Tail container logs against a regex. Leave the workload field empty to create a global rule. To override an existing global for one workload, use the per-workload override action on the workload detail page.",
    "stat": {
      "total": "TOTAL",
      "global": "GLOBAL",
      "workload": "WORKLOAD",
      "overrides": "OVERRIDES",
      "activeTails": "ACTIVE TAILS",
      "droppedBucket": "RATE-LIMITED",
      "droppedCooldown": "COOLED DOWN",
      "compileErrors": "COMPILE ERRORS"
    },
    "stats": {
      "heading": "Scanner stats",
      "headingSub": "Engine drop counters and last-snapshot compile errors. Counters reset on server restart.",
      "noCompileErrors": "All rules compile cleanly.",
      "compileErrorsHeading": "Compile errors (rule dropped from snapshot)",
      "tailsExplain": "Per-container tail goroutines currently driven by the scanner manager."
    },
    "toolbar": {
      "newButton": "New rule",
      "backToList": "Back to rules"
    },
    "filter": {
      "all": "ALL",
      "global": "GLOBAL",
      "workload": "WORKLOAD",
      "overrides": "OVERRIDES"
    },
    "empty": {
      "heading": "No rules yet",
      "body": "Start with a global rule like (?i)\\bpanic\\b at severity error, then narrow per-workload via overrides on the workload detail page.",
      "cta": "Create the first rule"
    },
    "list": {
      "name": "Name",
      "pattern": "Pattern",
      "scope": "Scope",
      "severity": "Severity",
      "streams": "Streams",
      "status": "Status",
      "open": "Open"
    },
    "detail": {
      "config": "Configuration",
      "configSub": "id #{id} · scope {scope}",
      "regexTest": "Regex test",
      "regexTestSub": "Live preview uses the browser's JavaScript regex engine. Click \"Run server test\" to evaluate against Go's RE2 — authoritative and the only reliable signal for RE2-only constructs.",
      "runServerTest": "Run server test",
      "testing": "Testing…",
      "serverTestHint": "Enter a sample line above first",
      "serverTestSendHint": "Send sample to backend /test endpoint",
      "serverMatch": "SERVER MATCH",
      "serverNoMatch": "NO MATCH",
      "serverNoMatchHint": "server regex did not match the sample",
      "serverError": "ERROR",
      "dangerZone": "Danger zone",
      "dangerZoneSub": "Deleting a global rule cascade-removes its per-workload overrides.",
      "deleteButton": "Delete rule",
      "deleteTitle": "Delete rule?",
      "deleteMessage": "Rule \"{name}\" will be removed immediately. Per-workload overrides referencing it will be cascade-deleted."
    },
    "form": {
      "name": "Name",
      "namePlaceholder": "e.g. Panic in worker",
      "pattern": "Pattern",
      "regex": "REGEX",
      "patternPlaceholder": "(?i)\\bpanic\\b",
      "invalidRegex": "Invalid regex — server will reject.",
      "matchShape": "Match shape",
      "matchShapeOpts": "SEVERITY · STREAMS · COOLDOWN",
      "severity": "Severity",
      "streams": "Streams",
      "cooldown": "Cooldown (s)",
      "cooldownHint": "Cooldown is per-rule per-container — the same rule firing on two containers stays independent. Token bucket caps per-container emissions at 10 events / 60s to prevent flooding event_log.",
      "scope": "Scope",
      "scopePlaceholder": "empty for global rule, or paste a workload id",
      "scopeHint": "Workload-scoped rules apply only to that workload's containers. Per-workload overrides are easier to create from the workload detail page.",
      "scopeGlobal": "Global (applies to every workload)",
      "scopePick": "Pick workload…",
      "scopePickTitle": "Pick a workload",
      "scopeClear": "Make global",
      "scopeSelected": "Workload",
      "scopeUnknown": "Unknown workload",
      "enabled": "Enabled",
      "enabledHint": "Disabled rules stay in the table but never fire.",
      "required": "REQUIRED",
      "optional": "OPTIONAL",
      "submit": "Forge rule",
      "submitting": "Forging…"
    },
    "scope": {
      "global": "global",
      "workload": "workload {id}",
      "override": "override of #{id}",
      "overrideShort": "override #{id}"
    },
    "status": {
      "enabled": "enabled",
      "disabled": "disabled",
      "on": "on",
      "off": "off"
    },
    "panel": {
      "heading": "Log rules",
      "subEmpty": "No effective rules for this workload",
      "subCount": "{count} effective rules",
      "subCountOne": "1 effective rule",
      "emptyHint": "This workload has no log scan rules applied. Create one via New rule — globals apply automatically; this workload can also have its own narrower rules or overrides.",
      "newRule": "New rule",
      "footerHint": "Global rules apply to every workload. Workload rules apply only here. Override rows substitute for a global on this workload — edit them to disable or change severity per-workload without touching the global.",
      "override": "Override",
      "overriding": "Overriding…",
      "overrideTitle": "Create a per-workload override of this global rule"
    }
  },
  "redeployTriggers": {
    "section": "The Forge",
    "title": "Redeploy triggers",
    "titleNew": "Forge a new trigger",
    "titleSingular": "Trigger",
    "lede": "Sources of redeploy signals — registry pushes, git events, manual fires, schedules, webhooks, log matches. Each trigger lives once and fans out to every workload bound to it.",
    "ledeNew": "Pick a kind, name it, and decide whether external systems may poke it via webhook. Bind it to one or more workloads from the workload page after creation.",
    "ledeDetail": "Edit the trigger config, manage its webhook ingress, and review every workload listening to this signal.",
    "stat": {
      "total": "TOTAL",
      "byKind": "{kind}",
      "withWebhook": "WEBHOOK ON",
      "boundWorkloads": "WORKLOADS"
    },
    "kind": {
      "registry": "Registry",
      "git": "Git",
      "manual": "Manual",
      "schedule": "Schedule",
      "webhook": "Webhook",
      "logscan": "Log scan",
      "unknown": "Unknown"
    },
    "kindShort": {
      "registry": "REG",
      "git": "GIT",
      "manual": "MAN",
      "schedule": "CRN",
      "webhook": "HK",
      "logscan": "LOG",
      "unknown": "?"
    },
    "kindHint": {
      "registry": "Watch a container image; fire when a new tag matching the pattern is pushed.",
      "git": "Fire when a configured branch advances or a tag matching the pattern is created.",
      "manual": "Fires only via the workload's Deploy button or POST /workloads/{id}/deploy.",
      "schedule": "Fires on a fixed cron-style schedule.",
      "webhook": "Pure webhook — fires when the ingress URL is hit.",
      "logscan": "Fires when an upstream log-scan rule matches a tailed line.",
      "unknown": "Unknown trigger kind — fall back to the raw JSON editor."
    },
    "toolbar": {
      "newButton": "New trigger",
      "backToList": "Back to triggers"
    },
    "filter": {
      "all": "ALL",
      "ariaLabel": "Filter by kind"
    },
    "empty": {
      "heading": "No triggers yet",
      "body": "A trigger is the source of a redeploy signal — a registry watcher, git hook, manual button, scheduled fire, or webhook. Create one and bind it to as many workloads as you like.",
      "cta": "Forge the first trigger"
    },
    "list": {
      "name": "Name",
      "kind": "Kind",
      "bindings": "Workloads",
      "webhook": "Webhook",
      "created": "Created",
      "open": "Open",
      "webhookOn": "ON",
      "webhookOff": "—",
      "noBindings": "—",
      "bindingsCount": "{count}"
    },
    "detail": {
      "config": "Trigger configuration",
      "configSub": "kind {kind} · id {id} · updated {updatedAt}",
      "webhook": "Webhook ingress",
      "webhookSub": "When enabled, external systems can fire this trigger by posting to the URL below. Each workload bound to it will be redeployed in turn.",
      "webhookEnable": "Enable webhook ingress",
      "webhookEnableHint": "When off, the trigger fires only via internal sources (its kind config) and the manual deploy button.",
      "webhookRequireSig": "Require HMAC signature",
      "webhookRequireSigHint": "Reject requests without a valid X-Hub-Signature-256 header. Recommended whenever the URL is reachable from the public internet.",
      "webhookUrlLabel": "Ingress URL",
      "webhookUrlNote": "Paste this into your CI / registry / GitHub webhook settings. The secret segment is the bearer — treat it like a password.",
      "webhookCopy": "Copy",
      "webhookCopied": "Copied",
      "webhookRotate": "Rotate secret",
      "webhookRotating": "Rotating…",
      "webhookDisabledNote": "Webhook ingress is disabled. Toggle it on, save, and the URL will appear here.",
      "bindings": "Bound workloads",
      "bindingsSub": "Every workload listening to this trigger. To bind a new workload, open the workload page and add this trigger from there.",
      "bindingsEmpty": "No workloads are bound to this trigger yet. Open a workload and bind this trigger from its Triggers panel.",
      "bindingsListItem": {
        "openWorkload": "Open workload",
        "unbind": "Unbind"
      },
      "bindingEnabledHint": "Disable to keep the binding but stop this trigger from redeploying that workload.",
      "dangerZone": "Danger zone",
      "dangerZoneSub": "Trigger deletion is immediate. All bindings to it are cascade-removed.",
      "deleteButton": "Delete trigger",
      "deleteTitle": "Delete trigger?",
      "deleteMessage": "Trigger \"{name}\" will be removed immediately, along with {count} binding(s). This cannot be undone.",
      "rotateTitle": "Rotate webhook secret?",
      "rotateMessage": "The current ingress URL stops working immediately. Update every external integration with the new URL after rotation.",
      "rotateConfirm": "Rotate now",
      "unbindTitle": "Unbind workload?",
      "unbindMessage": "Workload \"{name}\" will stop redeploying when this trigger fires. The workload itself is not deleted.",
      "unbindConfirm": "Unbind",
      "lastFired": "Last fired",
      "lastFiredNever": "Never fired",
      "scheduleStatus": "Schedule status",
      "scheduleStatusSub": "Operational state of the internal scheduler for this trigger. Fire-now skips ahead of the next natural window and resets the cadence to start counting from now.",
      "fireNow": "Fire now",
      "fireNowTitle": "Dispatch this trigger immediately and reset the next-fire window.",
      "fireNowDisabledTitle": "Bind at least one workload before firing.",
      "firing": "Firing…",
      "fireConfirmTitle": "Fire schedule trigger?",
      "fireConfirmMessage": "Trigger \"{name}\" will fire immediately and fan out to its {count} bound workload(s). The next natural fire window will be one full interval from now.",
      "fireConfirm": "Fire now",
      "fireResult": "Fired · deployed {deployed}/{bindings} · errored {errored}"
    },
    "form": {
      "kindLabel": "Kind",
      "kindHint": "Pick the source of the redeploy signal. The form below adapts to the kind.",
      "name": "Name",
      "namePlaceholder": "e.g. ghcr.io/me/api · main",
      "required": "REQUIRED",
      "configLabel": "Configuration",
      "image": "Image reference",
      "imagePlaceholder": "registry.example.com/owner/app",
      "imageHint": "Full image reference without the tag — Tinyforge matches new tags pushed under it.",
      "browseImages": "Browse",
      "browseImagesHint": "Pick an image from a configured registry instead of typing the reference.",
      "browseImagesTitle": "Select an image",
      "browseImagesSearch": "Search images…",
      "tagPattern": "Tag pattern",
      "tagPatternPlaceholder": "*",
      "tagPatternHint": "path.Match glob (e.g. v*, release-*). Use * to match every tag.",
      "repo": "Repository",
      "repoPlaceholder": "owner/name",
      "repoHint": "Provider-agnostic owner/name slug as advertised by the git host.",
      "mode": "Mode",
      "modePush": "Push to branch",
      "modeTag": "Tag created",
      "branch": "Branch",
      "branchPlaceholder": "main",
      "branchHint": "Only push events advancing this branch fire the trigger.",
      "branchPattern": "Branch pattern (preview deploys)",
      "branchPatternPlaceholder": "feat/* or * for any branch",
      "branchPatternHint": "When set, any push to a matching branch spawns a per-branch preview deploy. Leave empty to disable previews.",
      "manualNote": "Manual triggers carry no config. They fire only via the workload's Deploy button or POST /workloads/{id}/deploy.",
      "scheduleNote": "Fires on a fixed interval driven by Tinyforge's internal scheduler. No external webhook is required — enable the webhook ingress below only if a CI also needs to fire it on demand.",
      "intervalPresets": "Quick presets",
      "intervalPreset": {
        "hourly": "Hourly",
        "daily": "Daily",
        "weekly": "Weekly"
      },
      "interval": "Interval",
      "intervalHint": "Go duration (e.g. \"30m\", \"6h\", \"24h\", \"168h\"). Minimum 1 minute.",
      "scheduleReference": "Pinned reference (optional)",
      "scheduleReferencePlaceholder": "stable",
      "scheduleReferenceHint": "Optional tag, branch, or revision the source plugin should re-pull each fire. Leave empty to let the source use its default.",
      "unknownNote": "This kind has no built-in form yet. Use the JSON editor below; the server validates the shape.",
      "advancedToggle": "Advanced JSON",
      "advancedHint": "Power-user fallback — replaces the structured form with the raw config payload.",
      "configJson": "Config JSON",
      "configJsonHint": "Must parse as a valid JSON object. The shape is validated server-side per kind.",
      "invalidJson": "Invalid JSON — server will reject.",
      "webhookEnabled": "Enable webhook ingress now",
      "webhookEnabledHint": "Generates a secret URL that external systems can hit to fire the trigger.",
      "webhookRequireSig": "Require HMAC signature",
      "webhookRequireSigHint": "Reject unsigned requests. The secret is the same one embedded in the URL — sign the body with HMAC-SHA256 and send it as X-Hub-Signature-256.",
      "submit": "Forge trigger",
      "submitting": "Forging…",
      "cancel": "Cancel"
    },
    "binding": {
      "enabled": "Enabled",
      "disabled": "Disabled"
    }
  },
  "apps": {
    "list": {
      "eyebrowSuffix": "APPS",
      "title": "Apps",
      "ledePrefix": "Plugin-native deployables —",
      "ledeKindImage": "image",
      "ledeKindCompose": "compose",
      "ledeKindStatic": "static",
      "ledeMiddle": ", or",
      "ledeSuffix": ", with pluggable redeploy triggers. Legacy projects, stacks, and sites continue to live under their own sections during the cutover.",
      "statTotal": "TOTAL",
      "statImage": "IMAGE",
      "statCompose": "COMPOSE",
      "statStatic": "STATIC",
      "refresh": "Refresh",
      "newApp": "New App",
      "filterAriaLabel": "Filter by source plugin",
      "filterAll": "ALL",
      "loadError": "Failed to load apps",
      "alertTag": "ERR",
      "emptyTitle": "No apps yet",
      "emptyBody": "Apps unify image, compose, and static deployables behind a single plugin-driven surface. Forge your first one to see it light up here.",
      "emptyCta": "Forge the first app",
      "colName": "Name",
      "colSource": "Source",
      "colTrigger": "Trigger",
      "colCreated": "Created",
      "colActions": "Actions",
      "rowOpen": "Open",
      "gitopsBadge": "GitOps",
      "gitopsBadgeTitle": "Deploy config for this app is managed from its repo."
    },
    "new": {
      "pageTitle": "New App · Tinyforge",
      "wizard": {
        "stepBasics": "Basics",
        "stepConfigure": "Configure",
        "stepTrigger": "Trigger",
        "stepReview": "Review",
        "next": "Next",
        "back": "Back"
      },
      "backLabel": "Back to apps",
      "eyebrowSuffix": "NEW APP",
      "title": "Forge a new app",
      "ledePrefix": "Create a plugin-native workload.",
      "ledeSourceLabel": "Source",
      "ledeSourceMid": "= how it deploys (image, compose, static). Pick or create a",
      "ledeTriggerLabel": "trigger",
      "ledeSuffix": "below — when one fires, the source plugin redeploys.",
      "loadingKinds": "Loading available plugin kinds…",
      "alertTag": "ERR",
      "fieldName": "Name",
      "fieldNameRequired": "REQUIRED",
      "fieldRequired": "Required",
      "fieldNamePlaceholder": "my-app",
      "fieldNameHint": "Lowercase, no spaces. Becomes part of container names and subdomains.",
      "fieldSourcePlugin": "Source plugin",
      "fieldSourceLabel": "Source",
      "fieldSourceHint": "Populated from the running daemon — only plugins compiled in show up. Triggers (registry / git / manual) are configured below as standalone records.",
      "fieldSourceConfig": "Source config",
      "fieldConfigYaml": "YAML",
      "fieldConfigForm": "FORM",
      "fieldConfigJson": "JSON",
      "advancedJson": "Edit as JSON",
      "backToForm": "Back to form",
      "resetSample": "Reset sample",
      "switchToJsonTitle": "Switch to the raw JSON editor",
      "switchToFormTitle": "Switch back to the form",
      "jsonOk": "JSON OK",
      "jsonInvalid": "JSON INVALID",
      "linesUnit": "lines",
      "composeHeader": "compose.yaml · compose",
      "composeAriaLabel": "Compose YAML",
      "composeProjectLabel": "Compose project name (optional)",
      "composeProjectPlaceholder": "(defaults to sanitized workload name)",
      "composePlaceholder": "services:\n  web:\n    image: nginx:alpine\n    ports:\n      - \"80\"",
      "imageHeader": "image source · runtime knobs",
      "imageRefLabel": "Image (registry path)",
      "imageRefPlaceholder": "registry.example.com/owner/app",
      "imageRefHint": "Fully-qualified reference; the tag is set per-deploy via the trigger or the Default tag field below.",
      "imagePort": "Port",
      "imageHealthcheck": "Healthcheck path",
      "imageDefaultTag": "Default tag",
      "imageRegistryLabel": "Registry (for private pulls)",
      "imageRegistryPublic": "(public — no auth)",
      "imageRegistryHint": "Match the name from the Registries settings page. Leave empty for public images.",
      "imageCpu": "CPU limit",
      "imageCpuHint": "Cores, 0 = ∞",
      "imageMemory": "Memory limit",
      "imageMemoryHint": "MB, 0 = ∞",
      "imageMax": "Max instances",
      "imageMaxHint": "1 = strict blue-green.",
      "imageFoot": "Env vars and volume mounts live in their own panels on the workload detail page after creation.",
      "dockerfileHeader": "dockerfile source · build from a git repo",
      "dockerfileBuildEyebrow": "build · dockerfile",
      "dockerfileContextPath": "Build context",
      "dockerfileContextPathPlaceholder": "(empty = repo root)",
      "dockerfilePath": "Dockerfile path",
      "dockerfilePort": "Container port",
      "dockerfilePortRequired": "Container port is required — pick the port your app listens on (1–65535).",
      "dockerfileFoot": "Tinyforge clones the repo, builds the image from the Dockerfile, and runs one container. Env vars and volumes live in the detail page after creation.",
      "staticHeader": "static source · pages from a repo",
      "staticProvider": "Provider",
      "staticBaseUrl": "Base URL",
      "staticBaseUrlPlaceholder": "https://git.example.com",
      "staticRepoOwner": "Repo owner",
      "staticRepoOwnerPlaceholder": "owner",
      "staticRepoName": "Repo name",
      "staticRepoNamePlaceholder": "pages",
      "staticBranch": "Branch",
      "staticBranchPlaceholder": "main",
      "staticFolder": "Folder path (optional)",
      "staticFolderPlaceholder": "(repo root)",
      "staticToken": "Access token (private repos)",
      "staticTokenPlaceholder": "(leave blank for public repos)",
      "staticTokenHint": "Encrypted at rest. Required only when the repo is private.",
      "staticMode": "Mode",
      "staticModeStaticDesc": "— serve files via nginx; zero runtime overhead.",
      "staticModeDenoDesc": "— Deno runtime container with optional dynamic routing.",
      "staticRenderMarkdown": "Render markdown",
      "staticRenderMarkdownDesc": "— auto-render <code>.md</code> files as HTML pages.",
      "sourceReportCommitStatus": "Report commit status",
      "sourceReportCommitStatusDesc": "— report deploy status back to the Git provider as a commit status on the deployed commit.",
      "deployStrategy": {
        "label": "Deploy strategy",
        "recreateName": "Recreate",
        "recreateDesc": "Stop the old container, then start the new one. A brief window of downtime during the swap.",
        "blueGreenName": "Zero-downtime",
        "blueGreenDesc": "Start the new container, health-check it, then switch traffic and retire the old one. No downtime.",
        "defaultBadge": "default",
        "denoCaveat": "Deno sites with persistent storage fall back to recreate to avoid two writers on the same volume."
      },
      "staticFoot": "The webhook secret for git push triggers lives on the workload's Webhook panel after creation.",
      "staticDetectProvider": "Detect",
      "staticDetectedOk": "Detected: {provider}",
      "staticDetectedFailed": "Detection failed: {error}",
      "staticTestConnection": "Test connection",
      "staticConnectionOk": "Connected",
      "staticConnectionFailed": "Connection failed: {error}",
      "staticBrowseRepos": "Browse",
      "staticBrowseBranches": "Browse branches",
      "staticBrowseFolders": "Browse folders",
      "staticPickerRepoTitle": "Select repository",
      "staticPickerRepoPlaceholder": "Filter repositories…",
      "staticPickerBranchTitle": "Select branch",
      "staticPickerBranchPlaceholder": "Filter branches…",
      "staticFolderRoot": "/ (root)",
      "staticFolderSelectedPrefix": "Selected folder:",
      "staticTreeLoading": "Loading folder tree…",
      "staticTreeEmpty": "No folders found in this branch.",
      "staticDenoAutoDetected": "Auto-detected an <code>api/</code> folder — switched to Deno mode.",
      "imageConflictTag": "IMAGE IN USE",
      "imageConflictChecking": "Checking for conflicts…",
      "imageConflictHeading": "{count} workload(s) already use this image:",
      "imageConflictOpenBtn": "Open",
      "imageConflictAcknowledgeNote": "If this is intentional (for example a separate stage), continue to create a new workload.",
      "imageConflictBlockedSubmit": "Conflicts detected for this image — review the list above, then click Create again to proceed.",
      "sourceConfigJsonTitle": "source_config.json · {kind}",
      "sourceConfigJsonAria": "Source plugin configuration (JSON)",
      "triggerNumLabel": "Trigger",
      "triggerNumOptional": "OPTIONAL",
      "triggerNewTag": "NEW",
      "triggerPickTag": "PICK",
      "triggerSkipTag": "SKIP",
      "noteSkipTag": "SKIP",
      "noteEmptyTag": "∅",
      "faceLabel": "Public face",
      "faceOptional": "OPTIONAL",
      "faceSubdomain": "Subdomain",
      "faceSubdomainPlaceholder": "myapp",
      "faceDomain": "Domain",
      "faceDomainPlaceholder": "(inherit from settings)",
      "facePort": "Target port",
      "faceHint": "Leave blank to skip provisioning a proxy route. Filling any field creates a single face row attached to this workload.",
      "cancel": "Cancel",
      "submit": "Forge app",
      "submitting": "Forging…",
      "submitAnyway": "Forge anyway",
      "unsavedChanges": "You have unsaved changes to this app. Leave without creating it?",
      "unsavedChangesTitle": "Unsaved changes",
      "unsavedChangesConfirm": "Leave",
      "errors": {
        "detectionFailed": "Couldn't detect a Git provider at that URL. Check the base URL is correct and reachable.",
        "connectionFailed": "Couldn't reach the repository. Check the provider URL, owner/repo, and access token (for private repos).",
        "reposFailed": "Couldn't list repositories. Check the base URL and access token.",
        "branchesFailed": "Couldn't list branches. Check the repository and access token.",
        "treeFailed": "Couldn't load the folder tree. Check the repository, branch, and access token.",
        "sourceConfigInvalid": "Source config is not valid JSON.",
        "triggerBindUnknown": "unknown error",
        "createFailed": "Workload create failed.",
        "inspectFailed": "Couldn't inspect that image — make sure it's pulled locally and the reference is correct."
      },
      "imageInspect": "Inspect",
      "imageInspectHint": "Pulls port + healthcheck from the image so you don't have to type them.",
      "imageInspectOk": "Inspected — port + healthcheck filled.",
      "triggers": {
        "section": "Trigger",
        "sectionSub": "Optional. Pick how this app gets a redeploy signal — registry watcher, git event, or manual button.",
        "modeInline": "Add a trigger",
        "modeInlineHint": "Creates a brand-new trigger record bound to this app — fits the common 1:1 case.",
        "modePick": "Pick existing trigger",
        "modePickHint": "Bind an existing trigger so multiple apps share one signal.",
        "modeSkip": "Skip — add later",
        "modeSkipHint": "The app is created without any trigger binding. Manual deploys still work.",
        "switchToPick": "Pick existing trigger →",
        "switchToInline": "← Create a new trigger instead",
        "switchToSkip": "Skip for now",
        "pickPlaceholder": "Select a trigger…",
        "pickEmpty": "No triggers exist yet — create one inline above, or visit /triggers.",
        "pickLabel": "Existing trigger",
        "pickHint": "The same trigger can be bound to many apps. Manage standalone triggers under /triggers.",
        "pickWebhookOn": "WEBHOOK ON",
        "skippedNote": "No trigger will be bound. You can add one from the app's Triggers panel after it's created.",
        "bindError": "App created, but the trigger binding failed: {error}. Open the app's Triggers panel to retry."
      },
      "manifest": {
        "title": "Manifest",
        "name": "Name",
        "source": "Source",
        "trigger": "Trigger",
        "publicFace": "Public face",
        "unnamed": "(unnamed)",
        "registryPublic": "public registry",
        "folderRoot": "root",
        "triggerManual": "Manual only",
        "internalOnly": "Internal only"
      }
    },
    "detail": {
      "pageTitleFallback": "App",
      "backLabel": "Back to apps",
      "eyebrowSuffix": "APP",
      "kickerId": "id: {id}",
      "loading": "Loading workload…",
      "loadError": "Failed to load app",
      "deployError": "Deploy failed",
      "saveError": "Save failed",
      "deleteError": "Delete failed",
      "activity": {
        "title": "Activity",
        "subtitle": "Recent deploys and events for this app",
        "empty": "No activity yet. Deploys and events will appear here.",
        "recentNote": "Showing recent activity.",
        "loadMore": "Load more",
        "filterAll": "All",
        "filterErrors": "Errors",
        "noErrors": "No errors in the loaded activity."
      },
      "runtimeState": {
        "title": "Sync status",
        "sub": "Last successful sync of the source repo and the current container state.",
        "status": "Status",
        "lastCommit": "Last commit",
        "lastSync": "Last sync",
        "container": "Container",
        "neverDeployed": "Never deployed. Click Deploy to publish for the first time.",
        "loading": "Loading runtime state…"
      },
      "storage": {
        "title": "Persistent storage",
        "sub": "Mounted at /app/data inside the container.",
        "used": "Used",
        "limit": "Limit",
        "unlimited": "unlimited",
        "unavailable": "Usage probe unavailable (container may be stopped).",
        "loading": "Computing usage…"
      },
      "buildLog": {
        "title": "Build log",
        "sub": "Live tail of the Docker daemon's build output.",
        "clear": "Clear"
      },
      "notifications": {
        "title": "Notification routes",
        "sub": "Multi-destination fan-out for deploy/build events. Falls back to the workload's legacy URL when empty.",
        "loading": "Loading routes…",
        "empty": "No per-workload notification routes configured. Add one to get a per-channel destination.",
        "addFirst": "Add first route",
        "add": "Add route",
        "edit": "Edit route",
        "delete": "Delete route",
        "name": "Name",
        "namePlaceholder": "Slack #alerts",
        "url": "Webhook URL",
        "secret": "Signing secret",
        "secretPlaceholder": "Optional — receiver verifies HMAC if set",
        "secretEditPlaceholder": "Leave empty to keep the existing secret",
        "secretHint": "HMAC-SHA256 over the request body, sent as X-Hub-Signature-256.",
        "eventTypes": "Event types",
        "eventTypesPlaceholder": "deploy_failure,build_failure (empty = all)",
        "eventTypesHint": "Comma-separated allow-list. Empty means every event fires this route.",
        "enabled": "Enabled",
        "save": "Save route",
        "saving": "Saving…",
        "cancel": "Cancel",
        "allEvents": "all events",
        "signed": "signed",
        "disabled": "disabled",
        "confirmDeleteTitle": "Delete notification route?",
        "confirmDeleteMessage": "This route will stop firing immediately. The workload's legacy notification URL (if set) will resume catching events when no routes match."
      },
      "snapshots": {
        "title": "Data snapshots",
        "sub": "Capture this app's host-bind data volumes as a downloadable archive.",
        "loading": "Loading snapshots…",
        "empty": "No snapshots yet.",
        "coverage": "{count} volume(s) will be captured.",
        "noCoverage": "No host-bind volumes to snapshot. Snapshots cover image-based apps with absolute, stage, or project volume scopes.",
        "liveWarning": "Snapshots are taken live — stop the app first for a fully consistent copy of databases.",
        "skippedTitle": "{count} volume(s) can't be snapshotted yet:",
        "labelPlaceholder": "Optional label (e.g. before upgrade)",
        "create": "Snapshot now",
        "creating": "Capturing…",
        "created": "Snapshot created.",
        "createFailed": "Failed to create snapshot",
        "colLabel": "Label",
        "colCreated": "Created",
        "colVolumes": "Volumes",
        "colSize": "Size",
        "unlabeled": "(unlabeled)",
        "download": "Download",
        "delete": "Delete",
        "deleted": "Snapshot deleted.",
        "downloadFailed": "Failed to download snapshot",
        "deleteFailed": "Failed to delete snapshot",
        "confirmDeleteTitle": "Delete snapshot?",
        "confirmDeleteMessage": "This permanently deletes the snapshot archive. This cannot be undone."
      },
      "deployHistory": {
        "title": "Deploy history",
        "sub": "Every deploy of this app, newest first. Roll back to redeploy a previous version.",
        "loading": "Loading deploy history…",
        "empty": "No deploys recorded yet.",
        "colWhen": "When",
        "colReason": "Reason",
        "colReference": "Version",
        "colOutcome": "Outcome",
        "colBy": "By",
        "rollback": "Roll back",
        "rolledBack": "Rolling back to {ref}…",
        "rollbackFailed": "Rollback failed",
        "confirmTitle": "Roll back to this version?",
        "confirmMessage": "This redeploys {ref} as a new deploy. The current version is replaced once the rollback is healthy."
      },
      "metrics": {
        "title": "Resource usage",
        "sub": "CPU and memory for this app's containers over time.",
        "loading": "Loading metrics…",
        "empty": "No metrics yet. Collection may be disabled, or this app hasn't run long enough to sample.",
        "windowLabel": "Time window",
        "cpuSeries": "CPU",
        "memorySeries": "Memory"
      },
      "gitops": {
        "title": "GitOps",
        "sub": "Declare this app's deploy config in your repo and sync it on demand.",
        "loading": "Loading GitOps status…",
        "disabledLead": "GitOps is off for this app.",
        "disabledSub": "Enable it to manage the deploy config from",
        "badge": "GitOps",
        "badgeTitle": "Deploy config for this app is managed from its repo.",
        "toggleLabel": "Enabled",
        "toggleAria": "Enable GitOps for this app",
        "toggleHint": "Read the deploy config from a file in this app's repo.",
        "enabledToast": "GitOps enabled.",
        "disabledToast": "GitOps disabled.",
        "toggleFailed": "Could not update GitOps settings.",
        "pillSynced": "Synced",
        "pillChangesOne": "1 change",
        "pillChangesMany": "{count} changes",
        "pillNoFile": "No file",
        "pillFetchFailed": "Fetch failed",
        "pillInvalid": "Invalid",
        "pillDisabled": "Disabled",
        "metaPath": "Path",
        "metaCommit": "Commit",
        "metaLastSync": "Last sync",
        "metaNeverSynced": "Never synced",
        "inSyncTitle": "Live config matches the repo",
        "inSyncSub": "Every declared field is already applied. Nothing to sync.",
        "driftAria": "Fields that differ between the repo file and the live config",
        "driftColField": "Field",
        "driftColRepo": "Repo",
        "driftColLive": "Live",
        "driftFoot": "Syncing applies the repo values, replacing the live ones above.",
        "noFileLead": "No config file on the branch",
        "noFileSub": "GitOps is on, but nothing was found at",
        "fetchFailedLead": "Couldn't read the config from the repo",
        "invalidLead": "The repo config couldn't be parsed",
        "copy": "Copy",
        "copied": "Copied",
        "copyAria": "Copy the config file to the clipboard",
        "syncHintDrift": "Sync to apply the repo config to this app's live config.",
        "syncHintClean": "Live config already matches the repo.",
        "syncNow": "Sync now",
        "syncing": "Syncing…",
        "syncedToast": "Synced {count} field(s) from {sha}.",
        "syncFailed": "Sync failed.",
        "confirmTitle": "Apply the repo config?",
        "confirmMessage": "This applies the repo config to this app's live config. The current values for the declared fields are replaced.",
        "gateTitle": "Managed by .tinyforge.yml",
        "gateBody": "Edit the config in the repo and Sync — changes made here are overwritten on the next sync.",
        "gateFieldsLabel": "Managed fields",
        "field": {
          "port": "Port",
          "healthcheck": "Healthcheck",
          "deploy_strategy": "Deploy strategy"
        }
      },
      "toolbar": {
        "stop": "Stop",
        "start": "Start",
        "openSite": "Open",
        "more": "More"
      },
      "liveBadge": {
        "running": "RUNNING",
        "transitioning": "TRANSITIONING",
        "stopped": "STOPPED",
        "notDeployed": "NOT DEPLOYED",
        "mixed": "MIXED · {running}/{total} RUNNING"
      },
      "stats": {
        "title": "Resource usage",
        "sub": "CPU and memory of the running container.",
        "subMany": "CPU and memory of each of the {count} containers."
      },
      "webhooks": {
        "title": "Webhook bindings",
        "sub": "Triggers wired to this app — manage URLs and signing on the trigger detail page.",
        "openTrigger": "Open trigger",
        "disabled": "disabled",
        "empty": "No triggers bound to this app."
      },
      "errors": {
        "stopFailed": "Stop failed.",
        "stopNothing": "Nothing to stop — no running container.",
        "stopAllFailed": "Stop failed — all containers refused to stop.",
        "startFailed": "Start failed.",
        "startNothing": "Nothing to start — deploy first to create a container.",
        "startAllFailed": "Start failed — all containers refused to start.",
        "runtimeStateFailed": "Failed to load runtime state.",
        "storageFailed": "Failed to load storage usage."
      },
      "alertTag": "ERR",
      "createdAt": "created",
      "refreshLabel": "Refresh",
      "editButton": "Edit",
      "deleteButton": "Delete",
      "editTitle": "Edit configuration",
      "editSubPrefix": "Source",
      "editSubSuffix": "· triggers managed in the Triggers panel below",
      "editFieldName": "Name",
      "editFieldParent": "Parent workload",
      "editFieldOptional": "OPTIONAL",
      "editFieldParentPlaceholder": "workload UUID (blank for root)",
      "editSourceConfig": "Source config",
      "editConfigYaml": "YAML",
      "editConfigForm": "FORM",
      "editConfigJson": "JSON",
      "advancedJson": "Advanced JSON",
      "backToForm": "Back to form",
      "switchToJsonTitle": "Switch to the raw JSON editor",
      "switchToFormTitle": "Switch back to the form",
      "jsonOk": "JSON OK",
      "jsonInvalid": "JSON INVALID",
      "editComposeProject": "Compose project name (optional)",
      "editComposeProjectPlaceholder": "(defaults to sanitized workload name)",
      "editComposePlaceholder": "services:\n  web:\n    image: nginx:alpine",
      "editComposeAria": "Compose YAML",
      "editComposeHeader": "compose.yaml",
      "editImageHeader": "image source · runtime knobs",
      "editImageRef": "Image (registry path)",
      "editImageRefPlaceholder": "registry.example.com/owner/app",
      "editImagePort": "Port",
      "editImageHealthcheck": "Healthcheck path",
      "editImageDefaultTag": "Default tag",
      "editImageRegistry": "Registry (for private pulls)",
      "editImageRegistryPublic": "(public — no auth)",
      "editImageCpu": "CPU limit (cores, 0 = ∞)",
      "editImageMemory": "Memory limit (MB, 0 = ∞)",
      "editImageMax": "Max instances",
      "editImageFoot": "Env vars and volume mounts use their own panels below — saving here preserves them.",
      "editStaticHeader": "static source · pages from a repo",
      "editStaticProvider": "Provider",
      "editStaticBaseUrl": "Base URL",
      "editStaticBaseUrlPlaceholder": "https://git.example.com",
      "editStaticRepoOwner": "Repo owner",
      "editStaticRepoName": "Repo name",
      "editStaticBranch": "Branch",
      "editStaticFolder": "Folder path (optional)",
      "editStaticFolderPlaceholder": "(repo root)",
      "editStaticToken": "Access token (private repos)",
      "editStaticTokenPlaceholder": "(leave blank for public repos)",
      "editStaticMode": "Mode",
      "editStaticModeStaticDesc": "— serve files via nginx.",
      "editStaticModeDenoDesc": "— Deno runtime with dynamic routing.",
      "editStaticRenderMarkdown": "Render markdown",
      "editStaticRenderMarkdownDesc": "— auto-render <code>.md</code> as HTML.",
      "editDockerfileHeader": "dockerfile source · build from a git repo",
      "editDockerfileBuildEyebrow": "build · dockerfile",
      "editDockerfileContextPath": "Build context",
      "editDockerfileContextPathPlaceholder": "(empty = repo root)",
      "editDockerfilePath": "Dockerfile path",
      "editDockerfilePort": "Container port",
      "editTestConnection": "Test connection",
      "editTestConnectionOk": "Connection OK",
      "editTestConnectionFailed": "Connection failed: {error}",
      "editTestConnectionUnknownError": "Unknown error",
      "overrideKeyUnitSingular": "KEY",
      "overrideKeyUnitPlural": "KEYS",
      "editTestConnectionIncomplete": "Fill provider, base URL, owner, and name first.",
      "editSourceJsonHeader": "source_config.json",
      "editSourceJsonAria": "Source plugin configuration (JSON)",
      "editPublicFaces": "Public faces",
      "editPublicFacesTag": "JSON ARRAY",
      "editPublicFacesHeader": "public_faces.json",
      "editPublicFacesAria": "Public faces configuration (JSON array)",
      "editCancel": "Cancel",
      "editSave": "Save changes",
      "editSaving": "Saving…",
      "manualDeployTitle": "Manual deploy",
      "manualDeployOk": "OK",
      "manualDeployDispatched": "Dispatched {reference} as {by}",
      "manualDeployRefAria": "Deploy reference",
      "manualDeployRefPlaceholder": "reference (image tag, git sha, blank for default)",
      "manualDeployButton": "Deploy",
      "manualDeployDispatching": "Dispatching…",
      "manualDeployHint": "Use a specific image tag, git sha, or branch to force a deploy. Leave blank to use the default reference resolved by the source plugin.",
      "containersTitle": "Containers",
      "containersEmpty": "No containers yet",
      "containersCount": "{count} reconciled",
      "containersEmptyInline": "No containers yet — deploy to spin one up.",
      "containersColRole": "Role",
      "containersColState": "State",
      "containersColImage": "Image",
      "containersColSubdomain": "Subdomain",
      "containersColLastSeen": "Last seen",
      "containersColActions": "Actions",
      "containersLogsAction": "Logs",
      "chainTitle": "Chain",
      "chainSubFromParent": "promotes from a parent",
      "chainSubParentOf": "parent of",
      "chainChildSingular": "child",
      "chainChildPlural": "children",
      "chainParentLabel": "Parent",
      "chainSelfLabel": "This",
      "chainChildrenLabel": "Children",
      "chainPromoteButton": "Promote from parent",
      "chainPromoteToChild": "Promote to this",
      "chainPromoting": "Promoting…",
      "chainPromoteConfirmTitle": "Promote version?",
      "chainPromoteConfirmMessage": "Promote the running version of {source} to {target} and deploy it?",
      "chainPromoteConfirmYes": "Promote & deploy",
      "chainPromoteOk": "Promoted {tag} to {target}",
      "chainPromoteFailed": "Promotion failed",
      "chainHint": "Set <code>parent_workload_id</code> on a workload to build a chain. Image-source children can promote the parent's currently-running tag with one click.",
      "previews": {
        "title": "Preview environments",
        "subEmpty": "no active previews",
        "subCountOne": "1 active preview",
        "subCount": "{count} active previews",
        "tag": "Preview",
        "tagTitle": "Per-branch preview deploy of this workload",
        "armedEmpty": "No active previews — push to a branch matching",
        "noneEmpty": "No active previews yet.",
        "open": "Open",
        "noUrl": "no public URL",
        "teardown": "Tear down",
        "teardownTitle": "Tear down preview?",
        "teardownMessage": "This deletes the preview for branch \"{name}\" and removes its containers and proxy routes. Pushing to the branch again will recreate it.",
        "teardownConfirm": "Tear down",
        "teardownPending": "Tearing down…",
        "teardownFailed": "Teardown failed",
        "stateRunning": "Running",
        "statePending": "Pending",
        "stateStopped": "Stopped",
        "stateUnknown": "Unknown",
        "hint": "Previews are created automatically when a push lands on a branch matching a git trigger's <code>branch_pattern</code>, and torn down when the branch is deleted. Each gets its own slug-prefixed subdomain."
      },
      "volumesTitle": "Volumes",
      "volumesEmpty": "No mounts",
      "volumesCountSingular": "{count} mount",
      "volumesCountPlural": "{count} mounts",
      "volumesColTarget": "Target",
      "volumesColSource": "Source",
      "volumesColScope": "Scope",
      "volumesColUpdated": "Updated",
      "volumesColActions": "Actions",
      "volumeSource": "Source (host)",
      "volumeSourcePlaceholder": "/srv/data/myapp",
      "volumeTarget": "Target (container)",
      "volumeTargetPlaceholder": "/data",
      "volumeScope": "Scope",
      "volumeAddButton": "Add / Replace",
      "volumeSaving": "Saving…",
      "volumeHint": "Absolute mounts bind a host path into the container. Non-absolute scopes are accepted for future use; only absolute is honoured at deploy time today.",
      "volumeTargetError": "Target must be an absolute container path (e.g. /data)",
      "volumeSetFailed": "Failed to set volume",
      "volumeDeleteFailed": "Failed to delete volume",
      "envTitle": "Env",
      "envEmpty": "No overrides",
      "envCountSingular": "{count} override",
      "envCountPlural": "{count} overrides",
      "envColKey": "Key",
      "envColValue": "Value",
      "envColUpdated": "Updated",
      "envColActions": "Actions",
      "envEncrypted": "ENCRYPTED",
      "envKey": "Key",
      "envKeyPlaceholder": "DATABASE_URL",
      "envValue": "Value",
      "envValuePlaceholder": "(empty to unset)",
      "envEncryptLabel": "Encrypt at rest",
      "envAddButton": "Add / Replace",
      "envSaving": "Saving…",
      "envHint": "Encrypted values are write-only after store — the API redacts them on read. Rotate by setting a new value.",
      "envKeyRequired": "Key is required",
      "envSetFailed": "Failed to set env",
      "envDeleteFailed": "Failed to delete env",
      "sourceConfigTitle": "Source config",
      "sourceConfigCopy": "Copy",
      "sourceConfigCopied": "Copied",
      "sourceConfigCopyAria": "Copy source config",
      "publicFacesTitle": "Public faces",
      "publicFacesCopyAria": "Copy public faces",
      "deleteConfirmTitle": "Delete this app?",
      "deleteConfirmMessage": "Tears down all containers and proxy routes owned by \"{name}\", then removes the row. This cannot be undone.",
      "deleteConfirmFallbackName": "this workload",
      "deleteConfirmYes": "Yes, delete",
      "deleteConfirmDeleting": "Deleting…",
      "manualDeploySub": "Bypasses configured triggers and dispatches through the source plugin directly.",
      "chainTriggersZero": "no triggers",
      "chainTriggersOne": "1 trigger",
      "chainTriggersMany": "{count} triggers",
      "bindings": {
        "title": "Triggers",
        "subEmpty": "No triggers bound. Manual deploys still work — add a trigger to wire up registry / git / webhook redeploys.",
        "subCount": "{count} trigger bound",
        "subCountMany": "{count} triggers bound",
        "addButton": "Add trigger",
        "openTrigger": "View trigger",
        "unbindAction": "Unbind",
        "rowEnabled": "Enabled",
        "rowDisabled": "Disabled",
        "rowEnableHint": "Disable to keep the binding but stop this trigger from redeploying the app.",
        "loading": "Loading triggers…",
        "loadError": "Failed to load trigger bindings",
        "unbindTitle": "Unbind trigger?",
        "unbindMessage": "Trigger \"{name}\" will stop redeploying this app. The trigger record itself is not deleted — it stays in /triggers and remains bound to any other apps.",
        "unbindConfirm": "Unbind",
        "modal": {
          "title": "Add trigger",
          "subtitle": "Bind a trigger to this app — create a new one inline, or pick an existing trigger to share.",
          "tabInline": "Create new",
          "tabPick": "Bind existing",
          "submitInline": "Create & bind",
          "submitPick": "Bind",
          "submitting": "Binding…",
          "cancel": "Cancel",
          "error": "Bind failed",
          "pickPlaceholder": "Select a trigger…",
          "pickEmpty": "No triggers exist yet — switch to \"Create new\" to make one.",
          "pickLabel": "Existing trigger",
          "pickKind": "Filter by kind",
          "pickKindAll": "All kinds"
        },
        "override": {
          "toggle": "Override",
          "title": "Per-binding overrides",
          "subtitle": "Override fields of the trigger's config for this app only. Top-level keys you set here win; everything else inherits from the trigger.",
          "badgeOne": "OVERRIDES 1 FIELD",
          "badgeMany": "OVERRIDES {count} FIELDS",
          "badgeTitle": "This binding overrides one or more fields of the trigger's config.",
          "baseLabel": "Trigger config",
          "baseLoading": "Loading trigger config…",
          "baseHint": "Read-only view of the parent trigger's config. Edit it from the trigger page if it should change for every binding.",
          "editLabel": "Override (JSON object)",
          "editHint": "Top-level merge: only keys present here override the trigger. Leave the editor as {} to inherit verbatim.",
          "previewLabel": "Effective config",
          "previewHint": "Preview of what this binding will see when the trigger fires (trigger config with the override merged on top).",
          "invalidJson": "Override must be a JSON object.",
          "tooLarge": "Override is {size} B — exceeds the {limit} B server limit.",
          "errInvalidJson": "Cannot save: override is not a valid JSON object.",
          "errTooLarge": "Cannot save: override exceeds the 8 KiB server limit.",
          "saveButton": "Save override",
          "saving": "Saving…",
          "resetButton": "Reset to inherit",
          "closeButton": "Close"
        }
      }
    }
  }
}
