feat: Forge design system app-wide + Stacks i18n
Build / build (push) Successful in 10m47s

Promotes the Forge visual language from the Stacks feature into a
global design system used across the app:

- app.css: Forge utilities (dot-grid backdrop, eyebrow, ember,
  display/lede, status pills, stat grid, panels, registration
  marks, alert, terminal, buttons). CSS variables alias the forge
  display font to the app's standard sans stack (Inter, now
  properly self-hosted via @fontsource/inter).
- +layout.svelte: reskinned sidebar brand, active nav rail,
  mobile top bar, global h1/h2 typography overrides, main dot-grid
  backdrop.
- Shared components reskinned: EmptyState (breathing-ember empty
  mark), StatusBadge (mono pills with pulse), ConfirmDialog
  (registration marks + forge buttons).
- Dashboard (+page.svelte): ForgeHero header, forge-stat-grid,
  Instrument-style section titles with accent.
- New ForgeHero component for reusable hero headers.

Stacks feature fully localized (EN + RU):
- 80+ keys under stacks.* covering list, new, detail, revisions,
  logs, errors, status labels, delete/rollback dialogs.
- Russian uses forge vocabulary (куются/наковальня/куём/etc).
- $t() wired through all three Stacks pages.
This commit is contained in:
2026-04-16 04:17:42 +03:00
parent 75424a5f25
commit 0fd92fdfa3
14 changed files with 1251 additions and 343 deletions
+85 -16
View File
@@ -25,12 +25,6 @@
oncancel
}: Props = $props();
const confirmClass = $derived(
confirmVariant === 'danger'
? 'bg-[var(--color-danger)] hover:bg-[var(--color-danger-dark)] focus-visible:outline-[var(--color-danger)]'
: 'bg-[var(--color-brand-600)] hover:bg-[var(--color-brand-700)] focus-visible:outline-[var(--color-brand-600)]'
);
const iconBgClass = $derived(
confirmVariant === 'danger'
? 'bg-[var(--color-danger-light)]'
@@ -50,28 +44,29 @@
<!-- Dialog -->
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="w-full max-w-md rounded-2xl bg-[var(--surface-card)] p-6 shadow-xl animate-scale-in">
<div class="dlg w-full max-w-md animate-scale-in">
<span class="dlg-reg dlg-reg-tl"></span>
<span class="dlg-reg dlg-reg-tr"></span>
<span class="dlg-reg dlg-reg-bl"></span>
<span class="dlg-reg dlg-reg-br"></span>
<div class="flex items-start gap-4">
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full {iconBgClass}">
<IconAlert size={20} class={iconColorClass} />
</div>
<div class="flex-1">
<h3 class="text-lg font-semibold text-[var(--text-primary)]">{title}</h3>
<p class="mt-2 text-sm text-[var(--text-secondary)] leading-relaxed">{message}</p>
<h3 class="dlg-title">{title}</h3>
<p class="dlg-msg">{message}</p>
</div>
</div>
<div class="mt-6 flex justify-end gap-3">
<button
type="button"
class="rounded-lg px-4 py-2 text-sm font-medium text-[var(--text-secondary)] hover:bg-[var(--surface-card-hover)] transition-colors active:animate-press"
onclick={oncancel}
>
<div class="mt-6 flex justify-end gap-2">
<button type="button" class="forge-btn-ghost" onclick={oncancel}>
{$t('common.cancel')}
</button>
<button
type="button"
class="rounded-lg px-4 py-2 text-sm font-medium text-white {confirmClass} shadow-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 active:animate-press"
class="dlg-confirm {confirmVariant}"
onclick={onconfirm}
>
{confirmLabel}
@@ -80,3 +75,77 @@
</div>
</div>
{/if}
<style>
.dlg {
position: relative;
background: var(--surface-card);
border: 1px solid var(--border-primary);
border-radius: var(--radius-2xl);
padding: 1.5rem;
box-shadow: var(--shadow-md);
}
.dlg-reg {
position: absolute;
width: 10px; height: 10px;
border-color: var(--color-brand-500);
border-style: solid;
border-width: 0;
pointer-events: none;
}
.dlg-reg-tl { top: -1px; left: -1px; border-top-width: 2px; border-left-width: 2px; border-top-left-radius: var(--radius-2xl); }
.dlg-reg-tr { top: -1px; right: -1px; border-top-width: 2px; border-right-width: 2px; border-top-right-radius: var(--radius-2xl); }
.dlg-reg-bl { bottom: -1px; left: -1px; border-bottom-width: 2px; border-left-width: 2px; border-bottom-left-radius: var(--radius-2xl); }
.dlg-reg-br { bottom: -1px; right: -1px; border-bottom-width: 2px; border-right-width: 2px; border-bottom-right-radius: var(--radius-2xl); }
.dlg-title {
font-family: var(--font-family-sans);
font-weight: 700;
font-size: 1.15rem;
letter-spacing: -0.01em;
line-height: 1.25;
color: var(--text-primary);
margin: 0;
}
.dlg-msg {
font-size: 0.9rem;
color: var(--text-secondary);
line-height: 1.5;
margin: 0.5rem 0 0;
}
.dlg-confirm {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.55rem 1rem;
border: 0;
border-radius: var(--radius-lg);
font-family: var(--forge-mono);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #fff;
cursor: pointer;
transition: transform 150ms ease, box-shadow 150ms ease;
}
.dlg-confirm.primary {
background: var(--color-brand-600);
box-shadow: 0 0 0 0 var(--forge-glow);
}
.dlg-confirm.primary:hover {
background: var(--color-brand-700);
transform: translateY(-1px);
box-shadow: 0 0 0 4px var(--forge-glow);
}
.dlg-confirm.danger {
background: var(--color-danger);
box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-danger) 30%, transparent);
}
.dlg-confirm.danger:hover {
background: var(--color-danger-dark);
transform: translateY(-1px);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-danger) 30%, transparent);
}
</style>
+56 -48
View File
@@ -1,6 +1,3 @@
<!--
Task 9: Empty state component with SVG illustration and call-to-action.
-->
<script lang="ts">
interface Props {
title: string;
@@ -17,69 +14,80 @@
actionLabel = '',
actionHref = '',
onaction,
icon = 'projects'
}: Props = $props();
</script>
<div class="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-[var(--border-primary)] px-6 py-16 text-center animate-fade-in">
<!-- SVG Illustration -->
<div class="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-[var(--color-brand-50)]">
{#if icon === 'projects'}
<svg class="h-8 w-8 text-[var(--color-brand-500)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
<path d="M12 10v6" /><path d="M9 13h6" />
</svg>
{:else if icon === 'instances'}
<svg class="h-8 w-8 text-[var(--color-brand-500)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" /><rect width="20" height="8" x="2" y="14" rx="2" ry="2" /><line x1="6" x2="6.01" y1="6" y2="6" /><line x1="6" x2="6.01" y1="18" y2="18" />
</svg>
{:else if icon === 'deploys'}
<svg class="h-8 w-8 text-[var(--color-brand-500)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M15.59 14.37a6 6 0 0 1-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 0 0 6.16-12.12A14.98 14.98 0 0 0 9.631 8.41m5.96 5.96a14.926 14.926 0 0 1-5.841 2.58M16.5 9a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z" />
</svg>
{:else if icon === 'registries'}
<svg class="h-8 w-8 text-[var(--color-brand-500)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<ellipse cx="12" cy="5" rx="9" ry="3" /><path d="M3 5v14a9 3 0 0 0 18 0V5" /><path d="M3 12a9 3 0 0 0 18 0" />
</svg>
{:else if icon === 'volumes'}
<svg class="h-8 w-8 text-[var(--color-brand-500)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" x2="2" y1="12" y2="12" /><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
</svg>
{:else if icon === 'users'}
<svg class="h-8 w-8 text-[var(--color-brand-500)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
{/if}
<div class="empty">
<div class="empty-mark">
<span></span><span></span><span></span>
</div>
<h3 class="text-base font-semibold text-[var(--text-primary)]">{title}</h3>
<h3 class="empty-title">{title}</h3>
{#if description}
<p class="mt-1 max-w-sm text-sm text-[var(--text-secondary)]">{description}</p>
<p class="empty-desc">{description}</p>
{/if}
{#if actionLabel}
{#if actionHref}
<a
href={actionHref}
class="mt-4 inline-flex items-center gap-2 rounded-lg bg-[var(--color-brand-600)] px-4 py-2 text-sm font-medium text-white shadow-sm transition-all duration-150 hover:bg-[var(--color-brand-700)] active:animate-press"
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<a href={actionHref} class="forge-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14" /><path d="M12 5v14" />
</svg>
{actionLabel}
<span>{actionLabel}</span>
</a>
{:else if onaction}
<button
type="button"
onclick={onaction}
class="mt-4 inline-flex items-center gap-2 rounded-lg bg-[var(--color-brand-600)] px-4 py-2 text-sm font-medium text-white shadow-sm transition-all duration-150 hover:bg-[var(--color-brand-700)] active:animate-press"
>
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<button type="button" onclick={onaction} class="forge-btn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14" /><path d="M12 5v14" />
</svg>
{actionLabel}
<span>{actionLabel}</span>
</button>
{/if}
{/if}
</div>
<style>
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.25rem;
text-align: center;
padding: 4rem 2rem;
border: 1px dashed var(--border-primary);
border-radius: var(--radius-2xl);
background: var(--surface-card);
}
.empty-mark {
display: inline-flex; gap: 4px;
margin-bottom: 1.5rem;
}
.empty-mark span {
width: 10px; height: 10px; border-radius: 50%;
background: var(--border-input);
}
.empty-mark span:nth-child(2) {
background: var(--color-brand-600);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-brand-500) 18%, transparent);
animation: forge-breathe 2.4s ease-in-out infinite;
}
.empty-title {
font-family: var(--font-family-sans);
font-weight: 700;
font-size: 1.5rem;
letter-spacing: -0.01em;
line-height: 1.2;
margin: 0 0 0.4rem;
color: var(--text-primary);
}
.empty-desc {
color: var(--text-secondary);
max-width: 42ch;
font-size: 0.95rem;
line-height: 1.5;
margin: 0 0 1.5rem;
}
.forge-btn { margin-top: 0.5rem; }
</style>
+72
View File
@@ -0,0 +1,72 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
eyebrow?: string;
eyebrowSuffix?: string;
title: string;
accent?: string;
lede?: string;
lede_html?: Snippet;
toolbar?: Snippet;
size?: 'md' | 'lg' | 'xl';
}
const {
eyebrow = 'THE FORGE',
eyebrowSuffix,
title,
accent = '.',
lede,
lede_html,
toolbar,
size = 'lg'
}: Props = $props();
</script>
<header class="hero">
<div class="top">
<span class="forge-eyebrow">
<span class="forge-ember"></span>
<span>{eyebrow}</span>
{#if eyebrowSuffix}
<span class="sep">//</span>
<span>{eyebrowSuffix}</span>
{/if}
</span>
{#if toolbar}
<div class="toolbar">{@render toolbar()}</div>
{/if}
</div>
<h1 class="forge-display" class:s-md={size === 'md'} class:s-lg={size === 'lg'} class:s-xl={size === 'xl'}>
{title}{#if accent}<span class="accent">{accent}</span>{/if}
</h1>
{#if lede_html}
<p class="forge-lede">{@render lede_html()}</p>
{:else if lede}
<p class="forge-lede">{lede}</p>
{/if}
</header>
<style>
.hero { margin-bottom: 2rem; }
.top {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-bottom: 1.25rem;
flex-wrap: wrap;
}
.toolbar { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.forge-display.s-md { font-size: clamp(1.75rem, 3.5vw, 2.25rem); }
.forge-display.s-lg { font-size: clamp(2rem, 4vw, 2.75rem); }
.forge-display.s-xl { font-size: clamp(2.5rem, 5vw, 3.5rem); }
@media (max-width: 640px) {
.top { align-items: flex-start; }
}
</style>
+25 -29
View File
@@ -1,6 +1,3 @@
<!--
Task 5, 11: Status badge with pulse animation for "running" status.
-->
<script lang="ts">
import type { InstanceStatus, DeployStatus } from '$lib/types';
@@ -13,35 +10,34 @@
const { status, size = 'md' }: Props = $props();
const colorMap: Record<string, { bg: string; text: string; dot: string }> = {
running: { bg: 'bg-emerald-50 dark:bg-emerald-950', text: 'text-emerald-700 dark:text-emerald-300', dot: 'bg-emerald-500' },
success: { bg: 'bg-emerald-50 dark:bg-emerald-950', text: 'text-emerald-700 dark:text-emerald-300', dot: 'bg-emerald-500' },
stopped: { bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-700 dark:text-gray-300', dot: 'bg-gray-400' },
failed: { bg: 'bg-red-50 dark:bg-red-950', text: 'text-red-700 dark:text-red-300', dot: 'bg-red-500' },
rolled_back: { bg: 'bg-red-50 dark:bg-red-950', text: 'text-red-700 dark:text-red-300', dot: 'bg-red-500' },
removing: { bg: 'bg-amber-50 dark:bg-amber-950', text: 'text-amber-700 dark:text-amber-300', dot: 'bg-amber-500' },
pending: { bg: 'bg-blue-50 dark:bg-blue-950', text: 'text-blue-700 dark:text-blue-300', dot: 'bg-blue-500' },
pulling: { bg: 'bg-blue-50 dark:bg-blue-950', text: 'text-blue-700 dark:text-blue-300', dot: 'bg-blue-500' },
starting: { bg: 'bg-amber-50 dark:bg-amber-950', text: 'text-amber-700 dark:text-amber-300', dot: 'bg-amber-500' },
configuring_proxy: { bg: 'bg-amber-50 dark:bg-amber-950', text: 'text-amber-700 dark:text-amber-300', dot: 'bg-amber-500' },
health_checking: { bg: 'bg-violet-50 dark:bg-violet-950', text: 'text-violet-700 dark:text-violet-300', dot: 'bg-violet-500' }
const variantMap: Record<string, string> = {
running: 'is-running',
success: 'is-running',
stopped: '',
failed: 'is-fail',
rolled_back: 'is-fail',
removing: 'is-warn',
pending: 'is-info',
pulling: 'is-info',
starting: 'is-warn',
configuring_proxy: 'is-warn',
health_checking: 'is-info',
deploying: 'is-info'
};
const fallback = { bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-700 dark:text-gray-300', dot: 'bg-gray-400' };
const colors = $derived(colorMap[status] ?? fallback);
const sizeClass = $derived(size === 'sm' ? 'text-xs px-2 py-0.5' : 'text-sm px-2.5 py-0.5');
const dotSize = $derived(size === 'sm' ? 'h-1.5 w-1.5' : 'h-2 w-2');
const label = $derived(status.replace(/_/g, ' '));
const isAnimated = $derived(status === 'running' || status === 'pulling' || status === 'starting' || status === 'health_checking');
const variant = $derived(variantMap[status] ?? '');
const label = $derived(status.replace(/_/g, ' ').toUpperCase());
</script>
<span class="inline-flex items-center gap-1.5 rounded-full font-medium {colors.bg} {colors.text} {sizeClass}">
<span class="relative flex {dotSize}">
{#if isAnimated}
<span class="absolute inline-flex h-full w-full animate-ping rounded-full {colors.dot} opacity-50"></span>
{/if}
<span class="relative inline-flex rounded-full {dotSize} {colors.dot}"></span>
</span>
<span class="forge-pill {variant}" class:sm={size === 'sm'}>
<span class="pulse"></span>
{label}
</span>
<style>
.forge-pill.sm {
padding: 0.15rem 0.45rem;
font-size: 0.58rem;
}
.forge-pill.sm .pulse { width: 5px; height: 5px; }
</style>
+122
View File
@@ -890,5 +890,127 @@
"language": {
"en": "English",
"ru": "Russian"
},
"stacks": {
"eyebrow": "THE FORGE",
"title": "Stacks",
"lede": "Compose blueprints, forged as <em>atomic units</em>. Spin up services, iterate on revisions, roll back without breaking a sweat.",
"newStack": "New stack",
"refresh": "Refresh",
"total": "Total",
"running": "Running",
"deploying": "Forging",
"failed": "Failed",
"stopped": "Cold",
"empty": {
"title": "The anvil is cold.",
"desc": "Upload a docker-compose.yml to forge your first stack."
},
"card": {
"noDescription": "No description",
"updated": "Updated",
"start": "Start",
"stop": "Stop",
"delete": "Delete",
"open": "Open"
},
"new": {
"eyebrow": "NEW BLUEPRINT",
"title": "Forge a new stack.",
"lede": "Upload or paste a <code>docker-compose.yml</code>. All services in the blueprint deploy as a single atomic unit.",
"back": "Stacks",
"name": "Name",
"namePlaceholder": "my-app-stack",
"nameHint": "Lowercase, hyphenated. Used as the compose project name.",
"description": "Description",
"descriptionPlaceholder": "What does this stack do?",
"composeYaml": "Compose YAML",
"required": "required",
"optional": "optional",
"loadSample": "Load sample",
"uploadFile": "Upload file",
"dropHere": "Drop a docker-compose.yml here",
"dropSub": "or click to browse · or use <strong>Load sample</strong> above",
"lines": "{n} lines",
"bytes": "{n} bytes",
"clear": "Clear",
"deployImmediate": "Deploy immediately",
"deployHint": "Strike while the iron's hot. If unchecked, the stack is saved cold.",
"cancel": "Cancel",
"forging": "Forging…",
"forgeAndDeploy": "Forge & deploy",
"saveBlueprint": "Save blueprint",
"errorRequired": "Name and compose YAML are required.",
"errorCreate": "Failed to create stack"
},
"detail": {
"manifest": "MANIFEST",
"loading": "Loading blueprint…",
"composeProject": "COMPOSE PROJECT",
"noDescription": "No description",
"refresh": "Refresh",
"start": "Start",
"stop": "Stop",
"delete": "Delete",
"fault": "FAULT",
"err": "ERR",
"stats": {
"services": "Services",
"servicesSub": "in blueprint",
"running": "Running",
"runningSub": "active containers",
"revisions": "Revisions",
"revisionsSub": "in history",
"current": "Current",
"currentSub": "deployed"
},
"services": {
"title": "Services",
"count": "{n} on the floor",
"empty": "— no containers running —"
},
"tabs": {
"blueprint": "Blueprint",
"revisions": "Revisions",
"logs": "Logs"
},
"yaml": {
"currentRevision": "Current revision",
"edit": "Edit & redeploy",
"cancel": "Cancel",
"forging": "Forging…",
"deployNew": "Deploy new revision"
},
"revisions": {
"current": "CURRENT",
"by": "by",
"rollback": "← Rollback to this revision",
"rollbackTitle": "Rollback to revision?",
"rollbackMessage": "Create a new revision from rev {n} and redeploy the stack.",
"rollbackConfirm": "Rollback"
},
"logs": {
"service": "Service:",
"allServices": "All services",
"fetching": "Fetching…",
"fetch": "Fetch logs",
"empty": "— no logs loaded. tap fetch. —"
},
"delete": {
"title": "Delete stack?",
"messageBase": "This runs 'docker compose down' and removes \"{name}\".",
"messageVolumes": " Named volumes will also be removed.",
"confirm": "Delete"
},
"errors": {
"load": "Failed to load stack",
"stop": "Stop failed",
"start": "Start failed",
"update": "Update failed",
"rollback": "Rollback failed",
"delete": "Delete failed",
"fetchLogs": "Failed to load logs"
}
}
}
}
+122
View File
@@ -890,5 +890,127 @@
"language": {
"en": "Английский",
"ru": "Русский"
},
"stacks": {
"eyebrow": "КУЗНИЦА",
"title": "Стеки",
"lede": "Compose-чертежи, выкованные как <em>атомарные единицы</em>. Запускайте сервисы, меняйте ревизии и откатывайтесь без нервов.",
"newStack": "Новый стек",
"refresh": "Обновить",
"total": "Всего",
"running": "Работают",
"deploying": "Куются",
"failed": "Сбой",
"stopped": "Холодные",
"empty": {
"title": "Наковальня остыла.",
"desc": "Загрузите docker-compose.yml, чтобы выковать первый стек."
},
"card": {
"noDescription": "Без описания",
"updated": "Обновлён",
"start": "Запустить",
"stop": "Остановить",
"delete": "Удалить",
"open": "Открыть"
},
"new": {
"eyebrow": "НОВЫЙ ЧЕРТЁЖ",
"title": "Выковать новый стек.",
"lede": "Загрузите или вставьте <code>docker-compose.yml</code>. Все сервисы чертежа разворачиваются как одна атомарная единица.",
"back": "Стеки",
"name": "Имя",
"namePlaceholder": "мой-стек",
"nameHint": "Строчные буквы, через дефис. Используется как имя compose-проекта.",
"description": "Описание",
"descriptionPlaceholder": "Что делает этот стек?",
"composeYaml": "Compose YAML",
"required": "обязательно",
"optional": "необязательно",
"loadSample": "Загрузить пример",
"uploadFile": "Загрузить файл",
"dropHere": "Перетащите сюда docker-compose.yml",
"dropSub": "или нажмите для выбора · или используйте <strong>Загрузить пример</strong> выше",
"lines": "{n} строк",
"bytes": "{n} байт",
"clear": "Очистить",
"deployImmediate": "Развернуть сразу",
"deployHint": "Куй железо, пока горячо. Без галочки стек сохраняется холодным.",
"cancel": "Отмена",
"forging": "Куём…",
"forgeAndDeploy": "Выковать и развернуть",
"saveBlueprint": "Сохранить чертёж",
"errorRequired": "Имя и compose YAML обязательны.",
"errorCreate": "Не удалось создать стек"
},
"detail": {
"manifest": "МАНИФЕСТ",
"loading": "Загрузка чертежа…",
"composeProject": "COMPOSE-ПРОЕКТ",
"noDescription": "Без описания",
"refresh": "Обновить",
"start": "Запустить",
"stop": "Остановить",
"delete": "Удалить",
"fault": "СБОЙ",
"err": "ОШБ",
"stats": {
"services": "Сервисы",
"servicesSub": "в чертеже",
"running": "Работают",
"runningSub": "активных контейнеров",
"revisions": "Ревизии",
"revisionsSub": "в истории",
"current": "Текущая",
"currentSub": "развёрнута"
},
"services": {
"title": "Сервисы",
"count": "{n} в работе",
"empty": "— нет запущенных контейнеров —"
},
"tabs": {
"blueprint": "Чертёж",
"revisions": "Ревизии",
"logs": "Логи"
},
"yaml": {
"currentRevision": "Текущая ревизия",
"edit": "Править и развернуть",
"cancel": "Отмена",
"forging": "Куём…",
"deployNew": "Развернуть новую ревизию"
},
"revisions": {
"current": "ТЕКУЩАЯ",
"by": "автор",
"rollback": "← Откатиться к этой ревизии",
"rollbackTitle": "Откатить ревизию?",
"rollbackMessage": "Создать новую ревизию из rev {n} и развернуть стек заново.",
"rollbackConfirm": "Откатить"
},
"logs": {
"service": "Сервис:",
"allServices": "Все сервисы",
"fetching": "Загрузка…",
"fetch": "Получить логи",
"empty": "— логи не загружены. нажмите получить. —"
},
"delete": {
"title": "Удалить стек?",
"messageBase": "Будет выполнен 'docker compose down' и удалён \"{name}\".",
"messageVolumes": " Именованные тома также будут удалены.",
"confirm": "Удалить"
},
"errors": {
"load": "Не удалось загрузить стек",
"stop": "Остановка не удалась",
"start": "Запуск не удался",
"update": "Обновление не удалось",
"rollback": "Откат не удался",
"delete": "Удаление не удалось",
"fetchLogs": "Не удалось загрузить логи"
}
}
}
}