diff --git a/package.json b/package.json index 8bb95c9..c7b742f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "lint:fix": "eslint . --fix", "lint:strict": "eslint . --max-warnings 0", "typecheck": "tsc --noEmit", + "i18n:check": "bash scripts/i18n-anti-pattern-check.sh", "wake-db": "tsx scripts/wake-db.ts", "seed": "tsx seed.ts", "seed:admin": "tsx scripts/seed-admin.ts", diff --git a/scripts/i18n-anti-pattern-check.sh b/scripts/i18n-anti-pattern-check.sh new file mode 100755 index 0000000..e01a2b5 --- /dev/null +++ b/scripts/i18n-anti-pattern-check.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# i18n anti-pattern audit. Counts the things the dictionary system should own +# but that currently live inline in JSX/TS: +# 1. Inline language ternaries: lang === 'ar' ? "..." : "..." +# 2. Hardcoded English JSX text inside [lang]/* routes +# 3. Locale-naive Date/Number formatters (toLocaleDateString without args) +# +# Exits 0 if every count is at-or-below the budget, 1 otherwise. Wire as +# `pnpm i18n:check` and lefthook pre-commit so the budget can only shrink. + +set -u + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" || exit 1 + +BUDGET_TERNARY="${I18N_BUDGET_TERNARY:-0}" +BUDGET_RAW_DATE="${I18N_BUDGET_RAW_DATE:-0}" +# Number-only `toLocaleString()` is only a real i18n bug for displayed numbers +# (Arabic-Indic digits etc.). Most call-sites today are price formatting where +# the surrounding currency literal is already locale-correct. Track but don't +# block by default — set I18N_BUDGET_RAW_NUMBER=0 once the cleanup lands. +BUDGET_RAW_NUMBER="${I18N_BUDGET_RAW_NUMBER:-100}" +# Hardcoded English JSX is harder to grep cleanly (false positives on icon +# names, prop values). Track as informational unless a budget is set. +BUDGET_HARDCODED="${I18N_BUDGET_HARDCODED:--1}" + +count_lang_ternary() { + grep -rEn "lang\s*===?\s*['\"]ar['\"]\s*\?" \ + src/app src/components --include="*.tsx" --include="*.ts" 2>/dev/null \ + | grep -v "i18n-anti-pattern-check" \ + | wc -l \ + | tr -d ' ' +} + +count_raw_date() { + grep -rEn "toLocaleDateString\(\s*\)|toLocaleDateString\(\s*undefined\s*\)" \ + src/app src/components src/lib --include="*.tsx" --include="*.ts" 2>/dev/null \ + | wc -l \ + | tr -d ' ' +} + +count_raw_number() { + grep -rEn "\.toLocaleString\(\s*\)" \ + src/app src/components src/lib --include="*.tsx" --include="*.ts" 2>/dev/null \ + | wc -l \ + | tr -d ' ' +} + +count_hardcoded() { + # Heuristic: capital-letter words inside JSX text, excluding common + # technical noise (className, generateMetadata, dict.*, t(...), etc.). + grep -rEn ">\s*[A-Z][a-z]+ [A-Z]?[a-z]" \ + src/app/\[lang\] --include="*.tsx" 2>/dev/null \ + | grep -vE "(import|from|className|//|/\*|>\\\$\\{|dict\\.|\\bt\\()" \ + | wc -l \ + | tr -d ' ' +} + +ternary=$(count_lang_ternary) +raw_date=$(count_raw_date) +raw_number=$(count_raw_number) +hardcoded=$(count_hardcoded) + +fail=0 +echo "i18n anti-pattern audit" +printf " inline lang ternaries : %4s (budget %s)\n" "$ternary" "$BUDGET_TERNARY" +printf " raw toLocaleDateString calls : %4s (budget %s)\n" "$raw_date" "$BUDGET_RAW_DATE" +printf " raw .toLocaleString() calls : %4s (budget %s)\n" "$raw_number" "$BUDGET_RAW_NUMBER" +printf " hardcoded English JSX : %4s (budget %s)\n" "$hardcoded" "$BUDGET_HARDCODED" + +if [ "$ternary" -gt "$BUDGET_TERNARY" ]; then fail=1; fi +if [ "$raw_date" -gt "$BUDGET_RAW_DATE" ]; then fail=1; fi +if [ "$raw_number" -gt "$BUDGET_RAW_NUMBER" ]; then fail=1; fi +if [ "$BUDGET_HARDCODED" -ge 0 ] && [ "$hardcoded" -gt "$BUDGET_HARDCODED" ]; then fail=1; fi + +if [ "$fail" -eq 0 ]; then + echo " PASS" +else + echo " FAIL — counts above their budgets" +fi +exit "$fail" diff --git a/src/app/[lang]/(auth)/join/page.tsx b/src/app/[lang]/(auth)/join/page.tsx index 6f54fbf..1ec7fb3 100644 --- a/src/app/[lang]/(auth)/join/page.tsx +++ b/src/app/[lang]/(auth)/join/page.tsx @@ -1,19 +1,19 @@ import { Metadata } from "next"; import { RegisterForm } from "@/components/auth/join/form"; import { createMetadata } from "@/lib/metadata"; +import { getDictionary } from "@/components/internationalization/dictionaries"; +import type { Locale } from "@/components/internationalization/config"; export async function generateMetadata({ params, }: { - params: Promise<{ lang: string }>; + params: Promise<{ lang: Locale }>; }): Promise { const { lang } = await params; + const m = (await getDictionary(lang)).pageMetadata.join; return createMetadata({ - title: lang === "ar" ? "إنشاء حساب" : "Join", - description: - lang === "ar" - ? "أنشئ حسابك الجديد" - : "Create your new account", + title: m.title, + description: m.description, locale: lang, path: "/join", }); diff --git a/src/app/[lang]/(auth)/login/page.tsx b/src/app/[lang]/(auth)/login/page.tsx index 8972fd7..aff067d 100644 --- a/src/app/[lang]/(auth)/login/page.tsx +++ b/src/app/[lang]/(auth)/login/page.tsx @@ -1,19 +1,19 @@ import { Metadata } from "next"; import { LoginForm } from "@/components/auth/login/form"; import { createMetadata } from "@/lib/metadata"; +import { getDictionary } from "@/components/internationalization/dictionaries"; +import type { Locale } from "@/components/internationalization/config"; export async function generateMetadata({ params, }: { - params: Promise<{ lang: string }>; + params: Promise<{ lang: Locale }>; }): Promise { const { lang } = await params; + const m = (await getDictionary(lang)).pageMetadata.login; return createMetadata({ - title: lang === "ar" ? "تسجيل الدخول" : "Login", - description: - lang === "ar" - ? "سجل دخولك إلى حسابك" - : "Sign in to your account", + title: m.title, + description: m.description, locale: lang, path: "/login", }); diff --git a/src/app/[lang]/(auth)/reset/page.tsx b/src/app/[lang]/(auth)/reset/page.tsx index ccc2d8b..6dab7af 100644 --- a/src/app/[lang]/(auth)/reset/page.tsx +++ b/src/app/[lang]/(auth)/reset/page.tsx @@ -1,19 +1,19 @@ import { Metadata } from "next"; import { ResetForm } from "@/components/auth/reset/form"; import { createMetadata } from "@/lib/metadata"; +import { getDictionary } from "@/components/internationalization/dictionaries"; +import type { Locale } from "@/components/internationalization/config"; export async function generateMetadata({ params, }: { - params: Promise<{ lang: string }>; + params: Promise<{ lang: Locale }>; }): Promise { const { lang } = await params; + const m = (await getDictionary(lang)).pageMetadata.reset; return createMetadata({ - title: lang === "ar" ? "إعادة تعيين كلمة المرور" : "Reset Password", - description: - lang === "ar" - ? "أعد تعيين كلمة المرور الخاصة بك" - : "Reset your password", + title: m.title, + description: m.description, locale: lang, path: "/reset", }); diff --git a/src/app/[lang]/(dashboard)/dashboard/properties/[id]/page.tsx b/src/app/[lang]/(dashboard)/dashboard/properties/[id]/page.tsx index a12f24d..0576e9f 100644 --- a/src/app/[lang]/(dashboard)/dashboard/properties/[id]/page.tsx +++ b/src/app/[lang]/(dashboard)/dashboard/properties/[id]/page.tsx @@ -21,12 +21,15 @@ import Link from "next/link"; import { useParams, usePathname } from "next/navigation"; import React, { useEffect, useState } from "react"; import { useDictionary } from "@/components/internationalization/dictionary-context"; +import { useLocale } from "@/components/internationalization/use-locale"; +import { formatDate } from "@/lib/i18n/formatters"; const PropertyManagement = () => { const { id } = useParams(); const pathname = usePathname(); const isAr = pathname?.startsWith("/ar"); const dict = useDictionary(); + const { locale: lang } = useLocale(); const propertyId = Number(id); const [property, setProperty] = useState(null); @@ -185,9 +188,9 @@ const PropertyManagement = () => {
- {new Date(lease.startDate).toLocaleDateString()} - + {formatDate(lease.startDate, lang)} -
-
{new Date(lease.endDate).toLocaleDateString()}
+
{formatDate(lease.endDate, lang)}
${lease.rent.toFixed(2)} diff --git a/src/app/[lang]/(dashboard)/managers/applications/[id]/page.tsx b/src/app/[lang]/(dashboard)/managers/applications/[id]/page.tsx index 02289a2..ce4070f 100644 --- a/src/app/[lang]/(dashboard)/managers/applications/[id]/page.tsx +++ b/src/app/[lang]/(dashboard)/managers/applications/[id]/page.tsx @@ -7,6 +7,8 @@ import { db } from "@/lib/db"; import { auth } from "@/lib/auth"; import { canOverride } from "@/lib/auth"; import { getDictionary } from "@/components/internationalization/dictionaries"; +import { formatDate } from "@/lib/i18n/formatters"; +import type { Locale } from "@/components/internationalization/config"; import ApplicationActions from "./actions"; /** @@ -70,7 +72,7 @@ export default async function ManagerApplicationDetailPage({ href={`/${lang}/managers/applications`} className="inline-flex items-center text-sm text-muted-foreground hover:underline mb-6" > - + {t.backToList ?? "Back to applications"} @@ -80,7 +82,7 @@ export default async function ManagerApplicationDetailPage({ {t.applicationFrom ?? "Application from"} {application.name}

- {new Date(application.applicationDate).toLocaleDateString()} + {formatDate(application.applicationDate, lang as Locale)}

@@ -119,8 +121,8 @@ export default async function ManagerApplicationDetailPage({

{t.lease ?? "Lease created"}

- {new Date(application.lease.startDate).toLocaleDateString()} →{" "} - {new Date(application.lease.endDate).toLocaleDateString()} + {formatDate(application.lease.startDate, lang as Locale)} →{" "} + {formatDate(application.lease.endDate, lang as Locale)}
{t.monthlyRent ?? "Monthly rent"}:{" "} diff --git a/src/app/[lang]/(dashboard)/managers/properties/[id]/page.tsx b/src/app/[lang]/(dashboard)/managers/properties/[id]/page.tsx index a801ed6..37697d0 100644 --- a/src/app/[lang]/(dashboard)/managers/properties/[id]/page.tsx +++ b/src/app/[lang]/(dashboard)/managers/properties/[id]/page.tsx @@ -21,12 +21,15 @@ import Link from "next/link"; import { useParams, usePathname } from "next/navigation"; import React, { useEffect, useState } from "react"; import { useDictionary } from "@/components/internationalization/dictionary-context"; +import { useLocale } from "@/components/internationalization/use-locale"; +import { formatDate } from "@/lib/i18n/formatters"; const PropertyTenants = () => { const { id } = useParams(); const pathname = usePathname(); const isAr = pathname?.startsWith("/ar"); const dict = useDictionary(); + const { locale: lang } = useLocale(); const propertyId = Number(id); const [property, setProperty] = useState(null); @@ -149,9 +152,9 @@ const PropertyTenants = () => {
- {new Date(lease.startDate).toLocaleDateString()} - + {formatDate(lease.startDate, lang)} -
-
{new Date(lease.endDate).toLocaleDateString()}
+
{formatDate(lease.endDate, lang)}
${lease.rent.toFixed(2)} diff --git a/src/app/[lang]/(dashboard)/managers/properties/page.tsx b/src/app/[lang]/(dashboard)/managers/properties/page.tsx index 6e4d1d4..5610fbb 100644 --- a/src/app/[lang]/(dashboard)/managers/properties/page.tsx +++ b/src/app/[lang]/(dashboard)/managers/properties/page.tsx @@ -1,5 +1,7 @@ import { Metadata } from "next"; import { createMetadata } from "@/lib/metadata"; +import { getDictionary } from "@/components/internationalization/dictionaries"; +import type { Locale } from "@/components/internationalization/config"; import ManagerPropertiesContent from "./content"; // Disable static generation for this page @@ -8,15 +10,13 @@ export const dynamic = 'force-dynamic'; export async function generateMetadata({ params, }: { - params: Promise<{ lang: string }>; + params: Promise<{ lang: Locale }>; }): Promise { const { lang } = await params; + const m = (await getDictionary(lang)).pageMetadata.managersProperties; return createMetadata({ - title: lang === "ar" ? "إدارة العقارات" : "Manage Properties", - description: - lang === "ar" - ? "عرض وإدارة عقاراتك" - : "View and manage your property listings", + title: m.title, + description: m.description, locale: lang, path: "/managers/properties", }); diff --git a/src/app/[lang]/(dashboard)/tenants/favorites/content.tsx b/src/app/[lang]/(dashboard)/tenants/favorites/content.tsx index 2e23ea3..9d85ba4 100644 --- a/src/app/[lang]/(dashboard)/tenants/favorites/content.tsx +++ b/src/app/[lang]/(dashboard)/tenants/favorites/content.tsx @@ -18,9 +18,10 @@ interface FavoritesContentProps { * updated list. */ export default async function FavoritesContent({ lang }: FavoritesContentProps) { - const dict = (await getDictionary(lang as "en" | "ar")) as unknown as Record>; + const dict = (await getDictionary(lang as "en" | "ar")) as unknown as Record>; const t = (dict.dashboard as Record> | undefined)?.favorites ?? {}; - const currency = dict.common?.currency ?? "$"; + const meta = (dict.pageMetadata as Record> | undefined)?.tenantsFavorites ?? {}; + const currency = (dict.common?.currency as string | undefined) ?? "$"; const favorites = (await getTenantFavorites()) as Array<{ id: number; title: string | null; @@ -32,8 +33,8 @@ export default async function FavoritesContent({ lang }: FavoritesContentProps) return (
{favorites.length === 0 ? ( diff --git a/src/app/[lang]/(dashboard)/tenants/favorites/page.tsx b/src/app/[lang]/(dashboard)/tenants/favorites/page.tsx index f8481e0..dcc07c1 100644 --- a/src/app/[lang]/(dashboard)/tenants/favorites/page.tsx +++ b/src/app/[lang]/(dashboard)/tenants/favorites/page.tsx @@ -1,5 +1,7 @@ import { Metadata } from "next"; import { createMetadata } from "@/lib/metadata"; +import { getDictionary } from "@/components/internationalization/dictionaries"; +import type { Locale } from "@/components/internationalization/config"; import FavoritesContent from "./content"; // Disable static generation for this page @@ -8,15 +10,13 @@ export const dynamic = 'force-dynamic'; export async function generateMetadata({ params, }: { - params: Promise<{ lang: string }>; + params: Promise<{ lang: Locale }>; }): Promise { const { lang } = await params; + const m = (await getDictionary(lang)).pageMetadata.tenantsFavorites; return createMetadata({ - title: lang === "ar" ? "المفضلة" : "Favorites", - description: - lang === "ar" - ? "تصفح وإدارة العقارات المفضلة لديك" - : "Browse and manage your saved property listings", + title: m.title, + description: m.description, locale: lang, path: "/tenants/favorites", }); diff --git a/src/app/[lang]/(dashboard)/tenants/payments/page.tsx b/src/app/[lang]/(dashboard)/tenants/payments/page.tsx index 6353e39..b80eb34 100644 --- a/src/app/[lang]/(dashboard)/tenants/payments/page.tsx +++ b/src/app/[lang]/(dashboard)/tenants/payments/page.tsx @@ -4,6 +4,8 @@ import { CreditCard, Home, Clock, Check, AlertCircle } from "lucide-react"; import { auth } from "@/lib/auth"; import { getUserPayments } from "@/lib/actions/payment-actions"; import { getDictionary } from "@/components/internationalization/dictionaries"; +import { formatDate } from "@/lib/i18n/formatters"; +import type { Locale } from "@/components/internationalization/config"; import { Table, TableBody, @@ -189,7 +191,7 @@ function PaymentsTable({
)}
- {new Date(p.dueDate).toLocaleDateString()} + {formatDate(p.dueDate, lang as Locale)} {currency} {p.amountDue.toLocaleString()} diff --git a/src/app/[lang]/(dashboard)/tenants/residences/[id]/page.tsx b/src/app/[lang]/(dashboard)/tenants/residences/[id]/page.tsx index d313dc0..277e416 100644 --- a/src/app/[lang]/(dashboard)/tenants/residences/[id]/page.tsx +++ b/src/app/[lang]/(dashboard)/tenants/residences/[id]/page.tsx @@ -35,6 +35,8 @@ import { User, } from "lucide-react"; import { useParams } from "next/navigation"; +import { useLocale } from "@/components/internationalization/use-locale"; +import { formatDate } from "@/lib/i18n/formatters"; import React from "react"; const PaymentMethod = () => { @@ -89,6 +91,7 @@ const ResidenceCard = ({ property: PropertyWithLocation; currentLease: Lease; }) => { + const { locale: lang } = useLocale(); return (
{/* Header */} @@ -124,21 +127,21 @@ const ResidenceCard = ({
Start Date:
- {new Date(currentLease.startDate).toLocaleDateString()} + {formatDate(currentLease.startDate, lang)}
End Date:
- {new Date(currentLease.endDate).toLocaleDateString()} + {formatDate(currentLease.endDate, lang)}
Next Payment:
- {new Date(currentLease.endDate).toLocaleDateString()} + {formatDate(currentLease.endDate, lang)}
@@ -160,6 +163,7 @@ const ResidenceCard = ({ }; const BillingHistory = ({ payments }: { payments: Payment[] }) => { + const { locale: lang } = useLocale(); return (
{/* Header */} @@ -217,7 +221,7 @@ const BillingHistory = ({ payments }: { payments: Payment[] }) => { - {new Date(payment.paymentDate).toLocaleDateString()} + {formatDate(payment.paymentDate, lang)} ${payment.amountPaid.toFixed(2)} diff --git a/src/app/[lang]/(dashboard)/tenants/residences/page.tsx b/src/app/[lang]/(dashboard)/tenants/residences/page.tsx index 3dc1bd8..4027b07 100644 --- a/src/app/[lang]/(dashboard)/tenants/residences/page.tsx +++ b/src/app/[lang]/(dashboard)/tenants/residences/page.tsx @@ -1,5 +1,7 @@ import { Metadata } from "next"; import { createMetadata } from "@/lib/metadata"; +import { getDictionary } from "@/components/internationalization/dictionaries"; +import type { Locale } from "@/components/internationalization/config"; import ResidencesContent from "./content"; // Disable static generation for this page @@ -8,15 +10,13 @@ export const dynamic = 'force-dynamic'; export async function generateMetadata({ params, }: { - params: Promise<{ lang: string }>; + params: Promise<{ lang: Locale }>; }): Promise { const { lang } = await params; + const m = (await getDictionary(lang)).pageMetadata.tenantsResidences; return createMetadata({ - title: lang === "ar" ? "مساكني" : "My Residences", - description: - lang === "ar" - ? "عرض وإدارة مساكنك الحالية" - : "View and manage your current living spaces", + title: m.title, + description: m.description, locale: lang, path: "/tenants/residences", }); diff --git a/src/app/[lang]/(dashboard)/tenants/trips/page.tsx b/src/app/[lang]/(dashboard)/tenants/trips/page.tsx index d556c10..19c1fd6 100644 --- a/src/app/[lang]/(dashboard)/tenants/trips/page.tsx +++ b/src/app/[lang]/(dashboard)/tenants/trips/page.tsx @@ -8,7 +8,8 @@ import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; import { Home, Bus, Calendar, MapPin, Clock, Download, Eye } from 'lucide-react'; import { format } from 'date-fns'; -import { ar, enUS } from 'date-fns/locale'; +import { dateLocaleFor } from '@/lib/i18n/date-locale'; +import { formatDate } from '@/lib/i18n/formatters'; import Link from 'next/link'; import { getMyBookings } from '@/lib/actions/transport-actions'; import { getGuestBookings, cancelBooking } from '@/lib/actions/booking-actions'; @@ -68,7 +69,7 @@ const TripsPage = () => { const { locale } = useLocale(); const params = useParams(); const lang = (params?.lang as string) ?? 'en'; - const dateLocale = locale === 'ar' ? ar : enUS; + const dateLocale = dateLocaleFor(locale); const [transportBookings, setTransportBookings] = useState([]); const [homeBookings, setHomeBookings] = useState([]); const [loading, setLoading] = useState(true); @@ -310,8 +311,8 @@ const HomeBookingCard = ({ booking, lang, dict, getStatusColor, isPast, onCancel
- {new Date(booking.checkIn).toLocaleDateString()} →{" "} - {new Date(booking.checkOut).toLocaleDateString()} + {formatDate(booking.checkIn, lang as 'en' | 'ar')} →{" "} + {formatDate(booking.checkOut, lang as 'en' | 'ar')}
{booking.guestCount} {dict.booking?.guestsPlural ?? "guests"}
@@ -355,7 +356,7 @@ interface TransportBookingCardProps { const TransportBookingCard = ({ booking, getStatusColor, isPast }: TransportBookingCardProps) => { const dict = useDictionary(); const { locale } = useLocale(); - const dateLocale = locale === 'ar' ? ar : enUS; + const dateLocale = dateLocaleFor(locale); return (
diff --git a/src/app/[lang]/(nondashboard)/landing/page.tsx b/src/app/[lang]/(nondashboard)/landing/page.tsx index 4de474a..9728ba7 100644 --- a/src/app/[lang]/(nondashboard)/landing/page.tsx +++ b/src/app/[lang]/(nondashboard)/landing/page.tsx @@ -1,20 +1,20 @@ import { Metadata } from "next"; import React from "react"; import { createMetadata } from "@/lib/metadata"; +import { getDictionary } from "@/components/internationalization/dictionaries"; +import type { Locale } from "@/components/internationalization/config"; import HeroSection from "@/components/site/HeroSection"; export async function generateMetadata({ params, }: { - params: Promise<{ lang: string }>; + params: Promise<{ lang: Locale }>; }): Promise { const { lang } = await params; + const m = (await getDictionary(lang)).pageMetadata.landing; return createMetadata({ - title: lang === "ar" ? "مرحباً" : "Welcome", - description: - lang === "ar" - ? "مرحباً بك في مكان — منصة الإيجار والاستضافة" - : "Welcome to Mkan — your rental and hosting platform", + title: m.title, + description: m.description, locale: lang, path: "/landing", }); diff --git a/src/app/[lang]/admin/bookings/page.tsx b/src/app/[lang]/admin/bookings/page.tsx index 7c5da8d..18494b5 100644 --- a/src/app/[lang]/admin/bookings/page.tsx +++ b/src/app/[lang]/admin/bookings/page.tsx @@ -15,7 +15,8 @@ export default async function AdminBookingsPage({ params: Promise<{ lang: string }>; }) { const { lang } = await params; - const dict = await getDictionary(lang as "en" | "ar"); + const locale = lang as "en" | "ar"; + const dict = await getDictionary(locale); const a = (dict as { admin?: Record }).admin ?? {}; const [homes, transport] = await Promise.all([ @@ -43,6 +44,7 @@ export default async function AdminBookingsPage({
; }) { const { lang } = await params; - const dict = await getDictionary(lang as "en" | "ar"); + const locale = lang as "en" | "ar"; + const dict = await getDictionary(locale); const a = (dict as { admin?: Record }).admin ?? {}; const [homes, transport] = await Promise.all([ @@ -43,6 +44,7 @@ export default async function AdminPaymentsPage({
{t.checkIn ?? "Check-in"}
-
{new Date(b.checkIn).toLocaleDateString()}
+
{formatDate(b.checkIn, lang as Locale)}
{t.checkOut ?? "Check-out"}
-
{new Date(b.checkOut).toLocaleDateString()}
+
{formatDate(b.checkOut, lang as Locale)}
diff --git a/src/app/[lang]/help/page.tsx b/src/app/[lang]/help/page.tsx index 46cb6f4..c6f87e4 100644 --- a/src/app/[lang]/help/page.tsx +++ b/src/app/[lang]/help/page.tsx @@ -1,5 +1,7 @@ import { Metadata } from "next"; import { createMetadata } from "@/lib/metadata"; +import { getDictionary } from "@/components/internationalization/dictionaries"; +import type { Locale } from "@/components/internationalization/config"; import HelpContent from "./content"; // Disable static generation for this page @@ -8,15 +10,13 @@ export const dynamic = 'force-dynamic'; export async function generateMetadata({ params, }: { - params: Promise<{ lang: string }>; + params: Promise<{ lang: Locale }>; }): Promise { const { lang } = await params; + const m = (await getDictionary(lang)).pageMetadata.help; return createMetadata({ - title: lang === "ar" ? "مركز المساعدة" : "Help Center", - description: - lang === "ar" - ? "احصل على المساعدة والدعم" - : "Get help and support", + title: m.title, + description: m.description, locale: lang, path: "/help", }); diff --git a/src/app/[lang]/host/page.tsx b/src/app/[lang]/host/page.tsx index a9f2ce4..063967b 100644 --- a/src/app/[lang]/host/page.tsx +++ b/src/app/[lang]/host/page.tsx @@ -1,5 +1,7 @@ import { Metadata } from "next"; import { createMetadata } from "@/lib/metadata"; +import { getDictionary } from "@/components/internationalization/dictionaries"; +import type { Locale } from "@/components/internationalization/config"; import BecomeAHostContent from "./content"; // Disable static generation for this page @@ -8,15 +10,13 @@ export const dynamic = 'force-dynamic'; export async function generateMetadata({ params, }: { - params: Promise<{ lang: string }>; + params: Promise<{ lang: Locale }>; }): Promise { const { lang } = await params; + const m = (await getDictionary(lang)).pageMetadata.host; return createMetadata({ - title: lang === "ar" ? "كن مضيفاً" : "Become a Host", - description: - lang === "ar" - ? "ابدأ باستضافة الضيوف وكسب الدخل" - : "Start hosting guests and earning income", + title: m.title, + description: m.description, locale: lang, path: "/host", }); diff --git a/src/app/[lang]/hosting/calendar/page.tsx b/src/app/[lang]/hosting/calendar/page.tsx index 6c3f947..47ebfba 100644 --- a/src/app/[lang]/hosting/calendar/page.tsx +++ b/src/app/[lang]/hosting/calendar/page.tsx @@ -18,11 +18,11 @@ export default function HostingCalendarPage() {
{monthLabel}
diff --git a/src/app/[lang]/hosting/listings/page.tsx b/src/app/[lang]/hosting/listings/page.tsx index 3597a29..8ef9ca0 100644 --- a/src/app/[lang]/hosting/listings/page.tsx +++ b/src/app/[lang]/hosting/listings/page.tsx @@ -1,5 +1,7 @@ import { Metadata } from "next"; import { createMetadata } from "@/lib/metadata"; +import { getDictionary } from "@/components/internationalization/dictionaries"; +import type { Locale } from "@/components/internationalization/config"; import HostingListingsContent from "./content"; // Disable static generation for this page @@ -8,15 +10,13 @@ export const dynamic = 'force-dynamic'; export async function generateMetadata({ params, }: { - params: Promise<{ lang: string }>; + params: Promise<{ lang: Locale }>; }): Promise { const { lang } = await params; + const m = (await getDictionary(lang)).pageMetadata.hostingListings; return createMetadata({ - title: lang === "ar" ? "إعلاناتي" : "My Listings", - description: - lang === "ar" - ? "عرض وإدارة إعلاناتك" - : "View and manage your listings", + title: m.title, + description: m.description, locale: lang, path: "/hosting/listings", }); diff --git a/src/app/[lang]/hosting/page.tsx b/src/app/[lang]/hosting/page.tsx index 8d0d311..442f197 100644 --- a/src/app/[lang]/hosting/page.tsx +++ b/src/app/[lang]/hosting/page.tsx @@ -1,5 +1,7 @@ import { Metadata } from "next"; import { createMetadata } from "@/lib/metadata"; +import { getDictionary } from "@/components/internationalization/dictionaries"; +import type { Locale } from "@/components/internationalization/config"; import HostingContent from "./content"; // Disable static generation for this page @@ -8,15 +10,13 @@ export const dynamic = 'force-dynamic'; export async function generateMetadata({ params, }: { - params: Promise<{ lang: string }>; + params: Promise<{ lang: Locale }>; }): Promise { const { lang } = await params; + const m = (await getDictionary(lang)).pageMetadata.hosting; return createMetadata({ - title: lang === "ar" ? "الاستضافة" : "Hosting", - description: - lang === "ar" - ? "إدارة حجوزاتك واستضافتك" - : "Manage your reservations and hosting", + title: m.title, + description: m.description, locale: lang, path: "/hosting", }); diff --git a/src/app/[lang]/layout.tsx b/src/app/[lang]/layout.tsx index 7714ae8..5c8428a 100644 --- a/src/app/[lang]/layout.tsx +++ b/src/app/[lang]/layout.tsx @@ -1,12 +1,22 @@ -import type { Metadata } from 'next'; +import type { Metadata, Viewport } from 'next'; import { Inter, Rubik } from 'next/font/google'; import { getDictionary } from '@/components/internationalization/dictionaries'; import { DictionaryProvider } from '@/components/internationalization/dictionary-context'; import { type Locale, localeConfig, i18n } from '@/components/internationalization/config'; +import { ReportIssue } from '@/components/report-issue'; import { Providers } from '../providers'; import { Toaster } from 'sonner'; import '../globals.css'; +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, + themeColor: [ + { media: '(prefers-color-scheme: light)', color: '#ffffff' }, + { media: '(prefers-color-scheme: dark)', color: '#000000' }, + ], +}; + // Enable ISR with 1-hour revalidation export const revalidate = 3600; @@ -28,7 +38,7 @@ export async function generateMetadata({ params: Promise<{ lang: string }>; }): Promise { const resolvedParams = await params; - const lang = (resolvedParams.lang as Locale) || 'en'; + const lang = (resolvedParams.lang as Locale) || i18n.defaultLocale; const dictionary = await getDictionary(lang); return { @@ -41,7 +51,7 @@ export async function generateMetadata({ languages: Object.keys(localeConfig).reduce((acc, locale) => ({ ...acc, [locale]: `/${locale}`, - }), { 'x-default': '/en' }), + }), { 'x-default': `/${i18n.defaultLocale}` }), }, other: { 'accept-language': lang, @@ -57,8 +67,8 @@ export default async function LocaleLayout({ params: Promise<{ lang: string }>; }) { const resolvedParams = await params; - const lang = (resolvedParams.lang as Locale) || 'en'; - const config = localeConfig[lang] || localeConfig['en']; + const lang = (resolvedParams.lang as Locale) || i18n.defaultLocale; + const config = localeConfig[lang] || localeConfig[i18n.defaultLocale]; const isRTL = config.dir === 'rtl'; const dictionary = await getDictionary(lang); @@ -71,12 +81,15 @@ export default async function LocaleLayout({ href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-background focus:text-foreground focus:border focus:border-border focus:rounded-md focus:m-2" > - {isRTL ? 'تخطي إلى المحتوى الرئيسي' : 'Skip to main content'} + {dictionary.common.skipToContent} {children} +
+ +
diff --git a/src/app/[lang]/listings/page.tsx b/src/app/[lang]/listings/page.tsx index 5b36922..cb5c986 100644 --- a/src/app/[lang]/listings/page.tsx +++ b/src/app/[lang]/listings/page.tsx @@ -166,29 +166,31 @@ export default async function ListingsPage({ searchParams, params: pageParams }: }; // Dictionary keys live at `rental.property.filters` and `rental.property.amenities`. - // Graceful fallbacks keep the UI readable even if a key is missing mid-rollout. + // `pick` returns the dict string if present, falling back to "" so the + // downstream component contract (string, not string | undefined) holds. const rentalProperty = (d.rental?.property ?? {}) as Record; - const rentalFilters = (rentalProperty.filters ?? {}) as Record; + const rentalFilters = (rentalProperty.filters ?? {}) as Record; const rentalAmenities = (rentalProperty.amenities ?? {}) as Record; const rentalPropertyTypes = (rentalProperty.types ?? {}) as Record; + const rentalPagination = (rentalProperty.pagination ?? {}) as Record; + const pick = (k: string, src: Record) => src[k] ?? ''; const filtersDict = { filters: { - title: rentalFilters.title ?? (lang === "ar" ? "الفلاتر" : "Filters"), - clearAll: rentalFilters.clearFilters ?? (lang === "ar" ? "مسح الكل" : "Clear all"), - showResults: - rentalFilters.showResults ?? (lang === "ar" ? "عرض {count} عقار" : "Show {count} places"), + title: pick('title', rentalFilters), + clearAll: pick('clearAll', rentalFilters), + showResults: pick('showResults', rentalFilters), }, price: { - label: rentalFilters.minPrice ?? (lang === "ar" ? "نطاق السعر" : "Price range"), + label: pick('priceRange', rentalFilters), currency: "$", }, - bedrooms: rentalFilters.bedrooms ?? (lang === "ar" ? "غرف النوم" : "Bedrooms"), - bathrooms: rentalFilters.bathrooms ?? (lang === "ar" ? "الحمامات" : "Bathrooms"), - propertyType: rentalFilters.propertyType ?? (lang === "ar" ? "نوع العقار" : "Property type"), - amenitiesLabel: rentalFilters.amenities ?? (lang === "ar" ? "المرافق" : "Amenities"), - anyLabel: lang === "ar" ? "الكل" : "Any", - mobileTriggerLabel: rentalFilters.title ?? (lang === "ar" ? "الفلاتر" : "Filters"), + bedrooms: pick('bedrooms', rentalFilters), + bathrooms: pick('bathrooms', rentalFilters), + propertyType: pick('propertyType', rentalFilters), + amenitiesLabel: pick('amenities', rentalFilters), + anyLabel: pick('any', rentalFilters), + mobileTriggerLabel: pick('title', rentalFilters), propertyTypes: rentalPropertyTypes as Partial>, amenityLabels: rentalAmenities as Partial>, }; @@ -261,12 +263,9 @@ export default async function ListingsPage({ searchParams, params: pageParams }: lang={lang} baseParams={params} dict={{ - previous: lang === "ar" ? "السابق" : "Previous", - next: lang === "ar" ? "التالي" : "Next", - pageOf: - lang === "ar" - ? "الصفحة {current} من {total}" - : "Page {current} of {total}", + previous: pick('previous', rentalPagination), + next: pick('next', rentalPagination), + pageOf: pick('pageOf', rentalPagination), }} />
diff --git a/src/app/[lang]/transport-host/[id]/bookings/page.tsx b/src/app/[lang]/transport-host/[id]/bookings/page.tsx index 36303aa..b6c8ff5 100644 --- a/src/app/[lang]/transport-host/[id]/bookings/page.tsx +++ b/src/app/[lang]/transport-host/[id]/bookings/page.tsx @@ -12,12 +12,15 @@ import { getOfficeBookings, updateBookingStatus, } from "@/lib/actions/transport-actions"; +import { formatDate } from "@/lib/i18n/formatters"; +import { useLocale } from "@/components/internationalization/use-locale"; type BookingsResult = Awaited>; type Booking = BookingsResult extends { bookings: infer B } ? (B extends Array ? I : never) : never; export default function TransportHostBookingsPage() { const params = useParams(); + const { locale: lang } = useLocale(); const officeId = Number(params.id); const [bookings, setBookings] = useState([]); const [loading, setLoading] = useState(true); @@ -90,7 +93,7 @@ export default function TransportHostBookingsPage() { {b.trip?.route?.origin?.city} → {b.trip?.route?.destination?.city}
- {new Date(b.trip?.departureDate).toLocaleDateString()} ·{" "} + {b.trip?.departureDate ? formatDate(b.trip.departureDate, lang) : '—'} ·{" "} {b.trip?.departureTime}
diff --git a/src/app/[lang]/transport-host/[id]/trips/page.tsx b/src/app/[lang]/transport-host/[id]/trips/page.tsx index bc3bfa1..ecc8208 100644 --- a/src/app/[lang]/transport-host/[id]/trips/page.tsx +++ b/src/app/[lang]/transport-host/[id]/trips/page.tsx @@ -9,12 +9,15 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Route as RouteIcon } from "lucide-react"; import { getTripsByOffice } from "@/lib/actions/transport-actions"; +import { formatDate } from "@/lib/i18n/formatters"; +import { useLocale } from "@/components/internationalization/use-locale"; type Trip = Awaited>[number]; type Bucket = "today" | "upcoming" | "past"; export default function TransportHostTripsPage() { const params = useParams(); + const { locale: lang } = useLocale(); const officeId = Number(params.id); const [trips, setTrips] = useState([]); const [bucket, setBucket] = useState("today"); @@ -85,7 +88,7 @@ export default function TransportHostTripsPage() {
-
{new Date(t.departureDate).toLocaleDateString()}
+
{formatDate(t.departureDate, lang)}
{t.departureTime}
diff --git a/src/app/[lang]/transport-host/page.tsx b/src/app/[lang]/transport-host/page.tsx index cdd4a74..17990f7 100644 --- a/src/app/[lang]/transport-host/page.tsx +++ b/src/app/[lang]/transport-host/page.tsx @@ -1,5 +1,7 @@ import { Metadata } from "next"; import { createMetadata } from "@/lib/metadata"; +import { getDictionary } from "@/components/internationalization/dictionaries"; +import type { Locale } from "@/components/internationalization/config"; import TransportHostContent from "./content"; export const dynamic = 'force-dynamic'; @@ -7,15 +9,13 @@ export const dynamic = 'force-dynamic'; export async function generateMetadata({ params, }: { - params: Promise<{ lang: string }>; + params: Promise<{ lang: Locale }>; }): Promise { const { lang } = await params; + const m = (await getDictionary(lang)).pageMetadata.transportHost; return createMetadata({ - title: lang === "ar" ? "مضيف النقل" : "Transport Host", - description: - lang === "ar" - ? "إدارة مكاتب النقل والحجوزات" - : "Manage your transport offices and bookings", + title: m.title, + description: m.description, locale: lang, path: "/transport-host", }); diff --git a/src/app/[lang]/transport/booking/[id]/page.tsx b/src/app/[lang]/transport/booking/[id]/page.tsx index e7f7282..e6451bc 100644 --- a/src/app/[lang]/transport/booking/[id]/page.tsx +++ b/src/app/[lang]/transport/booking/[id]/page.tsx @@ -20,7 +20,8 @@ import { Ticket, } from 'lucide-react'; import { format } from 'date-fns'; -import { ar, enUS } from 'date-fns/locale'; +import { dateLocaleFor } from '@/lib/i18n/date-locale'; +import type { Locale } from '@/components/internationalization/config'; import { getBooking } from '@/lib/actions/transport-actions'; import { getTransportDictionary } from '@/components/transport/transport-dictionary'; @@ -29,7 +30,7 @@ type BookingDetails = NonNullable>>; export default function BookingConfirmationPage() { const params = useParams(); const router = useRouter(); - const lang = params.lang as string; + const lang = params.lang as Locale; const bookingId = Number(params.id); const [booking, setBooking] = useState(null); @@ -37,7 +38,7 @@ export default function BookingConfirmationPage() { const t = getTransportDictionary(lang); // Locale for date-fns — gives Arabic users "الثلاثاء، ١٥ أبريل" instead of // "Tue, Apr 15". All `format()` calls in this file must pass this. - const dateLocale = lang === 'ar' ? ar : enUS; + const dateLocale = dateLocaleFor(lang); useEffect(() => { const fetchBooking = async () => { diff --git a/src/app/[lang]/transport/booking/checkout/page.tsx b/src/app/[lang]/transport/booking/checkout/page.tsx index ebd1efb..1f383f6 100644 --- a/src/app/[lang]/transport/booking/checkout/page.tsx +++ b/src/app/[lang]/transport/booking/checkout/page.tsx @@ -1,19 +1,19 @@ import { Metadata } from "next"; import { createMetadata } from "@/lib/metadata"; +import { getDictionary } from "@/components/internationalization/dictionaries"; +import type { Locale } from "@/components/internationalization/config"; import CheckoutContent from "./content"; export async function generateMetadata({ params, }: { - params: Promise<{ lang: string }>; + params: Promise<{ lang: Locale }>; }): Promise { const { lang } = await params; + const m = (await getDictionary(lang)).pageMetadata.transportCheckout; return createMetadata({ - title: lang === "ar" ? "الدفع" : "Checkout", - description: - lang === "ar" - ? "أكمل حجزك وادفع" - : "Complete your booking and pay", + title: m.title, + description: m.description, locale: lang, path: "/transport/booking/checkout", }); diff --git a/src/app/[lang]/transport/offices/[id]/page.tsx b/src/app/[lang]/transport/offices/[id]/page.tsx index aadc111..e20ac22 100644 --- a/src/app/[lang]/transport/offices/[id]/page.tsx +++ b/src/app/[lang]/transport/offices/[id]/page.tsx @@ -23,7 +23,8 @@ import { import { getTransportOffice, getOfficeTrips } from '@/lib/actions/transport-actions'; import { getTransportDictionary } from '@/components/transport/transport-dictionary'; import { format, addDays } from 'date-fns'; -import { ar, enUS } from 'date-fns/locale'; +import { dateLocaleFor } from '@/lib/i18n/date-locale'; +import type { Locale } from '@/components/internationalization/config'; type OfficeDetails = NonNullable>>; type Trip = Awaited>[number]; @@ -31,8 +32,8 @@ type Trip = Awaited>[number]; export default function OfficeDetailsPage() { const params = useParams(); const router = useRouter(); - const lang = params.lang as string; - const dateLocale = lang === 'ar' ? ar : enUS; + const lang = params.lang as Locale; + const dateLocale = dateLocaleFor(lang); const officeId = Number(params.id); const [office, setOffice] = useState(null); diff --git a/src/app/[lang]/transport/offices/page.tsx b/src/app/[lang]/transport/offices/page.tsx index c9c3065..356e427 100644 --- a/src/app/[lang]/transport/offices/page.tsx +++ b/src/app/[lang]/transport/offices/page.tsx @@ -1,19 +1,19 @@ import { Metadata } from "next"; import { createMetadata } from "@/lib/metadata"; +import { getDictionary } from "@/components/internationalization/dictionaries"; +import type { Locale } from "@/components/internationalization/config"; import OfficesListContent from "./content"; export async function generateMetadata({ params, }: { - params: Promise<{ lang: string }>; + params: Promise<{ lang: Locale }>; }): Promise { const { lang } = await params; + const m = (await getDictionary(lang)).pageMetadata.transportOffices; return createMetadata({ - title: lang === "ar" ? "مكاتب النقل" : "Transport Offices", - description: - lang === "ar" - ? "تصفح مكاتب النقل المتاحة" - : "Browse available transport offices", + title: m.title, + description: m.description, locale: lang, path: "/transport/offices", }); diff --git a/src/app/[lang]/transport/page.tsx b/src/app/[lang]/transport/page.tsx index 7292880..cca1840 100644 --- a/src/app/[lang]/transport/page.tsx +++ b/src/app/[lang]/transport/page.tsx @@ -17,13 +17,12 @@ import { createMetadata } from '@/lib/metadata'; // ISR: Revalidate every 10 minutes (assembly points rarely change) export const dynamic = 'force-dynamic'; -export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise { +export async function generateMetadata({ params }: { params: Promise<{ lang: Locale }> }): Promise { const { lang } = await params; + const dictionary = await getDictionary(lang); return createMetadata({ - title: lang === "ar" ? "النقل البري" : "Bus Transport", - description: lang === "ar" - ? "احجز رحلات الحافلات بين المدن السودانية" - : "Book intercity bus trips across Sudan", + title: dictionary.transport.metadataTitle, + description: dictionary.transport.metadataDescription, locale: lang, path: "/transport", }); @@ -218,8 +217,9 @@ export default async function TransportPage({ params }: TransportPageProps) {
{popularRoutes.map((route) => { - const fromLabel = lang === 'ar' ? (route.origin.nameAr ?? route.origin.city) : route.origin.city; - const toLabel = lang === 'ar' ? (route.destination.nameAr ?? route.destination.city) : route.destination.city; + const isArabic = lang === 'ar'; + const fromLabel = isArabic ? (route.origin.nameAr ?? route.origin.city) : route.origin.city; + const toLabel = isArabic ? (route.destination.nameAr ?? route.destination.city) : route.destination.city; const hours = Math.round(route.duration / 60); const query = new URLSearchParams({ originId: String(route.originId), @@ -242,10 +242,10 @@ export default async function TransportPage({ params }: TransportPageProps) {
- {lang === 'ar' ? `${hours} ساعة` : `${hours}h`} + {t.routes.hoursFormat.replace('{hours}', String(hours))} - {t.routes.pricePrefix} {route.basePrice.toLocaleString()} {lang === 'ar' ? 'ج.س' : 'SDG'} + {t.routes.pricePrefix} {route.basePrice.toLocaleString(lang)} {t.routes.currency}
diff --git a/src/app/[lang]/transport/search/page.tsx b/src/app/[lang]/transport/search/page.tsx index be43586..9dbe375 100644 --- a/src/app/[lang]/transport/search/page.tsx +++ b/src/app/[lang]/transport/search/page.tsx @@ -22,15 +22,13 @@ import type { Locale } from '@/components/internationalization/config'; export async function generateMetadata({ params, }: { - params: Promise<{ lang: string }>; + params: Promise<{ lang: Locale }>; }): Promise { const { lang } = await params; + const dictionary = await getDictionary(lang); return createMetadata({ - title: lang === "ar" ? "بحث النقل" : "Transport Search", - description: - lang === "ar" - ? "ابحث عن رحلات النقل المتاحة" - : "Search for available transport trips", + title: dictionary.transport.search.metadataTitle, + description: dictionary.transport.search.metadataDescription, locale: lang, path: "/transport/search", }); @@ -93,48 +91,49 @@ export default async function SearchPage({ const t = dictionary.transport; const { trips, total, page, pageCount, facets } = result; - const dateLocale = lang === 'ar' ? ar : undefined; + const isArabic = lang === 'ar'; + const dateLocale = isArabic ? ar : undefined; + const localizedNameKey = isArabic ? 'nameAr' : 'name'; const originLabel = parsed.originId - ? assemblyPoints.find((p) => p.id === parsed.originId)?.[lang === 'ar' ? 'nameAr' : 'name'] + ? assemblyPoints.find((p) => p.id === parsed.originId)?.[localizedNameKey] ?? parsed.origin ?? '' : parsed.origin ?? ''; const destinationLabel = parsed.destinationId - ? assemblyPoints.find((p) => p.id === parsed.destinationId)?.[lang === 'ar' ? 'nameAr' : 'name'] + ? assemblyPoints.find((p) => p.id === parsed.destinationId)?.[localizedNameKey] ?? parsed.destination ?? '' : parsed.destination ?? ''; - // Filter dictionary with graceful fallbacks while translations land const filterDict = { filters: { - title: t.search.filters?.title ?? t.search.filters ?? (lang === 'ar' ? 'الفلاتر' : 'Filters'), - clearAll: t.search.filters?.clearAll ?? (lang === 'ar' ? 'مسح الكل' : 'Clear all'), - showResults: t.search.filters?.showResults ?? (lang === 'ar' ? 'عرض {count} رحلة' : 'Show {count} trips'), + title: t.search.filters.title, + clearAll: t.search.filters.clearAll, + showResults: t.search.filters.showResults, }, sort: { - label: t.search.sort?.label ?? (lang === 'ar' ? 'ترتيب' : 'Sort'), - priceAsc: t.search.sort?.priceAsc ?? (lang === 'ar' ? 'السعر: من الأقل للأعلى' : 'Price: low to high'), - priceDesc: t.search.sort?.priceDesc ?? (lang === 'ar' ? 'السعر: من الأعلى للأقل' : 'Price: high to low'), - departureAsc: t.search.sort?.departureAsc ?? (lang === 'ar' ? 'الأبكر مغادرة' : 'Earliest departure'), - durationAsc: t.search.sort?.durationAsc ?? (lang === 'ar' ? 'الأقصر مدة' : 'Shortest duration'), + label: t.search.sort.label, + priceAsc: t.search.sort.priceAsc, + priceDesc: t.search.sort.priceDesc, + departureAsc: t.search.sort.departureAsc, + durationAsc: t.search.sort.durationAsc, }, timeOfDay: { - label: t.search.timeOfDay?.label ?? (lang === 'ar' ? 'وقت المغادرة' : 'Departure time'), - morning: t.search.timeOfDay?.morning ?? (lang === 'ar' ? 'الصباح' : 'Morning'), - afternoon: t.search.timeOfDay?.afternoon ?? (lang === 'ar' ? 'الظهيرة' : 'Afternoon'), - evening: t.search.timeOfDay?.evening ?? (lang === 'ar' ? 'المساء' : 'Evening'), - night: t.search.timeOfDay?.night ?? (lang === 'ar' ? 'الليل' : 'Night'), + label: t.search.timeOfDay.label, + morning: t.search.timeOfDay.morning, + afternoon: t.search.timeOfDay.afternoon, + evening: t.search.timeOfDay.evening, + night: t.search.timeOfDay.night, }, price: { - label: t.search.price?.label ?? (lang === 'ar' ? 'نطاق السعر' : 'Price range'), - currency: t.search.price?.currency ?? (lang === 'ar' ? 'ج.س' : 'SDG'), + label: t.search.price.label, + currency: t.search.price.currency, }, - amenitiesLabel: t.search.amenitiesLabel ?? (lang === 'ar' ? 'المرافق' : 'Amenities'), - officesLabel: t.search.officesLabel ?? (lang === 'ar' ? 'المشغّلون' : 'Operators'), + amenitiesLabel: t.search.amenitiesLabel, + officesLabel: t.search.officesLabel, amenities: t.host?.amenityLabels, - mobileTriggerLabel: t.search.filters?.title ?? (typeof t.search.filters === 'string' ? t.search.filters : (lang === 'ar' ? 'الفلاتر' : 'Filters')), + mobileTriggerLabel: t.search.filters.title, }; return ( @@ -223,6 +222,7 @@ export default async function SearchPage({ pageCount={pageCount} lang={lang} searchParams={spObject} + dict={t.search.pagination} /> )} @@ -245,16 +245,24 @@ export default async function SearchPage({ ); } +interface PaginationDict { + previous: string; + next: string; + pageOf: string; +} + function Pagination({ page, pageCount, lang, searchParams, + dict, }: { page: number; pageCount: number; lang: string; searchParams: Record; + dict: PaginationDict; }) { const buildHref = (p: number) => { const qs = new URLSearchParams(); @@ -265,6 +273,10 @@ function Pagination({ return `/${lang}/transport/search?${qs.toString()}`; }; + const pageLabel = dict.pageOf + .replace('{current}', String(page)) + .replace('{total}', String(pageCount)); + return (
- - {lang === 'ar' ? `الصفحة ${page} من ${pageCount}` : `Page ${page} of ${pageCount}`} - + {pageLabel} = pageCount} tabIndex={page >= pageCount ? -1 : 0} >
diff --git a/src/app/[lang]/transport/trips/[id]/page.tsx b/src/app/[lang]/transport/trips/[id]/page.tsx index 0317198..333af9a 100644 --- a/src/app/[lang]/transport/trips/[id]/page.tsx +++ b/src/app/[lang]/transport/trips/[id]/page.tsx @@ -22,7 +22,8 @@ import { CheckCircle2, } from 'lucide-react'; import { format } from 'date-fns'; -import { ar, enUS } from 'date-fns/locale'; +import { dateLocaleFor } from '@/lib/i18n/date-locale'; +import type { Locale } from '@/components/internationalization/config'; import { toast } from 'sonner'; import { getTripDetails, createBooking } from '@/lib/actions/transport-actions'; import { getTransportDictionary } from '@/components/transport/transport-dictionary'; @@ -48,8 +49,8 @@ const amenityIcons: Record = { export default function TripDetailsPage() { const params = useParams(); const router = useRouter(); - const lang = params.lang as string; - const dateLocale = lang === 'ar' ? ar : enUS; + const lang = params.lang as Locale; + const dateLocale = dateLocaleFor(lang); const tripId = Number(params.id); const [trip, setTrip] = useState(null); diff --git a/src/components/ApplicationCard.tsx b/src/components/ApplicationCard.tsx index 17ffb7d..254d453 100644 --- a/src/components/ApplicationCard.tsx +++ b/src/components/ApplicationCard.tsx @@ -2,6 +2,8 @@ import { Mail, MapPin, PhoneCall } from "lucide-react"; import Image from "next/image"; import React, { useState } from "react"; import { ApplicationWithDetails } from "@/lib/actions/application-actions"; +import { useLocale } from "@/components/internationalization/use-locale"; +import { formatDate } from "@/lib/i18n/formatters"; interface ApplicationCardProps { application: ApplicationWithDetails; @@ -14,6 +16,7 @@ const ApplicationCard = ({ userType, children, }: ApplicationCardProps) => { + const { locale: lang } = useLocale(); const [imgSrc, setImgSrc] = useState( application.listing.photoUrls?.[0] || "/placeholder.jpg" ); @@ -76,11 +79,11 @@ const ApplicationCard = ({
Start Date:{" "} - {application.lease?.startDate ? new Date(application.lease.startDate).toLocaleDateString() : 'N/A'} + {application.lease?.startDate ? formatDate(application.lease.startDate, lang) : 'N/A'}
End Date:{" "} - {application.lease?.endDate ? new Date(application.lease.endDate).toLocaleDateString() : 'N/A'} + {application.lease?.endDate ? formatDate(application.lease.endDate, lang) : 'N/A'}
Lease ID:{" "} diff --git a/src/components/HeartButton.tsx b/src/components/HeartButton.tsx index 810813f..21224da 100644 --- a/src/components/HeartButton.tsx +++ b/src/components/HeartButton.tsx @@ -9,7 +9,7 @@ interface HeartButtonProps { const HeartButton: React.FC = ({ listingId, currentUser }) => { return (