Localize workload-logs and external-access panels.

Add components.workloadLogs, components.deployStatus and
components.externalAccess dictionaries; move the workload logs panel and
the service external-access panel onto them (tabs, live-refresh hints,
access modes, countdown units, toasts) with RTL-aware spacing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-11 18:25:45 +03:30
parent f64d341c80
commit 63e368f464
5 changed files with 183 additions and 50 deletions
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import api from '@/lib/api'; import api from '@/lib/api';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useT } from '@/i18n/I18nProvider';
import type { Application, ServiceAccessGrant, ServiceAccessTarget } from '@/types'; import type { Application, ServiceAccessGrant, ServiceAccessTarget } from '@/types';
import { ExternalLink, ShieldAlert, Copy, Check, Eye, EyeOff } from 'lucide-react'; import { ExternalLink, ShieldAlert, Copy, Check, Eye, EyeOff } from 'lucide-react';
@@ -14,6 +15,7 @@ export function ServiceExternalAccessPanel({
appId: string; appId: string;
app: Application; app: Application;
}) { }) {
const ea = useT().components.externalAccess;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [accessTarget, setAccessTarget] = useState<ServiceAccessTarget>('database'); const [accessTarget, setAccessTarget] = useState<ServiceAccessTarget>('database');
const [accessDuration, setAccessDuration] = useState(60); const [accessDuration, setAccessDuration] = useState(60);
@@ -25,13 +27,13 @@ export function ServiceExternalAccessPanel({
const accessTargetOptions: { value: ServiceAccessTarget; label: string }[] = []; const accessTargetOptions: { value: ServiceAccessTarget; label: string }[] = [];
const hasDb = const hasDb =
(app.databaseType && app.databaseType !== 'none') || app.productType === 'managed_database'; (app.databaseType && app.databaseType !== 'none') || app.productType === 'managed_database';
if (hasDb) accessTargetOptions.push({ value: 'database', label: 'Database' }); if (hasDb) accessTargetOptions.push({ value: 'database', label: ea.targets.database });
if (app.enableRedis || app.productType === 'managed_redis') { if (app.enableRedis || app.productType === 'managed_redis') {
accessTargetOptions.push({ value: 'redis', label: 'Redis' }); accessTargetOptions.push({ value: 'redis', label: ea.targets.redis });
} }
if (app.enableRabbitmq || app.productType === 'managed_rabbitmq') { if (app.enableRabbitmq || app.productType === 'managed_rabbitmq') {
accessTargetOptions.push({ value: 'rabbitmq_amqp', label: 'RabbitMQ (AMQP)' }); accessTargetOptions.push({ value: 'rabbitmq_amqp', label: ea.targets.rabbitmq_amqp });
accessTargetOptions.push({ value: 'rabbitmq_management', label: 'RabbitMQ Management UI' }); accessTargetOptions.push({ value: 'rabbitmq_management', label: ea.targets.rabbitmq_management });
} }
const hasAccessTargets = accessTargetOptions.length > 0; const hasAccessTargets = accessTargetOptions.length > 0;
@@ -67,11 +69,11 @@ export function ServiceExternalAccessPanel({
onSuccess: () => { onSuccess: () => {
refetchAccessGrants(); refetchAccessGrants();
queryClient.invalidateQueries({ queryKey: ['access-grants', appId] }); queryClient.invalidateQueries({ queryKey: ['access-grants', appId] });
toast.success(accessPersistent ? 'Permanent external access enabled' : 'Temporary external access enabled'); toast.success(accessPersistent ? ea.permanentEnabled : ea.temporaryEnabled);
}, },
onError: (err: unknown) => { onError: (err: unknown) => {
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message; const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
toast.error(msg || 'Failed to enable access'); toast.error(msg || ea.enableFailed);
}, },
}); });
@@ -80,9 +82,9 @@ export function ServiceExternalAccessPanel({
onSuccess: () => { onSuccess: () => {
refetchAccessGrants(); refetchAccessGrants();
queryClient.invalidateQueries({ queryKey: ['access-grants', appId] }); queryClient.invalidateQueries({ queryKey: ['access-grants', appId] });
toast.success('Access revoked'); toast.success(ea.revoked);
}, },
onError: () => toast.error('Failed to revoke access'), onError: () => toast.error(ea.revokeFailed),
}); });
const copyToClipboard = (text: string, field: string) => { const copyToClipboard = (text: string, field: string) => {
@@ -95,16 +97,16 @@ export function ServiceExternalAccessPanel({
accessTargetOptions.find((o) => o.value === target)?.label || target; accessTargetOptions.find((o) => o.value === target)?.label || target;
const formatAccessCountdown = (grant: ServiceAccessGrant) => { const formatAccessCountdown = (grant: ServiceAccessGrant) => {
if (grant.persistent) return 'Permanent (until revoked)'; if (grant.persistent) return ea.permanentUntilRevoked;
const ms = new Date(grant.expiresAt).getTime() - accessNow; const ms = new Date(grant.expiresAt).getTime() - accessNow;
if (ms <= 0) return 'Expired'; if (ms <= 0) return ea.expired;
const totalSec = Math.floor(ms / 1000); const totalSec = Math.floor(ms / 1000);
const h = Math.floor(totalSec / 3600); const h = Math.floor(totalSec / 3600);
const m = Math.floor((totalSec % 3600) / 60); const m = Math.floor((totalSec % 3600) / 60);
const s = totalSec % 60; const s = totalSec % 60;
if (h > 0) return `${h}h ${m}m ${s}s`; if (h > 0) return `${h}${ea.hShort} ${m}${ea.mShort} ${s}${ea.sShort}`;
if (m > 0) return `${m}m ${s}s`; if (m > 0) return `${m}${ea.mShort} ${s}${ea.sShort}`;
return `${s}s`; return `${s}${ea.sShort}`;
}; };
if (!hasAccessTargets) return null; if (!hasAccessTargets) return null;
@@ -112,22 +114,21 @@ export function ServiceExternalAccessPanel({
return ( return (
<div className="card"> <div className="card">
<h2 className="text-lg font-semibold text-gray-900 mb-2 flex items-center gap-2"> <h2 className="text-lg font-semibold text-gray-900 mb-2 flex items-center gap-2">
<ExternalLink className="w-5 h-5" /> External access <ExternalLink className="w-5 h-5" /> {ea.title}
</h2> </h2>
<p className="text-xs text-amber-700 bg-amber-50 border border-amber-100 rounded-lg px-3 py-2 mb-4 flex items-start gap-2"> <p className="text-xs text-amber-700 bg-amber-50 border border-amber-100 rounded-lg px-3 py-2 mb-4 flex items-start gap-2">
<ShieldAlert className="w-4 h-4 shrink-0 mt-0.5" /> <ShieldAlert className="w-4 h-4 shrink-0 mt-0.5" />
Opens a NodePort on the cluster node IP. Anyone who can reach that IP may connect until the session ends or you {ea.warning}
revoke it. Use short durations for temporary access; permanent keeps the port open until revoked.
</p> </p>
{!app.latestImageTag ? ( {!app.latestImageTag ? (
<p className="text-sm text-gray-500">Deploy the service first to enable external access.</p> <p className="text-sm text-gray-500">{ea.deployFirst}</p>
) : ( ) : (
<> <>
<div className="bg-gray-50 rounded-xl p-4 mb-4 space-y-4"> <div className="bg-gray-50 rounded-xl p-4 mb-4 space-y-4">
<div className="flex flex-wrap gap-4 items-end"> <div className="flex flex-wrap gap-4 items-end">
<div className="flex-1 min-w-[160px]"> <div className="flex-1 min-w-[160px]">
<label className="text-xs font-medium text-gray-600 block mb-1">Service</label> <label className="text-xs font-medium text-gray-600 block mb-1">{ea.service}</label>
<select <select
value={accessTarget} value={accessTarget}
onChange={(e) => setAccessTarget(e.target.value as ServiceAccessTarget)} onChange={(e) => setAccessTarget(e.target.value as ServiceAccessTarget)}
@@ -141,7 +142,7 @@ export function ServiceExternalAccessPanel({
</select> </select>
</div> </div>
<div className="flex-1 min-w-[200px]"> <div className="flex-1 min-w-[200px]">
<label className="text-xs font-medium text-gray-600 block mb-1">Access mode</label> <label className="text-xs font-medium text-gray-600 block mb-1">{ea.accessMode}</label>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
type="button" type="button"
@@ -152,7 +153,7 @@ export function ServiceExternalAccessPanel({
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100' : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100'
}`} }`}
> >
Temporary {ea.temporary}
</button> </button>
<button <button
type="button" type="button"
@@ -163,7 +164,7 @@ export function ServiceExternalAccessPanel({
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100' : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100'
}`} }`}
> >
Always open {ea.alwaysOpen}
</button> </button>
</div> </div>
</div> </div>
@@ -171,7 +172,7 @@ export function ServiceExternalAccessPanel({
{!accessPersistent && ( {!accessPersistent && (
<div> <div>
<label className="text-xs font-medium text-gray-600 block mb-1">Duration</label> <label className="text-xs font-medium text-gray-600 block mb-1">{ea.duration}</label>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{[30, 60, 240].map((mins) => ( {[30, 60, 240].map((mins) => (
<button <button
@@ -193,7 +194,7 @@ export function ServiceExternalAccessPanel({
{accessPersistent && ( {accessPersistent && (
<p className="text-xs text-amber-800 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2"> <p className="text-xs text-amber-800 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
The port stays exposed until you click Revoke. Only use this when you need a stable external endpoint. {ea.persistentNote}
</p> </p>
)} )}
@@ -203,12 +204,12 @@ export function ServiceExternalAccessPanel({
disabled={createAccessMutation.isPending} disabled={createAccessMutation.isPending}
className="btn-primary text-sm" className="btn-primary text-sm"
> >
{createAccessMutation.isPending ? 'Opening…' : accessPersistent ? 'Open port permanently' : 'Enable access'} {createAccessMutation.isPending ? ea.opening : accessPersistent ? ea.openPermanently : ea.enableAccess}
</button> </button>
</div> </div>
{accessGrants.filter((g) => g.status === 'active').length === 0 ? ( {accessGrants.filter((g) => g.status === 'active').length === 0 ? (
<p className="text-sm text-gray-500">No active external access sessions.</p> <p className="text-sm text-gray-500">{ea.noSessions}</p>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{accessGrants {accessGrants
@@ -219,8 +220,8 @@ export function ServiceExternalAccessPanel({
<div> <div>
<span className="text-sm font-semibold text-gray-800">{accessTargetLabel(grant.target)}</span> <span className="text-sm font-semibold text-gray-800">{accessTargetLabel(grant.target)}</span>
{grant.persistent && ( {grant.persistent && (
<span className="ml-2 text-xs font-medium text-amber-700 bg-amber-50 px-2 py-0.5 rounded-full"> <span className="ml-2 rtl:ml-0 rtl:mr-2 text-xs font-medium text-amber-700 bg-amber-50 px-2 py-0.5 rounded-full">
Always open {ea.alwaysOpen}
</span> </span>
)} )}
<span className="ml-2 text-xs text-gray-500">{formatAccessCountdown(grant)}</span> <span className="ml-2 text-xs text-gray-500">{formatAccessCountdown(grant)}</span>
@@ -231,12 +232,12 @@ export function ServiceExternalAccessPanel({
disabled={revokeAccessMutation.isPending} disabled={revokeAccessMutation.isPending}
className="btn-secondary text-xs text-red-600 border-red-200 hover:bg-red-50" className="btn-secondary text-xs text-red-600 border-red-200 hover:bg-red-50"
> >
Revoke {ea.revoke}
</button> </button>
</div> </div>
<div className="space-y-1.5 text-sm font-mono"> <div className="space-y-1.5 text-sm font-mono">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="text-gray-500 text-xs font-sans">Endpoint</span> <span className="text-gray-500 text-xs font-sans">{ea.endpoint}</span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-gray-800"> <span className="text-gray-800">
{grant.host}:{grant.port} {grant.host}:{grant.port}
@@ -256,7 +257,7 @@ export function ServiceExternalAccessPanel({
</div> </div>
{grant.connection.url && ( {grant.connection.url && (
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="text-gray-500 text-xs font-sans">URL</span> <span className="text-gray-500 text-xs font-sans">{ea.url}</span>
<div className="flex items-center gap-2 max-w-[70%]"> <div className="flex items-center gap-2 max-w-[70%]">
<span className="text-gray-800 truncate text-xs" title={grant.connection.url}> <span className="text-gray-800 truncate text-xs" title={grant.connection.url}>
{showAccessSecret ? grant.connection.url : '••••••••••••'} {showAccessSecret ? grant.connection.url : '••••••••••••'}
+23 -19
View File
@@ -3,6 +3,7 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import api from '@/lib/api'; import api from '@/lib/api';
import { useT } from '@/i18n/I18nProvider';
import { FileText, ChevronDown, Monitor, Hammer, Pin } from 'lucide-react'; import { FileText, ChevronDown, Monitor, Hammer, Pin } from 'lucide-react';
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
@@ -29,6 +30,9 @@ export function WorkloadLogsPanel({
isStopped?: boolean; isStopped?: boolean;
emptyPodMessage?: string; emptyPodMessage?: string;
}) { }) {
const t = useT();
const wl = t.components.workloadLogs;
const statusLabel = (s: string) => (t.components.deployStatus as Record<string, string>)[s] ?? s;
const [showLogs, setShowLogs] = useState(false); const [showLogs, setShowLogs] = useState(false);
const [logTab, setLogTab] = useState<'pod' | 'build'>('pod'); const [logTab, setLogTab] = useState<'pod' | 'build'>('pod');
const logsEndRef = useRef<HTMLPreElement>(null); const logsEndRef = useRef<HTMLPreElement>(null);
@@ -60,38 +64,38 @@ export function WorkloadLogsPanel({
const podPlaceholder = const podPlaceholder =
emptyPodMessage || emptyPodMessage ||
(isRunning (isRunning
? 'Loading logs...' ? wl.loadingLogs
: isStopped : isStopped
? 'Service is stopped. Start it to see logs.' ? wl.serviceStopped
: 'Waiting for workload pods to be ready...'); : wl.waitingPods);
return ( return (
<div className="card"> <div className="card">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2"> <h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<FileText className="w-5 h-5" /> Logs <FileText className="w-5 h-5" /> {wl.logs}
</h2> </h2>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3 rtl:space-x-reverse">
{showLogs && logTab === 'pod' && ( {showLogs && logTab === 'pod' && (
<span className="text-xs text-gray-400 flex items-center space-x-1"> <span className="text-xs text-gray-400 flex items-center space-x-1 rtl:space-x-reverse">
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" /> <span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
<span>Live (every 3s)</span> <span>{wl.liveEvery3s}</span>
</span> </span>
)} )}
{showBuildLogs && showLogs && logTab === 'build' && ( {showBuildLogs && showLogs && logTab === 'build' && (
<span className="text-xs text-gray-400 flex items-center space-x-1"> <span className="text-xs text-gray-400 flex items-center space-x-1 rtl:space-x-reverse">
<span className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" /> <span className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
<span>Auto-refresh (every 5s)</span> <span>{wl.autoEvery5s}</span>
</span> </span>
)} )}
<button type="button" onClick={() => setShowLogs(!showLogs)} className="btn-secondary text-sm"> <button type="button" onClick={() => setShowLogs(!showLogs)} className="btn-secondary text-sm">
{showLogs ? ( {showLogs ? (
<> <>
<ChevronDown className="w-4 h-4 inline" /> Hide logs <ChevronDown className="w-4 h-4 inline" /> {wl.hideLogs}
</> </>
) : ( ) : (
<> <>
<FileText className="w-4 h-4 inline" /> Show logs <FileText className="w-4 h-4 inline" /> {wl.showLogs}
</> </>
)} )}
</button> </button>
@@ -109,7 +113,7 @@ export function WorkloadLogsPanel({
logTab === 'pod' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-500 hover:text-gray-700' logTab === 'pod' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-500 hover:text-gray-700'
}`} }`}
> >
<Monitor className="w-4 h-4 inline" /> Pod logs <Monitor className="w-4 h-4 inline" /> {wl.podLogs}
</button> </button>
<button <button
type="button" type="button"
@@ -118,11 +122,11 @@ export function WorkloadLogsPanel({
logTab === 'build' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-500 hover:text-gray-700' logTab === 'build' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-500 hover:text-gray-700'
}`} }`}
> >
<Hammer className="w-4 h-4 inline" /> Build logs <Hammer className="w-4 h-4 inline" /> {wl.buildLogs}
</button> </button>
</div> </div>
) : ( ) : (
<p className="text-xs text-gray-500">Workload pod output (no image build for this service).</p> <p className="text-xs text-gray-500">{wl.noBuildNote}</p>
)} )}
{logTab === 'pod' && ( {logTab === 'pod' && (
@@ -142,19 +146,19 @@ export function WorkloadLogsPanel({
<Pin className="w-3 h-3 inline" /> {buildLogsData.version} <Pin className="w-3 h-3 inline" /> {buildLogsData.version}
</span> </span>
<span className={`badge ${statusColors[buildLogsData.status] || 'badge-gray'}`}> <span className={`badge ${statusColors[buildLogsData.status] || 'badge-gray'}`}>
{buildLogsData.status} {statusLabel(buildLogsData.status)}
</span> </span>
</div> </div>
)} )}
<pre className="bg-gray-900 text-blue-300 p-4 rounded-xl text-xs font-mono overflow-x-auto max-h-[500px] overflow-y-auto whitespace-pre-wrap break-words"> <pre className="bg-gray-900 text-blue-300 p-4 rounded-xl text-xs font-mono overflow-x-auto max-h-[500px] overflow-y-auto whitespace-pre-wrap break-words">
{buildLogsData?.buildLog || {buildLogsData?.buildLog ||
(buildLogsData?.status === 'building' (buildLogsData?.status === 'building'
? 'Build in progress... Logs will appear when complete.' ? wl.buildInProgress
: buildLogsData?.status === 'pending' : buildLogsData?.status === 'pending'
? 'Build is pending...' ? wl.buildPending
: buildLogsData?.status === 'no_deployment' : buildLogsData?.status === 'no_deployment'
? 'No deployments yet. Deploy your app to see build logs.' ? wl.noDeployments
: 'No build logs available for this deployment.')} : wl.noBuildLogs)}
</pre> </pre>
</div> </div>
)} )}
+64
View File
@@ -188,6 +188,70 @@ const en: Dictionary = {
twoCores: '2 cores', twoCores: '2 cores',
storageGb: 'Storage (GB)', storageGb: 'Storage (GB)',
}, },
deployStatus: {
running: 'running',
pending: 'pending',
building: 'building',
deploying: 'deploying',
failed: 'failed',
build_failed: 'build_failed',
cancelled: 'cancelled',
stopped: 'stopped',
active: 'active',
suspended: 'suspended',
pending_deletion: 'pending_deletion',
},
workloadLogs: {
logs: 'Logs',
liveEvery3s: 'Live (every 3s)',
autoEvery5s: 'Auto-refresh (every 5s)',
hideLogs: 'Hide logs',
showLogs: 'Show logs',
podLogs: 'Pod logs',
buildLogs: 'Build logs',
noBuildNote: 'Workload pod output (no image build for this service).',
loadingLogs: 'Loading logs...',
serviceStopped: 'Service is stopped. Start it to see logs.',
waitingPods: 'Waiting for workload pods to be ready...',
buildInProgress: 'Build in progress... Logs will appear when complete.',
buildPending: 'Build is pending...',
noDeployments: 'No deployments yet. Deploy your app to see build logs.',
noBuildLogs: 'No build logs available for this deployment.',
},
externalAccess: {
title: 'External access',
warning: 'Opens a NodePort on the cluster node IP. Anyone who can reach that IP may connect until the session ends or you revoke it. Use short durations for temporary access; permanent keeps the port open until revoked.',
deployFirst: 'Deploy the service first to enable external access.',
service: 'Service',
accessMode: 'Access mode',
temporary: 'Temporary',
alwaysOpen: 'Always open',
duration: 'Duration',
persistentNote: 'The port stays exposed until you click Revoke. Only use this when you need a stable external endpoint.',
opening: 'Opening…',
openPermanently: 'Open port permanently',
enableAccess: 'Enable access',
noSessions: 'No active external access sessions.',
revoke: 'Revoke',
endpoint: 'Endpoint',
url: 'URL',
permanentUntilRevoked: 'Permanent (until revoked)',
expired: 'Expired',
targets: {
database: 'Database',
redis: 'Redis',
rabbitmq_amqp: 'RabbitMQ (AMQP)',
rabbitmq_management: 'RabbitMQ Management UI',
},
permanentEnabled: 'Permanent external access enabled',
temporaryEnabled: 'Temporary external access enabled',
enableFailed: 'Failed to enable access',
revoked: 'Access revoked',
revokeFailed: 'Failed to revoke access',
hShort: 'h',
mShort: 'm',
sShort: 's',
},
}, },
nav: { nav: {
+64
View File
@@ -187,6 +187,70 @@ const fa = {
twoCores: '۲ هسته', twoCores: '۲ هسته',
storageGb: 'فضای ذخیره (GB)', storageGb: 'فضای ذخیره (GB)',
}, },
deployStatus: {
running: 'در حال اجرا',
pending: 'در انتظار',
building: 'در حال ساخت',
deploying: 'در حال انتشار',
failed: 'ناموفق',
build_failed: 'ساخت ناموفق',
cancelled: 'لغوشده',
stopped: 'متوقف',
active: 'فعال',
suspended: 'معلق',
pending_deletion: 'در انتظار حذف',
},
workloadLogs: {
logs: 'لاگ‌ها',
liveEvery3s: 'زنده (هر ۳ث)',
autoEvery5s: 'تازه‌سازی خودکار (هر ۵ث)',
hideLogs: 'پنهان‌کردن لاگ‌ها',
showLogs: 'نمایش لاگ‌ها',
podLogs: 'لاگ پاد',
buildLogs: 'لاگ بیلد',
noBuildNote: 'خروجی پاد workload (این سرویس بیلد ایمیج ندارد).',
loadingLogs: 'در حال بارگذاری لاگ‌ها…',
serviceStopped: 'سرویس متوقف است. برای دیدن لاگ آن را اجرا کن.',
waitingPods: 'در انتظار آماده‌شدن پادهای workload…',
buildInProgress: 'بیلد در حال انجام… لاگ پس از تکمیل نمایش داده می‌شود.',
buildPending: 'بیلد در صف است…',
noDeployments: 'هنوز دیپلویی نیست. برای دیدن لاگ بیلد اپت را منتشر کن.',
noBuildLogs: 'لاگ بیلدی برای این دیپلوی موجود نیست.',
},
externalAccess: {
title: 'دسترسی خارجی',
warning: 'یک NodePort روی IP نود کلاستر باز می‌کند. هر کسی که به آن IP دسترسی داشته باشد تا پایان نشست یا لغو آن می‌تواند متصل شود. برای دسترسی موقت از مدت‌های کوتاه استفاده کن؛ حالت دائمی پورت را تا لغو باز نگه می‌دارد.',
deployFirst: 'برای فعال‌سازی دسترسی خارجی، اول سرویس را منتشر کن.',
service: 'سرویس',
accessMode: 'حالت دسترسی',
temporary: 'موقت',
alwaysOpen: 'همیشه باز',
duration: 'مدت',
persistentNote: 'پورت تا وقتی روی «لغو» کلیک کنی باز می‌ماند. فقط وقتی به یک نقطهٔ پایانیِ خارجیِ پایدار نیاز داری استفاده کن.',
opening: 'در حال باز کردن…',
openPermanently: 'باز کردن دائمی پورت',
enableAccess: 'فعال‌سازی دسترسی',
noSessions: 'نشست دسترسی خارجی فعالی وجود ندارد.',
revoke: 'لغو',
endpoint: 'نقطهٔ پایانی',
url: 'آدرس',
permanentUntilRevoked: 'دائمی (تا لغو)',
expired: 'منقضی‌شده',
targets: {
database: 'دیتابیس',
redis: 'Redis',
rabbitmq_amqp: 'RabbitMQ (AMQP)',
rabbitmq_management: 'رابط مدیریت RabbitMQ',
},
permanentEnabled: 'دسترسی خارجی دائمی فعال شد',
temporaryEnabled: 'دسترسی خارجی موقت فعال شد',
enableFailed: 'فعال‌سازی دسترسی ناموفق بود',
revoked: 'دسترسی لغو شد',
revokeFailed: 'لغو دسترسی ناموفق بود',
hShort: 'س',
mShort: 'د',
sShort: 'ث',
},
}, },
nav: { nav: {
File diff suppressed because one or more lines are too long