From 04c1411f5d047a65ec13f5f34f0129b2060122e2 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 4 Apr 2026 13:03:05 +0300 Subject: [PATCH] fix: extract hardcoded English strings to i18n system with Russian translations - Extract ~40 hardcoded strings from project detail, deploy, settings, credentials, registries, auth, env editor pages - Add corresponding Russian translations - Replace native confirm() default labels with i18n keys in ConfirmDialog - Fix InstanceCard pluralization to use i18n --- web/src/lib/components/ConfirmDialog.svelte | 2 +- web/src/lib/components/InstanceCard.svelte | 2 +- web/src/lib/i18n/en.json | 53 ++++++++++++++++--- web/src/lib/i18n/ru.json | 53 ++++++++++++++++--- web/src/routes/deploy/+page.svelte | 2 +- web/src/routes/projects/+page.svelte | 6 +-- web/src/routes/projects/[id]/+page.svelte | 46 ++++++++-------- web/src/routes/projects/[id]/env/+page.svelte | 6 +-- web/src/routes/settings/+page.svelte | 2 +- web/src/routes/settings/auth/+page.svelte | 4 +- .../routes/settings/credentials/+page.svelte | 2 +- .../routes/settings/registries/+page.svelte | 6 +-- 12 files changed, 129 insertions(+), 55 deletions(-) diff --git a/web/src/lib/components/ConfirmDialog.svelte b/web/src/lib/components/ConfirmDialog.svelte index 469b95c..9819fe7 100644 --- a/web/src/lib/components/ConfirmDialog.svelte +++ b/web/src/lib/components/ConfirmDialog.svelte @@ -19,7 +19,7 @@ open, title, message, - confirmLabel = 'Confirm', + confirmLabel = $t('common.confirm'), confirmVariant = 'primary', onconfirm, oncancel diff --git a/web/src/lib/components/InstanceCard.svelte b/web/src/lib/components/InstanceCard.svelte index feb7a9d..e4bea80 100644 --- a/web/src/lib/components/InstanceCard.svelte +++ b/web/src/lib/components/InstanceCard.svelte @@ -155,7 +155,7 @@ open={confirmAction !== null} title={confirmAction ? $t(`confirm.${confirmAction}Instance`) : ''} message={confirmAction ? $t(`instance.${confirmAction}Confirm`) : ''} - confirmLabel={confirmAction ? confirmAction.charAt(0).toUpperCase() + confirmAction.slice(1) : ''} + confirmLabel={confirmAction ? $t(`confirm.${confirmAction}Action`) : ''} confirmVariant={confirmAction === 'remove' ? 'danger' : 'primary'} onconfirm={() => { if (confirmAction) handleAction(confirmAction); }} oncancel={() => { confirmAction = null; }} diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 7c9dc97..ae2d2fe 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -55,7 +55,10 @@ "selectImage": "Select an image", "noImages": "No images found", "loadingImages": "Loading images...", - "imageLoadFailed": "Failed to load images" + "imageLoadFailed": "Failed to load images", + "alreadyAdded": "Already added", + "portHelpText": "Auto-detected from EXPOSE if empty", + "healthcheckHelpText": "Auto-detected from image if empty" }, "projectDetail": { "deleteProject": "Delete Project", @@ -86,7 +89,29 @@ "deleteConfirmMessage": "This will permanently delete the project '{name}' and all its stages, instances, and deploy history. This cannot be undone.", "loadFailed": "Failed to load project", "deleteFailed": "Failed to delete project", - "deployFailed": "Deploy failed" + "deployFailed": "Deploy failed", + "nameLabel": "Name *", + "imageLabel": "Image *", + "portLabel": "Port", + "healthcheckLabel": "Healthcheck Path", + "saving": "Saving...", + "addStage": "Add Stage", + "tagPattern": "Tag Pattern", + "tagPatternHelp": "Glob pattern (e.g., dev-*, v*)", + "maxInstances": "Max Instances", + "autoDeployLabel": "Auto Deploy", + "npmProxy": "NPM Proxy", + "creating": "Creating...", + "createStage": "Create Stage", + "noProxy": "No Proxy", + "deleteStage": "Delete stage", + "deleteStageConfirm": "Delete stage \"{name}\"?", + "stageCreated": "Stage \"{name}\" created", + "stageDeleted": "Stage \"{name}\" deleted", + "projectUpdated": "Project updated", + "updateFailed": "Failed to update project", + "stageCreateFailed": "Failed to create stage", + "stageDeleteFailed": "Failed to delete stage" }, "envEditor": { "title": "Environment Variables", @@ -117,7 +142,8 @@ "addFailed": "Failed to add env var", "updateFailed": "Failed to update env var", "deleteFailed": "Failed to delete env var", - "loadEnvFailed": "Failed to load env vars" + "loadEnvFailed": "Failed to load env vars", + "leaveEmptyToKeep": "Leave empty to keep current" }, "volumeEditor": { "title": "Volume Mounts", @@ -196,7 +222,8 @@ "selectImage": "Select an image from a registry", "noImages": "No images found", "loadingImages": "Loading...", - "imageLoadFailed": "Failed to load images" + "imageLoadFailed": "Failed to load images", + "lowercaseHint": "Lowercase with hyphens" }, "settings": { "title": "Settings", @@ -264,7 +291,9 @@ "testConnection": "Test Connection", "testingConnection": "Testing...", "connectionSuccess": "Connection successful", - "connectionFailed": "Connection failed" + "connectionFailed": "Connection failed", + "baseVolumePath": "Base Volume Path", + "baseVolumePathHelp": "Prepended to relative volume sources (e.g., /data + my-app/uploads = /data/my-app/uploads)" }, "settingsRegistries": { "title": "Container Registries", @@ -300,7 +329,10 @@ "deleteFailed": "Failed to delete registry", "testFailed": "Connection test failed", "loadFailed": "Failed to load registries", - "deleteConfirm": "Delete registry \"{name}\"? This cannot be undone." + "deleteConfirm": "Delete registry \"{name}\"? This cannot be undone.", + "healthChecking": "Checking...", + "healthConnected": "Connected", + "healthUnreachable": "Unreachable" }, "settingsCredentials": { "title": "Credentials", @@ -324,7 +356,8 @@ "registryTokens": "Registry Tokens", "registryTokensDesc": "Registry authentication tokens are managed per-registry in the", "registriesLink": "Registries", - "registryTokensSuffix": "section. Each registry stores its token encrypted in the database." + "registryTokensSuffix": "section. Each registry stores its token encrypted in the database.", + "notSet": "Not set" }, "settingsBackup": { "title": "Backup Management", @@ -395,6 +428,7 @@ "deleteFailed": "Failed to delete user", "deleteConfirm": "Are you sure you want to delete this user?", "usernameRequired": "Username and password are required", + "networkError": "Network error", "password": "Password" }, "login": { @@ -486,7 +520,10 @@ "stopInstance": "Stop Instance", "startInstance": "Start Instance", "restartInstance": "Restart Instance", - "removeInstance": "Remove Instance" + "removeInstance": "Remove Instance", + "stopAction": "Stop", + "restartAction": "Restart", + "removeAction": "Remove" }, "theme": { "light": "Light", diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index 4550d51..5b89677 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -55,7 +55,10 @@ "selectImage": "Выберите образ", "noImages": "Образы не найдены", "loadingImages": "Загрузка образов...", - "imageLoadFailed": "Не удалось загрузить образы" + "imageLoadFailed": "Не удалось загрузить образы", + "alreadyAdded": "Уже добавлен", + "portHelpText": "Автоопределение из EXPOSE, если пусто", + "healthcheckHelpText": "Автоопределение из образа, если пусто" }, "projectDetail": { "deleteProject": "Удалить проект", @@ -86,7 +89,29 @@ "deleteConfirmMessage": "Это безвозвратно удалит проект '{name}' и все его стадии, экземпляры и историю деплоев.", "loadFailed": "Не удалось загрузить проект", "deleteFailed": "Не удалось удалить проект", - "deployFailed": "Деплой не удался" + "deployFailed": "Деплой не удался", + "nameLabel": "Название *", + "imageLabel": "Образ *", + "portLabel": "Порт", + "healthcheckLabel": "Путь проверки", + "saving": "Сохранение...", + "addStage": "Добавить стадию", + "tagPattern": "Шаблон тега", + "tagPatternHelp": "Glob-шаблон (напр., dev-*, v*)", + "maxInstances": "Макс. экземпляров", + "autoDeployLabel": "Авто-деплой", + "npmProxy": "NPM прокси", + "creating": "Создание...", + "createStage": "Создать стадию", + "noProxy": "Без прокси", + "deleteStage": "Удалить стадию", + "deleteStageConfirm": "Удалить стадию \"{name}\"?", + "stageCreated": "Стадия \"{name}\" создана", + "stageDeleted": "Стадия \"{name}\" удалена", + "projectUpdated": "Проект обновлён", + "updateFailed": "Не удалось обновить проект", + "stageCreateFailed": "Не удалось создать стадию", + "stageDeleteFailed": "Не удалось удалить стадию" }, "envEditor": { "title": "Переменные окружения", @@ -117,7 +142,8 @@ "addFailed": "Не удалось добавить переменную", "updateFailed": "Не удалось обновить переменную", "deleteFailed": "Не удалось удалить переменную", - "loadEnvFailed": "Не удалось загрузить переменные" + "loadEnvFailed": "Не удалось загрузить переменные", + "leaveEmptyToKeep": "Оставьте пустым, чтобы сохранить текущее" }, "volumeEditor": { "title": "Тома", @@ -196,7 +222,8 @@ "selectImage": "Выберите образ из реестра", "noImages": "Образы не найдены", "loadingImages": "Загрузка...", - "imageLoadFailed": "Не удалось загрузить образы" + "imageLoadFailed": "Не удалось загрузить образы", + "lowercaseHint": "Строчные буквы и дефисы" }, "settings": { "title": "Настройки", @@ -264,7 +291,9 @@ "testConnection": "Проверить соединение", "testingConnection": "Проверка...", "connectionSuccess": "Соединение успешно", - "connectionFailed": "Ошибка соединения" + "connectionFailed": "Ошибка соединения", + "baseVolumePath": "Базовый путь томов", + "baseVolumePathHelp": "Добавляется к относительным путям источников (напр., /data + my-app/uploads = /data/my-app/uploads)" }, "settingsRegistries": { "title": "Реестры контейнеров", @@ -300,7 +329,10 @@ "deleteFailed": "Не удалось удалить реестр", "testFailed": "Тест подключения не удался", "loadFailed": "Не удалось загрузить реестры", - "deleteConfirm": "Удалить реестр «{name}»? Это действие необратимо." + "deleteConfirm": "Удалить реестр «{name}»? Это действие необратимо.", + "healthChecking": "Проверка...", + "healthConnected": "Подключено", + "healthUnreachable": "Недоступно" }, "settingsCredentials": { "title": "Учётные данные", @@ -324,7 +356,8 @@ "registryTokens": "Токены реестров", "registryTokensDesc": "Токены аутентификации реестров управляются для каждого реестра в разделе", "registriesLink": "Реестры", - "registryTokensSuffix": ". Каждый реестр хранит свой токен в зашифрованном виде." + "registryTokensSuffix": ". Каждый реестр хранит свой токен в зашифрованном виде.", + "notSet": "Не задано" }, "settingsBackup": { "title": "Управление резервными копиями", @@ -395,6 +428,7 @@ "deleteFailed": "Не удалось удалить пользователя", "deleteConfirm": "Вы уверены, что хотите удалить этого пользователя?", "usernameRequired": "Имя пользователя и пароль обязательны", + "networkError": "Ошибка сети", "password": "Пароль" }, "login": { @@ -486,7 +520,10 @@ "stopInstance": "Остановить экземпляр", "startInstance": "Запустить экземпляр", "restartInstance": "Перезапустить экземпляр", - "removeInstance": "Удалить экземпляр" + "removeInstance": "Удалить экземпляр", + "stopAction": "Остановить", + "restartAction": "Перезапустить", + "removeAction": "Удалить" }, "theme": { "light": "Светлая", diff --git a/web/src/routes/deploy/+page.svelte b/web/src/routes/deploy/+page.svelte index 25cce0d..4473342 100644 --- a/web/src/routes/deploy/+page.svelte +++ b/web/src/routes/deploy/+page.svelte @@ -227,7 +227,7 @@

{$t('quickDeploy.reviewDesc')}

- +
diff --git a/web/src/routes/projects/+page.svelte b/web/src/routes/projects/+page.svelte index 5ac6f40..068671b 100644 --- a/web/src/routes/projects/+page.svelte +++ b/web/src/routes/projects/+page.svelte @@ -48,7 +48,7 @@ description: alreadyAdded ? undefined : reg.name, group: reg.name, disabled: alreadyAdded, - disabledHint: alreadyAdded ? 'Already added' : undefined + disabledHint: alreadyAdded ? $t('projects.alreadyAdded') : undefined }); } } catch { @@ -184,8 +184,8 @@ onselect={selectPickedImage} onclose={() => { showImagePicker = false; }} /> - - + +
diff --git a/web/src/routes/projects/[id]/+page.svelte b/web/src/routes/projects/[id]/+page.svelte index bc2298a..675e8d8 100644 --- a/web/src/routes/projects/[id]/+page.svelte +++ b/web/src/routes/projects/[id]/+page.svelte @@ -47,12 +47,12 @@ enable_proxy: stageEnableProxy, max_instances: parseInt(stageMaxInstances) || 1, }); - toasts.success(`Stage "${stageName}" created`); + toasts.success($t('projectDetail.stageCreated', { name: stageName })); stageName = ''; stageTagPattern = '*'; stageAutoDeploy = true; stageEnableProxy = true; stageMaxInstances = '1'; showAddStage = false; await loadProject(); } catch (e) { - toasts.error(e instanceof Error ? e.message : 'Failed to create stage'); + toasts.error(e instanceof Error ? e.message : $t('projectDetail.stageCreateFailed')); } finally { addingStage = false; } @@ -85,11 +85,11 @@ port: parseInt(editPort) || 0, healthcheck: editHealthcheck.trim(), }); - toasts.success('Project updated'); + toasts.success($t('projectDetail.projectUpdated')); editing = false; await loadProject(); } catch (e) { - toasts.error(e instanceof Error ? e.message : 'Failed to update project'); + toasts.error(e instanceof Error ? e.message : $t('projectDetail.updateFailed')); } finally { saving = false; } @@ -98,10 +98,10 @@ async function handleDeleteStage(stageId: string, name: string) { try { await api.deleteStage(projectId, stageId); - toasts.success(`Stage "${name}" deleted`); + toasts.success($t('projectDetail.stageDeleted', { name })); await loadProject(); } catch (e) { - toasts.error(e instanceof Error ? e.message : 'Failed to delete stage'); + toasts.error(e instanceof Error ? e.message : $t('projectDetail.stageDeleteFailed')); } } let tagsLoading = $state(false); @@ -268,10 +268,10 @@
{#if editing}
- - - - + + + +
{:else} @@ -334,26 +334,26 @@ class="inline-flex items-center gap-1.5 rounded-lg {showAddStage ? 'border border-[var(--border-primary)] text-[var(--text-secondary)]' : 'bg-[var(--color-brand-600)] text-white'} px-3 py-1.5 text-xs font-medium transition-all hover:opacity-90" > {#if !showAddStage}{/if} - {showAddStage ? $t('projects.cancel') : 'Add Stage'} + {showAddStage ? $t('projects.cancel') : $t('projectDetail.addStage')}
{#if showAddStage}
- - - + + +
- +
- +
- +
- +
@@ -364,7 +364,7 @@ disabled={addingStage || !stageName.trim()} class="rounded-lg bg-[var(--color-brand-600)] px-4 py-2 text-sm font-medium text-white hover:bg-[var(--color-brand-700)] disabled:opacity-50 transition-all" > - {addingStage ? 'Creating...' : 'Create Stage'} + {addingStage ? $t('projectDetail.creating') : $t('projectDetail.createStage')}
@@ -391,7 +391,7 @@ {$t('projectDetail.requiresConfirm')} {/if} {#if !stage.enable_proxy} - No Proxy + {$t('projectDetail.noProxy')} {/if}
@@ -408,8 +408,8 @@
diff --git a/web/src/routes/settings/auth/+page.svelte b/web/src/routes/settings/auth/+page.svelte index 29b63a1..91bd119 100644 --- a/web/src/routes/settings/auth/+page.svelte +++ b/web/src/routes/settings/auth/+page.svelte @@ -57,7 +57,7 @@ const res = await fetch('/api/auth/settings', { method: 'PUT', headers: authHeaders(), body: JSON.stringify(settings) }); const envelope = await res.json(); if (envelope.success) message = $t('settingsAuth.saved'); else error = envelope.error ?? $t('settingsAuth.saveFailed'); - } catch (err: unknown) { error = err instanceof Error ? err.message : 'Network error'; } finally { saving = false; } + } catch (err: unknown) { error = err instanceof Error ? err.message : $t('settingsAuth.networkError'); } finally { saving = false; } } async function addUser() { @@ -67,7 +67,7 @@ const envelope = await res.json(); if (envelope.success) { newUsername = ''; newPassword = ''; newEmail = ''; newRole = 'viewer'; await loadUsers(); message = $t('settingsAuth.userCreated'); } else error = envelope.error ?? $t('settingsAuth.createFailed'); - } catch (err: unknown) { error = err instanceof Error ? err.message : 'Network error'; } + } catch (err: unknown) { error = err instanceof Error ? err.message : $t('settingsAuth.networkError'); } } async function deleteUser(id: string) { diff --git a/web/src/routes/settings/credentials/+page.svelte b/web/src/routes/settings/credentials/+page.svelte index 783116e..2940908 100644 --- a/web/src/routes/settings/credentials/+page.svelte +++ b/web/src/routes/settings/credentials/+page.svelte @@ -85,7 +85,7 @@

{item.label}

-

{item.value || 'Not set'}

+

{item.value || $t('settingsCredentials.notSet')}

{/each} diff --git a/web/src/routes/settings/registries/+page.svelte b/web/src/routes/settings/registries/+page.svelte index 2ec1fb6..900b516 100644 --- a/web/src/routes/settings/registries/+page.svelte +++ b/web/src/routes/settings/registries/+page.svelte @@ -159,11 +159,11 @@
{#if healthStatus[registry.id] === 'checking'} - + {:else if healthStatus[registry.id] === 'healthy'} - + {:else if healthStatus[registry.id] === 'unhealthy'} - + {/if}

{registry.name}

{registry.type}