diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx
new file mode 100644
index 00000000..0e1ad756
--- /dev/null
+++ b/app/[locale]/layout.tsx
@@ -0,0 +1,23 @@
+import { NextIntlClientProvider } from "next-intl";
+import { getMessages } from "next-intl/server";
+import { notFound } from "next/navigation";
+
+const locales = ["en", "es"];
+
+export default async function LocaleLayout({
+ children,
+ params: { locale },
+}: {
+ children: React.ReactNode;
+ params: { locale: string };
+}) {
+ if (!locales.includes(locale)) notFound();
+
+ const messages = await getMessages();
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx
new file mode 100644
index 00000000..7628e184
--- /dev/null
+++ b/app/[locale]/page.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+import { useRouter } from "next/navigation";
+import { PayEasyHero } from "@/components/ui/payeasy-hero";
+import { PayEasyLogo } from "@/components/ui/payeasy-logo";
+import Features from "@/components/landing/Features";
+import HowItWorks from "@/components/landing/HowItWorks";
+import Stellar from "@/components/landing/Stellar";
+import CTA from "@/components/landing/CTA";
+import Footer from "@/components/landing/Footer";
+
+export default function LocalePage() {
+ const t = useTranslations();
+ const router = useRouter();
+
+ return (
+
+ }
+ navigation={[
+ { label: t("nav.features"), href: "#features" },
+ { label: t("nav.howItWorks"), href: "#how-it-works" },
+ { label: t("nav.stellar"), href: "#stellar" },
+ ]}
+ ctaButton={{
+ label: t("nav.connectWallet"),
+ onClick: () => router.push("/connect"),
+ }}
+ title={t("hero.title")}
+ subtitle={t("hero.subtitle")}
+ primaryAction={{
+ label: t("hero.getStarted"),
+ onClick: () => router.push("/connect"),
+ }}
+ secondaryAction={{
+ label: t("hero.viewOnGithub"),
+ onClick: () =>
+ window.open("https://github.com/Ogstevyn/payeasy", "_blank"),
+ }}
+ disclaimer={t("hero.disclaimer")}
+ socialProof={{
+ avatars: [
+ "https://i.pravatar.cc/150?img=11",
+ "https://i.pravatar.cc/150?img=12",
+ "https://i.pravatar.cc/150?img=13",
+ "https://i.pravatar.cc/150?img=14",
+ ],
+ text: t("hero.socialProof"),
+ }}
+ stats={[
+ { value: "~$0.00001", label: t("hero.stats.transactionFee") },
+ { value: "~5 sec", label: t("hero.stats.settlementTime") },
+ { value: "100%", label: t("hero.stats.transparency") },
+ ]}
+ programs={[
+ {
+ image:
+ "https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=400&h=500&fit=crop",
+ category: t("hero.programs.findRoommates"),
+ title: t("hero.programs.findRoommatesDesc"),
+ },
+ {
+ image:
+ "https://images.unsplash.com/photo-1554224155-6726b3ff858f?w=400&h=500&fit=crop",
+ category: t("hero.programs.smartEscrow"),
+ title: t("hero.programs.smartEscrowDesc"),
+ },
+ {
+ image:
+ "https://images.unsplash.com/photo-1551836022-d5d88e9218df?w=400&h=500&fit=crop",
+ category: t("hero.programs.instantPayments"),
+ title: t("hero.programs.instantPaymentsDesc"),
+ },
+ {
+ image:
+ "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=400&h=500&fit=crop",
+ category: t("hero.programs.moveIn"),
+ title: t("hero.programs.moveInDesc"),
+ },
+ {
+ image:
+ "https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=400&h=500&fit=crop",
+ category: t("hero.programs.community"),
+ title: t("hero.programs.communityDesc"),
+ },
+ ]}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/landing/Navbar.tsx b/components/landing/Navbar.tsx
index f2fd15f4..3826c586 100644
--- a/components/landing/Navbar.tsx
+++ b/components/landing/Navbar.tsx
@@ -5,6 +5,7 @@ import { Menu, X, LogIn, UserPlus, LogOut, User } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import ConnectWalletButton from "@/components/wallet/ConnectWalletButton";
+import { LanguageSelector } from "@/components/ui/language-selector";
import { useActiveSection } from "@/hooks/useActiveSection";
import { ThemeToggle } from "@/components/ui/theme-toggle";
import { useEmailAuth } from "@/context/EmailAuthContext";
@@ -72,6 +73,18 @@ export default function Navbar() {
{/* CTA Buttons */}
+
+
+ Sign In
+
+
router.prefetch("/connect")}
+ >
+
+ Connect Wallet
+
{user ? (
<>
@@ -137,6 +150,21 @@ export default function Navbar() {
))}
+
+
+
+
+ Sign In
+
+
router.prefetch("/connect")}
+ >
+
+ Connect Wallet
+
+
{user ? (
<>
diff --git a/components/ui/glossary-term.tsx b/components/ui/glossary-term.tsx
new file mode 100644
index 00000000..505feb00
--- /dev/null
+++ b/components/ui/glossary-term.tsx
@@ -0,0 +1,69 @@
+"use client";
+
+import { useRef, useState } from "react";
+import { usePathname } from "next/navigation";
+import { getDefinition, type GlossaryLocale } from "@/lib/glossary";
+
+interface GlossaryTermProps {
+ term: string;
+ children: React.ReactNode;
+ locale?: GlossaryLocale;
+}
+
+export function GlossaryTerm({ term, children, locale }: GlossaryTermProps) {
+ const pathname = usePathname();
+ const [isVisible, setIsVisible] = useState(false);
+ const timerRef = useRef
| null>(null);
+
+ const detectedLocale: GlossaryLocale =
+ locale ?? (pathname.startsWith("/es") ? "es" : "en");
+ const definition = getDefinition(term, detectedLocale);
+
+ if (!definition) return <>{children}>;
+
+ const show = () => {
+ if (timerRef.current) clearTimeout(timerRef.current);
+ setIsVisible(true);
+ };
+
+ const hide = () => {
+ timerRef.current = setTimeout(() => setIsVisible(false), 100);
+ };
+
+ const tooltipId = `glossary-${term.replace(/\s+/g, "-")}`;
+
+ return (
+
+
+ {children}
+
+ {isVisible && (
+
+ {term}
+ {definition}
+
+
+ )}
+
+ );
+}
diff --git a/components/ui/language-selector.tsx b/components/ui/language-selector.tsx
new file mode 100644
index 00000000..83bcd34a
--- /dev/null
+++ b/components/ui/language-selector.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import { useState, useRef, useEffect } from "react";
+import { usePathname, useRouter } from "next/navigation";
+
+const languages = [
+ { code: "en", name: "English", flag: "🇬🇧" },
+ { code: "es", name: "Español", flag: "🇪🇸" },
+] as const;
+
+type LanguageCode = (typeof languages)[number]["code"];
+
+export function LanguageSelector() {
+ const [isOpen, setIsOpen] = useState(false);
+ const pathname = usePathname();
+ const router = useRouter();
+ const ref = useRef(null);
+
+ const currentLocale: LanguageCode = pathname.startsWith("/es") ? "es" : "en";
+ const current = languages.find((l) => l.code === currentLocale) ?? languages[0];
+
+ useEffect(() => {
+ const saved = localStorage.getItem("preferred-locale") as LanguageCode | null;
+ if (saved && saved !== currentLocale && pathname === "/") {
+ router.push(saved === "es" ? "/es" : "/");
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (ref.current && !ref.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ }
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => document.removeEventListener("mousedown", handleClickOutside);
+ }, []);
+
+ const handleSelect = (code: LanguageCode) => {
+ localStorage.setItem("preferred-locale", code);
+ router.push(code === "es" ? "/es" : "/");
+ setIsOpen(false);
+ };
+
+ return (
+
+
+
+ {isOpen && (
+
+ {languages.map((lang) => (
+ - handleSelect(lang.code)}
+ className="flex items-center gap-2 px-3 py-2 text-sm cursor-pointer text-dark-400 hover:text-white hover:bg-white/5 transition-colors"
+ >
+ {lang.flag}
+ {lang.name}
+ {lang.code === currentLocale && (
+
+ )}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/i18n.ts b/i18n.ts
new file mode 100644
index 00000000..6a9e30c4
--- /dev/null
+++ b/i18n.ts
@@ -0,0 +1,13 @@
+import { getRequestConfig } from "next-intl/server";
+import { notFound } from "next/navigation";
+
+export const locales = ["en", "es"] as const;
+export type Locale = (typeof locales)[number];
+
+export default getRequestConfig(async ({ locale }) => {
+ if (!locales.includes(locale as Locale)) notFound();
+
+ return {
+ messages: (await import(`./messages/${locale}.json`)).default,
+ };
+});
diff --git a/lib/glossary.ts b/lib/glossary.ts
new file mode 100644
index 00000000..f6fcfa59
--- /dev/null
+++ b/lib/glossary.ts
@@ -0,0 +1,46 @@
+export type GlossaryLocale = "en" | "es";
+
+export interface GlossaryEntry {
+ en: string;
+ es: string;
+}
+
+export const glossary: Record = {
+ escrow: {
+ en: "A financial arrangement where a neutral third party holds funds until agreed conditions are met.",
+ es: "Un acuerdo financiero donde un tercero neutral retiene fondos hasta que se cumplan las condiciones acordadas.",
+ },
+ XLM: {
+ en: "Stellar Lumens — the native cryptocurrency of the Stellar network used to pay transaction fees.",
+ es: "Stellar Lumens: la criptomoneda nativa de la red Stellar utilizada para pagar las comisiones de transacción.",
+ },
+ Soroban: {
+ en: "Stellar's smart contract platform enabling decentralised applications with low fees and fast execution.",
+ es: "La plataforma de contratos inteligentes de Stellar que permite aplicaciones descentralizadas con comisiones bajas y ejecución rápida.",
+ },
+ Freighter: {
+ en: "A browser wallet extension for the Stellar network used to sign and submit transactions.",
+ es: "Una extensión de billetera para el navegador de la red Stellar utilizada para firmar y enviar transacciones.",
+ },
+ "gas fee": {
+ en: "A small fee paid to process a transaction on a blockchain network.",
+ es: "Una pequeña comisión que se paga para procesar una transacción en una red blockchain.",
+ },
+ "smart contract": {
+ en: "Self-executing code stored on a blockchain that automatically enforces the terms of an agreement.",
+ es: "Código autoejecutado almacenado en una blockchain que hace cumplir automáticamente los términos de un acuerdo.",
+ },
+ testnet: {
+ en: "A test version of the blockchain where developers experiment without using real funds.",
+ es: "Una versión de prueba de la blockchain donde los desarrolladores experimentan sin usar fondos reales.",
+ },
+};
+
+export function getDefinition(
+ term: string,
+ locale: GlossaryLocale = "en"
+): string | null {
+ const entry = glossary[term];
+ if (!entry) return null;
+ return entry[locale] ?? entry.en;
+}
diff --git a/messages/en.json b/messages/en.json
new file mode 100644
index 00000000..5969f8a6
--- /dev/null
+++ b/messages/en.json
@@ -0,0 +1,66 @@
+{
+ "nav": {
+ "features": "Features",
+ "howItWorks": "How It Works",
+ "stellar": "Stellar",
+ "signIn": "Sign In",
+ "connectWallet": "Connect Wallet"
+ },
+ "hero": {
+ "title": "Split Rent. Trust the Chain.",
+ "subtitle": "Find roommates, split rent, and pay securely through Stellar blockchain escrow. Transparent, trustless, and effortless.",
+ "getStarted": "Get Started",
+ "viewOnGithub": "View on GitHub",
+ "disclaimer": "Powered by Stellar Blockchain",
+ "socialProof": "Trusted by roommates worldwide",
+ "stats": {
+ "transactionFee": "Transaction Fee",
+ "settlementTime": "Settlement Time",
+ "transparency": "On-Chain Transparency"
+ },
+ "programs": {
+ "findRoommates": "FIND ROOMMATES",
+ "findRoommatesDesc": "Browse verified listings & profiles",
+ "smartEscrow": "SMART ESCROW",
+ "smartEscrowDesc": "Trustless rent collection on-chain",
+ "instantPayments": "INSTANT PAYMENTS",
+ "instantPaymentsDesc": "Settle in seconds, not days",
+ "moveIn": "MOVE IN",
+ "moveInDesc": "Secure your new home with confidence",
+ "community": "COMMUNITY",
+ "communityDesc": "Connect with trusted roommates"
+ }
+ },
+ "errors": {
+ "walletNotConnected": "Wallet not connected",
+ "insufficientBalance": "Insufficient balance",
+ "transactionFailed": "Transaction failed",
+ "networkError": "Network error. Please try again.",
+ "invalidAddress": "Invalid wallet address"
+ },
+ "forms": {
+ "amount": "Amount",
+ "recipient": "Recipient",
+ "memo": "Memo",
+ "submit": "Submit",
+ "cancel": "Cancel",
+ "confirm": "Confirm"
+ },
+ "escrow": {
+ "create": "Create Escrow",
+ "deposit": "Deposit",
+ "release": "Release",
+ "refund": "Refund",
+ "status": {
+ "pending": "Pending",
+ "active": "Active",
+ "completed": "Completed",
+ "disputed": "Disputed"
+ }
+ },
+ "language": {
+ "en": "English",
+ "es": "Español",
+ "select": "Select language"
+ }
+}
diff --git a/messages/es.json b/messages/es.json
new file mode 100644
index 00000000..74c059a7
--- /dev/null
+++ b/messages/es.json
@@ -0,0 +1,66 @@
+{
+ "nav": {
+ "features": "Funcionalidades",
+ "howItWorks": "Cómo Funciona",
+ "stellar": "Stellar",
+ "signIn": "Iniciar sesión",
+ "connectWallet": "Conectar billetera"
+ },
+ "hero": {
+ "title": "Divida el alquiler. Confíe en la cadena.",
+ "subtitle": "Encuentre compañeros de cuarto, divida el alquiler y pague de forma segura a través del depósito en garantía de Stellar. Transparente, sin intermediarios y sin esfuerzo.",
+ "getStarted": "Comenzar",
+ "viewOnGithub": "Ver en GitHub",
+ "disclaimer": "Impulsado por Stellar Blockchain",
+ "socialProof": "Con la confianza de compañeros de cuarto en todo el mundo",
+ "stats": {
+ "transactionFee": "Comisión por transacción",
+ "settlementTime": "Tiempo de liquidación",
+ "transparency": "Transparencia en cadena"
+ },
+ "programs": {
+ "findRoommates": "ENCONTRAR COMPAÑEROS",
+ "findRoommatesDesc": "Explore perfiles y anuncios verificados",
+ "smartEscrow": "DEPÓSITO INTELIGENTE",
+ "smartEscrowDesc": "Cobro de alquiler en cadena sin intermediarios",
+ "instantPayments": "PAGOS INSTANTÁNEOS",
+ "instantPaymentsDesc": "Liquide en segundos, no en días",
+ "moveIn": "MUDARSE",
+ "moveInDesc": "Asegure su nuevo hogar con confianza",
+ "community": "COMUNIDAD",
+ "communityDesc": "Conéctese con compañeros de cuarto de confianza"
+ }
+ },
+ "errors": {
+ "walletNotConnected": "Billetera no conectada",
+ "insufficientBalance": "Saldo insuficiente",
+ "transactionFailed": "Transacción fallida",
+ "networkError": "Error de red. Por favor, inténtelo de nuevo.",
+ "invalidAddress": "Dirección de billetera no válida"
+ },
+ "forms": {
+ "amount": "Monto",
+ "recipient": "Destinatario",
+ "memo": "Nota",
+ "submit": "Enviar",
+ "cancel": "Cancelar",
+ "confirm": "Confirmar"
+ },
+ "escrow": {
+ "create": "Crear depósito en garantía",
+ "deposit": "Depositar",
+ "release": "Liberar",
+ "refund": "Reembolsar",
+ "status": {
+ "pending": "Pendiente",
+ "active": "Activo",
+ "completed": "Completado",
+ "disputed": "En disputa"
+ }
+ },
+ "language": {
+ "en": "English",
+ "es": "Español",
+ "select": "Seleccionar idioma"
+ }
+}
diff --git a/middleware.ts b/middleware.ts
index 7ade34ad..588e38c6 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -1,3 +1,14 @@
+import createMiddleware from "next-intl/middleware";
+
+export default createMiddleware({
+ locales: ["en", "es"],
+ defaultLocale: "en",
+ localeDetection: true,
+ localePrefix: "as-needed",
+});
+
+export const config = {
+ matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
diff --git a/next.config.js b/next.config.js
index dc6c445b..4edcaede 100644
--- a/next.config.js
+++ b/next.config.js
@@ -8,6 +8,8 @@ try {
// Keep builds working until the dependency is installed locally.
}
+const createNextIntlPlugin = require("next-intl/plugin");
+const withNextIntl = createNextIntlPlugin("./i18n.ts");
let withSentryConfig = (config) => config;
try {
@@ -85,4 +87,5 @@ const nextConfig = {
},
};
+module.exports = withBundleAnalyzer(withNextIntl(nextConfig));
module.exports = withSentryConfig(withBundleAnalyzer(nextConfig));
diff --git a/package.json b/package.json
index a3e64054..e2c51b7e 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"dependencies": {
"@sentry/nextjs": "^10.55.0",
"@stellar/freighter-api": "^6.0.1",
+ "next-intl": "^3.15.0",
"@stellar/stellar-sdk": "^14.6.1",
"@tanstack/react-query": "^5.100.14",
"@tanstack/react-virtual": "^3.13.26",