Localize applications and services list pages.
Move the apps and managed-services list pages onto the i18n dictionaries (titles, table headers, lifecycle/expiry labels, delete confirmations, toast messages) and the locale-aware Link, reusing the shared status and lifecycle/expiry copy. Add RTL-aware table alignment and icon spacing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import Link from 'next/link';
|
||||
import { Link } from '@/i18n/Link';
|
||||
import { useT } from '@/i18n/I18nProvider';
|
||||
import type { Dictionary } from '@/i18n/dictionaries/fa';
|
||||
import api from '@/lib/api';
|
||||
import type { Application } from '@/types';
|
||||
import { Rocket, Package, Hexagon, Database, Box, AlertTriangle, Clock } from 'lucide-react';
|
||||
@@ -33,28 +35,37 @@ const lifecycleColors: Record<string, string> = {
|
||||
deleted: 'text-gray-500 bg-gray-100',
|
||||
};
|
||||
|
||||
const lifecycleLabels: Record<string, string> = {
|
||||
active: 'Active',
|
||||
suspended: 'Suspended — Unpaid',
|
||||
pending_deletion: 'Pending Deletion',
|
||||
deleted: 'Deleted',
|
||||
};
|
||||
type AppsDict = Dictionary['dashboard']['apps'];
|
||||
|
||||
function formatExpiry(expiresAt?: string): { text: string; urgent: boolean } {
|
||||
function statusLabel(status: string, t: Dictionary): string {
|
||||
return (t.dashboard.status as Record<string, string>)[status] ?? status;
|
||||
}
|
||||
|
||||
function lifecycleLabel(lifecycle: string, a: AppsDict): string {
|
||||
return (a.lifecycle as Record<string, string>)[lifecycle] ?? lifecycle;
|
||||
}
|
||||
|
||||
function formatExpiry(expiresAt: string | undefined, a: AppsDict): { text: string; urgent: boolean } {
|
||||
if (!expiresAt) return { text: '—', urgent: false };
|
||||
const now = new Date();
|
||||
const exp = new Date(expiresAt);
|
||||
const diff = exp.getTime() - now.getTime();
|
||||
if (diff <= 0) return { text: 'Expired', urgent: true };
|
||||
if (diff <= 0) return { text: a.expired, urgent: true };
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(hours / 24);
|
||||
if (days > 0) return { text: `${days}d ${hours % 24}h remaining`, urgent: days < 3 };
|
||||
if (hours > 0) return { text: `${hours}h remaining`, urgent: hours < 6 };
|
||||
if (days > 0)
|
||||
return {
|
||||
text: a.daysHours.replace('{d}', String(days)).replace('{h}', String(hours % 24)),
|
||||
urgent: days < 3,
|
||||
};
|
||||
if (hours > 0) return { text: a.hours.replace('{h}', String(hours)), urgent: hours < 6 };
|
||||
const mins = Math.floor(diff / 60000);
|
||||
return { text: `${mins}m remaining`, urgent: true };
|
||||
return { text: a.mins.replace('{m}', String(mins)), urgent: true };
|
||||
}
|
||||
|
||||
export default function AppsPage() {
|
||||
const t = useT();
|
||||
const a = t.dashboard.apps;
|
||||
const confirm = useConfirm();
|
||||
|
||||
const { data: appsRaw = [], isLoading } = useQuery<Application[]>({
|
||||
@@ -65,8 +76,8 @@ export default function AppsPage() {
|
||||
|
||||
const { deleteApplication, isDeleting, isAnyDeleting } = useApplicationDelete({
|
||||
invalidateKeys: [['applications', 'application'], ['applications']],
|
||||
successMessage: 'Application deleted',
|
||||
successWithCreditMessage: 'Application deleted. Your prepaid resources are shown on the dashboard.',
|
||||
successMessage: a.deleted,
|
||||
successWithCreditMessage: a.deletedWithCredit,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
@@ -96,21 +107,21 @@ export default function AppsPage() {
|
||||
<div className="space-y-6">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1 className="page-title">My Applications</h1>
|
||||
<p className="page-subtitle">{apps.length} application{apps.length !== 1 ? 's' : ''}</p>
|
||||
<h1 className="page-title">{a.title}</h1>
|
||||
<p className="page-subtitle">{a.count.replace('{n}', String(apps.length))}</p>
|
||||
</div>
|
||||
<Link href="/dashboard/deploy" className="btn-primary">
|
||||
<Rocket className="w-4 h-4 mr-1 inline" /> New Application
|
||||
<Rocket className="w-4 h-4 mr-1 rtl:mr-0 rtl:ml-1 inline" /> {a.newApplication}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{apps.length === 0 ? (
|
||||
<div className="card text-center py-16">
|
||||
<Package className="w-12 h-12 mx-auto mb-4 text-gray-300" />
|
||||
<p className="text-gray-600 text-lg font-medium">No applications yet</p>
|
||||
<p className="text-gray-400 mt-1 text-sm">Deploy your first application to get started.</p>
|
||||
<p className="text-gray-600 text-lg font-medium">{a.noApplications}</p>
|
||||
<p className="text-gray-400 mt-1 text-sm">{a.emptyDesc}</p>
|
||||
<Link href="/dashboard/deploy" className="btn-primary mt-6 inline-flex items-center gap-1">
|
||||
<Rocket className="w-4 h-4" /> Deploy your first app
|
||||
<Rocket className="w-4 h-4" /> {a.deployFirst}
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
@@ -119,19 +130,19 @@ export default function AppsPage() {
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50/80">
|
||||
<tr>
|
||||
<th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider min-w-[300px]">Application</th>
|
||||
<th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Runtime</th>
|
||||
<th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Service Status</th>
|
||||
<th className="px-6 py-3.5 text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">Expiry</th>
|
||||
<th className="px-6 py-3.5 text-right text-xs font-semibold text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
<th className="px-6 py-3.5 text-left rtl:text-right text-xs font-semibold text-gray-500 uppercase tracking-wider min-w-[300px]">{a.colApplication}</th>
|
||||
<th className="px-6 py-3.5 text-left rtl:text-right text-xs font-semibold text-gray-500 uppercase tracking-wider">{a.colRuntime}</th>
|
||||
<th className="px-6 py-3.5 text-left rtl:text-right text-xs font-semibold text-gray-500 uppercase tracking-wider">{a.colStatus}</th>
|
||||
<th className="px-6 py-3.5 text-left rtl:text-right text-xs font-semibold text-gray-500 uppercase tracking-wider">{a.colServiceStatus}</th>
|
||||
<th className="px-6 py-3.5 text-left rtl:text-right text-xs font-semibold text-gray-500 uppercase tracking-wider">{a.colExpiry}</th>
|
||||
<th className="px-6 py-3.5 text-right rtl:text-left text-xs font-semibold text-gray-500 uppercase tracking-wider">{a.colActions}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{apps.map((app) => {
|
||||
const latestStatus = app.deployments?.[0]?.status || 'pending';
|
||||
const lifecycle = app.lifecycleStatus || 'active';
|
||||
const expiry = formatExpiry(app.planExpiresAt);
|
||||
const expiry = formatExpiry(app.planExpiresAt, a);
|
||||
const rowDeleting = isDeleting(app.id);
|
||||
return (
|
||||
<tr
|
||||
@@ -148,12 +159,12 @@ export default function AppsPage() {
|
||||
</td>
|
||||
<td className={`px-6 py-4 text-sm text-gray-600 capitalize ${rowDeleting ? deletingRowContentClass : ''}`}>{app.runtime}</td>
|
||||
<td className={`px-6 py-4 ${rowDeleting ? deletingRowContentClass : ''}`}>
|
||||
<span className={`badge ${statusColors[latestStatus] || 'badge-gray'}`}>{latestStatus}</span>
|
||||
<span className={`badge ${statusColors[latestStatus] || 'badge-gray'}`}>{statusLabel(latestStatus, t)}</span>
|
||||
</td>
|
||||
<td className={`px-6 py-4 ${rowDeleting ? deletingRowContentClass : ''}`}>
|
||||
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold ${lifecycleColors[lifecycle] || 'text-gray-500 bg-gray-100'}`}>
|
||||
{lifecycle === 'suspended' && <AlertTriangle className="w-3 h-3" />}
|
||||
{lifecycleLabels[lifecycle] || lifecycle}
|
||||
{lifecycleLabel(lifecycle, a)}
|
||||
</span>
|
||||
</td>
|
||||
<td className={`px-6 py-4 ${rowDeleting ? deletingRowContentClass : ''}`}>
|
||||
@@ -163,22 +174,22 @@ export default function AppsPage() {
|
||||
{expiry.text}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">No plan</span>
|
||||
<span className="text-xs text-gray-400">{a.noPlan}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className={`px-6 py-4 text-right ${rowDeleting ? deletingRowContentClass : ''}`}>
|
||||
<td className={`px-6 py-4 text-right rtl:text-left ${rowDeleting ? deletingRowContentClass : ''}`}>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Link href={`/dashboard/apps/${app.id}`} className="btn-ghost text-xs px-3 py-1.5">
|
||||
View
|
||||
{a.view}
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isAnyDeleting}
|
||||
onClick={async () => {
|
||||
const ok = await confirm({
|
||||
title: 'Delete Application',
|
||||
message: `Permanently delete "${app.name}" and all its data?\n\nIf your plan still has time left, the prepaid resources will appear on your dashboard for use on a new app at no extra charge.`,
|
||||
confirmText: 'Delete',
|
||||
title: a.deleteTitle,
|
||||
message: a.deleteMessage.replace('{name}', app.name),
|
||||
confirmText: t.common.delete,
|
||||
variant: 'danger',
|
||||
});
|
||||
if (ok) deleteApplication(app.id);
|
||||
@@ -206,7 +217,7 @@ export default function AppsPage() {
|
||||
{apps.map((app) => {
|
||||
const latestStatus = app.deployments?.[0]?.status || 'pending';
|
||||
const lifecycle = app.lifecycleStatus || 'active';
|
||||
const expiry = formatExpiry(app.planExpiresAt);
|
||||
const expiry = formatExpiry(app.planExpiresAt, a);
|
||||
return (
|
||||
<Link
|
||||
key={app.id}
|
||||
@@ -223,12 +234,12 @@ export default function AppsPage() {
|
||||
<p className="text-xs text-gray-500 capitalize">{app.runtime}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`badge ${statusColors[latestStatus] || 'badge-gray'}`}>{latestStatus}</span>
|
||||
<span className={`badge ${statusColors[latestStatus] || 'badge-gray'}`}>{statusLabel(latestStatus, t)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold ${lifecycleColors[lifecycle] || 'text-gray-500 bg-gray-100'}`}>
|
||||
{lifecycle === 'suspended' && <AlertTriangle className="w-3 h-3" />}
|
||||
{lifecycleLabels[lifecycle] || lifecycle}
|
||||
{lifecycleLabel(lifecycle, a)}
|
||||
</span>
|
||||
{app.planExpiresAt && (
|
||||
<span className={`inline-flex items-center gap-1 text-xs ${expiry.urgent ? 'text-red-600 font-semibold' : 'text-gray-500'}`}>
|
||||
@@ -242,7 +253,7 @@ export default function AppsPage() {
|
||||
<Database className="w-3 h-3" /> {app.databaseType}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Box className="w-3 h-3" /> {app.replicas} replica{app.replicas > 1 ? 's' : ''}
|
||||
<Box className="w-3 h-3" /> {app.replicas} {app.replicas > 1 ? t.dashboard.home.replicaPlural : t.dashboard.home.replica}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import Link from 'next/link';
|
||||
import { Link } from '@/i18n/Link';
|
||||
import { useT } from '@/i18n/I18nProvider';
|
||||
import type { Dictionary } from '@/i18n/dictionaries/fa';
|
||||
import api from '@/lib/api';
|
||||
import type { Application } from '@/types';
|
||||
import { managedServiceTypeLabel } from '@/lib/optional-service-defaults';
|
||||
@@ -23,12 +25,15 @@ const lifecycleColors: Record<string, string> = {
|
||||
deleted: 'text-gray-500 bg-gray-100',
|
||||
};
|
||||
|
||||
const lifecycleLabels: Record<string, string> = {
|
||||
active: 'Active',
|
||||
suspended: 'Suspended — Unpaid',
|
||||
pending_deletion: 'Pending Deletion',
|
||||
deleted: 'Deleted',
|
||||
};
|
||||
type AppsDict = Dictionary['dashboard']['apps'];
|
||||
|
||||
function statusLabel(status: string, t: Dictionary): string {
|
||||
return (t.dashboard.status as Record<string, string>)[status] ?? status;
|
||||
}
|
||||
|
||||
function lifecycleLabel(lifecycle: string, a: AppsDict): string {
|
||||
return (a.lifecycle as Record<string, string>)[lifecycle] ?? lifecycle;
|
||||
}
|
||||
|
||||
function serviceSubtitle(app: Application): string {
|
||||
if (app.productType === 'managed_database') {
|
||||
@@ -43,19 +48,26 @@ function serviceSubtitle(app: Application): string {
|
||||
return app.databaseType !== 'none' ? app.databaseType : '—';
|
||||
}
|
||||
|
||||
function formatExpiry(expiresAt?: string): { text: string; urgent: boolean } {
|
||||
function formatExpiry(expiresAt: string | undefined, a: AppsDict): { text: string; urgent: boolean } {
|
||||
if (!expiresAt) return { text: '—', urgent: false };
|
||||
const diff = new Date(expiresAt).getTime() - Date.now();
|
||||
if (diff <= 0) return { text: 'Expired', urgent: true };
|
||||
if (diff <= 0) return { text: a.expired, urgent: true };
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(hours / 24);
|
||||
if (days > 0) return { text: `${days}d ${hours % 24}h remaining`, urgent: days < 3 };
|
||||
if (hours > 0) return { text: `${hours}h remaining`, urgent: hours < 6 };
|
||||
if (days > 0)
|
||||
return {
|
||||
text: a.daysHours.replace('{d}', String(days)).replace('{h}', String(hours % 24)),
|
||||
urgent: days < 3,
|
||||
};
|
||||
if (hours > 0) return { text: a.hours.replace('{h}', String(hours)), urgent: hours < 6 };
|
||||
const mins = Math.floor(diff / 60000);
|
||||
return { text: `${mins}m remaining`, urgent: true };
|
||||
return { text: a.mins.replace('{m}', String(mins)), urgent: true };
|
||||
}
|
||||
|
||||
export default function ServicesPage() {
|
||||
const t = useT();
|
||||
const s = t.dashboard.services;
|
||||
const appsDict = t.dashboard.apps;
|
||||
const confirm = useConfirm();
|
||||
|
||||
const { data: servicesRaw = [], isLoading } = useQuery<Application[]>({
|
||||
@@ -66,8 +78,8 @@ export default function ServicesPage() {
|
||||
|
||||
const { deleteApplication, isDeleting, isAnyDeleting } = useApplicationDelete({
|
||||
invalidateKeys: [['applications', 'managed']],
|
||||
successMessage: 'Service deleted',
|
||||
successWithCreditMessage: 'Service deleted. Prepaid resources are on your dashboard.',
|
||||
successMessage: s.deleted,
|
||||
successWithCreditMessage: s.deletedWithCredit,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
@@ -96,32 +108,28 @@ export default function ServicesPage() {
|
||||
<div className="space-y-6">
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1 className="page-title">Databases & Services</h1>
|
||||
<p className="page-subtitle">
|
||||
Standalone databases, Redis, and RabbitMQ — {services.length} service{services.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
<h1 className="page-title">{s.title}</h1>
|
||||
<p className="page-subtitle">{s.subtitle.replace('{n}', String(services.length))}</p>
|
||||
</div>
|
||||
<Link href="/dashboard/services/new" className="btn-primary">
|
||||
<Plus className="w-4 h-4 mr-1 inline" /> New Service
|
||||
<Plus className="w-4 h-4 mr-1 rtl:mr-0 rtl:ml-1 inline" /> {s.newService}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{services.length === 0 ? (
|
||||
<div className="card text-center py-16">
|
||||
<Database className="w-12 h-12 mx-auto mb-4 text-gray-300" />
|
||||
<p className="text-gray-600 text-lg font-medium">No managed services yet</p>
|
||||
<p className="text-gray-400 mt-1 text-sm">
|
||||
Provision a database, Redis, or RabbitMQ without deploying a full application.
|
||||
</p>
|
||||
<p className="text-gray-600 text-lg font-medium">{s.noServices}</p>
|
||||
<p className="text-gray-400 mt-1 text-sm">{s.emptyDesc}</p>
|
||||
<Link href="/dashboard/services/new" className="btn-primary mt-6 inline-flex items-center gap-1">
|
||||
<Plus className="w-4 h-4" /> Create your first service
|
||||
<Plus className="w-4 h-4" /> {s.createFirst}
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{services.map((svc) => {
|
||||
const lifecycle = svc.lifecycleStatus || 'active';
|
||||
const expiry = formatExpiry(svc.planExpiresAt);
|
||||
const expiry = formatExpiry(svc.planExpiresAt, appsDict);
|
||||
const latestStatus = svc.deployments?.[0]?.status || 'pending';
|
||||
const cardDeleting = isDeleting(svc.id);
|
||||
return (
|
||||
@@ -150,7 +158,7 @@ export default function ServicesPage() {
|
||||
</Link>
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
||||
<span className={`badge ${latestStatus === 'running' ? 'badge-green' : 'badge-yellow'}`}>
|
||||
{latestStatus}
|
||||
{statusLabel(latestStatus, t)}
|
||||
</span>
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold ${
|
||||
@@ -158,20 +166,23 @@ export default function ServicesPage() {
|
||||
}`}
|
||||
>
|
||||
{lifecycle === 'suspended' && <AlertTriangle className="w-3 h-3" />}
|
||||
{lifecycleLabels[lifecycle] || lifecycle}
|
||||
{lifecycleLabel(lifecycle, appsDict)}
|
||||
</span>
|
||||
<span className={`text-xs ${expiry.urgent ? 'text-red-600 font-semibold' : 'text-gray-500'}`}>
|
||||
{svc.planExpiresAt && (
|
||||
<span className={`inline-flex items-center gap-1 text-xs ${expiry.urgent ? 'text-red-600 font-semibold' : 'text-gray-500'}`}>
|
||||
<Clock className="w-3 h-3" />
|
||||
{expiry.text}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn-ghost text-sm text-red-600 disabled:opacity-50 disabled:pointer-events-none"
|
||||
disabled={isAnyDeleting}
|
||||
onClick={async () => {
|
||||
const ok = await confirm({
|
||||
title: 'Delete service?',
|
||||
message: `Permanently delete "${svc.name}"? This cannot be undone.`,
|
||||
confirmText: 'Delete',
|
||||
title: s.deleteTitle,
|
||||
message: s.deleteMessage.replace('{name}', svc.name),
|
||||
confirmText: t.common.delete,
|
||||
variant: 'danger',
|
||||
});
|
||||
if (ok) deleteApplication(svc.id);
|
||||
|
||||
@@ -206,6 +206,49 @@ const en: Dictionary = {
|
||||
replica: 'replica',
|
||||
replicaPlural: 'replicas',
|
||||
},
|
||||
apps: {
|
||||
title: 'My Applications',
|
||||
count: '{n} application(s)',
|
||||
newApplication: 'New Application',
|
||||
noApplications: 'No applications yet',
|
||||
emptyDesc: 'Deploy your first application to get started.',
|
||||
deployFirst: 'Deploy your first app',
|
||||
colApplication: 'Application',
|
||||
colRuntime: 'Runtime',
|
||||
colStatus: 'Status',
|
||||
colServiceStatus: 'Service Status',
|
||||
colExpiry: 'Expiry',
|
||||
colActions: 'Actions',
|
||||
view: 'View',
|
||||
lifecycle: {
|
||||
active: 'Active',
|
||||
suspended: 'Suspended — Unpaid',
|
||||
pending_deletion: 'Pending Deletion',
|
||||
deleted: 'Deleted',
|
||||
},
|
||||
expired: 'Expired',
|
||||
daysHours: '{d}d {h}h remaining',
|
||||
hours: '{h}h remaining',
|
||||
mins: '{m}m remaining',
|
||||
noPlan: 'No plan',
|
||||
deleteTitle: 'Delete Application',
|
||||
deleteMessage:
|
||||
'Permanently delete “{name}” and all its data?\n\nIf your plan still has time left, the prepaid resources will appear on your dashboard for use on a new app at no extra charge.',
|
||||
deleted: 'Application deleted',
|
||||
deletedWithCredit: 'Application deleted. Your prepaid resources are shown on the dashboard.',
|
||||
},
|
||||
services: {
|
||||
title: 'Databases & Services',
|
||||
subtitle: 'Standalone databases, Redis, and RabbitMQ — {n} service(s)',
|
||||
newService: 'New Service',
|
||||
noServices: 'No managed services yet',
|
||||
emptyDesc: 'Provision a database, Redis, or RabbitMQ without deploying a full application.',
|
||||
createFirst: 'Create your first service',
|
||||
deleteTitle: 'Delete service?',
|
||||
deleteMessage: 'Permanently delete “{name}”? This cannot be undone.',
|
||||
deleted: 'Service deleted',
|
||||
deletedWithCredit: 'Service deleted. Prepaid resources are on your dashboard.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -205,6 +205,49 @@ const fa = {
|
||||
replica: 'نمونه',
|
||||
replicaPlural: 'نمونه',
|
||||
},
|
||||
apps: {
|
||||
title: 'اپلیکیشنهای من',
|
||||
count: '{n} اپلیکیشن',
|
||||
newApplication: 'اپلیکیشن جدید',
|
||||
noApplications: 'هنوز اپلیکیشنی نداری',
|
||||
emptyDesc: 'برای شروع، اولین اپلیکیشنت را منتشر کن.',
|
||||
deployFirst: 'اولین اپت را منتشر کن',
|
||||
colApplication: 'اپلیکیشن',
|
||||
colRuntime: 'محیط اجرا',
|
||||
colStatus: 'وضعیت',
|
||||
colServiceStatus: 'وضعیت سرویس',
|
||||
colExpiry: 'انقضا',
|
||||
colActions: 'عملیات',
|
||||
view: 'مشاهده',
|
||||
lifecycle: {
|
||||
active: 'فعال',
|
||||
suspended: 'معلق — پرداختنشده',
|
||||
pending_deletion: 'در انتظار حذف',
|
||||
deleted: 'حذفشده',
|
||||
},
|
||||
expired: 'منقضی شده',
|
||||
daysHours: '{d} روز و {h} ساعت باقیمانده',
|
||||
hours: '{h} ساعت باقیمانده',
|
||||
mins: '{m} دقیقه باقیمانده',
|
||||
noPlan: 'بدون پلن',
|
||||
deleteTitle: 'حذف اپلیکیشن',
|
||||
deleteMessage:
|
||||
'اپلیکیشن «{name}» و همهٔ دادههایش برای همیشه حذف شود؟\n\nاگر از پلنت زمان باقی مانده باشد، منابع پیشپرداختشده روی داشبوردت نمایش داده میشود تا بدون هزینهٔ اضافه روی اپ جدید استفاده کنی.',
|
||||
deleted: 'اپلیکیشن حذف شد',
|
||||
deletedWithCredit: 'اپلیکیشن حذف شد. منابع پیشپرداختت روی داشبورد نمایش داده میشود.',
|
||||
},
|
||||
services: {
|
||||
title: 'دیتابیسها و سرویسها',
|
||||
subtitle: 'دیتابیسهای مستقل، Redis و RabbitMQ — {n} سرویس',
|
||||
newService: 'سرویس جدید',
|
||||
noServices: 'هنوز سرویس مدیریتشدهای نداری',
|
||||
emptyDesc: 'بدون انتشار یک اپلیکیشن کامل، یک دیتابیس، Redis یا RabbitMQ بساز.',
|
||||
createFirst: 'اولین سرویست را بساز',
|
||||
deleteTitle: 'حذف سرویس؟',
|
||||
deleteMessage: 'سرویس «{name}» برای همیشه حذف شود؟ این عمل قابل بازگشت نیست.',
|
||||
deleted: 'سرویس حذف شد',
|
||||
deletedWithCredit: 'سرویس حذف شد. منابع پیشپرداختت روی داشبورد است.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user