Serve the site in English, Spanish and Arabic, add a captcha, make the message optional

- Move every route under app/[lang]; a proxy keeps English on unprefixed URLs and serves /es and /ar
- Interface strings and homepage copy per locale; content pages load a translated JSON when one exists
- Language switch in the header, hreflang alternates and a multilingual sitemap
- Right-to-left layout for Arabic with logical CSS properties, Noto Sans Arabic and no letter-spacing
- Self-hosted proof-of-work "I'm not a robot" check (no third-party account) replaces Turnstile
- The message field is optional; failed attempts refill the form instead of clearing it
This commit is contained in:
Alejandro Veras
2026-10-13 10:49:14 -04:00
parent 2f9b63ccac
commit 5bf472b735
65 changed files with 2386 additions and 756 deletions
+85
View File
@@ -0,0 +1,85 @@
import { Suspense } from "react";
import type { Metadata, Viewport } from "next";
import { Inter, Noto_Sans_Arabic } from "next/font/google";
import "../globals.css";
import SiteHeader from "@/components/site-header";
import SiteFooter from "@/components/site-footer";
import { dirOf, locales, localize, ogLocale } from "@/i18n/config";
import { mainNav } from "@/i18n/nav";
import { getUI } from "@/i18n/server";
import { localeAlternates } from "@/lib/metadata";
import { SITE_URL } from "@/lib/routes";
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
display: "swap",
});
// Only Arabic pages use it, so it isn't preloaded everywhere.
const arabic = Noto_Sans_Arabic({
variable: "--font-arabic",
subsets: ["arabic"],
display: "swap",
preload: false,
});
export function generateStaticParams() {
return locales.map((lang) => ({ lang }));
}
export async function generateMetadata(): Promise<Metadata> {
const { locale, t } = await getUI();
return {
metadataBase: new URL(SITE_URL),
title: { default: t.meta.title, template: "%s" },
description: t.meta.description,
alternates: localeAlternates("/", locale),
openGraph: {
type: "website",
url: localize("/", locale),
siteName: "AI Growth Technologies",
locale: ogLocale[locale],
title: t.meta.title,
description: t.meta.description,
},
};
}
export const viewport: Viewport = {
themeColor: "#000000",
colorScheme: "dark",
};
export default async function RootLayout({ children }: LayoutProps<"/[lang]">) {
const { locale, t } = await getUI();
const headerLabels = {
primary: t.nav.primary,
bookCall: t.bookCall,
openMenu: t.nav.openMenu,
closeMenu: t.nav.closeMenu,
language: t.nav.language,
logoHome: t.logoHome,
};
return (
<html
lang={locale}
dir={dirOf(locale)}
className={`${inter.variable} ${arabic.variable}`}
data-scroll-behavior="smooth"
>
<body>
<a href="#main" className="skip-link">
{t.skip}
</a>
{/* The header reads the pathname, which is request data on dynamic routes. */}
<Suspense>
<SiteHeader locale={locale} nav={mainNav(t, locale)} t={headerLabels} />
</Suspense>
<main id="main">{children}</main>
<SiteFooter />
</body>
</html>
);
}