Skip to content
Merged
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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
77 changes: 77 additions & 0 deletions app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { Metadata } from "next";
import "./globals.css";
import { Lora, Figtree } from "next/font/google";
import { Nav } from "@/components/layout/Nav";
import { Footer } from "@/components/layout/Footer";
// import { CarCursor } from "@/components/ui/CarCursor";

const lora = Lora({
subsets: ["latin"],
variable: "--font-heading",
weight: ["400", "500", "600", "700"],
style: ["normal", "italic"],
display: "swap",
});

const figtree = Figtree({
subsets: ["latin"],
variable: "--font-body",
weight: ["400", "500", "600", "700", "800", "900"],
display: "swap",
});

export const metadata: Metadata = {
metadataBase: new URL(
process.env.NEXT_PUBLIC_SITE_URL ?? "https://getvindicated.org"
),
title: {
default: "VINdicated | Educate. Empower. Vindicate.",
template: "%s | VINdicated",
},
description:
"VINdicated is a nonprofit built on the belief that car knowledge should be public knowledge. We break down the systems that allow consumer discrimination to thrive.",
openGraph: {
type: "website",
siteName: "VINdicated",
title: {
default: "VINdicated | Educate. Empower. Vindicate.",
template: "%s | VINdicated",
},
description:
"VINdicated is a nonprofit built on the belief that car knowledge should be public knowledge. We break down the systems that allow consumer discrimination to thrive.",
url: "/",
images: [
{
url: "/illus-woman-dealership.png",
alt: "VINdicated",
},
],
},
twitter: {
card: "summary_large_image",
title: {
default: "VINdicated | Educate. Empower. Vindicate.",
template: "%s | VINdicated",
},
description:
"VINdicated is a nonprofit built on the belief that car knowledge should be public knowledge. We break down the systems that allow consumer discrimination to thrive.",
images: ["/illus-woman-dealership.png"],
},
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={`${lora.variable} ${figtree.variable}`}>
<body className="min-h-screen antialiased">
{/* <CarCursor /> */}
<Nav />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
72 changes: 1 addition & 71 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,7 @@
import type { Metadata } from "next";
import "./globals.css";
import { Lora, Figtree } from "next/font/google";
import { Nav } from "@/components/layout/Nav";
import { Footer } from "@/components/layout/Footer";
// import { CarCursor } from "@/components/ui/CarCursor";

const lora = Lora({
subsets: ["latin"],
variable: "--font-heading",
weight: ["400", "500", "600", "700"],
style: ["normal", "italic"],
display: "swap",
});

const figtree = Figtree({
subsets: ["latin"],
variable: "--font-body",
weight: ["400", "500", "600", "700", "800", "900"],
display: "swap",
});

export const metadata: Metadata = {
metadataBase: new URL(
process.env.NEXT_PUBLIC_SITE_URL ?? "https://getvindicated.org"
),
title: {
default: "VINdicated | Educate. Empower. Vindicate.",
template: "%s | VINdicated",
},
description:
"VINdicated is a nonprofit built on the belief that car knowledge should be public knowledge. We break down the systems that allow consumer discrimination to thrive.",
openGraph: {
type: "website",
siteName: "VINdicated",
title: {
default: "VINdicated | Educate. Empower. Vindicate.",
template: "%s | VINdicated",
},
description:
"VINdicated is a nonprofit built on the belief that car knowledge should be public knowledge. We break down the systems that allow consumer discrimination to thrive.",
url: "/",
images: [
{
url: "/illus-woman-dealership.png",
alt: "VINdicated",
},
],
},
twitter: {
card: "summary_large_image",
title: {
default: "VINdicated | Educate. Empower. Vindicate.",
template: "%s | VINdicated",
},
description:
"VINdicated is a nonprofit built on the belief that car knowledge should be public knowledge. We break down the systems that allow consumer discrimination to thrive.",
images: ["/illus-woman-dealership.png"],
},
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={`${lora.variable} ${figtree.variable}`}>
<body className="min-h-screen antialiased">
{/* <CarCursor /> */}
<Nav />
<main>{children}</main>
<Footer />
</body>
</html>
);
return children;
}
76 changes: 76 additions & 0 deletions components/layout/LanguageSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"use client";

import { useState, useRef, useEffect } from "react";
import { usePathname, useRouter } from "next/navigation";
import { locales, localeLabels, type Locale } from "@/lib/i18n/config";

export function LanguageSwitcher({ currentLocale }: { currentLocale: Locale }) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const pathname = usePathname();
const router = useRouter();

useEffect(() => {
function onClickOutside(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
}
document.addEventListener("mousedown", onClickOutside);
return () => document.removeEventListener("mousedown", onClickOutside);
}, []);

function switchTo(locale: Locale) {
const segments = pathname.split("/");
segments[1] = locale;
router.push(segments.join("/"));
setOpen(false);
}

return (
<div className="relative" ref={ref}>
<button
onClick={() => setOpen((o) => !o)}
className="text-[0.85rem] font-semibold px-3 py-2 rounded-lg transition-colors duration-150"
style={{
color: "#fff",
border: "1px solid var(--color-border)",
background: open ? "rgba(149,51,165,0.14)" : "transparent",
}}
aria-haspopup="listbox"
aria-expanded={open}
>
{localeLabels[currentLocale]}
</button>

{open && (
<div
className="absolute right-0 mt-2 rounded-xl overflow-hidden z-50 min-w-[160px]"
style={{
background: "var(--color-bg-surface)",
border: "1px solid var(--color-border)",
boxShadow: "0 10px 40px rgba(0,0,0,0.5)",
}}
role="listbox"
>
{locales.map((locale) => (
<button
key={locale}
onClick={() => switchTo(locale)}
className="w-full text-left px-4 py-2.5 text-[0.9rem] transition-colors duration-150"
style={{
color: "#fff",
background: locale === currentLocale ? "rgba(149,51,165,0.16)" : "transparent",
fontWeight: locale === currentLocale ? 700 : 400,
}}
role="option"
aria-selected={locale === currentLocale}
>
{localeLabels[locale]}
</button>
))}
</div>
)}
</div>
);
}
50 changes: 50 additions & 0 deletions lib/i18n/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// lib/i18n/config.ts
export type Locale =
| "en"
| "es"
| "zh-Hans"
| "tl"
| "vi"
| "ko"
| "fa"
| "hy"
| "hi"
| "ar-EG";

export const locales: Locale[] = [
"en",
"es",
"zh-Hans",
"tl",
"vi",
"ko",
"fa",
"hy",
"hi",
"ar-EG"
];

export const defaultLocale: Locale = "en";

export const localeLabels: Record<Locale, string> = {
en: "English",
es: "Español",
"zh-Hans": "简体中文",
tl: "Tagalog",
vi: "Tiếng Việt",
ko: "한국어",
fa: "فارسی",
hy: "Հայերեն",
hi: "हिन्दी",
"ar-EG": "العربية المصرية",
};

export const rtlLocales: Locale[] = ["fa", "ar-EG"];

export function isRtl(locale: Locale): boolean {
return rtlLocales.includes(locale);
}

export function isValidLocale(value: string): value is Locale {
return locales.includes(value as Locale);
}
18 changes: 18 additions & 0 deletions lib/i18n/dictionaries/ar-EG.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"dir": "rtl",
"nav": {
"home": "الرئيسية",
"about": "عننا",
"inspection": "دليل الفحص",
"fraud": "الوقاية من النصب",
"documents": "شرح المستندات",
"research": "أبحاث",
"contact": "تواصل معنا"
},
"footer": {
"tagline": "نتعلّم. نتمكّن. ننصف حقنا.",
"subtagline": "تثقيف مجاني عن السيارات للستات والمهاجرين وأول مرة بيشتروا عربية.",
"copyright": "© 2026 VINdicated. جميع الحقوق محفوظة. أسستها رنا درويش.",
"mission": "منظمة غير هادفة للربح ملتزمة بالمساواة بين الجنسين في تجارة السيارات."
}
}
17 changes: 17 additions & 0 deletions lib/i18n/dictionaries/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"nav": {
"home": "Home",
"about": "About Us",
"inspection": "Inspection Guide",
"fraud": "Fraud Prevention",
"documents": "Documents Decoded",
"research": "Research",
"contact": "Contact"
},
"footer": {
"tagline": "Educate. Empower. Vindicate.",
"subtagline": "Free automotive education for women, immigrants, and first-time buyers.",
"copyright": "© 2026 VINdicated. All rights reserved. Founded by Rana Darwich.",
"mission": "A nonprofit committed to gender equity in automotive commerce."
}
}
17 changes: 17 additions & 0 deletions lib/i18n/dictionaries/es.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"nav": {
"home": "Inicio",
"about": "Sobre Nosotros",
"inspection": "Guía de Inspección",
"fraud": "Prevención de Fraude",
"documents": "Documentos Descifrados",
"research": "Investigación",
"contact": "Contacto"
},
"footer": {
"tagline": "Educar. Empoderar. Reivindicar.",
"subtagline": "Educación automotriz gratuita para mujeres, inmigrantes y compradores primerizos.",
"copyright": "© 2026 VINdicated. Todos los derechos reservados. Fundado por Rana Darwich.",
"mission": "Una organización sin fines de lucro comprometida con la equidad de género en el comercio automotriz."
}
}
18 changes: 18 additions & 0 deletions lib/i18n/dictionaries/fa.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"dir": "rtl",
"nav": {
"home": "خانه",
"about": "درباره ما",
"inspection": "راهنمای بازرسی",
"fraud": "پیشگیری از کلاهبرداری",
"documents": "رمزگشایی اسناد",
"research": "پژوهش",
"contact": "تماس با ما"
},
"footer": {
"tagline": "آموزش. توانمندسازی. احقاق حق.",
"subtagline": "آموزش رایگان خودرو برای زنان، مهاجران و خریداران بار اول.",
"copyright": "© ۲۰۲۶ VINdicated. تمامی حقوق محفوظ است. بنیان‌گذار: رعنا درویش.",
"mission": "یک سازمان غیرانتفاعی متعهد به برابری جنسیتی در تجارت خودرو."
}
}
17 changes: 17 additions & 0 deletions lib/i18n/dictionaries/hi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"nav": {
"home": "होम",
"about": "हमारे बारे में",
"inspection": "निरीक्षण गाइड",
"fraud": "धोखाधड़ी से बचाव",
"documents": "दस्तावेज़ों की व्याख्या",
"research": "शोध",
"contact": "संपर्क करें"
},
"footer": {
"tagline": "शिक्षित करें। सशक्त बनाएं। न्याय दिलाएं।",
"subtagline": "महिलाओं, प्रवासियों और पहली बार कार खरीदने वालों के लिए मुफ़्त ऑटोमोटिव शिक्षा।",
"copyright": "© 2026 VINdicated. सर्वाधिकार सुरक्षित। संस्थापक: राणा दरविश।",
"mission": "ऑटोमोटिव व्यापार में लैंगिक समानता के लिए प्रतिबद्ध एक गैर-लाभकारी संस्था।"
}
}
17 changes: 17 additions & 0 deletions lib/i18n/dictionaries/hy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"nav": {
"home": "Գլխավոր",
"about": "Մեր Մասին",
"inspection": "Ստուգման Ուղեցույց",
"fraud": "Խարդախության Կանխարգելում",
"documents": "Փաստաթղթերի Վերծանում",
"research": "Հետազոտություն",
"contact": "Կապ"
},
"footer": {
"tagline": "Կրթել. Հզորացնել. Պաշտպանել.",
"subtagline": "Անվճար ավտոմեքենայի կրթություն կանանց, միգրանտների և առաջին անգամ գնորդների համար։",
"copyright": "© 2026 VINdicated: Բոլոր իրավունքները պաշտպանված են: Հիմնադրվել է Ռանա Դարվիշի կողմից:",
"mission": "Ոչ առևտրային կազմակերպություն, նվիրված ավտոառևտրում գենդերային հավասարությանը:"
}
}
Loading