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:
keyhan
2026-06-11 12:28:59 +03:30
parent e0964e6528
commit 2bc86c03b8
5 changed files with 181 additions and 73 deletions
+50 -39
View File
@@ -1,7 +1,9 @@
'use client'; 'use client';
import { useQuery } from '@tanstack/react-query'; 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 api from '@/lib/api';
import type { Application } from '@/types'; import type { Application } from '@/types';
import { Rocket, Package, Hexagon, Database, Box, AlertTriangle, Clock } from 'lucide-react'; 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', deleted: 'text-gray-500 bg-gray-100',
}; };
const lifecycleLabels: Record<string, string> = { type AppsDict = Dictionary['dashboard']['apps'];
active: 'Active',
suspended: 'Suspended — Unpaid',
pending_deletion: 'Pending Deletion',
deleted: 'Deleted',
};
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 }; if (!expiresAt) return { text: '—', urgent: false };
const now = new Date(); const now = new Date();
const exp = new Date(expiresAt); const exp = new Date(expiresAt);
const diff = exp.getTime() - now.getTime(); 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 hours = Math.floor(diff / 3600000);
const days = Math.floor(hours / 24); const days = Math.floor(hours / 24);
if (days > 0) return { text: `${days}d ${hours % 24}h remaining`, urgent: days < 3 }; if (days > 0)
if (hours > 0) return { text: `${hours}h remaining`, urgent: hours < 6 }; 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); 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() { export default function AppsPage() {
const t = useT();
const a = t.dashboard.apps;
const confirm = useConfirm(); const confirm = useConfirm();
const { data: appsRaw = [], isLoading } = useQuery<Application[]>({ const { data: appsRaw = [], isLoading } = useQuery<Application[]>({
@@ -65,8 +76,8 @@ export default function AppsPage() {
const { deleteApplication, isDeleting, isAnyDeleting } = useApplicationDelete({ const { deleteApplication, isDeleting, isAnyDeleting } = useApplicationDelete({
invalidateKeys: [['applications', 'application'], ['applications']], invalidateKeys: [['applications', 'application'], ['applications']],
successMessage: 'Application deleted', successMessage: a.deleted,
successWithCreditMessage: 'Application deleted. Your prepaid resources are shown on the dashboard.', successWithCreditMessage: a.deletedWithCredit,
}); });
if (isLoading) { if (isLoading) {
@@ -96,21 +107,21 @@ export default function AppsPage() {
<div className="space-y-6"> <div className="space-y-6">
<div className="page-header"> <div className="page-header">
<div> <div>
<h1 className="page-title">My Applications</h1> <h1 className="page-title">{a.title}</h1>
<p className="page-subtitle">{apps.length} application{apps.length !== 1 ? 's' : ''}</p> <p className="page-subtitle">{a.count.replace('{n}', String(apps.length))}</p>
</div> </div>
<Link href="/dashboard/deploy" className="btn-primary"> <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> </Link>
</div> </div>
{apps.length === 0 ? ( {apps.length === 0 ? (
<div className="card text-center py-16"> <div className="card text-center py-16">
<Package className="w-12 h-12 mx-auto mb-4 text-gray-300" /> <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-600 text-lg font-medium">{a.noApplications}</p>
<p className="text-gray-400 mt-1 text-sm">Deploy your first application to get started.</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"> <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> </Link>
</div> </div>
) : ( ) : (
@@ -119,19 +130,19 @@ export default function AppsPage() {
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50/80"> <thead className="bg-gray-50/80">
<tr> <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 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 text-xs font-semibold text-gray-500 uppercase tracking-wider">Runtime</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 text-xs font-semibold text-gray-500 uppercase tracking-wider">Status</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 text-xs font-semibold text-gray-500 uppercase tracking-wider">Service Status</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 text-xs font-semibold text-gray-500 uppercase tracking-wider">Expiry</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 text-xs font-semibold text-gray-500 uppercase tracking-wider">Actions</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> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-100"> <tbody className="divide-y divide-gray-100">
{apps.map((app) => { {apps.map((app) => {
const latestStatus = app.deployments?.[0]?.status || 'pending'; const latestStatus = app.deployments?.[0]?.status || 'pending';
const lifecycle = app.lifecycleStatus || 'active'; const lifecycle = app.lifecycleStatus || 'active';
const expiry = formatExpiry(app.planExpiresAt); const expiry = formatExpiry(app.planExpiresAt, a);
const rowDeleting = isDeleting(app.id); const rowDeleting = isDeleting(app.id);
return ( return (
<tr <tr
@@ -148,12 +159,12 @@ export default function AppsPage() {
</td> </td>
<td className={`px-6 py-4 text-sm text-gray-600 capitalize ${rowDeleting ? deletingRowContentClass : ''}`}>{app.runtime}</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 : ''}`}> <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>
<td className={`px-6 py-4 ${rowDeleting ? deletingRowContentClass : ''}`}> <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'}`}> <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" />} {lifecycle === 'suspended' && <AlertTriangle className="w-3 h-3" />}
{lifecycleLabels[lifecycle] || lifecycle} {lifecycleLabel(lifecycle, a)}
</span> </span>
</td> </td>
<td className={`px-6 py-4 ${rowDeleting ? deletingRowContentClass : ''}`}> <td className={`px-6 py-4 ${rowDeleting ? deletingRowContentClass : ''}`}>
@@ -163,22 +174,22 @@ export default function AppsPage() {
{expiry.text} {expiry.text}
</span> </span>
) : ( ) : (
<span className="text-xs text-gray-400">No plan</span> <span className="text-xs text-gray-400">{a.noPlan}</span>
)} )}
</td> </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"> <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"> <Link href={`/dashboard/apps/${app.id}`} className="btn-ghost text-xs px-3 py-1.5">
View {a.view}
</Link> </Link>
<button <button
type="button" type="button"
disabled={isAnyDeleting} disabled={isAnyDeleting}
onClick={async () => { onClick={async () => {
const ok = await confirm({ const ok = await confirm({
title: 'Delete Application', title: a.deleteTitle,
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.`, message: a.deleteMessage.replace('{name}', app.name),
confirmText: 'Delete', confirmText: t.common.delete,
variant: 'danger', variant: 'danger',
}); });
if (ok) deleteApplication(app.id); if (ok) deleteApplication(app.id);
@@ -206,7 +217,7 @@ export default function AppsPage() {
{apps.map((app) => { {apps.map((app) => {
const latestStatus = app.deployments?.[0]?.status || 'pending'; const latestStatus = app.deployments?.[0]?.status || 'pending';
const lifecycle = app.lifecycleStatus || 'active'; const lifecycle = app.lifecycleStatus || 'active';
const expiry = formatExpiry(app.planExpiresAt); const expiry = formatExpiry(app.planExpiresAt, a);
return ( return (
<Link <Link
key={app.id} key={app.id}
@@ -223,12 +234,12 @@ export default function AppsPage() {
<p className="text-xs text-gray-500 capitalize">{app.runtime}</p> <p className="text-xs text-gray-500 capitalize">{app.runtime}</p>
</div> </div>
</div> </div>
<span className={`badge ${statusColors[latestStatus] || 'badge-gray'}`}>{latestStatus}</span> <span className={`badge ${statusColors[latestStatus] || 'badge-gray'}`}>{statusLabel(latestStatus, t)}</span>
</div> </div>
<div className="flex items-center gap-3 mb-2"> <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'}`}> <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" />} {lifecycle === 'suspended' && <AlertTriangle className="w-3 h-3" />}
{lifecycleLabels[lifecycle] || lifecycle} {lifecycleLabel(lifecycle, a)}
</span> </span>
{app.planExpiresAt && ( {app.planExpiresAt && (
<span className={`inline-flex items-center gap-1 text-xs ${expiry.urgent ? 'text-red-600 font-semibold' : 'text-gray-500'}`}> <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} <Database className="w-3 h-3" /> {app.databaseType}
</span> </span>
<span className="flex items-center gap-1"> <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> </span>
</div> </div>
</Link> </Link>
@@ -1,7 +1,9 @@
'use client'; 'use client';
import { useQuery } from '@tanstack/react-query'; 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 api from '@/lib/api';
import type { Application } from '@/types'; import type { Application } from '@/types';
import { managedServiceTypeLabel } from '@/lib/optional-service-defaults'; import { managedServiceTypeLabel } from '@/lib/optional-service-defaults';
@@ -23,12 +25,15 @@ const lifecycleColors: Record<string, string> = {
deleted: 'text-gray-500 bg-gray-100', deleted: 'text-gray-500 bg-gray-100',
}; };
const lifecycleLabels: Record<string, string> = { type AppsDict = Dictionary['dashboard']['apps'];
active: 'Active',
suspended: 'Suspended — Unpaid', function statusLabel(status: string, t: Dictionary): string {
pending_deletion: 'Pending Deletion', return (t.dashboard.status as Record<string, string>)[status] ?? status;
deleted: 'Deleted', }
};
function lifecycleLabel(lifecycle: string, a: AppsDict): string {
return (a.lifecycle as Record<string, string>)[lifecycle] ?? lifecycle;
}
function serviceSubtitle(app: Application): string { function serviceSubtitle(app: Application): string {
if (app.productType === 'managed_database') { if (app.productType === 'managed_database') {
@@ -43,19 +48,26 @@ function serviceSubtitle(app: Application): string {
return app.databaseType !== 'none' ? app.databaseType : '—'; 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 }; if (!expiresAt) return { text: '—', urgent: false };
const diff = new Date(expiresAt).getTime() - Date.now(); 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 hours = Math.floor(diff / 3600000);
const days = Math.floor(hours / 24); const days = Math.floor(hours / 24);
if (days > 0) return { text: `${days}d ${hours % 24}h remaining`, urgent: days < 3 }; if (days > 0)
if (hours > 0) return { text: `${hours}h remaining`, urgent: hours < 6 }; 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); 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() { export default function ServicesPage() {
const t = useT();
const s = t.dashboard.services;
const appsDict = t.dashboard.apps;
const confirm = useConfirm(); const confirm = useConfirm();
const { data: servicesRaw = [], isLoading } = useQuery<Application[]>({ const { data: servicesRaw = [], isLoading } = useQuery<Application[]>({
@@ -66,8 +78,8 @@ export default function ServicesPage() {
const { deleteApplication, isDeleting, isAnyDeleting } = useApplicationDelete({ const { deleteApplication, isDeleting, isAnyDeleting } = useApplicationDelete({
invalidateKeys: [['applications', 'managed']], invalidateKeys: [['applications', 'managed']],
successMessage: 'Service deleted', successMessage: s.deleted,
successWithCreditMessage: 'Service deleted. Prepaid resources are on your dashboard.', successWithCreditMessage: s.deletedWithCredit,
}); });
if (isLoading) { if (isLoading) {
@@ -96,32 +108,28 @@ export default function ServicesPage() {
<div className="space-y-6"> <div className="space-y-6">
<div className="page-header"> <div className="page-header">
<div> <div>
<h1 className="page-title">Databases & Services</h1> <h1 className="page-title">{s.title}</h1>
<p className="page-subtitle"> <p className="page-subtitle">{s.subtitle.replace('{n}', String(services.length))}</p>
Standalone databases, Redis, and RabbitMQ {services.length} service{services.length !== 1 ? 's' : ''}
</p>
</div> </div>
<Link href="/dashboard/services/new" className="btn-primary"> <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> </Link>
</div> </div>
{services.length === 0 ? ( {services.length === 0 ? (
<div className="card text-center py-16"> <div className="card text-center py-16">
<Database className="w-12 h-12 mx-auto mb-4 text-gray-300" /> <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-600 text-lg font-medium">{s.noServices}</p>
<p className="text-gray-400 mt-1 text-sm"> <p className="text-gray-400 mt-1 text-sm">{s.emptyDesc}</p>
Provision a database, Redis, or RabbitMQ without deploying a full application.
</p>
<Link href="/dashboard/services/new" className="btn-primary mt-6 inline-flex items-center gap-1"> <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> </Link>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{services.map((svc) => { {services.map((svc) => {
const lifecycle = svc.lifecycleStatus || 'active'; const lifecycle = svc.lifecycleStatus || 'active';
const expiry = formatExpiry(svc.planExpiresAt); const expiry = formatExpiry(svc.planExpiresAt, appsDict);
const latestStatus = svc.deployments?.[0]?.status || 'pending'; const latestStatus = svc.deployments?.[0]?.status || 'pending';
const cardDeleting = isDeleting(svc.id); const cardDeleting = isDeleting(svc.id);
return ( return (
@@ -150,7 +158,7 @@ export default function ServicesPage() {
</Link> </Link>
<div className="flex flex-wrap items-center gap-2 sm:gap-3"> <div className="flex flex-wrap items-center gap-2 sm:gap-3">
<span className={`badge ${latestStatus === 'running' ? 'badge-green' : 'badge-yellow'}`}> <span className={`badge ${latestStatus === 'running' ? 'badge-green' : 'badge-yellow'}`}>
{latestStatus} {statusLabel(latestStatus, t)}
</span> </span>
<span <span
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold ${ 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" />} {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'}`}>
{expiry.text}
</span> </span>
{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 <button
type="button" type="button"
className="btn-ghost text-sm text-red-600 disabled:opacity-50 disabled:pointer-events-none" className="btn-ghost text-sm text-red-600 disabled:opacity-50 disabled:pointer-events-none"
disabled={isAnyDeleting} disabled={isAnyDeleting}
onClick={async () => { onClick={async () => {
const ok = await confirm({ const ok = await confirm({
title: 'Delete service?', title: s.deleteTitle,
message: `Permanently delete "${svc.name}"? This cannot be undone.`, message: s.deleteMessage.replace('{name}', svc.name),
confirmText: 'Delete', confirmText: t.common.delete,
variant: 'danger', variant: 'danger',
}); });
if (ok) deleteApplication(svc.id); if (ok) deleteApplication(svc.id);
+43
View File
@@ -206,6 +206,49 @@ const en: Dictionary = {
replica: 'replica', replica: 'replica',
replicaPlural: 'replicas', 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.',
},
}, },
}; };
+43
View File
@@ -205,6 +205,49 @@ const fa = {
replica: 'نمونه', replica: 'نمونه',
replicaPlural: 'نمونه', 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