Files
tiny-forge/web/src/lib/i18n/ru.json
T
alexei.dolgolyov ea55d31177
Build / build (push) Successful in 10m43s
feat(discovery+runtime): restore static-site wizard discovery + close /sites/[id] feature parity
Two-stage feature arc closing the gaps left by the hard legacy cutover.
The static-site creation wizard regains its auto-discovery + connection-test
flow; /apps/[id] grows the runtime/storage/lifecycle surface the legacy
/sites/[id] page used to expose.

Backend (Go)
- internal/api/discovery.go: six admin-gated endpoints wrapping
  staticsite.GitProvider — POST /api/discovery/git/{detect-provider,
  test-connection,repos,branches,tree} + GET /api/discovery/image/conflicts.
  Identifier validation (validateGitIdent / validateGitBranch) at the
  boundary so provider URL interpolation cannot be hijacked via `..`.
  Upstream errors scrubbed: detailed slog on the server, generic 502 to
  the client (mitigates token-reflection-in-error-page).
- internal/api/workload_runtime.go: four endpoints —
  GET /api/workloads/{id}/runtime-state decodes containers.extra_json for
  static workloads; GET /api/workloads/{id}/storage execs `du -sb /app/data`
  with a 30s in-process cache (storageProbeCache) so polling can't turn
  into per-request execs; POST /api/workloads/{id}/{stop,start} iterate
  ListContainersByWorkload and call docker.StopContainer / StartContainer,
  returning 200 / 409 (nothing to act on) / 502 (all failed).
- internal/staticsite/safehttp.go: NewSafeHTTPClient + ValidateBaseURL +
  blockReason. DialContext re-resolves hostnames and refuses loopback /
  link-local / multicast / unspecified addresses. RFC1918 + ULA explicitly
  allowed (self-hosted Gitea on LAN is the dominant deployment).
  Replaced four raw &http.Client{} constructions in the provider files.
- internal/staticsite/gitlab_provider.go: url.PathEscape each segment in
  the raw-file URL builder for parity with projectPath().
- Test coverage: 26 cases in discovery_test.go (image-tag stripping,
  source-config decoding, conflict scenarios, validator boundaries,
  scheme rejection), 14 in workload_runtime_test.go (404 / 409 / nil-docker
  / probe-cache), 16 in safehttp_test.go (URL validation + block-reason
  policy matrix + live dial against loopback + AWS metadata literals).

Frontend (Svelte 5 + runes)
- web/src/lib/api.ts: typed wrappers for every endpoint, AbortSignal
  threaded through post(); ApiError exported so callers can narrow on
  e.status; new DetectedGitProvider narrow union.
- web/src/routes/apps/new/+page.svelte: static-form discovery controls
  (auto-detect provider, test connection, repo / branch / folder
  EntityPickers, Deno auto-detect); image-form conflict panel with
  debounced lookup + double-click submit guard ("Forge anyway") + Inspect
  button that pre-fills port/healthcheck; English error fallbacks routed
  through apps.new.errors.* (en + ru).
- web/src/routes/apps/[id]/+page.svelte: runtime-state panel + storage
  panel + Stop / Start / Open-site toolbar; universal live-state badge
  in the hero lede for image/compose/static (RUNNING / TRANSITIONING /
  STOPPED / NOT DEPLOYED / MIXED · n/m RUNNING); ContainerStats panel
  per row (auto-collapsing native <details> when N > 2); read-only
  webhook bindings summary card; responsive toolbar overflow with native
  <details> at <640px (z-index 100 above sticky nav).
- web/src/app.css: project-wide .forge-btn-ghost:focus-visible outline.

Hardening from go-reviewer + security-reviewer + typescript-reviewer +
frontend-design UI/UX subagents (0 CRITICAL, all HIGH/BLOCKER addressed
inline, IMPORTANT applied before commit):
- AbortController + per-call sequence tokens on every long-running
  fetch (loadRuntimeState / loadStorage / loadTriggerMeta / inspectImage /
  listImageConflicts) plus onDestroy cleanup so late resolves cannot
  mutate dead component state.
- doStop / doStart snapshot and restore `error` across the finally-block
  reload so a load()-cleared message doesn't hide a real failure.
- triggersById refreshed after inline trigger creation so the webhook
  card doesn't silently exclude the just-created trigger.
- Live-state badge wraps in role=status / aria-live=polite (no redundant
  aria-label).
- Webhook row has a single click target (was two pointing at the same URL).
- Empty webhook section hides entirely.
- Dropped role=menu / role=menuitem from the overflow menu (they would
  promise arrow-key nav we don't wire; native Tab + ESC carry it).

Doc
- docs/CODEMAPS/INDEX.md + new docs/CODEMAPS/discovery-and-runtime.md
  map the endpoint surface, security posture, frontend integration
  patterns, and an "add a new probe" recipe.

Verification
- svelte-check: 0 errors, 3 pre-existing a11y warnings.
- go build + go vet + go test ./...: all green.
- i18n parity: en + ru at 1413 keys each.
- Live smoke against :8090: 404 / 409 / 502 envelopes correct, discovery
  sanity passes, ProbeError surfaces on no-container path.
2026-05-16 21:35:51 +03:00

1608 lines
103 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"app": {
"name": "Tinyforge",
"version": "v0.1"
},
"layout": {
"serviceStatus": "Состояние служб"
},
"health": {
"connected": "подключён",
"disconnected": "отключён",
"rawError": "Технические детали",
"retryNow": "Проверить сейчас"
},
"nav": {
"dashboard": "Панель",
"apps": "Приложения",
"eventTriggers": "Триггеры",
"logScanRules": "Лог-правила",
"triggers": "Триггеры",
"proxies": "Прокси",
"events": "События",
"settings": "Настройки",
"logout": "Выйти",
"dns": "DNS-записи",
"containers": "Контейнеры"
},
"dashboard": {
"title": "Панель управления",
"newApp": "Новое приложение",
"totalWorkloads": "Всего нагрузок",
"runningContainers": "Запущенных контейнеров",
"failedContainers": "Сбойных контейнеров",
"recentWorkloads": "Недавние нагрузки",
"retry": "Повторить",
"noWorkloads": "Нагрузок пока нет.",
"noWorkloadsDesc": "Создайте приложение и выкуйте первую нагрузку, чтобы начать.",
"loadFailed": "Не удалось загрузить панель",
"staleContainers": "Устаревшие контейнеры",
"unusedImagesWarning": "Неиспользуемые Docker-образы занимают дисковое пространство",
"unusedImages": "неиспользуемых образов",
"systemHealth": "Состояние системы",
"daemons": "Демоны",
"systemResources": "Системные ресурсы",
"systemResourcesSubtitle": "CPU, память, диск и топ потребителей"
},
"resources": {
"cpuCores": "Ядра CPU",
"memory": "Память",
"running": "Запущено",
"dockerDisk": "Диск Docker",
"workloadUtilization": "Использование нагрузкой",
"windowMinutes": "{n} минут",
"windowHours": "{n} часов",
"noSamples": "Пока нет данных — сбор идёт каждые {interval}с.",
"collectionDisabled": "Сбор статистики отключён. Включите его в Настройках, чтобы заполнить график.",
"diskImages": "Образы",
"diskContainers": "Контейнеры",
"diskVolumes": "Тома",
"diskBuildCache": "Кэш сборки",
"reclaimable": "{size} можно освободить",
"topConsumers": "Топ потребителей",
"byCpu": "по CPU",
"byMemory": "по памяти",
"noRunning": "Нет запущенных контейнеров.",
"instance": "экземпляр",
"site": "сайт",
"showHistory": "Показать историю",
"hideHistory": "Скрыть историю",
"cpuSeries": "CPU %",
"memorySeries": "Память %",
"loading": "Загрузка…",
"sectionTitle": "Ресурсы",
"showLogs": "Показать логи",
"hideLogs": "Скрыть логи",
"dockerUnavailable": "Docker недоступен. Проверьте, что демон запущен."
},
"statsSettings": {
"intervalLabel": "Интервал сбора статистики (с)",
"intervalHelp": "Как часто собираются замеры ресурсов. 0 отключает сбор. Диапазон: 5–300с.",
"retentionLabel": "Хранение статистики (часы)",
"retentionHelp": "Как долго хранятся замеры ресурсов. 0 отключает сбор. Диапазон: 0–24ч."
},
"tagPicker": {
"registry": "Реестр",
"local": "Локальный"
},
"settings": {
"title": "Настройки",
"general": "Общие",
"integrations": "Интеграции",
"dns": "DNS",
"maintenance": "Обслуживание",
"registries": "Реестры",
"credentials": "Учётные данные",
"authentication": "Аутентификация",
"backup": "Резервные копии",
"appearance": "Внешний вид",
"groupMain": "Обзор",
"groupProxy": "Маршрутизация",
"groupSystem": "Система",
"groupSecurity": "Безопасность",
"staleThreshold": "Порог устаревания (дни)",
"staleThresholdHelp": "Контейнеры, неактивные дольше этого срока, будут помечены как устаревшие.",
"dockerCleanup": "Очистка Docker-образов",
"dockerCleanupHelp": "Удаление неиспользуемых Docker-образов ваших проектов. Удаляются только образы, не используемые активными экземплярами.",
"pruneThreshold": "Порог предупреждения (МБ)",
"pruneThresholdHelp": "Показывать предупреждение на дашборде, когда неиспользуемые образы превышают этот размер. 0 = отключено.",
"pruneImages": "Очистить неиспользуемые образы",
"pruning": "Очистка...",
"pruneResult": "Удалено {count} образов, освобождено {mb} МБ",
"pruneConfirmMessage": "Будут удалены неиспользуемые Docker-образы ваших проектов. Образы активных экземпляров не затрагиваются.",
"pruneFailed": "Не удалось очистить образы",
"proxyProvider": "Провайдер прокси",
"proxyProviderHelp": "Выберите способ управления обратным прокси для развёрнутых контейнеров.",
"proxyNone": "Нет",
"proxyNoneDesc": "Без прокси — контейнеры доступны напрямую по порту",
"proxyNpm": "Nginx Proxy Manager",
"proxyNpmDesc": "Маршруты через NPM API (настройте учётные данные ниже)",
"npm": "Nginx Proxy Manager",
"traefik": "Traefik",
"traefikLabelsTitle": "Справка по Docker-меткам",
"traefikLabelsDesc": "Эти метки автоматически добавляются к развёрнутым контейнерам. Показаны для справки.",
"proxyTraefik": "Traefik",
"proxyTraefikDesc": "Автообнаружение через Docker-метки — без API-вызовов",
"proxyNoneWarning": "Переключение на «Нет» не удаляет существующие прокси-маршруты. Возможно, потребуется очистить их вручную.",
"traefikEntrypoint": "Точка входа",
"traefikEntrypointHelp": "Имя точки входа Traefik для HTTPS-маршрутов",
"traefikCertResolver": "Резолвер сертификатов",
"traefikCertResolverHelp": "Имя резолвера TLS-сертификатов (напр., letsencrypt)",
"traefikNetwork": "Docker-сеть",
"traefikNetworkHelp": "Сеть, которую слушает Traefik (оставьте пустым для глобальной сети)",
"traefikApiUrl": "URL API Traefik",
"traefikApiUrlHelp": "Необязательно — для проверки состояния (напр., http://traefik:8080)"
},
"settingsGeneral": {
"title": "Общие настройки",
"globalConfig": "Глобальная конфигурация",
"globalConfigDesc": "Базовая инфраструктура: домен, сеть и интервал опроса, используемые Tinyforge для оркестрации контейнеров.",
"configureNpm": "Выбран Nginx Proxy Manager.",
"configureTraefik": "Выбран Traefik.",
"configureLink": "Настроить провайдера",
"domain": "Домен",
"domainHelp": "Базовый домен для маршрутизации (напр., example.com → stage-dev-app.example.com)",
"serverIp": "IP сервера (Docker Host)",
"serverIpHelp": "IP машины с Docker. Используется для удалённого NPM.",
"publicIp": "Публичный IP (для DNS)",
"publicIpHelp": "IP для DNS A-записей — обычно адрес прокси/балансировщика. Если пусто, используется IP сервера.",
"dockerNetwork": "Docker-сеть",
"dockerNetworkHelp": "Docker-сеть, общая для контейнеров и прокси. Должна совпадать с сетью NPM/Traefik.",
"subdomainPattern": "Шаблон поддомена",
"subdomainPatternHelp": "Шаблон для автоматически генерируемых поддоменов",
"subdomainVarsTitle": "Доступные переменные",
"varProject": "Имя проекта",
"varStage": "Имя стадии",
"varTag": "Тег образа",
"varPort": "Порт контейнера",
"pollingInterval": "Интервал опроса (секунды)",
"pollingIntervalHelp": "Как часто проверять реестры на новые теги (60-86400)",
"notificationUrl": "URL уведомлений",
"notificationUrlHelp": "URL вебхука для уведомлений о деплоях",
"saveSettings": "Сохранить настройки",
"saving": "Сохранение...",
"saved": "Настройки успешно сохранены",
"saveFailed": "Не удалось сохранить настройки",
"loadFailed": "Не удалось загрузить настройки",
"webhookUrl": "URL вебхука",
"webhookDesc": "Этот секретный URL получает уведомления о push-событиях из вашего CI-пайплайна.",
"noWebhookUrl": "URL вебхука не настроен",
"copy": "Копировать",
"copied": "URL вебхука скопирован в буфер обмена",
"regenerateUrl": "Перегенерировать URL",
"regenerating": "Перегенерация...",
"regenerated": "URL вебхука перегенерирован",
"regenerateFailed": "Не удалось перегенерировать URL вебхука",
"regenerateWarning": "Внимание: перегенерация сделает текущий URL недействительным. Обновите ваши CI-пайплайны.",
"sslCertificate": "SSL-сертификат",
"sslCertificateHelp": "Wildcard-сертификат из NPM для автоматического SSL на прокси-хостах",
"selectCertificate": "Выбрать сертификат",
"noCertificate": "Нет (без SSL)",
"clearCertificate": "Очистить",
"loadingCertificates": "Загрузка сертификатов...",
"noCertificatesFound": "Wildcard-сертификаты в NPM не найдены",
"dnsConfig": "Настройки DNS",
"wildcardDns": "Wildcard DNS настроен",
"wildcardDnsHelp": "Когда включено, все поддомены разрешаются на ваш сервер через wildcard DNS правило. Отключите для управления DNS-записями для каждого сервиса.",
"dnsProvider": "DNS-провайдер",
"dnsProviderHelp": "Выберите DNS-провайдера для автоматического управления записями",
"cloudflareApiToken": "API-токен Cloudflare",
"cloudflareApiTokenHelp": "API-токен с правами редактирования DNS для вашей зоны",
"cloudflareApiTokenPlaceholder": "Введите API-токен Cloudflare",
"cloudflareApiTokenConfigured": "API-токен настроен",
"cloudflareZone": "Зона Cloudflare",
"cloudflareZoneHelp": "Выберите DNS-зону для управления записями",
"selectZone": "Выбрать зону",
"noZone": "Зона не выбрана",
"loadingZones": "Загрузка зон...",
"noZonesFound": "Зоны для этого токена не найдены",
"testConnection": "Проверить соединение",
"testingConnection": "Проверка...",
"connectionSuccess": "Соединение успешно",
"connectionFailed": "Ошибка соединения",
"baseVolumePath": "Базовый путь томов",
"baseVolumePathHelp": "Добавляется к относительным путям источников (напр., /data + my-app/uploads = /data/my-app/uploads)"
},
"settingsRegistries": {
"title": "Реестры контейнеров",
"description": "Управление реестрами контейнеров для обнаружения образов.",
"addRegistry": "Добавить реестр",
"editRegistry": "Редактировать реестр",
"addNewRegistry": "Добавить новый реестр",
"name": "Название",
"nameHelp": "Понятное название для этого реестра",
"url": "URL",
"urlHelp": "Базовый URL реестра",
"type": "Тип",
"typeHelp": "Тип реестра для совместимости API",
"token": "Токен",
"tokenHelpNew": "API-токен для аутентификации",
"tokenHelpEdit": "Оставьте пустым, чтобы сохранить текущий токен",
"owner": "Владелец",
"ownerHelp": "Владельцы пакетов через запятую (напр., alexei,my-org)",
"save": "Сохранить",
"saving": "Сохранение...",
"update": "Обновить",
"test": "Тест",
"testing": "Тестирование...",
"edit": "Изменить",
"delete": "Удалить",
"noRegistries": "Реестры ещё не настроены.",
"addFirst": "Добавьте первый реестр",
"registryUpdated": "Реестр обновлён",
"registryAdded": "Реестр добавлен",
"registryDeleted": "Реестр «{name}» удалён",
"testSuccess": "Подключение к «{name}» успешно",
"saveFailed": "Не удалось сохранить реестр",
"deleteFailed": "Не удалось удалить реестр",
"testFailed": "Тест подключения не удался",
"loadFailed": "Не удалось загрузить реестры",
"deleteConfirm": "Удалить реестр «{name}»? Это действие необратимо.",
"healthChecking": "Проверка...",
"healthConnected": "Подключено",
"healthUnreachable": "Недоступно"
},
"settingsNpm": {
"testConnection": "Проверить соединение",
"testing": "Проверка...",
"testSuccess": "Подключение к NPM успешно",
"testFailed": "Не удалось подключиться к NPM",
"saveFailedConnection": "Невозможно сохранить — проверка соединения не пройдена",
"remoteMode": "Удалённый NPM",
"remoteModeHelp": "Включите, если NPM работает на другой машине. Перенаправление на IP сервера с опубликованными портами.",
"remoteModeWarning": "Требуется IP сервера в общих настройках. Порты автоматически привязываются к случайным портам хоста.",
"accessList": "Список доступа по умолчанию",
"accessListHelp": "Список доступа NPM для HTTP-аутентификации на прокси-хостах. Можно переопределить для каждого проекта.",
"noAccessList": "Глобальные настройки",
"selectAccessList": "Выберите список доступа",
"noAccessLists": "Списки доступа в NPM не найдены",
"accessListLoadFailed": "Не удалось загрузить списки доступа"
},
"settingsCredentials": {
"title": "Учётные данные",
"description": "Управление учётными данными для Nginx Proxy Manager и токенами реестров. Все значения зашифрованы.",
"npm": "Nginx Proxy Manager",
"npmDesc": "Учётные данные для управления прокси-хостами через NPM API",
"configured": "Настроено",
"npmUrl": "URL NPM",
"npmUrlHelp": "URL API Nginx Proxy Manager",
"email": "Email",
"emailHelp": "Email администратора NPM",
"password": "Пароль",
"passwordHelpNew": "Пароль администратора NPM (будет зашифрован)",
"passwordHelpEdit": "Введите новый пароль для замены текущего",
"changeCredentials": "Изменить учётные данные",
"save": "Сохранить",
"saving": "Сохранение...",
"saved": "Учётные данные NPM сохранены",
"saveFailed": "Не удалось сохранить учётные данные NPM",
"loadFailed": "Не удалось загрузить учётные данные",
"registryTokens": "Токены реестров",
"registryTokensDesc": "Токены аутентификации реестров управляются для каждого реестра в разделе",
"registriesLink": "Реестры",
"registryTokensSuffix": ". Каждый реестр хранит свой токен в зашифрованном виде.",
"notSet": "Не задано"
},
"settingsBackup": {
"title": "Управление резервными копиями",
"description": "Управление резервными копиями базы данных и настройка автоматического резервного копирования.",
"autoBackup": "Автоматическое резервное копирование",
"autoBackupHelp": "Автоматически создавать резервные копии с заданным интервалом.",
"interval": "Интервал копирования",
"intervalHelp": "Как часто создавать автоматические резервные копии.",
"intervalHours": "{hours} часов",
"retention": "Количество хранимых копий",
"retentionHelp": "Максимальное количество хранимых резервных копий. Старые удаляются первыми.",
"backupNow": "Создать копию",
"creatingBackup": "Создание...",
"backupCreated": "Резервная копия создана",
"backupFailed": "Не удалось создать резервную копию",
"backupList": "Резервные копии",
"noBackups": "Резервных копий пока нет. Создайте вручную или включите автоматическое копирование.",
"columnFilename": "Файл",
"columnSize": "Размер",
"columnType": "Тип",
"columnDate": "Создано",
"columnActions": "Действия",
"download": "Скачать",
"delete": "Удалить",
"restore": "Восстановить",
"deleteConfirm": "Вы уверены, что хотите удалить эту резервную копию?",
"deleted": "Резервная копия удалена",
"deleteFailed": "Не удалось удалить резервную копию",
"restoreConfirm": "Вы уверены, что хотите восстановить из этой копии? Текущая база данных будет заменена и сервер будет перезапущен. Все текущие данные будут потеряны.",
"restoreWarning": "Это действие необратимо!",
"restored": "База данных восстановлена. Сервер перезапускается...",
"restoreFailed": "Не удалось восстановить резервную копию",
"typeManual": "Ручная",
"typeAuto": "Авто",
"typePreDeploy": "Перед деплоем",
"preDeploy": "Снимок перед каждым деплоем",
"preDeployHelp": "Создавать снимок БД Tinyforge в начале каждого деплоя проекта. Независимо от периодического расписания выше; восстанавливается из списка ниже по типу «Перед деплоем».",
"save": "Сохранить",
"saving": "Сохранение...",
"saved": "Настройки копирования сохранены",
"saveFailed": "Не удалось сохранить настройки копирования"
},
"settingsAuth": {
"title": "Настройки аутентификации",
"description": "Настройка режима аутентификации и управление пользователями.",
"authMode": "Режим аутентификации",
"local": "Локальный (логин/пароль)",
"oidc": "OIDC (SSO)",
"oidcConfig": "Конфигурация OIDC-провайдера",
"issuerUrl": "URL издателя",
"clientId": "ID клиента",
"clientSecret": "Секрет клиента",
"redirectUrl": "URL перенаправления",
"saveSettings": "Сохранить настройки",
"saving": "Сохранение...",
"saved": "Настройки сохранены",
"saveFailed": "Не удалось сохранить",
"loadFailed": "Не удалось загрузить настройки",
"localUsers": "Локальные пользователи",
"username": "Имя пользователя",
"email": "Email",
"role": "Роль",
"created": "Создан",
"noUsers": "Пользователи не найдены.",
"addUser": "Добавить пользователя",
"viewer": "Наблюдатель",
"admin": "Администратор",
"userCreated": "Пользователь создан",
"userDeleted": "Пользователь удалён",
"createFailed": "Не удалось создать пользователя",
"deleteFailed": "Не удалось удалить пользователя",
"deleteConfirm": "Вы уверены, что хотите удалить этого пользователя?",
"usernameRequired": "Имя пользователя и пароль обязательны",
"networkError": "Ошибка сети",
"password": "Пароль"
},
"login": {
"title": "Tinyforge",
"subtitle": "Войдите в свой аккаунт",
"username": "Имя пользователя",
"password": "Пароль",
"signIn": "Войти",
"signingIn": "Вход...",
"or": "или",
"ssoButton": "Войти через SSO (OIDC)",
"loginFailed": "Ошибка входа",
"networkError": "Ошибка сети"
},
"proxies": {
"title": "Прокси-маршруты",
"description": "Активные прокси-маршруты от контейнеров и статических сайтов.",
"domain": "Домен",
"project": "Проект / Сайт",
"stage": "Этап / Режим",
"tag": "Тег",
"port": "Порт",
"status": "Статус",
"source": "Источник",
"sourceContainer": "Контейнер",
"sourceStatic": "Статический сайт",
"sourceDeno": "Deno-сайт",
"filterAll": "Все",
"filterContainers": "Контейнеры",
"filterSites": "Сайты",
"noRoutes": "Нет прокси-маршрутов",
"noRoutesDesc": "Прокси-маршруты создаются автоматически при развёртывании контейнера с прокси или публикации статического сайта.",
"searchPlaceholder": "Поиск по домену, проекту или тегу...",
"noMatch": "Нет маршрутов, соответствующих поиску.",
"loadFailed": "Не удалось загрузить прокси-маршруты",
"route": "маршрут",
"routes": "маршрутов"
},
"common": {
"cancel": "Отмена",
"confirm": "Подтвердить",
"delete": "Удалить",
"edit": "Изменить",
"change": "Изменить",
"save": "Сохранить",
"retry": "Повторить",
"loading": "Загрузка...",
"noData": "Нет данных",
"project": "Проект",
"stack": "Стек",
"site": "Сайт",
"back": "Назад",
"actions": "Действия",
"stop": "Остановить",
"start": "Запустить",
"restart": "Перезапустить",
"remove": "Удалить",
"instance": "экземпляр",
"instances": "экземпляров",
"next": "Далее",
"yes": "Да",
"no": "Нет",
"saving": "Сохранение...",
"refresh": "Обновить",
"all": "Все",
"running": "Работает",
"stopped": "Остановлен",
"missing": "Отсутствует"
},
"containers": {
"errLoad": "Не удалось загрузить контейнеры",
"searchPlaceholder": "Поиск по нагрузке, роли, образу, поддомену…",
"kindFilterLabel": "Тип нагрузки",
"stateFilterLabel": "Состояние контейнера",
"emptyTitle": "Нет контейнеров",
"emptyDesc": "Разверните проект, стек или сайт — контейнеры появятся здесь.",
"noMatch": "Нет контейнеров, подходящих под фильтры.",
"showingN": "Показано {visible} из {total} контейнеров",
"col": {
"workload": "Нагрузка",
"kind": "Тип",
"role": "Роль",
"image": "Образ",
"state": "Состояние",
"subdomain": "Поддомен",
"lastSeen": "Замечен"
}
},
"empty": {
"noRegistries": "Нет реестров",
"noRegistriesDesc": "Добавьте реестр контейнеров для обнаружения образов.",
"noUsers": "Нет пользователей",
"noUsersDesc": "Добавьте локальных пользователей для управления доступом."
},
"validation": {
"required": "Поле {field} обязательно",
"invalidUrl": "Неверный формат URL",
"invalidDomain": "Неверный формат домена",
"invalidIp": "Неверный формат IP",
"invalidEmail": "Неверный формат email",
"invalidPort": "Порт должен быть от 1 до 65535",
"invalidPollingInterval": "Интервал опроса должен быть от 60 до 86400 секунд",
"invalidProjectName": "Допускаются только строчные буквы, цифры и дефисы",
"requiredWhenUpdating": "Поле {field} обязательно при обновлении учётных данных",
"requiredForNew": "Поле {field} обязательно для новых реестров"
},
"theme": {
"light": "Светлая",
"dark": "Тёмная",
"system": "Системная"
},
"entityPicker": {
"search": "Поиск...",
"noResults": "Ничего не найдено"
},
"stale": {
"title": "Устаревшие контейнеры",
"noStale": "Нет устаревших контейнеров",
"noStaleDesc": "Все контейнеры исправны и работают.",
"cleanup": "Очистить",
"cleanupAll": "Очистить все",
"confirmCleanup": "Это остановит и удалит контейнер. Продолжить?",
"confirmBulkCleanup": "Это остановит и удалит все устаревшие контейнеры. Продолжить?",
"daysStale": "дней устарел",
"lastAlive": "Последний раз жив",
"count": "Устаревшие",
"cleanedUp": "Контейнер очищен",
"bulkCleanedUp": "{count} контейнеров очищено",
"cleanupFailed": "Не удалось очистить",
"loadFailed": "Не удалось загрузить устаревшие контейнеры"
},
"logs": {
"title": "Логи контейнера",
"lines": "строк",
"follow": "Следить",
"following": "Слежение...",
"loading": "Загрузка логов...",
"noLogs": "Нет вывода логов"
},
"events": {
"title": "Журнал событий",
"noEvents": "Событий не найдено",
"noEventsDesc": "События будут отображаться здесь по мере их возникновения.",
"loadMore": "Загрузить ещё",
"newEvents": "новых событий",
"totalCount": "всего {count}",
"clearAll": "Очистить всё",
"clearAllTitle": "Очистить журнал событий",
"clearAllMessage": "Все записи журнала событий будут удалены безвозвратно.",
"cleared": "Удалено {count} событий",
"clearFailed": "Не удалось очистить события",
"filter": {
"severity": "Уровень",
"source": "Источник",
"dateRange": "Период",
"search": "Поиск событий...",
"lastHour": "Последний час",
"last24h": "Последние 24 часа",
"last7d": "Последние 7 дней",
"allTime": "За всё время",
"clear": "Сбросить фильтры"
},
"severity": {
"info": "Инфо",
"warn": "Предупреждение",
"error": "Ошибка"
},
"source": {
"deploy": "Развёртывание",
"static_site": "Статический сайт",
"stale_scanner": "Сканер устаревших",
"stale_cleanup": "Очистка устаревших",
"admin": "Администратор"
},
"metadata": "Подробности"
},
"stats": {
"cpu": "ЦП",
"mem": "ОЗУ",
"unavailable": "Статистика недоступна"
},
"systemHealth": {
"title": "Состояние системы",
"containers": "Контейнеры",
"proxies": "Прокси",
"recentErrors": "Недавние ошибки"
},
"daemons": {
"title": "Демоны",
"refresh": "Обновить",
"refreshing": "Обновление",
"docker": "Docker Engine",
"npm": "Nginx Proxy Manager",
"traefik": "Traefik",
"proxy": "Прокси",
"online": "Онлайн",
"offline": "Оффлайн",
"notConfigured": "Не настроено",
"containers": "Контейнеры",
"running": "Запущено",
"paused": "Пауза",
"stopped": "Остановлено",
"version": "Версия",
"apiVersion": "Версия API",
"platform": "Платформа",
"kernel": "Ядро",
"cpu": "CPU",
"memory": "Память",
"storage": "Хранилище",
"images": "Образы",
"latency": "Задержка",
"rootDir": "Корневой каталог",
"provider": "Провайдер",
"endpoint": "Адрес",
"proxyHosts": "Прокси-хосты",
"managed": "наши",
"external": "внешние",
"accessLists": "Списки доступа",
"certificates": "Сертификаты",
"dockerHint": "Проверьте, что Docker-демон запущен и сокет доступен.",
"proxyHint": "Проверьте URL прокси, учётные данные и доступность сервиса.",
"noProxyDesc": "Провайдер прокси не настроен. Tinyforge поддерживает Nginx Proxy Manager или Traefik.",
"configureProxy": "Настроить в параметрах",
"dockerNotReachable": "Docker-демон недоступен.",
"dockerUnreachable": "Docker недоступен",
"proxyUnreachable": "Прокси недоступен",
"reachable": "доступен"
},
"dns": {
"title": "DNS-записи",
"description": "Просмотр и управление DNS-записями, созданными Tinyforge.",
"wildcardActive": "Режим Wildcard DNS активен",
"wildcardActiveDesc": "DNS-записи управляются внешне через wildcard DNS. Отключите wildcard DNS в настройках для индивидуального управления записями.",
"refresh": "Обновить",
"syncNow": "Синхронизировать",
"syncing": "Синхронизация...",
"syncComplete": "Синхронизация завершена: {created} создано, {deleted} удалено, {synced} уже синхронизировано",
"syncFailed": "Ошибка синхронизации DNS",
"searchPlaceholder": "Поиск по FQDN...",
"allConsumers": "Все потребители",
"managed": "Управляемые (инстансы)",
"standalone": "Автономные прокси",
"orphaned": "Осиротевшие",
"allStatuses": "Все статусы",
"statusSynced": "Синхронизировано",
"statusMissing": "Отсутствует",
"statusOrphaned": "Осиротевшее",
"columnFqdn": "FQDN",
"columnType": "Тип",
"columnValue": "Значение",
"columnConsumer": "Потребитель",
"columnStatus": "Статус",
"columnActions": "Действия",
"noConsumer": "Нет потребителя",
"noRecords": "DNS-записи не найдены. Записи появятся здесь после развёртывания сервисов.",
"noMatchingRecords": "Нет записей, соответствующих текущим фильтрам.",
"deleteRecord": "Удалить запись",
"recordDeleted": "DNS-запись {fqdn} удалена",
"deleteFailed": "Не удалось удалить DNS-запись",
"loadFailed": "Не удалось загрузить DNS-записи",
"totalRecords": "Всего: {count}",
"syncedCount": "Синхронизировано: {count}",
"missingCount": "Отсутствует: {count}",
"orphanedCount": "Осиротевших: {count}"
},
"language": {
"en": "Английский",
"ru": "Русский"
},
"timezone": {
"eyebrow": "The Forge // Хронограф",
"title": "Часовой пояс отображения",
"subtitle": "Все даты в Tinyforge — лог событий, деплои, бэкапы, сайты — показываются в этом поясе.",
"modeLabel": "Режим определения",
"modeAuto": "Автоопределение",
"modeManual": "Вручную",
"autoDetect": "Автоопределение из браузера",
"autoBadge": "Авто",
"activeZone": "Активный пояс",
"changeZone": "Сменить часовой пояс",
"clickToChange": "Нажмите, чтобы выбрать пояс →",
"pickerTitle": "Выбор часового пояса",
"pickerPlaceholder": "Поиск — город, регион, смещение UTC…",
"groupAuto": "Определение",
"groupPopular": "Популярные",
"groupAll": "Все пояса",
"previewFull": "Полная метка времени",
"previewDate": "Только дата",
"previewHint": "Метки времени в логе событий будут выглядеть именно так."
},
"settingsDns": {
"title": "Настройка DNS",
"description": "Выберите, использовать ли wildcard-запись или отдельные поддомены, управляемые DNS-провайдером."
},
"settingsIntegrations": {
"title": "Интеграции",
"outgoing": "Исходящие уведомления",
"outgoingDesc": "Куда Tinyforge отправляет события деплоев и алертов. Укажите webhook-URL (Apprise, Discord, Slack, свой обработчик).",
"incoming": "Входящие вебхуки",
"incomingMovedDesc": "Входящие вебхуки теперь привязаны к конкретному проекту или сайту. Откройте страницу проекта или статического сайта, чтобы увидеть и перегенерировать URL."
},
"webhookLog": {
"title": "Последние доставки вебхуков",
"description": "Последние 14 дней входящих вебхуков — результат, состояние подписи и причина. Обновляется каждые 30 секунд.",
"refresh": "Обновить",
"loadFailed": "Не удалось загрузить журнал доставок",
"empty": "Пока нет доставок.",
"colTime": "Когда",
"colStatus": "Статус",
"colOutcome": "Результат",
"colSignature": "Подпись",
"colDetail": "Подробности",
"colSource": "Источник",
"outcome": {
"deploy": "Развёрнуто",
"skip": "Пропущено",
"rejected": "Отклонено",
"not_found": "Не найдено",
"bad_request": "Неверный запрос",
"error": "Ошибка"
},
"sig": {
"valid": "верна",
"invalid": "неверна",
"missing": "отсутствует",
"unconfigured": "выкл"
}
},
"webhookPanel": {
"copy": "Копировать",
"copied": "Webhook-URL скопирован в буфер обмена",
"copyFailed": "Не удалось скопировать",
"noUrl": "Webhook-URL не настроен",
"loadFailed": "Не удалось загрузить webhook-URL",
"regenerate": "Перегенерировать URL",
"regenerated": "Webhook-URL перегенерирован",
"regenerateFailed": "Не удалось перегенерировать webhook-URL",
"regenerateWarning": "Перегенерация инвалидирует текущий URL. Обновите CI-пайплайны и Git-вебхуки, использующие его.",
"confirmRegenerate": "Заменить текущий URL?",
"confirmYes": "Перегенерировать",
"confirmNo": "Отмена",
"signingTitle": "Подпись входящих вебхуков (HMAC)",
"signingDesc": "Проверка подписи HMAC-SHA256 — утечка только URL не позволит подделать запрос. Совместимо с секретами вебхуков Gitea/GitHub.",
"signingActive": "Секрет подписи настроен.",
"signingInactive": "Секрет подписи не задан — входящие запросы не проверяются помимо URL.",
"signingIssue": "Сгенерировать секрет",
"signingRotate": "Перевыпустить секрет",
"signingDisable": "Отключить подпись",
"signingDisableConfirm": "Отключить",
"signingIssued": "Новый секрет подписи выпущен — скопируйте его сейчас",
"signingIssueFailed": "Не удалось сгенерировать секрет подписи",
"signingDisabled": "Подпись отключена",
"signingDisableFailed": "Не удалось отключить подпись",
"signingShownOnce": "Скопируйте секрет сейчас — он больше не будет показан.",
"signingDismiss": "Скрыть",
"signingHint": "Используйте это значение как webhook-секрет в Gitea/GitHub/GitLab. Tinyforge ожидает заголовок {header}.",
"signingCopied": "Секрет подписи скопирован в буфер обмена",
"requireSignature": "Требовать подпись",
"requireSignatureHelp": "Отклонять запросы без действительной подписи. Сначала сгенерируйте секрет.",
"signingRequireFailed": "Не удалось обновить требование подписи"
},
"outgoingWebhook": {
"signingOn": "Подпись включена",
"signingOff": "Без подписи",
"signingSecret": "HMAC-секрет",
"noSecret": "Секрет не задан — исходящие события отправляются без подписи.",
"reveal": "Показать",
"generate": "Сгенерировать",
"copy": "Копировать",
"copied": "Секрет скопирован в буфер обмена",
"copyFailed": "Не удалось скопировать",
"loadFailed": "Не удалось загрузить секрет",
"regenerate": "Перегенерировать",
"regenerated": "Секрет перегенерирован",
"regenerateFailed": "Не удалось перегенерировать секрет",
"confirmRegenerateTitle": "Перегенерировать секрет?",
"confirmRegenerate": "Текущий секрет инвалидируется немедленно. Все получатели должны быть обновлены синхронно — иначе начнут отклонять события.",
"confirmDisableTitle": "Отключить HMAC-подпись?",
"confirmDisable": "Будущие события пойдут без заголовка X-Hub-Signature-256. Получатели, требующие подпись, начнут их отклонять.",
"confirmYes": "Подтвердить",
"confirmNo": "Отмена",
"disable": "Отключить подпись",
"disabled": "Подпись отключена",
"disableFailed": "Не удалось отключить подпись",
"sendTestTitle": "Отправить тестовое событие",
"sendTestHelp": "Отправляет синтетическое событие \"test\" на разрешённый URL с текущим секретом.",
"sendTest": "Отправить тест",
"sending": "Отправка…",
"testFailed": "Не удалось отправить тестовое событие",
"tier": "Уровень",
"signed": "Подписано",
"unsigned": "Без подписи",
"deliveryId": "Доставка",
"responseBody": "Тело ответа",
"networkError": "Сетевая ошибка",
"fallbackTo": "URL не задан на этом уровне — события унаследуются от {label}.",
"noUrlConfigured": "URL не задан. Настройте его выше перед тестом."
},
"settingsMaintenance": {
"title": "Обслуживание",
"thresholds": "Пороги",
"thresholdsDesc": "Настройте, когда Tinyforge помечает контейнеры как устаревшие и предупреждает о неиспользуемых образах.",
"dangerZone": "Опасная зона"
},
"observability": {
"section": "Наблюдаемость",
"manage": "управление",
"loading": "Загрузка…",
"anyEvent": "любое событие",
"noUrlSet": "URL не настроен",
"configured": "НАСТРОЕН",
"clear": "Очистить",
"advanced": "Расширенно",
"cancel": "Отмена",
"save": "Сохранить",
"saving": "Сохранение…",
"delete": "Удалить",
"deleting": "Удаление…",
"refresh": "Обновить",
"open": "Открыть",
"edit": "Изменить",
"back": "Назад",
"regex": {
"sampleLabel": "Пример строки",
"placeholder": "вставьте сюда характерную строку лога",
"promptType": "введите образец для проверки шаблона",
"noMatch": "НЕТ СОВПАДЕНИЯ",
"noMatchHint": "шаблон не совпал с этой строкой",
"match": "СОВПАЛО",
"invalid": "REGEX",
"captures": "Группы"
}
},
"triggers": {
"title": "Триггеры событий",
"titleNew": "Новый триггер",
"titleSingular": "Триггер",
"lede": "Фильтруйте записи журнала событий (события деплоев, вывод сканера логов, будущие источники) и отправляйте webhook при совпадении. Фильтры объединяются по И; пустой фильтр означает «совпадает всё».",
"ledeNew": "Создайте правило «фильтр + действие». Диспетчер объединяет фильтры по И. Оставьте поле пустым, чтобы пропустить это измерение.",
"stat": {
"total": "ВСЕГО",
"enabled": "ВКЛЮЧЕНО",
"disabled": "ВЫКЛЮЧЕНО"
},
"toolbar": {
"newButton": "Новый триггер",
"backToList": "К списку триггеров"
},
"empty": {
"heading": "Триггеров пока нет",
"body": "Настройте триггер, чтобы пересылать записи журнала событий в Slack, мост уведомлений или любой HTTP-приёмник. Tinyforge подписывает запросы заголовком X-Hub-Signature-256, если задан секрет.",
"cta": "Создать первый триггер"
},
"list": {
"name": "Имя",
"filters": "Фильтры",
"action": "Действие",
"status": "Статус",
"open": "Открыть"
},
"detail": {
"config": "Конфигурация",
"configSub": "id #{id} · обновлено {updatedAt}",
"dangerZone": "Опасная зона",
"dangerZoneSub": "Удаление триггера происходит сразу. Восстановления нет.",
"sendTest": "Отправить тест",
"sending": "Отправка…",
"testHttp": "HTTP {code}",
"testSigned": "подписано",
"testOk": "OK",
"testFail": "ОШИБКА",
"deleteButton": "Удалить триггер",
"deleteTitle": "Удалить триггер?",
"deleteMessage": "Триггер «{name}» будет удалён немедленно. Действие необратимо."
},
"form": {
"name": "Имя",
"namePlaceholder": "например, Slack #alerts при сбое деплоя",
"required": "ОБЯЗАТЕЛЬНО",
"andComposed": "ОБЪЕДИНЕНИЕ ПО И",
"filtersLabel": "Фильтры",
"actionLabel": "Действие",
"actionWebhookBadge": "WEBHOOK",
"severityCsv": "Уровень (CSV)",
"severityPlaceholder": "warn,error",
"sourceCsv": "Источник (CSV)",
"sourcePlaceholder": "deploy,logscan",
"messageRegex": "Регулярное выражение сообщения (необязательно)",
"messageRegexPlaceholder": "(?i)\\bpanic\\b",
"invalidRegex": "Некорректный regex — сервер отклонит.",
"urlLabel": "URL",
"urlPlaceholder": "https://hooks.slack.com/services/...",
"secretLabel": "HMAC-секрет (необязательно)",
"secretPlaceholder": "оставьте пустым для неподписанной доставки",
"secretHint": "Приёмники проверяют X-Hub-Signature-256 по сырому телу запроса.",
"secretRotateHint": "Хранится в зашифрованном виде. После создания API не возвращает значение — оставьте плейсхолдер без изменений, чтобы сохранить существующий секрет, введите новое значение для смены или очистите и сохраните, чтобы отключить подпись.",
"enabled": "Включён",
"enabledHint": "Выключенные триггеры остаются в таблице, но не срабатывают.",
"submit": "Создать триггер",
"submitting": "Создание…",
"webhookUrl": "URL webhook"
},
"status": {
"enabled": "включён",
"disabled": "выключен"
}
},
"logscan": {
"title": "Правила сканирования логов",
"titleNew": "Новое правило",
"titleSingular": "Правило",
"lede": "Регулярные выражения, которые сканер применяет к потоку логов каждого работающего контейнера. Совпавшие строки попадают в event_log с уровнем правила, откуда триггеры событий передают их на настроенные webhook-приёмники. Включено {enabled} из {total}.",
"ledeNew": "Сканируйте логи контейнеров по регулярному выражению. Оставьте поле «нагрузка» пустым, чтобы создать глобальное правило. Чтобы переопределить глобальное для одной нагрузки, используйте действие «Переопределить» на странице нагрузки.",
"stat": {
"total": "ВСЕГО",
"global": "ГЛОБАЛЬНЫЕ",
"workload": "НАГРУЗКА",
"overrides": "ПЕРЕОПРЕДЕЛЕНИЯ",
"activeTails": "АКТИВНЫХ TAIL",
"droppedBucket": "ЛИМИТ",
"droppedCooldown": "COOLDOWN",
"compileErrors": "ОШИБКИ КОМПИЛЯЦИИ"
},
"stats": {
"heading": "Статистика сканера",
"headingSub": "Счётчики отбрасываний движка и ошибки компиляции из последнего снимка. Счётчики сбрасываются при перезапуске сервера.",
"noCompileErrors": "Все правила компилируются без ошибок.",
"compileErrorsHeading": "Ошибки компиляции (правило отброшено из снимка)",
"tailsExplain": "Сейчас открыто goroutine-tail'ов по контейнерам у менеджера сканера."
},
"toolbar": {
"newButton": "Новое правило",
"backToList": "К списку правил"
},
"filter": {
"all": "ВСЕ",
"global": "ГЛОБАЛЬНЫЕ",
"workload": "НАГРУЗКА",
"overrides": "ПЕРЕОПРЕДЕЛЕНИЯ"
},
"empty": {
"heading": "Правил пока нет",
"body": "Начните с глобального правила вроде (?i)\\bpanic\\b с уровнем error, затем сужайте по нагрузкам через переопределения на странице нагрузки.",
"cta": "Создать первое правило"
},
"list": {
"name": "Имя",
"pattern": "Шаблон",
"scope": "Область",
"severity": "Уровень",
"streams": "Потоки",
"status": "Статус",
"open": "Открыть"
},
"detail": {
"config": "Конфигурация",
"configSub": "id #{id} · область {scope}",
"regexTest": "Проверка regex",
"regexTestSub": "Предпросмотр использует JavaScript-движок regex в браузере. Нажмите «Проверить на сервере», чтобы получить авторитетную проверку Go RE2 — это единственный надёжный сигнал для конструкций, специфичных для RE2.",
"runServerTest": "Проверить на сервере",
"testing": "Проверка…",
"serverTestHint": "Сначала введите пример строки выше",
"serverTestSendHint": "Отправить пример на backend /test",
"serverMatch": "СОВПАЛО (СЕРВЕР)",
"serverNoMatch": "НЕТ СОВПАДЕНИЯ",
"serverNoMatchHint": "серверный regex не совпал с примером",
"serverError": "ОШИБКА",
"dangerZone": "Опасная зона",
"dangerZoneSub": "Удаление глобального правила каскадно удаляет его переопределения для нагрузок.",
"deleteButton": "Удалить правило",
"deleteTitle": "Удалить правило?",
"deleteMessage": "Правило «{name}» будет удалено немедленно. Переопределения по нагрузкам, ссылающиеся на него, также удалятся."
},
"form": {
"name": "Имя",
"namePlaceholder": "например, Panic в воркере",
"pattern": "Шаблон",
"regex": "REGEX",
"patternPlaceholder": "(?i)\\bpanic\\b",
"invalidRegex": "Некорректный regex — сервер отклонит.",
"matchShape": "Параметры совпадения",
"matchShapeOpts": "УРОВЕНЬ · ПОТОКИ · COOLDOWN",
"severity": "Уровень",
"streams": "Потоки",
"cooldown": "Cooldown (с)",
"cooldownHint": "Cooldown — на правило × на контейнер: одно правило, срабатывающее в двух контейнерах, считается независимо. Token bucket ограничивает выдачу на контейнер до 10 событий / 60с, чтобы не переполнить event_log.",
"scope": "Область",
"scopePlaceholder": "пусто для глобального правила или вставьте id нагрузки",
"scopeHint": "Правила области нагрузки применяются только к её контейнерам. Переопределения для отдельных нагрузок проще создавать со страницы нагрузки.",
"scopeGlobal": "Глобально (применяется ко всем нагрузкам)",
"scopePick": "Выбрать нагрузку…",
"scopePickTitle": "Выберите нагрузку",
"scopeClear": "Сделать глобальным",
"scopeSelected": "Нагрузка",
"scopeUnknown": "Неизвестная нагрузка",
"enabled": "Включено",
"enabledHint": "Выключенные правила остаются в таблице, но не срабатывают.",
"required": "ОБЯЗАТЕЛЬНО",
"optional": "НЕОБЯЗАТЕЛЬНО",
"submit": "Создать правило",
"submitting": "Создание…"
},
"scope": {
"global": "глобальное",
"workload": "нагрузка {id}",
"override": "переопределение #{id}",
"overrideShort": "переопр. #{id}"
},
"status": {
"enabled": "включено",
"disabled": "выключено",
"on": "вкл",
"off": "выкл"
},
"panel": {
"heading": "Лог-правила",
"subEmpty": "Для этой нагрузки правил нет",
"subCount": "Действует правил: {count}",
"subCountOne": "Действует 1 правило",
"emptyHint": "Для этой нагрузки нет правил сканирования логов. Создайте через «Новое правило» — глобальные правила применяются автоматически; для этой нагрузки также можно завести свои или переопределения.",
"newRule": "Новое правило",
"footerHint": "Глобальные правила применяются ко всем нагрузкам. Правила нагрузки — только здесь. Переопределения замещают глобальное для этой нагрузки — изменяйте уровень или отключайте их, не трогая исходное глобальное.",
"override": "Переопределить",
"overriding": "Переопределение…",
"overrideTitle": "Создать переопределение глобального правила для этой нагрузки"
}
},
"redeployTriggers": {
"section": "Кузница",
"title": "Триггеры передеплоя",
"titleNew": "Новый триггер",
"titleSingular": "Триггер",
"lede": "Источники сигналов передеплоя — push в registry, события git, ручной запуск, расписания, webhook'и, совпадения в логах. Триггер создаётся один раз и веером раздаёт сигнал всем привязанным к нему нагрузкам.",
"ledeNew": "Выберите вид, дайте имя и решите, могут ли внешние системы дёргать его через webhook. Привязку к нагрузкам делайте со страницы нагрузки после создания.",
"ledeDetail": "Редактируйте конфигурацию триггера, управляйте webhook-приёмом и просматривайте все нагрузки, слушающие этот сигнал.",
"stat": {
"total": "ВСЕГО",
"byKind": "{kind}",
"withWebhook": "С WEBHOOK",
"boundWorkloads": "НАГРУЗОК"
},
"kind": {
"registry": "Registry",
"git": "Git",
"manual": "Ручной",
"schedule": "Расписание",
"webhook": "Webhook",
"logscan": "Лог-скан",
"unknown": "Неизвестный"
},
"kindShort": {
"registry": "REG",
"git": "GIT",
"manual": "MAN",
"schedule": "CRN",
"webhook": "HK",
"logscan": "LOG",
"unknown": "?"
},
"kindHint": {
"registry": "Следит за образом контейнера; срабатывает при push нового тега, подходящего под шаблон.",
"git": "Срабатывает при продвижении указанной ветки или создании тега, подходящего под шаблон.",
"manual": "Срабатывает только через кнопку Deploy на странице нагрузки или POST /workloads/{id}/deploy.",
"schedule": "Срабатывает по фиксированному cron-расписанию.",
"webhook": "Чистый webhook — срабатывает при обращении к URL приёма.",
"logscan": "Срабатывает, когда правило сканирования логов совпадает со строкой.",
"unknown": "Неизвестный вид триггера — используйте сырой JSON-редактор."
},
"toolbar": {
"newButton": "Новый триггер",
"backToList": "К списку триггеров"
},
"filter": {
"all": "ВСЕ",
"ariaLabel": "Фильтр по виду"
},
"empty": {
"heading": "Триггеров пока нет",
"body": "Триггер — источник сигнала передеплоя: registry-watcher, git-hook, ручная кнопка, расписание или webhook. Создайте один и привяжите к скольким угодно нагрузкам.",
"cta": "Создать первый триггер"
},
"list": {
"name": "Имя",
"kind": "Вид",
"bindings": "Нагрузки",
"webhook": "Webhook",
"created": "Создан",
"open": "Открыть",
"webhookOn": "ВКЛ",
"webhookOff": "—",
"noBindings": "—",
"bindingsCount": "{count}"
},
"detail": {
"config": "Конфигурация триггера",
"configSub": "вид {kind} · id {id} · обновлено {updatedAt}",
"webhook": "Webhook-приём",
"webhookSub": "Когда включено, внешние системы могут дёргать триггер по URL ниже. Каждая привязанная нагрузка будет передеплоена по очереди.",
"webhookEnable": "Включить webhook-приём",
"webhookEnableHint": "Когда выключено, триггер срабатывает только из внутренних источников (по конфигу его вида) и кнопки ручного деплоя.",
"webhookRequireSig": "Требовать HMAC-подпись",
"webhookRequireSigHint": "Отклонять запросы без корректного X-Hub-Signature-256. Рекомендуется, если URL доступен из публичной сети.",
"webhookUrlLabel": "URL приёма",
"webhookUrlNote": "Вставьте это в настройки CI / registry / webhook GitHub. Сегмент-секрет — это пароль, обращайтесь как с паролем.",
"webhookCopy": "Копировать",
"webhookCopied": "Скопировано",
"webhookRotate": "Сменить секрет",
"webhookRotating": "Смена…",
"webhookDisabledNote": "Webhook-приём выключен. Включите тумблер, сохраните — и URL появится здесь.",
"bindings": "Привязанные нагрузки",
"bindingsSub": "Все нагрузки, слушающие этот триггер. Чтобы привязать новую нагрузку, откройте её страницу и добавьте этот триггер оттуда.",
"bindingsEmpty": "К этому триггеру пока не привязана ни одна нагрузка. Откройте нагрузку и привяжите этот триггер из её панели «Триггеры».",
"bindingsListItem": {
"openWorkload": "Открыть нагрузку",
"unbind": "Отвязать"
},
"bindingEnabledHint": "Выключите, чтобы оставить привязку, но запретить триггеру передеплоить эту нагрузку.",
"dangerZone": "Опасная зона",
"dangerZoneSub": "Удаление триггера происходит сразу. Все привязки к нему удаляются каскадом.",
"deleteButton": "Удалить триггер",
"deleteTitle": "Удалить триггер?",
"deleteMessage": "Триггер «{name}» будет удалён немедленно вместе с {count} привязкой(-ами). Действие необратимо.",
"rotateTitle": "Сменить секрет webhook?",
"rotateMessage": "Текущий URL приёма перестанет работать сразу. После смены обновите URL во всех внешних интеграциях.",
"rotateConfirm": "Сменить",
"unbindTitle": "Отвязать нагрузку?",
"unbindMessage": "Нагрузка «{name}» перестанет передеплоиваться при срабатывании этого триггера. Сама нагрузка не удаляется.",
"unbindConfirm": "Отвязать",
"lastFired": "Последний запуск",
"lastFiredNever": "Ни разу не срабатывал",
"scheduleStatus": "Состояние расписания",
"scheduleStatusSub": "Рабочее состояние внутреннего планировщика для этого триггера. «Запустить сейчас» сдвигает следующий запуск и начинает отсчёт нового интервала с этого момента.",
"fireNow": "Запустить сейчас",
"fireNowTitle": "Запустить триггер немедленно и сбросить окно следующего срабатывания.",
"fireNowDisabledTitle": "Привяжите хотя бы одну нагрузку перед запуском.",
"firing": "Запуск…",
"fireConfirmTitle": "Запустить триггер расписания?",
"fireConfirmMessage": "Триггер «{name}» сработает немедленно и развернёт {count} связанных нагрузок. Следующий естественный запуск будет через полный интервал от текущего момента.",
"fireConfirm": "Запустить",
"fireResult": "Сработал · задеплоено {deployed}/{bindings} · ошибок {errored}"
},
"form": {
"kindLabel": "Вид",
"kindHint": "Выберите источник сигнала передеплоя. Форма ниже подстраивается под вид.",
"name": "Имя",
"namePlaceholder": "например, ghcr.io/me/api · main",
"required": "ОБЯЗАТЕЛЬНО",
"configLabel": "Конфигурация",
"image": "Ссылка на образ",
"imagePlaceholder": "registry.example.com/owner/app",
"imageHint": "Полная ссылка на образ без тега — Tinyforge ловит новые теги, выкладываемые под этой ссылкой.",
"tagPattern": "Шаблон тега",
"tagPatternPlaceholder": "*",
"tagPatternHint": "Glob path.Match (например, v*, release-*). * совпадает с любым тегом.",
"repo": "Репозиторий",
"repoPlaceholder": "owner/name",
"repoHint": "owner/name в формате git-хостинга, не зависит от провайдера.",
"mode": "Режим",
"modePush": "Push в ветку",
"modeTag": "Создание тега",
"branch": "Ветка",
"branchPlaceholder": "main",
"branchHint": "Только push'и, продвигающие эту ветку, дёргают триггер.",
"manualNote": "У ручных триггеров нет конфига. Они срабатывают только через кнопку Deploy на странице нагрузки или POST /workloads/{id}/deploy.",
"scheduleNote": "Срабатывает по фиксированному интервалу, который ведёт внутренний планировщик Tinyforge. Внешний webhook не нужен — включите его ниже только если CI тоже должен запускать триггер вручную.",
"intervalPresets": "Быстрые пресеты",
"intervalPreset": {
"hourly": "Каждый час",
"daily": "Каждый день",
"weekly": "Каждую неделю"
},
"interval": "Интервал",
"intervalHint": "Длительность в формате Go (например «30m», «6h», «24h», «168h»). Минимум 1 минута.",
"scheduleReference": "Фиксированная ссылка (опционально)",
"scheduleReferencePlaceholder": "stable",
"scheduleReferenceHint": "Опциональный тег, ветка или ревизия, которые источник будет подтягивать на каждом срабатывании. Оставьте пустым, чтобы использовать значение по умолчанию.",
"unknownNote": "У этого вида ещё нет встроенной формы. Используйте JSON-редактор ниже; сервер валидирует форму.",
"advancedToggle": "Расширенный JSON",
"advancedHint": "Запасной вариант для опытных пользователей — заменяет структурированную форму сырым payload'ом.",
"configJson": "JSON конфигурации",
"configJsonHint": "Должен распарситься как корректный JSON-объект. Структура проверяется сервером по виду.",
"invalidJson": "Некорректный JSON — сервер отклонит.",
"webhookEnabled": "Включить webhook-приём сразу",
"webhookEnabledHint": "Генерирует секретный URL, по которому внешние системы могут дёргать триггер.",
"webhookRequireSig": "Требовать HMAC-подпись",
"webhookRequireSigHint": "Отклонять неподписанные запросы. Секрет — тот же, что вшит в URL — подпишите тело HMAC-SHA256 и пришлите в X-Hub-Signature-256.",
"submit": "Создать триггер",
"submitting": "Создание…",
"cancel": "Отмена"
},
"binding": {
"enabled": "Включена",
"disabled": "Выключена"
}
},
"apps": {
"list": {
"eyebrowSuffix": "ПРИЛОЖЕНИЯ",
"title": "Приложения",
"ledePrefix": "Plugin-native деплои —",
"ledeKindImage": "image",
"ledeKindCompose": "compose",
"ledeKindStatic": "static",
"ledeMiddle": ", или",
"ledeSuffix": ", с подключаемыми триггерами передеплоя. Старые проекты, стеки и сайты на время переезда остаются в собственных разделах.",
"statTotal": "ВСЕГО",
"statImage": "IMAGE",
"statCompose": "COMPOSE",
"statStatic": "STATIC",
"refresh": "Обновить",
"newApp": "Новое приложение",
"filterAriaLabel": "Фильтр по source-плагину",
"filterAll": "ВСЕ",
"loadError": "Не удалось загрузить приложения",
"alertTag": "ОШ",
"emptyTitle": "Приложений пока нет",
"emptyBody": "Приложения объединяют image, compose и static-деплои за единым plugin-driven интерфейсом. Создайте первое, чтобы увидеть его здесь.",
"emptyCta": "Создать первое приложение",
"colName": "Имя",
"colSource": "Источник",
"colTrigger": "Триггер",
"colCreated": "Создано",
"colActions": "Действия",
"rowOpen": "Открыть"
},
"new": {
"pageTitle": "Новое приложение · Tinyforge",
"backLabel": "К приложениям",
"eyebrowSuffix": "НОВОЕ ПРИЛОЖЕНИЕ",
"title": "Создать приложение",
"ledePrefix": "Создайте plugin-native нагрузку.",
"ledeSourceLabel": "Источник",
"ledeSourceMid": "= как она деплоится (image, compose, static). Выберите или создайте",
"ledeTriggerLabel": "триггер",
"ledeSuffix": "ниже — при срабатывании source-плагин передеплоит приложение.",
"loadingKinds": "Загрузка доступных видов плагинов…",
"alertTag": "ОШ",
"fieldName": "Имя",
"fieldNameRequired": "ОБЯЗАТЕЛЬНО",
"fieldNamePlaceholder": "my-app",
"fieldNameHint": "В нижнем регистре, без пробелов. Используется в именах контейнеров и поддоменах.",
"fieldSourcePlugin": "Source-плагин",
"fieldSourceLabel": "Источник",
"fieldSourceHint": "Список берётся из запущенного демона — видны только вкомпилированные плагины. Триггеры (registry / git / manual) настраиваются ниже как отдельные записи.",
"fieldSourceConfig": "Конфигурация источника",
"fieldConfigYaml": "YAML",
"fieldConfigForm": "ФОРМА",
"fieldConfigJson": "JSON",
"advancedJson": "Расширенный JSON",
"backToForm": "К форме",
"resetSample": "Сбросить к примеру",
"switchToJsonTitle": "Переключиться на сырой JSON-редактор",
"switchToFormTitle": "Вернуться к форме",
"jsonOk": "JSON ОК",
"jsonInvalid": "JSON НЕВЕРНЫЙ",
"linesUnit": "строк",
"composeHeader": "compose.yaml · compose",
"composeAriaLabel": "Compose YAML",
"composeProjectLabel": "Имя compose-проекта (опционально)",
"composeProjectPlaceholder": "(по умолчанию — нормализованное имя нагрузки)",
"composePlaceholder": "services:\n web:\n image: nginx:alpine\n ports:\n - \"80\"",
"imageHeader": "image-источник · параметры рантайма",
"imageRefLabel": "Образ (путь в реестре)",
"imageRefPlaceholder": "registry.example.com/owner/app",
"imageRefHint": "Полная ссылка без тега; тег задаётся при деплое — триггером или полем «Тег по умолчанию» ниже.",
"imagePort": "Порт",
"imageHealthcheck": "Путь healthcheck",
"imageDefaultTag": "Тег по умолчанию",
"imageRegistryLabel": "Реестр (для приватных pull-ов)",
"imageRegistryPublic": "(публичный — без авторизации)",
"imageRegistryHint": "Имя должно совпадать с записью на странице «Реестры». Оставьте пустым для публичных образов.",
"imageCpu": "Лимит CPU (ядра, 0 = ∞)",
"imageMemory": "Лимит памяти (МБ, 0 = ∞)",
"imageMax": "Макс. инстансов",
"imageMaxHint": "1 = строгий blue-green.",
"imageFoot": "Переменные окружения и тома задаются в отдельных панелях на странице нагрузки после создания.",
"staticHeader": "static-источник · страницы из репозитория",
"staticProvider": "Провайдер",
"staticBaseUrl": "Base URL",
"staticBaseUrlPlaceholder": "https://git.example.com",
"staticRepoOwner": "Владелец репозитория",
"staticRepoOwnerPlaceholder": "owner",
"staticRepoName": "Имя репозитория",
"staticRepoNamePlaceholder": "pages",
"staticBranch": "Ветка",
"staticBranchPlaceholder": "main",
"staticFolder": "Папка (опционально)",
"staticFolderPlaceholder": "(корень репозитория)",
"staticToken": "Токен доступа (приватные репозитории)",
"staticTokenPlaceholder": "(оставьте пустым для публичных репозиториев)",
"staticTokenHint": "Шифруется при хранении. Нужен только для приватных репозиториев.",
"staticMode": "Режим",
"staticModeStaticDesc": "— раздача файлов через nginx; ноль рантайм-оверхеда.",
"staticModeDenoDesc": "— Deno-рантайм с опциональной динамической маршрутизацией.",
"staticRenderMarkdown": "Рендерить markdown",
"staticRenderMarkdownDesc": "— автоматически отдавать <code>.md</code> файлы как HTML-страницы.",
"staticFoot": "Секрет вебхука для git push-триггеров появляется в панели вебхука нагрузки после создания.",
"staticDetectProvider": "Определить",
"staticDetectedOk": "Определено: {provider}",
"staticDetectedFailed": "Не удалось определить: {error}",
"staticTestConnection": "Проверить соединение",
"staticConnectionOk": "Соединение установлено",
"staticConnectionFailed": "Ошибка соединения: {error}",
"staticBrowseRepos": "Выбрать репозиторий",
"staticBrowseBranches": "Выбрать ветку",
"staticBrowseFolders": "Выбрать папку",
"staticPickerRepoTitle": "Выбор репозитория",
"staticPickerRepoPlaceholder": "Фильтр репозиториев…",
"staticPickerBranchTitle": "Выбор ветки",
"staticPickerBranchPlaceholder": "Фильтр веток…",
"staticFolderRoot": "/ (корень)",
"staticFolderSelectedPrefix": "Выбранная папка:",
"staticTreeLoading": "Загрузка дерева папок…",
"staticTreeEmpty": "В этой ветке нет папок.",
"staticDenoAutoDetected": "Обнаружена папка <code>api/</code> — режим автоматически переключён на Deno.",
"imageConflictTag": "ОБРАЗ УЖЕ ИСПОЛЬЗУЕТСЯ",
"imageConflictHeading": "Этот образ уже используется в {count} нагрузке(ах):",
"imageConflictOpenBtn": "Открыть",
"imageConflictAcknowledgeNote": "Если это намеренно (например, отдельный этап), нажмите «Создать» ещё раз для продолжения.",
"imageConflictBlockedSubmit": "Обнаружены конфликты по этому образу — изучите список выше и нажмите «Создать» повторно для продолжения.",
"sourceConfigJsonTitle": "source_config.json · {kind}",
"sourceConfigJsonAria": "Конфигурация source-плагина (JSON)",
"triggerNumLabel": "Триггер",
"triggerNumOptional": "ОПЦИОНАЛЬНО",
"triggerNewTag": "НОВЫЙ",
"triggerPickTag": "ВЫБРАТЬ",
"triggerSkipTag": "ПРОПУСК",
"noteSkipTag": "ПРОПУСК",
"noteEmptyTag": "∅",
"faceLabel": "Публичный фронт",
"faceOptional": "ОПЦИОНАЛЬНО",
"faceSubdomain": "Поддомен",
"faceSubdomainPlaceholder": "myapp",
"faceDomain": "Домен",
"faceDomainPlaceholder": "(наследуется из настроек)",
"facePort": "Целевой порт",
"faceHint": "Оставьте пустым, чтобы не создавать прокси-маршрут. Заполнение любого поля создаст одну запись фронта, привязанную к этой нагрузке.",
"cancel": "Отмена",
"submit": "Создать приложение",
"submitting": "Создание…",
"submitAnyway": "Всё равно создать",
"errors": {
"detectionFailed": "Не удалось определить провайдера.",
"connectionFailed": "Ошибка соединения.",
"reposFailed": "Не удалось загрузить репозитории.",
"branchesFailed": "Не удалось загрузить ветки.",
"treeFailed": "Не удалось загрузить дерево папок.",
"sourceConfigInvalid": "source_config не является корректным JSON.",
"triggerBindUnknown": "неизвестная ошибка",
"createFailed": "Не удалось создать нагрузку.",
"inspectFailed": "Не удалось проинспектировать образ."
},
"imageInspect": "Инспектировать",
"imageInspectHint": "Подставляет порт и healthcheck из образа, чтобы не вводить вручную.",
"imageInspectOk": "Готово — порт и healthcheck подставлены.",
"imageInspectError": "Ошибка инспекции: {error}",
"triggers": {
"section": "Триггер",
"sectionSub": "Необязательно. Выберите, откуда придёт сигнал передеплоя — слежение за реестром, git-событие или ручная кнопка.",
"modeInline": "Создать триггер",
"modeInlineHint": "Создаёт новую запись триггера, привязанную к этому приложению — подходит для частого случая 1:1.",
"modePick": "Выбрать существующий",
"modePickHint": "Привязать существующий триггер, чтобы несколько приложений делили один сигнал.",
"modeSkip": "Пропустить — добавить позже",
"modeSkipHint": "Приложение создаётся без привязки триггера. Ручной деплой по-прежнему работает.",
"switchToPick": "Выбрать существующий →",
"switchToInline": "← Создать новый триггер",
"switchToSkip": "Пропустить",
"pickPlaceholder": "Выберите триггер…",
"pickEmpty": "Триггеров ещё нет — создайте один выше или перейдите в /triggers.",
"pickLabel": "Существующий триггер",
"pickHint": "Один триггер можно привязать к нескольким приложениям. Управление автономными триггерами — в /triggers.",
"pickWebhookOn": "ВЕБХУК ВКЛ",
"skippedNote": "Триггер не будет привязан. Добавьте его из панели «Триггеры» в карточке приложения после создания.",
"bindError": "Приложение создано, но привязка триггера не удалась: {error}. Откройте панель «Триггеры» в карточке, чтобы повторить."
}
},
"detail": {
"pageTitleFallback": "Приложение",
"backLabel": "К приложениям",
"eyebrowSuffix": "ПРИЛОЖЕНИЕ",
"kickerId": "id: {id}",
"loading": "Загрузка нагрузки…",
"loadError": "Не удалось загрузить приложение",
"deployError": "Деплой не удался",
"saveError": "Сохранение не удалось",
"deleteError": "Удаление не удалось",
"runtimeState": {
"title": "Статус синхронизации",
"sub": "Последняя успешная синхронизация репозитория и текущее состояние контейнера.",
"status": "Статус",
"lastCommit": "Последний коммит",
"lastSync": "Последняя синхронизация",
"container": "Контейнер",
"neverDeployed": "Ещё не разворачивалось. Нажмите «Деплой», чтобы опубликовать впервые.",
"loading": "Загрузка состояния…"
},
"storage": {
"title": "Постоянное хранилище",
"sub": "Смонтировано в /app/data внутри контейнера.",
"used": "Использовано",
"limit": "Лимит",
"unlimited": "без лимита",
"unavailable": "Не удалось получить размер (контейнер мог быть остановлен).",
"loading": "Вычисление размера…"
},
"toolbar": {
"stop": "Стоп",
"start": "Старт",
"openSite": "Открыть",
"more": "Ещё"
},
"liveBadge": {
"running": "РАБОТАЕТ",
"transitioning": "ПЕРЕХОД",
"stopped": "ОСТАНОВЛЕНО",
"notDeployed": "НЕ РАЗВЁРНУТО",
"mixed": "СМЕШАННО · {running}/{total} РАБОТАЕТ"
},
"stats": {
"title": "Ресурсы",
"sub": "CPU и память запущенного контейнера.",
"subMany": "CPU и память по каждому из {count} контейнеров."
},
"webhooks": {
"title": "Привязки вебхуков",
"sub": "Триггеры, привязанные к приложению — управление URL и подписями на странице триггера.",
"openTrigger": "Открыть триггер",
"disabled": "отключён",
"empty": "К приложению не привязан ни один триггер."
},
"errors": {
"stopFailed": "Не удалось остановить.",
"stopNothing": "Останавливать нечего — нет запущенного контейнера.",
"stopAllFailed": "Остановка не удалась — все контейнеры отклонили запрос.",
"startFailed": "Не удалось запустить.",
"startNothing": "Запускать нечего — сначала выполните Деплой, чтобы создать контейнер.",
"startAllFailed": "Запуск не удался — все контейнеры отклонили запрос.",
"runtimeStateFailed": "Не удалось загрузить состояние.",
"storageFailed": "Не удалось загрузить размер хранилища."
},
"alertTag": "ОШ",
"createdAt": "создано",
"refreshLabel": "Обновить",
"editButton": "Изменить",
"deleteButton": "Удалить",
"editTitle": "Редактирование конфигурации",
"editSubPrefix": "Источник",
"editSubSuffix": "· триггеры настраиваются в панели «Триггеры» ниже",
"editFieldName": "Имя",
"editFieldParent": "Родительская нагрузка",
"editFieldOptional": "ОПЦИОНАЛЬНО",
"editFieldParentPlaceholder": "UUID нагрузки (пусто для корневой)",
"editSourceConfig": "Конфигурация источника",
"editConfigYaml": "YAML",
"editConfigForm": "ФОРМА",
"editConfigJson": "JSON",
"advancedJson": "Расширенный JSON",
"backToForm": "К форме",
"switchToJsonTitle": "Переключиться на сырой JSON-редактор",
"switchToFormTitle": "Вернуться к форме",
"jsonOk": "JSON ОК",
"jsonInvalid": "JSON НЕВЕРНЫЙ",
"editComposeProject": "Имя compose-проекта (опционально)",
"editComposeProjectPlaceholder": "(по умолчанию — нормализованное имя нагрузки)",
"editComposePlaceholder": "services:\n web:\n image: nginx:alpine",
"editComposeAria": "Compose YAML",
"editComposeHeader": "compose.yaml",
"editImageHeader": "image-источник · параметры рантайма",
"editImageRef": "Образ (путь в реестре)",
"editImageRefPlaceholder": "registry.example.com/owner/app",
"editImagePort": "Порт",
"editImageHealthcheck": "Путь healthcheck",
"editImageDefaultTag": "Тег по умолчанию",
"editImageRegistry": "Реестр (для приватных pull-ов)",
"editImageRegistryPublic": "(публичный — без авторизации)",
"editImageCpu": "Лимит CPU (ядра, 0 = ∞)",
"editImageMemory": "Лимит памяти (МБ, 0 = ∞)",
"editImageMax": "Макс. инстансов",
"editImageFoot": "Переменные окружения и тома живут в своих панелях ниже — сохранение здесь их не затронет.",
"editStaticHeader": "static-источник · страницы из репозитория",
"editStaticProvider": "Провайдер",
"editStaticBaseUrl": "Base URL",
"editStaticBaseUrlPlaceholder": "https://git.example.com",
"editStaticRepoOwner": "Владелец репозитория",
"editStaticRepoName": "Имя репозитория",
"editStaticBranch": "Ветка",
"editStaticFolder": "Папка (опционально)",
"editStaticFolderPlaceholder": "(корень репозитория)",
"editStaticToken": "Токен доступа (приватные репозитории)",
"editStaticTokenPlaceholder": "(оставьте пустым для публичных репозиториев)",
"editStaticMode": "Режим",
"editStaticModeStaticDesc": "— раздача файлов через nginx.",
"editStaticModeDenoDesc": "— Deno-рантайм с динамической маршрутизацией.",
"editStaticRenderMarkdown": "Рендерить markdown",
"editStaticRenderMarkdownDesc": "— автоматически отдавать <code>.md</code> как HTML.",
"editSourceJsonHeader": "source_config.json",
"editSourceJsonAria": "Конфигурация source-плагина (JSON)",
"editPublicFaces": "Публичные фронты",
"editPublicFacesTag": "JSON МАССИВ",
"editPublicFacesHeader": "public_faces.json",
"editPublicFacesAria": "Конфигурация публичных фронтов (JSON-массив)",
"editCancel": "Отмена",
"editSave": "Сохранить",
"editSaving": "Сохранение…",
"manualDeployTitle": "Ручной деплой",
"manualDeployOk": "ОК",
"manualDeployDispatched": "Отправлено {reference} как {by}",
"manualDeployRefAria": "Референс для деплоя",
"manualDeployRefPlaceholder": "референс (тег образа, git sha; пусто — по умолчанию)",
"manualDeployButton": "Деплой",
"manualDeployDispatching": "Отправка…",
"manualDeployHint": "Задайте конкретный тег образа, git sha или ветку, чтобы принудительно деплоить. Оставьте пустым, чтобы source-плагин выбрал референс по умолчанию.",
"containersTitle": "Контейнеры",
"containersEmpty": "Контейнеров пока нет",
"containersCount": "{count} согласовано",
"containersEmptyInline": "Контейнеров пока нет — задеплойте, чтобы поднять первый.",
"containersColRole": "Роль",
"containersColState": "Состояние",
"containersColImage": "Образ",
"containersColSubdomain": "Поддомен",
"containersColLastSeen": "Последний раз виден",
"containersColActions": "Действия",
"containersLogsAction": "Логи",
"chainTitle": "Цепочка",
"chainSubFromParent": "продвигается от родителя",
"chainSubParentOf": "родитель для",
"chainChildSingular": "дочерней нагрузки",
"chainChildPlural": "дочерних нагрузок",
"chainParentLabel": "Родитель",
"chainSelfLabel": "Эта",
"chainChildrenLabel": "Дочерние",
"chainPromoteButton": "Продвинуть от родителя",
"chainPromoting": "Продвижение…",
"chainHint": "Задайте <code>parent_workload_id</code> у нагрузки, чтобы построить цепочку. Дочерние image-источники могут одним кликом продвинуть текущий запущенный тег родителя.",
"volumesTitle": "Тома",
"volumesEmpty": "Нет монтирований",
"volumesCountSingular": "{count} монтирование",
"volumesCountPlural": "{count} монтирований",
"volumesColTarget": "Цель",
"volumesColSource": "Источник",
"volumesColScope": "Скоуп",
"volumesColUpdated": "Обновлено",
"volumesColActions": "Действия",
"volumeSource": "Источник (хост)",
"volumeSourcePlaceholder": "/srv/data/myapp",
"volumeTarget": "Цель (контейнер)",
"volumeTargetPlaceholder": "/data",
"volumeScope": "Скоуп",
"volumeAddButton": "Добавить / Заменить",
"volumeSaving": "Сохранение…",
"volumeHint": "Абсолютные монтирования прокидывают путь хоста в контейнер. Другие скоупы приняты для будущих сценариев; сегодня при деплое применяется только absolute.",
"volumeTargetError": "Цель должна быть абсолютным путём в контейнере (например, /data)",
"volumeSetFailed": "Не удалось задать том",
"volumeDeleteFailed": "Не удалось удалить том",
"envTitle": "Окружение",
"envEmpty": "Нет переопределений",
"envCountSingular": "{count} переопределение",
"envCountPlural": "{count} переопределений",
"envColKey": "Ключ",
"envColValue": "Значение",
"envColUpdated": "Обновлено",
"envColActions": "Действия",
"envEncrypted": "ЗАШИФРОВАНО",
"envKey": "Ключ",
"envKeyPlaceholder": "DATABASE_URL",
"envValue": "Значение",
"envValuePlaceholder": "(пусто — снять)",
"envEncryptLabel": "Шифровать при хранении",
"envAddButton": "Добавить / Заменить",
"envSaving": "Сохранение…",
"envHint": "Зашифрованные значения после записи доступны только на запись — API скрывает их при чтении. Ротация — установка нового значения.",
"envKeyRequired": "Ключ обязателен",
"envSetFailed": "Не удалось задать переменную",
"envDeleteFailed": "Не удалось удалить переменную",
"sourceConfigTitle": "Конфигурация источника",
"sourceConfigCopy": "Копировать",
"sourceConfigCopied": "Скопировано",
"sourceConfigCopyAria": "Копировать конфиг источника",
"publicFacesTitle": "Публичные фронты",
"publicFacesCopyAria": "Копировать публичные фронты",
"deleteConfirmTitle": "Удалить это приложение?",
"deleteConfirmMessage": "Сносит все контейнеры и прокси-маршруты, принадлежащие «{name}», затем удаляет запись. Это нельзя отменить.",
"deleteConfirmFallbackName": "эта нагрузка",
"deleteConfirmYes": "Да, удалить",
"deleteConfirmDeleting": "Удаление…",
"manualDeploySub": "Обходит настроенные триггеры и отправляет деплой напрямую через source-плагин.",
"chainTriggersZero": "без триггеров",
"chainTriggersOne": "1 триггер",
"chainTriggersMany": "{count} триггер(ов)",
"bindings": {
"title": "Триггеры",
"subEmpty": "Триггеры не привязаны. Ручной деплой работает — добавьте триггер, чтобы подключить передеплой по реестру / git / вебхуку.",
"subCount": "{count} привязанный триггер",
"subCountMany": "{count} привязанных триггеров",
"addButton": "Добавить триггер",
"openTrigger": "Открыть триггер",
"unbindAction": "Отвязать",
"rowEnabled": "Включён",
"rowDisabled": "Выключен",
"rowEnableHint": "Отключите, чтобы сохранить привязку, но остановить передеплой этого приложения.",
"loading": "Загрузка триггеров…",
"loadError": "Не удалось загрузить привязки триггеров",
"unbindTitle": "Отвязать триггер?",
"unbindMessage": "Триггер «{name}» перестанет передеплоить это приложение. Сам триггер не удаляется — он остаётся в /triggers и сохраняет привязки к другим приложениям.",
"unbindConfirm": "Отвязать",
"modal": {
"title": "Добавить триггер",
"subtitle": "Привяжите триггер к этому приложению — создайте новый или выберите существующий, чтобы делить его.",
"tabInline": "Создать новый",
"tabPick": "Выбрать существующий",
"submitInline": "Создать и привязать",
"submitPick": "Привязать",
"submitting": "Привязка…",
"cancel": "Отмена",
"error": "Не удалось привязать",
"pickPlaceholder": "Выберите триггер…",
"pickEmpty": "Триггеров ещё нет — переключитесь на «Создать новый», чтобы добавить.",
"pickLabel": "Существующий триггер",
"pickKind": "Фильтр по виду",
"pickKindAll": "Все виды"
},
"override": {
"toggle": "Переопределить",
"title": "Переопределения привязки",
"subtitle": "Переопределите поля конфига триггера только для этого приложения. Верхнеуровневые ключи отсюда побеждают; остальное наследуется из триггера.",
"badgeOne": "ПЕРЕОПРЕДЕЛЕНО: 1 ПОЛЕ",
"badgeMany": "ПЕРЕОПРЕДЕЛЕНО ПОЛЕЙ: {count}",
"badgeTitle": "Эта привязка переопределяет одно или несколько полей конфига триггера.",
"baseLabel": "Конфиг триггера",
"baseLoading": "Загрузка конфига триггера…",
"baseHint": "Конфиг родительского триггера в режиме чтения. Редактируйте его на странице триггера, если изменения нужны для всех привязок.",
"editLabel": "Переопределение (JSON-объект)",
"editHint": "Слияние по верхнему уровню: переопределяются только указанные здесь ключи. Оставьте {} — будет наследоваться без изменений.",
"previewLabel": "Итоговый конфиг",
"previewHint": "Предпросмотр того, что увидит эта привязка при срабатывании триггера (конфиг триггера + наложенное переопределение).",
"invalidJson": "Переопределение должно быть JSON-объектом.",
"tooLarge": "Размер переопределения — {size} Б, превышает серверный лимит {limit} Б.",
"errInvalidJson": "Нельзя сохранить: переопределение не является валидным JSON-объектом.",
"errTooLarge": "Нельзя сохранить: переопределение превышает серверный лимит 8 КиБ.",
"saveButton": "Сохранить переопределение",
"saving": "Сохранение…",
"resetButton": "Сбросить к наследованию",
"closeButton": "Закрыть"
}
}
}
}
}