Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
81 changes: 81 additions & 0 deletions scripts/i18n-anti-pattern-check.sh
Original file line number Diff line number Diff line change
@@ -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"
12 changes: 6 additions & 6 deletions src/app/[lang]/(auth)/join/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Metadata> {
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",
});
Expand Down
12 changes: 6 additions & 6 deletions src/app/[lang]/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Metadata> {
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",
});
Expand Down
12 changes: 6 additions & 6 deletions src/app/[lang]/(auth)/reset/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Metadata> {
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",
});
Expand Down
7 changes: 5 additions & 2 deletions src/app/[lang]/(dashboard)/dashboard/properties/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,20 @@
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<any>(null);

Check warning on line 35 in src/app/[lang]/(dashboard)/dashboard/properties/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const [leases, setLeases] = useState<any[]>([]);

Check warning on line 36 in src/app/[lang]/(dashboard)/dashboard/properties/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const [payments, setPayments] = useState<any[]>([]);

Check warning on line 37 in src/app/[lang]/(dashboard)/dashboard/properties/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

Expand All @@ -54,7 +57,7 @@
};
}

let leasesData: any[] = [];

Check warning on line 60 in src/app/[lang]/(dashboard)/dashboard/properties/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
try {
leasesData = await getListingLeases(propertyId);
} catch (leaseError) {
Expand Down Expand Up @@ -82,7 +85,7 @@
setPayments([]);
}
}
} catch (err: any) {

Check warning on line 88 in src/app/[lang]/(dashboard)/dashboard/properties/[id]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
console.error("Error fetching data:", err);
setError(err.message || "Error loading property details");
} finally {
Expand Down Expand Up @@ -185,9 +188,9 @@
</TableCell>
<TableCell>
<div>
{new Date(lease.startDate).toLocaleDateString()} -
{formatDate(lease.startDate, lang)} -
</div>
<div>{new Date(lease.endDate).toLocaleDateString()}</div>
<div>{formatDate(lease.endDate, lang)}</div>
</TableCell>
<TableCell>${lease.rent.toFixed(2)}</TableCell>
<TableCell>
Expand Down
10 changes: 6 additions & 4 deletions src/app/[lang]/(dashboard)/managers/applications/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down Expand Up @@ -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"
>
<ArrowLeft className="w-4 h-4 me-1" />
<ArrowLeft className="w-4 h-4 me-1 rtl:rotate-180" />
{t.backToList ?? "Back to applications"}
</Link>

Expand All @@ -80,7 +82,7 @@ export default async function ManagerApplicationDetailPage({
{t.applicationFrom ?? "Application from"} {application.name}
</h1>
<p className="text-sm text-muted-foreground">
{new Date(application.applicationDate).toLocaleDateString()}
{formatDate(application.applicationDate, lang as Locale)}
</p>
</div>
<span className={`px-3 py-1 rounded-full text-xs font-medium ${statusColor}`}>
Expand Down Expand Up @@ -119,8 +121,8 @@ export default async function ManagerApplicationDetailPage({
<h2 className="text-lg font-medium">{t.lease ?? "Lease created"}</h2>
<div className="flex items-center gap-2 text-sm">
<Calendar className="w-4 h-4 text-muted-foreground" />
{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)}
</div>
<div className="text-sm">
{t.monthlyRent ?? "Monthly rent"}:{" "}
Expand Down
7 changes: 5 additions & 2 deletions src/app/[lang]/(dashboard)/managers/properties/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>(null);
Expand Down Expand Up @@ -149,9 +152,9 @@ const PropertyTenants = () => {
</TableCell>
<TableCell>
<div>
{new Date(lease.startDate).toLocaleDateString()} -
{formatDate(lease.startDate, lang)} -
</div>
<div>{new Date(lease.endDate).toLocaleDateString()}</div>
<div>{formatDate(lease.endDate, lang)}</div>
</TableCell>
<TableCell>${lease.rent.toFixed(2)}</TableCell>
<TableCell>
Expand Down
12 changes: 6 additions & 6 deletions src/app/[lang]/(dashboard)/managers/properties/page.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -8,15 +10,13 @@ export const dynamic = 'force-dynamic';
export async function generateMetadata({
params,
}: {
params: Promise<{ lang: string }>;
params: Promise<{ lang: Locale }>;
}): Promise<Metadata> {
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",
});
Expand Down
9 changes: 5 additions & 4 deletions src/app/[lang]/(dashboard)/tenants/favorites/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Record<string, string>>;
const dict = (await getDictionary(lang as "en" | "ar")) as unknown as Record<string, Record<string, unknown>>;
const t = (dict.dashboard as Record<string, Record<string, string>> | undefined)?.favorites ?? {};
const currency = dict.common?.currency ?? "$";
const meta = (dict.pageMetadata as Record<string, Record<string, string>> | undefined)?.tenantsFavorites ?? {};
const currency = (dict.common?.currency as string | undefined) ?? "$";
const favorites = (await getTenantFavorites()) as Array<{
id: number;
title: string | null;
Expand All @@ -32,8 +33,8 @@ export default async function FavoritesContent({ lang }: FavoritesContentProps)
return (
<div className="dashboard-container p-6 space-y-6">
<Header
title={t.title ?? (lang === "ar" ? "المفضلة" : "Favorites")}
subtitle={t.subtitle ?? (lang === "ar" ? "تصفح وإدارة العقارات المحفوظة" : "Browse and manage your saved properties")}
title={t.title ?? meta.title ?? ''}
subtitle={t.subtitle ?? meta.subtitle ?? ''}
/>

{favorites.length === 0 ? (
Expand Down
12 changes: 6 additions & 6 deletions src/app/[lang]/(dashboard)/tenants/favorites/page.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -8,15 +10,13 @@ export const dynamic = 'force-dynamic';
export async function generateMetadata({
params,
}: {
params: Promise<{ lang: string }>;
params: Promise<{ lang: Locale }>;
}): Promise<Metadata> {
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",
});
Expand Down
4 changes: 3 additions & 1 deletion src/app/[lang]/(dashboard)/tenants/payments/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -189,7 +191,7 @@ function PaymentsTable({
</div>
)}
</TableCell>
<TableCell>{new Date(p.dueDate).toLocaleDateString()}</TableCell>
<TableCell>{formatDate(p.dueDate, lang as Locale)}</TableCell>
<TableCell className="font-medium">
{currency}
{p.amountDue.toLocaleString()}
Expand Down
Loading
Loading