Localize the managed-service detail page.

Move the service detail view (header actions, renewal banner, config,
deployment history, DB/Redis/RabbitMQ connection panels, renew modal)
onto a dashboard.serviceDetail dictionary with localized product-type and
deploy-status labels, locale-aware dates and RTL-aware layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-11 22:48:47 +03:30
parent deef498f65
commit 8d423f8224
4 changed files with 228 additions and 86 deletions
@@ -1,18 +1,19 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useParams, useRouter } from 'next/navigation';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '@/lib/api';
import { toast } from 'react-toastify';
import { Link } from '@/i18n/Link';
import { useT, useLocale } from '@/i18n/I18nProvider';
import { useLocalizedRouter } from '@/i18n/navigation';
import type {
Application,
Deployment,
OptionalServiceCredentials,
OptionalServiceResourcesMap,
} from '@/types';
import { managedServiceTypeLabel } from '@/lib/optional-service-defaults';
import {
ArrowLeft,
Database,
@@ -61,8 +62,13 @@ function dbPort(databaseType: string): string {
type AppWithOptional = Application & { optionalServiceResources?: OptionalServiceResourcesMap };
export default function ManagedServiceDetailPage() {
const t = useT();
const sd = t.dashboard.serviceDetail;
const locale = useLocale();
const typeLabel = (pt?: string) => (pt && (sd.productTypes as Record<string, string>)[pt]) || pt || '';
const statusLabel = (s: string) => (t.components.deployStatus as Record<string, string>)[s] ?? s;
const params = useParams();
const router = useRouter();
const router = useLocalizedRouter();
const queryClient = useQueryClient();
const { notifyDeployStarted } = useDeployProgressActions();
const confirm = useConfirm();
@@ -121,14 +127,14 @@ export default function ManagedServiceDetailPage() {
notifyDeployStarted(serviceId, app?.name);
},
onSuccess: () => {
toast.success('Provisioning started');
toast.success(sd.provisioningStarted);
queryClient.invalidateQueries({ queryKey: ['deployments', serviceId] });
queryClient.invalidateQueries({ queryKey: ['application', serviceId] });
queryClient.invalidateQueries({ queryKey: ['applications'] });
},
onError: () => {
useDeployProgressStore.getState().stopTracking(serviceId);
toast.error('Failed to start provisioning');
toast.error(sd.provisioningFailed);
},
});
@@ -138,7 +144,7 @@ export default function ManagedServiceDetailPage() {
notifyDeployStarted(serviceId, app?.name);
},
onSuccess: () => {
toast.success('Re-provisioning started');
toast.success(sd.reprovisioningStarted);
queryClient.invalidateQueries({ queryKey: ['deployments', serviceId] });
queryClient.invalidateQueries({ queryKey: ['application', serviceId] });
queryClient.invalidateQueries({ queryKey: ['applications'] });
@@ -146,29 +152,29 @@ export default function ManagedServiceDetailPage() {
onError: (err: unknown) => {
useDeployProgressStore.getState().stopTracking(serviceId);
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
toast.error(msg || 'Failed to re-provision service');
toast.error(msg || sd.reprovisionFailed);
},
});
const restartMutation = useMutation({
mutationFn: () => api.post(`/deployments/applications/${serviceId}/restart`),
onSuccess: () => toast.success('Service restarted'),
onSuccess: () => toast.success(sd.serviceRestarted),
onError: (err: unknown) => {
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
toast.error(msg || 'Failed to restart service');
toast.error(msg || sd.restartFailed);
},
});
const renewMutation = useMutation({
mutationFn: (cycle: string) => api.post(`/billing/applications/${serviceId}/renew`, { cycle }),
onSuccess: () => {
toast.success('Service renewed');
toast.success(sd.serviceRenewed);
queryClient.invalidateQueries({ queryKey: ['application', serviceId] });
setShowRenewalModal(false);
},
onError: (err: unknown) => {
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
toast.error(msg || 'Renewal failed');
toast.error(msg || sd.renewalFailed);
},
});
@@ -177,13 +183,13 @@ export default function ManagedServiceDetailPage() {
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['application', serviceId] });
if (data?.resourceCredit) {
toast.success('Service deleted. Prepaid resources are on your dashboard.');
toast.success(sd.deletedWithCredit);
} else {
toast.success('Service deleted');
toast.success(sd.deleted);
}
router.push('/dashboard/services');
},
onError: () => toast.error('Failed to delete'),
onError: () => toast.error(sd.deleteFailed),
});
const copyToClipboard = useCallback((text: string, field: string) => {
@@ -256,12 +262,12 @@ export default function ManagedServiceDetailPage() {
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div className="flex items-center gap-3">
<Link href="/dashboard/services" className="btn-ghost">
<ArrowLeft className="w-4 h-4" />
<ArrowLeft className="w-4 h-4 rtl:rotate-180" />
</Link>
<div>
<h1 className="page-title">{app.name}</h1>
<p className="text-sm text-gray-500">
{managedServiceTypeLabel(app.productType)}
{typeLabel(app.productType)}
{app.dbVersion ? ` · v${app.dbVersion}` : ''}
{app.redisVersion && app.productType === 'managed_redis' ? ` · v${app.redisVersion}` : ''}
{app.rabbitmqVersion && app.productType === 'managed_rabbitmq'
@@ -272,7 +278,7 @@ export default function ManagedServiceDetailPage() {
</div>
<div className="flex flex-wrap gap-2">
<span className={`badge ${latestStatus === 'running' ? 'badge-green' : 'badge-yellow'}`}>
{latestStatus}
{statusLabel(latestStatus)}
</span>
{!isDeployed && (
<button
@@ -281,7 +287,7 @@ export default function ManagedServiceDetailPage() {
onClick={() => deployMutation.mutate()}
disabled={deployMutation.isPending || needsRenewal}
>
<Rocket className="w-4 h-4 inline mr-1" /> Deploy
<Rocket className="w-4 h-4 inline mr-1 rtl:mr-0 rtl:ml-1" /> {sd.deploy}
</button>
)}
{isDeployed && !needsRenewal && !isInProgress && (
@@ -290,14 +296,14 @@ export default function ManagedServiceDetailPage() {
className="btn-primary text-sm"
onClick={() => redeployMutation.mutate()}
disabled={redeployMutation.isPending}
title="Re-run Helm install for this service"
title={sd.redeployTitle}
>
{redeployMutation.isPending ? (
<Clock className="w-4 h-4 inline animate-spin" />
) : (
<RefreshCw className="w-4 h-4 inline mr-1" />
)}
Redeploy
{sd.redeploy}
</button>
)}
{isDeployed && isRunning && (
@@ -312,7 +318,7 @@ export default function ManagedServiceDetailPage() {
) : (
<RotateCw className="w-4 h-4 inline mr-1" />
)}
Restart
{sd.restart}
</button>
)}
<button
@@ -321,15 +327,15 @@ export default function ManagedServiceDetailPage() {
disabled={pageLocked}
onClick={async () => {
const ok = await confirm({
title: 'Delete service?',
message: `Delete "${app.name}" permanently?`,
confirmText: 'Delete',
title: sd.deleteServiceTitle,
message: sd.deleteServiceMessage.replace('{name}', app.name),
confirmText: t.common.delete,
variant: 'danger',
});
if (ok) deleteApplication(serviceId);
}}
>
Delete
{t.common.delete}
</button>
</div>
</div>
@@ -339,8 +345,8 @@ export default function ManagedServiceDetailPage() {
<div className="flex items-center gap-3 flex-1">
<AlertTriangle className="w-6 h-6 text-amber-600 shrink-0" />
<div>
<h3 className="font-semibold text-amber-800">Payment required</h3>
<p className="text-sm text-amber-600">Renew to restore this service.</p>
<h3 className="font-semibold text-amber-800">{sd.paymentRequired}</h3>
<p className="text-sm text-amber-600">{sd.renewToRestore}</p>
</div>
</div>
<button
@@ -348,7 +354,7 @@ export default function ManagedServiceDetailPage() {
onClick={() => setShowRenewalModal(true)}
className="btn-primary bg-amber-600 hover:bg-amber-700"
>
<CreditCard className="w-4 h-4 inline mr-1" /> Renew
<CreditCard className="w-4 h-4 inline mr-1 rtl:mr-0 rtl:ml-1" /> {sd.renew}
</button>
</div>
)}
@@ -356,23 +362,23 @@ export default function ManagedServiceDetailPage() {
{app.planExpiresAt && (
<div className="card flex items-center gap-3 text-sm text-gray-600">
<Clock className="w-4 h-4" />
Plan expires: {new Date(app.planExpiresAt).toLocaleString()}
{app.billingCycle && ` (${app.billingCycle})`}
{sd.planExpires.replace('{date}', new Date(app.planExpiresAt).toLocaleString(locale))}
{app.billingCycle && ` (${(sd.cycles as Record<string,string>)[app.billingCycle] ?? app.billingCycle})`}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="card">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Configuration</h2>
<h2 className="text-lg font-semibold text-gray-900 mb-4">{sd.configuration}</h2>
<dl className="space-y-3">
<div className="flex justify-between">
<dt className="text-sm text-gray-500">Service type</dt>
<dd className="text-sm font-medium text-gray-900">{managedServiceTypeLabel(app.productType)}</dd>
<dt className="text-sm text-gray-500">{sd.serviceType}</dt>
<dd className="text-sm font-medium text-gray-900">{typeLabel(app.productType)}</dd>
</div>
{app.productType === 'managed_database' && (
<>
<div className="flex justify-between">
<dt className="text-sm text-gray-500">Database engine</dt>
<dt className="text-sm text-gray-500">{sd.databaseEngine}</dt>
<dd className="text-sm font-medium text-gray-900">
{app.databaseType}
{app.databaseType !== 'none' && app.dbVersion ? ` v${app.dbVersion}` : ''}
@@ -380,7 +386,7 @@ export default function ManagedServiceDetailPage() {
</div>
{app.databaseType !== 'none' && (
<div className="flex justify-between">
<dt className="text-sm text-gray-500">Storage</dt>
<dt className="text-sm text-gray-500">{sd.storage}</dt>
<dd className="text-sm font-medium text-gray-900">{app.dbStorageSize || '1Gi'}</dd>
</div>
)}
@@ -388,33 +394,33 @@ export default function ManagedServiceDetailPage() {
)}
{app.productType === 'managed_redis' && app.redisVersion && (
<div className="flex justify-between">
<dt className="text-sm text-gray-500">Redis version</dt>
<dt className="text-sm text-gray-500">{sd.redisVersion}</dt>
<dd className="text-sm font-medium text-gray-900">v{app.redisVersion}</dd>
</div>
)}
{app.productType === 'managed_rabbitmq' && app.rabbitmqVersion && (
<div className="flex justify-between">
<dt className="text-sm text-gray-500">RabbitMQ version</dt>
<dt className="text-sm text-gray-500">{sd.rabbitmqVersion}</dt>
<dd className="text-sm font-medium text-gray-900">v{app.rabbitmqVersion}</dd>
</div>
)}
<div className="flex justify-between">
<dt className="text-sm text-gray-500">CPU</dt>
<dt className="text-sm text-gray-500">{sd.cpu}</dt>
<dd className="text-sm font-medium text-gray-900">{cpuDisplay}</dd>
</div>
<div className="flex justify-between">
<dt className="text-sm text-gray-500">Memory</dt>
<dt className="text-sm text-gray-500">{sd.memory}</dt>
<dd className="text-sm font-medium text-gray-900">{memDisplay}</dd>
</div>
{app.billingCycle && (
<div className="flex justify-between">
<dt className="text-sm text-gray-500">Billing</dt>
<dd className="text-sm font-medium text-gray-900 capitalize">{app.billingCycle}</dd>
<dt className="text-sm text-gray-500">{sd.billing}</dt>
<dd className="text-sm font-medium text-gray-900">{(sd.cycles as Record<string,string>)[app.billingCycle] ?? app.billingCycle}</dd>
</div>
)}
{app.latestImageTag && (
<div className="flex justify-between">
<dt className="text-sm text-gray-500">Deploy marker</dt>
<dt className="text-sm text-gray-500">{sd.deployMarker}</dt>
<dd className="text-xs font-mono text-gray-700 truncate max-w-[200px]" title={app.latestImageTag}>
{app.latestImageTag}
</dd>
@@ -424,19 +430,19 @@ export default function ManagedServiceDetailPage() {
</div>
<div className="card">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Deployment History</h2>
<h2 className="text-lg font-semibold text-gray-900 mb-4">{sd.deploymentHistory}</h2>
{deployments.length === 0 ? (
<div className="text-center py-8">
<Package className="w-8 h-8 mx-auto text-gray-300 mb-2" />
<p className="text-gray-500 text-sm">No deployments yet</p>
<p className="text-gray-400 text-xs mt-1">Click Deploy to provision this service</p>
<p className="text-gray-500 text-sm">{sd.noDeployments}</p>
<p className="text-gray-400 text-xs mt-1">{sd.noDeploymentsHint}</p>
</div>
) : (
<div className="space-y-3 max-h-72 overflow-y-auto">
{deployments.slice(0, 10).map((d) => (
<div key={d.id} className="flex items-center justify-between p-3 bg-gray-50/80 rounded-xl">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-gray-900 truncate">{d.version || d.imageTag || 'Provision'}</p>
<p className="text-sm font-medium text-gray-900 truncate">{d.version || d.imageTag || sd.provision}</p>
<p className="text-xs text-gray-500">{new Date(d.createdAt).toLocaleString()}</p>
{d.errorMessage && (
<p className="text-xs text-red-500 mt-1 truncate" title={d.errorMessage}>
@@ -444,8 +450,8 @@ export default function ManagedServiceDetailPage() {
</p>
)}
</div>
<span className={`badge ${statusColors[d.status] || 'badge-gray'} ml-2 shrink-0`}>
{d.status}
<span className={`badge ${statusColors[d.status] || 'badge-gray'} ml-2 rtl:ml-0 rtl:mr-2 shrink-0`}>
{statusLabel(d.status)}
</span>
</div>
))}
@@ -457,16 +463,16 @@ export default function ManagedServiceDetailPage() {
{app.productType === 'managed_database' && app.databaseType !== 'none' && (
<div className="card">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Database className="w-5 h-5 text-blue-500" /> Connection
<Database className="w-5 h-5 text-blue-500" /> {sd.connection}
<span className="badge badge-blue text-xs">{app.databaseType}</span>
</h2>
<div className="bg-gray-50 rounded-xl p-4 space-y-2">
<h3 className="text-sm font-semibold text-gray-700 mb-3">Internal cluster</h3>
<h3 className="text-sm font-semibold text-gray-700 mb-3">{sd.internalCluster}</h3>
{[
{ label: 'Host', value: `${app.name}-db`, field: 'host' },
{ label: 'Port', value: dbPort(app.databaseType), field: 'port' },
{ label: 'Database', value: app.name.replace(/-/g, '_'), field: 'database' },
{ label: 'Username', value: app.dbUsername || 'appuser', field: 'username' },
{ label: sd.host, value: `${app.name}-db`, field: 'host' },
{ label: sd.port, value: dbPort(app.databaseType), field: 'port' },
{ label: sd.database, value: app.name.replace(/-/g, '_'), field: 'database' },
{ label: sd.username, value: app.dbUsername || 'appuser', field: 'username' },
].map(({ label, value, field }) => (
<div key={field} className="flex justify-between items-center">
<span className="text-sm text-gray-500">{label}</span>
@@ -487,7 +493,7 @@ export default function ManagedServiceDetailPage() {
</div>
))}
<div className="flex justify-between items-center">
<span className="text-sm text-gray-500">Password</span>
<span className="text-sm text-gray-500">{sd.password}</span>
<div className="flex items-center gap-1 font-mono text-sm">
{showDbPassword ? app.dbPassword || '—' : '••••••••'}
<button
@@ -511,7 +517,7 @@ export default function ManagedServiceDetailPage() {
</div>
</div>
<p className="text-xs text-gray-400 pt-2 border-t border-gray-200">
Use external access below for internet-facing connections.
{sd.useExternalNote}
</p>
</div>
</div>
@@ -521,7 +527,7 @@ export default function ManagedServiceDetailPage() {
<div className="card">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold flex items-center gap-2">
<KeyRound className="w-5 h-5" /> Connection
<KeyRound className="w-5 h-5" /> {sd.connection}
</h2>
<button
type="button"
@@ -529,23 +535,23 @@ export default function ManagedServiceDetailPage() {
className="btn-secondary text-xs inline-flex items-center gap-1"
>
{showServiceSecrets ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
{showServiceSecrets ? 'Hide secrets' : 'Show secrets'}
{showServiceSecrets ? sd.hideSecrets : sd.showSecrets}
</button>
</div>
{app.productType === 'managed_redis' && (
<div className="bg-gray-50 rounded-xl p-4 space-y-2">
<h3 className="text-sm font-semibold text-gray-700">Redis (internal)</h3>
<h3 className="text-sm font-semibold text-gray-700">{sd.redisInternal}</h3>
{[
{ label: 'Host', value: serviceCredentials?.redis?.host || `${app.name}-redis`, field: 'redis-host' },
{ label: 'Port', value: String(serviceCredentials?.redis?.port || 6379), field: 'redis-port' },
{ label: sd.host, value: serviceCredentials?.redis?.host || `${app.name}-redis`, field: 'redis-host' },
{ label: sd.port, value: String(serviceCredentials?.redis?.port || 6379), field: 'redis-port' },
{
label: 'Password',
label: sd.password,
value: serviceCredentials?.redis?.password || '',
field: 'redis-password',
secret: true,
},
{ label: 'URL', value: serviceCredentials?.redis?.url || '', field: 'redis-url', secret: true },
{ label: sd.url, value: serviceCredentials?.redis?.url || '', field: 'redis-url', secret: true },
].map(({ label, value, field, secret }) => (
<div key={field} className="flex items-center justify-between gap-3">
<span className="text-sm text-gray-500">{label}</span>
@@ -573,42 +579,42 @@ export default function ManagedServiceDetailPage() {
{app.productType === 'managed_rabbitmq' && (
<div className="bg-gray-50 rounded-xl p-4 space-y-2">
<h3 className="text-sm font-semibold text-gray-700">RabbitMQ (internal)</h3>
<h3 className="text-sm font-semibold text-gray-700">{sd.rabbitmqInternal}</h3>
{[
{
label: 'Host',
label: sd.host,
value: serviceCredentials?.rabbitmq?.host || `${app.name}-rabbitmq`,
field: 'rabbit-host',
},
{
label: 'AMQP Port',
label: sd.amqpPort,
value: String(serviceCredentials?.rabbitmq?.amqpPort || 5672),
field: 'rabbit-amqp-port',
},
{
label: 'Management Port',
label: sd.managementPort,
value: String(serviceCredentials?.rabbitmq?.managementPort || 15672),
field: 'rabbit-mgmt-port',
},
{
label: 'Username',
label: sd.username,
value: serviceCredentials?.rabbitmq?.username || 'appuser',
field: 'rabbit-user',
},
{
label: 'Password',
label: sd.password,
value: serviceCredentials?.rabbitmq?.password || '',
field: 'rabbit-password',
secret: true,
},
{
label: 'AMQP URL',
label: sd.amqpUrl,
value: serviceCredentials?.rabbitmq?.amqpUrl || '',
field: 'rabbit-amqp-url',
secret: true,
},
{
label: 'Management URL',
label: sd.managementUrl,
value: serviceCredentials?.rabbitmq?.managementUrl || '',
field: 'rabbit-mgmt-url',
},
@@ -638,7 +644,7 @@ export default function ManagedServiceDetailPage() {
)}
{!isDeployed && (
<p className="text-xs text-gray-400 mt-3">Deploy the service to load live credentials.</p>
<p className="text-xs text-gray-400 mt-3">{sd.deployToLoadCreds}</p>
)}
</div>
)}
@@ -664,23 +670,23 @@ export default function ManagedServiceDetailPage() {
isStopped={isStopped}
emptyPodMessage={
isRunning
? 'Loading logs...'
? sd.loadingLogs
: isStopped
? 'Service is stopped.'
: 'Deploy or re-provision the service to see workload logs.'
? sd.serviceStopped
: sd.deployToSeeLogs
}
/>
{showRenewalModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6">
<h2 className="text-xl font-bold mb-2">Renew service</h2>
<h2 className="text-xl font-bold mb-2">{sd.renewService}</h2>
<p className="text-sm text-gray-500 mb-4">&quot;{app.name}&quot;</p>
<div className="bg-gray-50 rounded-xl p-3 mb-4 flex justify-between text-sm">
<span className="flex items-center gap-2">
<Wallet className="w-4 h-4" /> Wallet
<Wallet className="w-4 h-4" /> {sd.wallet}
</span>
<strong>{walletData?.balance?.toLocaleString() ?? 0} T</strong>
<strong>{walletData?.balance?.toLocaleString('en-US') ?? 0} {t.common.currencyShort}</strong>
</div>
<div className="space-y-2 mb-6">
{(['hourly', 'monthly', 'yearly'] as const).map((cycle) => (
@@ -691,16 +697,16 @@ export default function ManagedServiceDetailPage() {
}`}
onClick={() => setSelectedCycle(cycle)}
>
<span className="capitalize font-medium">{cycle}</span>
<span className="font-medium">{(sd.cycles as Record<string,string>)[cycle]}</span>
<span className="font-bold">
{renewalCostData?.costs[cycle]?.toLocaleString() ?? '—'} T
{renewalCostData?.costs[cycle]?.toLocaleString('en-US') ?? '—'} {t.common.currencyShort}
</span>
</label>
))}
</div>
<div className="flex gap-2">
<button type="button" className="btn-secondary flex-1" onClick={() => setShowRenewalModal(false)}>
Cancel
{t.common.cancel}
</button>
<button
type="button"
@@ -708,13 +714,13 @@ export default function ManagedServiceDetailPage() {
disabled={renewMutation.isPending}
onClick={() => renewMutation.mutate(selectedCycle)}
>
<RefreshCw className="w-4 h-4 inline mr-1" />
Pay from wallet
<RefreshCw className="w-4 h-4 inline mr-1 rtl:mr-0 rtl:ml-1" />
{sd.payFromWallet}
</button>
</div>
{renewalCost != null && walletData && walletData.balance < renewalCost && (
<p className="text-xs text-amber-600 mt-3">
Insufficient wallet balance. Top up your wallet or pay via invoice.
{sd.insufficientRenew}
</p>
)}
</div>
+68
View File
@@ -949,6 +949,74 @@ const en: Dictionary = {
insufficientBalance: 'Insufficient wallet balance',
serviceDesc: 'Service: {name} ({cycle})',
},
serviceDetail: {
productTypes: {
managed_database: 'Managed Database',
managed_redis: 'Managed Redis',
managed_rabbitmq: 'Managed RabbitMQ',
},
deploy: 'Deploy',
redeploy: 'Redeploy',
redeployTitle: 'Re-run Helm install for this service',
restart: 'Restart',
deleteServiceTitle: 'Delete service?',
deleteServiceMessage: 'Delete “{name}” permanently?',
provisioningStarted: 'Provisioning started',
provisioningFailed: 'Failed to start provisioning',
reprovisioningStarted: 'Re-provisioning started',
reprovisionFailed: 'Failed to re-provision service',
serviceRestarted: 'Service restarted',
restartFailed: 'Failed to restart service',
serviceRenewed: 'Service renewed',
renewalFailed: 'Renewal failed',
deletedWithCredit: 'Service deleted. Prepaid resources are on your dashboard.',
deleted: 'Service deleted',
deleteFailed: 'Failed to delete',
paymentRequired: 'Payment required',
renewToRestore: 'Renew to restore this service.',
renew: 'Renew',
planExpires: 'Plan expires: {date}',
configuration: 'Configuration',
serviceType: 'Service type',
databaseEngine: 'Database engine',
storage: 'Storage',
redisVersion: 'Redis version',
rabbitmqVersion: 'RabbitMQ version',
cpu: 'CPU',
memory: 'Memory',
billing: 'Billing',
deployMarker: 'Deploy marker',
deploymentHistory: 'Deployment History',
noDeployments: 'No deployments yet',
noDeploymentsHint: 'Click Deploy to provision this service',
provision: 'Provision',
connection: 'Connection',
internalCluster: 'Internal cluster',
host: 'Host',
port: 'Port',
database: 'Database',
username: 'Username',
password: 'Password',
useExternalNote: 'Use external access below for internet-facing connections.',
hideSecrets: 'Hide secrets',
showSecrets: 'Show secrets',
redisInternal: 'Redis (internal)',
rabbitmqInternal: 'RabbitMQ (internal)',
url: 'URL',
amqpPort: 'AMQP Port',
managementPort: 'Management Port',
amqpUrl: 'AMQP URL',
managementUrl: 'Management URL',
deployToLoadCreds: 'Deploy the service to load live credentials.',
loadingLogs: 'Loading logs...',
serviceStopped: 'Service is stopped.',
deployToSeeLogs: 'Deploy or re-provision the service to see workload logs.',
renewService: 'Renew service',
wallet: 'Wallet',
payFromWallet: 'Pay from wallet',
insufficientRenew: 'Insufficient wallet balance. Top up your wallet or pay via invoice.',
cycles: { hourly: 'Hourly', monthly: 'Monthly', yearly: 'Yearly' },
},
},
};
+68
View File
@@ -948,6 +948,74 @@ const fa = {
insufficientBalance: 'موجودی کیف‌پول کافی نیست',
serviceDesc: 'سرویس: {name} ({cycle})',
},
serviceDetail: {
productTypes: {
managed_database: 'دیتابیس مدیریت‌شده',
managed_redis: 'Redis مدیریت‌شده',
managed_rabbitmq: 'RabbitMQ مدیریت‌شده',
},
deploy: 'انتشار',
redeploy: 'انتشار مجدد',
redeployTitle: 'اجرای مجدد Helm install برای این سرویس',
restart: 'ری‌استارت',
deleteServiceTitle: 'حذف سرویس؟',
deleteServiceMessage: 'حذف «{name}» به‌صورت دائمی؟',
provisioningStarted: 'راه‌اندازی آغاز شد',
provisioningFailed: 'شروع راه‌اندازی ناموفق بود',
reprovisioningStarted: 'راه‌اندازی مجدد آغاز شد',
reprovisionFailed: 'راه‌اندازی مجدد سرویس ناموفق بود',
serviceRestarted: 'سرویس ری‌استارت شد',
restartFailed: 'ری‌استارت سرویس ناموفق بود',
serviceRenewed: 'سرویس تمدید شد',
renewalFailed: 'تمدید ناموفق بود',
deletedWithCredit: 'سرویس حذف شد. منابع پیش‌پرداختت روی داشبورد است.',
deleted: 'سرویس حذف شد',
deleteFailed: 'حذف ناموفق بود',
paymentRequired: 'پرداخت لازم است',
renewToRestore: 'برای بازگرداندن این سرویس تمدید کن.',
renew: 'تمدید',
planExpires: 'انقضای پلن: {date}',
configuration: 'پیکربندی',
serviceType: 'نوع سرویس',
databaseEngine: 'موتور دیتابیس',
storage: 'فضای ذخیره',
redisVersion: 'نسخهٔ Redis',
rabbitmqVersion: 'نسخهٔ RabbitMQ',
cpu: 'CPU',
memory: 'حافظه',
billing: 'صورت‌حساب',
deployMarker: 'نشان دیپلوی',
deploymentHistory: 'تاریخچهٔ دیپلوی',
noDeployments: 'هنوز دیپلویی نیست',
noDeploymentsHint: 'برای راه‌اندازی این سرویس روی «انتشار» بزن',
provision: 'راه‌اندازی',
connection: 'اتصال',
internalCluster: 'کلاستر داخلی',
host: 'هاست',
port: 'پورت',
database: 'دیتابیس',
username: 'نام کاربری',
password: 'رمز عبور',
useExternalNote: 'برای اتصال‌های اینترنتی از دسترسی خارجی زیر استفاده کن.',
hideSecrets: 'پنهان‌کردن اسرار',
showSecrets: 'نمایش اسرار',
redisInternal: 'Redis (داخلی)',
rabbitmqInternal: 'RabbitMQ (داخلی)',
url: 'آدرس',
amqpPort: 'پورت AMQP',
managementPort: 'پورت مدیریت',
amqpUrl: 'آدرس AMQP',
managementUrl: 'آدرس مدیریت',
deployToLoadCreds: 'برای بارگذاری اطلاعات ورود زنده، سرویس را منتشر کن.',
loadingLogs: 'در حال بارگذاری لاگ‌ها…',
serviceStopped: 'سرویس متوقف است.',
deployToSeeLogs: 'برای دیدن لاگ workload، سرویس را منتشر یا راه‌اندازی مجدد کن.',
renewService: 'تمدید سرویس',
wallet: 'کیف‌پول',
payFromWallet: 'پرداخت از کیف‌پول',
insufficientRenew: 'موجودی کیف‌پول کافی نیست. کیف‌پول را شارژ کن یا از طریق فاکتور پرداخت کن.',
cycles: { hourly: 'ساعتی', monthly: 'ماهانه', yearly: 'سالانه' },
},
},
};