Localize dashboard shell and home page.

Convert the dashboard layout (sidebar nav, header, role badges, wallet,
sign-out, language switcher) and the dashboard home page to the i18n
dictionaries, including a shared deployment-status label map. Switch their
links/router to the locale-aware helpers and add RTL-aware spacing.

Remaining dashboard page bodies and shared components still render English
copy and will be localized in follow-up commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-11 12:15:21 +03:30
parent 34993d417f
commit e0964e6528
5 changed files with 187 additions and 77 deletions
+46 -39
View File
@@ -1,11 +1,14 @@
'use client';
import { useEffect, useState, type ReactNode } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';
import { useAuthStore } from '@/lib/store';
import api from '@/lib/api';
import { Link } from '@/i18n/Link';
import { useLocalizedRouter, usePathname } from '@/i18n/navigation';
import { useT } from '@/i18n/I18nProvider';
import { LanguageSwitcher } from '@/i18n/LanguageSwitcher';
import type { Dictionary } from '@/i18n/dictionaries/fa';
import { DeploymentProgressManager } from '@/components/deployment-progress-manager';
import { useDeployProgressStore } from '@/lib/deploy-progress-store';
import {
@@ -31,43 +34,45 @@ import {
Database,
} from 'lucide-react';
type NavItem = { href: string; label: string; icon: ReactNode };
type NavKey = keyof Dictionary['nav'];
type NavItem = { href: string; labelKey: NavKey; icon: ReactNode };
const userNavItems: NavItem[] = [
{ href: '/dashboard', label: 'Dashboard', icon: <LayoutDashboard className="w-4 h-4" /> },
{ href: '/dashboard/apps', label: 'Applications', icon: <Package className="w-4 h-4" /> },
{ href: '/dashboard/services', label: 'Databases & Services', icon: <Database className="w-4 h-4" /> },
{ href: '/dashboard/logs', label: 'Logs', icon: <ScrollText className="w-4 h-4" /> },
{ href: '/dashboard/deploy', label: 'New Deploy', icon: <Rocket className="w-4 h-4" /> },
{ href: '/dashboard/wallet', label: 'Wallet', icon: <Wallet className="w-4 h-4" /> },
{ href: '/dashboard/invoices', label: 'Invoices', icon: <FileText className="w-4 h-4" /> },
{ href: '/dashboard/tickets', label: 'Tickets', icon: <Ticket className="w-4 h-4" /> },
{ href: '/dashboard', labelKey: 'dashboard', icon: <LayoutDashboard className="w-4 h-4" /> },
{ href: '/dashboard/apps', labelKey: 'applications', icon: <Package className="w-4 h-4" /> },
{ href: '/dashboard/services', labelKey: 'services', icon: <Database className="w-4 h-4" /> },
{ href: '/dashboard/logs', labelKey: 'logs', icon: <ScrollText className="w-4 h-4" /> },
{ href: '/dashboard/deploy', labelKey: 'newDeploy', icon: <Rocket className="w-4 h-4" /> },
{ href: '/dashboard/wallet', labelKey: 'wallet', icon: <Wallet className="w-4 h-4" /> },
{ href: '/dashboard/invoices', labelKey: 'invoices', icon: <FileText className="w-4 h-4" /> },
{ href: '/dashboard/tickets', labelKey: 'tickets', icon: <Ticket className="w-4 h-4" /> },
];
const adminNavItems: NavItem[] = [
{ href: '/dashboard/admin/users', label: 'Users', icon: <Users className="w-4 h-4" /> },
{ href: '/dashboard/admin/apps', label: 'All Applications', icon: <Boxes className="w-4 h-4" /> },
{ href: '/dashboard/admin/billing', label: 'Billing Plans', icon: <CreditCard className="w-4 h-4" /> },
{ href: '/dashboard/admin/invoices', label: 'Invoices', icon: <FileText className="w-4 h-4" /> },
{ href: '/dashboard/admin/clusters', label: 'Clusters', icon: <Server className="w-4 h-4" /> },
{ href: '/dashboard/admin/pools', label: 'Cluster Pools', icon: <Scale className="w-4 h-4" /> },
{ href: '/dashboard/admin/tickets', label: 'All Tickets', icon: <ClipboardList className="w-4 h-4" /> },
{ href: '/dashboard/admin/users', labelKey: 'users', icon: <Users className="w-4 h-4" /> },
{ href: '/dashboard/admin/apps', labelKey: 'allApplications', icon: <Boxes className="w-4 h-4" /> },
{ href: '/dashboard/admin/billing', labelKey: 'billingPlans', icon: <CreditCard className="w-4 h-4" /> },
{ href: '/dashboard/admin/invoices', labelKey: 'invoices', icon: <FileText className="w-4 h-4" /> },
{ href: '/dashboard/admin/clusters', labelKey: 'clusters', icon: <Server className="w-4 h-4" /> },
{ href: '/dashboard/admin/pools', labelKey: 'clusterPools', icon: <Scale className="w-4 h-4" /> },
{ href: '/dashboard/admin/tickets', labelKey: 'allTickets', icon: <ClipboardList className="w-4 h-4" /> },
];
const technicalNavItems: NavItem[] = [
{ href: '/dashboard/admin/users', label: 'Users', icon: <Users className="w-4 h-4" /> },
{ href: '/dashboard/admin/apps', label: 'All Applications', icon: <Boxes className="w-4 h-4" /> },
{ href: '/dashboard/staff/tickets', label: 'Technical Tickets', icon: <Wrench className="w-4 h-4" /> },
{ href: '/dashboard/admin/users', labelKey: 'users', icon: <Users className="w-4 h-4" /> },
{ href: '/dashboard/admin/apps', labelKey: 'allApplications', icon: <Boxes className="w-4 h-4" /> },
{ href: '/dashboard/staff/tickets', labelKey: 'technicalTickets', icon: <Wrench className="w-4 h-4" /> },
];
const salesNavItems: NavItem[] = [
{ href: '/dashboard/admin/users', label: 'Users', icon: <Users className="w-4 h-4" /> },
{ href: '/dashboard/staff/tickets', label: 'Sales Tickets', icon: <Briefcase className="w-4 h-4" /> },
{ href: '/dashboard/admin/users', labelKey: 'users', icon: <Users className="w-4 h-4" /> },
{ href: '/dashboard/staff/tickets', labelKey: 'salesTickets', icon: <Briefcase className="w-4 h-4" /> },
];
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
const t = useT();
const { user, isAuthenticated, isLoading, logout } = useAuthStore();
const router = useRouter();
const router = useLocalizedRouter();
const pathname = usePathname();
const [sidebarOpen, setSidebarOpen] = useState(false);
const deployBarMinimized = useDeployProgressStore((s) => s.minimized);
@@ -106,7 +111,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="flex flex-col items-center gap-3">
<div className="animate-spin rounded-full h-10 w-10 border-2 border-primary-600 border-t-transparent" />
<span className="text-sm text-gray-500">Loading...</span>
<span className="text-sm text-gray-500">{t.common.loading}</span>
</div>
</div>
);
@@ -126,7 +131,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
}`}
>
{item.icon}
<span className="flex-1">{item.label}</span>
<span className="flex-1">{t.nav[item.labelKey]}</span>
{badge !== undefined && badge > 0 && (
<span className="min-w-[20px] h-5 flex items-center justify-center px-1.5 text-xs font-bold rounded-full bg-red-500 text-white">
{badge}
@@ -160,7 +165,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
<>
<div className="pt-5 pb-2">
<p className="px-3 text-[11px] font-bold text-gray-400 uppercase tracking-widest">
Admin
{t.nav.sectionAdmin}
</p>
</div>
{adminNavItems.map((item) => (
@@ -173,7 +178,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
<>
<div className="pt-5 pb-2">
<p className="px-3 text-[11px] font-bold text-gray-400 uppercase tracking-widest">
Technical
{t.nav.sectionTechnical}
</p>
</div>
{technicalNavItems.map((item) => (
@@ -186,7 +191,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
<>
<div className="pt-5 pb-2">
<p className="px-3 text-[11px] font-bold text-gray-400 uppercase tracking-widest">
Sales
{t.nav.sectionSales}
</p>
</div>
{salesNavItems.map((item) => (
@@ -221,13 +226,13 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
{/* Mobile sidebar */}
<aside
className={`fixed inset-y-0 left-0 z-50 w-64 bg-white border-r border-gray-200 p-4 transform transition-transform duration-200 ease-in-out lg:hidden ${
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
className={`fixed inset-y-0 left-0 rtl:left-auto rtl:right-0 z-50 w-64 bg-white border-r rtl:border-r-0 rtl:border-l border-gray-200 p-4 transform transition-transform duration-200 ease-in-out lg:hidden ${
sidebarOpen ? 'translate-x-0' : '-translate-x-full rtl:translate-x-full'
}`}
>
<div className="flex items-center justify-between mb-6">
<Link href="/dashboard" className="text-lg font-bold text-primary-600">
<Cloud className="w-5 h-5 inline mr-1" /> CloudHost
<Cloud className="w-5 h-5 inline mr-1 rtl:mr-0 rtl:ml-1" /> {t.common.appName}
</Link>
<button onClick={() => setSidebarOpen(false)} className="btn-icon">
<X className="w-4 h-4" />
@@ -254,7 +259,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
</button>
<Link href="/dashboard" className="text-xl font-bold text-primary-600 flex items-center gap-2">
<Cloud className="w-6 h-6" />
<span className="hidden sm:inline">CloudHost</span>
<span className="hidden sm:inline">{t.common.appName}</span>
</Link>
</div>
<div className="flex items-center gap-3">
@@ -262,27 +267,29 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
<Link
href="/dashboard/wallet"
className="hidden sm:flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-gray-100 hover:bg-gray-200 transition-colors text-sm"
title="Wallet Balance"
title={t.nav.walletBalanceTitle}
>
<Wallet className="w-3.5 h-3.5 text-primary-600" />
<span className="font-bold text-gray-800">
{walletData ? Number(walletData.balance).toLocaleString('en-US') : '...'}
</span>
<span className="text-gray-500 text-xs">T</span>
<span className="text-gray-500 text-xs">{t.common.currencyShort}</span>
</Link>
<LanguageSwitcher className="text-gray-600 hover:bg-gray-100 hover:text-gray-900" />
<div className="hidden sm:flex items-center gap-2 text-sm">
<span className="text-gray-600 font-medium">
{user?.firstName} {user?.lastName}
</span>
{user?.role === 'admin' && (
<span className="badge-purple">Admin</span>
<span className="badge-purple">{t.roles.admin}</span>
)}
{user?.role === 'technical' && (
<span className="px-2 py-0.5 text-xs font-semibold rounded-full bg-blue-100 text-blue-700">Technical</span>
<span className="px-2 py-0.5 text-xs font-semibold rounded-full bg-blue-100 text-blue-700">{t.roles.technical}</span>
)}
{user?.role === 'sales' && (
<span className="px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-700">Sales</span>
<span className="px-2 py-0.5 text-xs font-semibold rounded-full bg-green-100 text-green-700">{t.roles.sales}</span>
)}
</div>
<button
@@ -290,7 +297,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
className="btn-ghost text-gray-500 hover:text-red-600"
>
<LogOut className="w-4 h-4 sm:hidden" />
<span className="hidden sm:inline">Sign out</span>
<span className="hidden sm:inline">{t.common.signOut}</span>
</button>
</div>
</div>