Fix dynamic default locale routing

This commit is contained in:
MOH
2026-03-15 04:41:08 +01:00
parent 50c3f5cce9
commit 364f761420
13 changed files with 73 additions and 32 deletions
+10 -3
View File
@@ -5,7 +5,7 @@ import { isRedirectError } from "next/dist/client/components/redirect-error";
import { z } from "zod"; import { z } from "zod";
import { enforceContactRateLimit, verifyTurnstileToken } from "@/lib/contact-guard"; import { enforceContactRateLimit, verifyTurnstileToken } from "@/lib/contact-guard";
import { getContactProtectionSettings } from "@/lib/app-config"; import { getContactProtectionSettings, getSiteSettings } from "@/lib/app-config";
import { getLocalizedPath, resolveLocale } from "@/lib/locale"; import { getLocalizedPath, resolveLocale } from "@/lib/locale";
import { sendContactMessage } from "@/lib/mail"; import { sendContactMessage } from "@/lib/mail";
@@ -60,7 +60,8 @@ function getStringValue(formData: FormData, key: string) {
export async function submitContactFormAction(formData: FormData) { export async function submitContactFormAction(formData: FormData) {
const locale = resolveLocale(String(formData.get("locale") ?? "")); const locale = resolveLocale(String(formData.get("locale") ?? ""));
const contactPath = getLocalizedPath(locale, "/contact"); const siteSettings = await getSiteSettings();
const contactPath = getLocalizedPath(locale, "/contact", siteSettings.defaultLocale);
try { try {
const protectionSettings = await getContactProtectionSettings(); const protectionSettings = await getContactProtectionSettings();
@@ -86,7 +87,13 @@ export async function submitContactFormAction(formData: FormData) {
message: values.message, message: values.message,
}); });
redirect(withMessage(getLocalizedPath(locale, "/success"), "success", contactSuccessMessages[locale])); redirect(
withMessage(
getLocalizedPath(locale, "/success", siteSettings.defaultLocale),
"success",
contactSuccessMessages[locale],
),
);
} catch (error) { } catch (error) {
if (isRedirectError(error)) { if (isRedirectError(error)) {
throw error; throw error;
+4 -3
View File
@@ -7,7 +7,7 @@ import { PageHero } from "@/components/layout/page-hero";
import { MotionFade } from "@/components/motion-fade"; import { MotionFade } from "@/components/motion-fade";
import { ContactForm } from "@/components/site/contact-form"; import { ContactForm } from "@/components/site/contact-form";
import { buildLocalizedMetadata } from "@/lib/metadata"; import { buildLocalizedMetadata } from "@/lib/metadata";
import { getPublicContactProtectionSettings } from "@/lib/app-config"; import { getPublicContactProtectionSettings, getSiteSettings } from "@/lib/app-config";
import { getLocalizedPath, resolveLocale } from "@/lib/locale"; import { getLocalizedPath, resolveLocale } from "@/lib/locale";
import { AppCard } from "@/components/ui/app-card"; import { AppCard } from "@/components/ui/app-card";
import { CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -36,9 +36,10 @@ export async function generateMetadata({ params }: ContactPageProps): Promise<Me
export default async function ContactPage({ params }: ContactPageProps) { export default async function ContactPage({ params }: ContactPageProps) {
const { locale } = await params; const { locale } = await params;
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const [t, protection] = await Promise.all([ const [t, protection, siteSettings] = await Promise.all([
getTranslations({ locale: localeKey, namespace: "contactPage" }), getTranslations({ locale: localeKey, namespace: "contactPage" }),
getPublicContactProtectionSettings(), getPublicContactProtectionSettings(),
getSiteSettings(),
]); ]);
return ( return (
@@ -82,7 +83,7 @@ export default async function ContactPage({ params }: ContactPageProps) {
<ContactForm <ContactForm
action={submitContactFormAction} action={submitContactFormAction}
locale={localeKey} locale={localeKey}
previewHref={getLocalizedPath(localeKey, "/success")} previewHref={getLocalizedPath(localeKey, "/success", siteSettings.defaultLocale)}
protection={protection} protection={protection}
copy={{ copy={{
name: t("name"), name: t("name"),
+6 -4
View File
@@ -6,7 +6,7 @@ import { SiteAmbientBackdrop } from "@/components/layout/site-ambient-backdrop";
import { SiteFooter } from "@/components/layout/site-footer"; import { SiteFooter } from "@/components/layout/site-footer";
import { SiteHeader } from "@/components/layout/site-header"; import { SiteHeader } from "@/components/layout/site-header";
import { isAdminAuthenticated } from "@/lib/admin-auth"; import { isAdminAuthenticated } from "@/lib/admin-auth";
import { getMaintenanceMode, getSiteSettingsMediaBindings } from "@/lib/app-config"; import { getMaintenanceMode, getSiteSettings, getSiteSettingsMediaBindings } from "@/lib/app-config";
import { getLocalizedPath, resolveLocale } from "@/lib/locale"; import { getLocalizedPath, resolveLocale } from "@/lib/locale";
type SiteLayoutProps = { type SiteLayoutProps = {
@@ -24,14 +24,15 @@ export default async function SiteLayout({ children, params }: SiteLayoutProps)
const { locale } = await params; const { locale } = await params;
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const [maintenanceEnabled, mediaBindings] = await Promise.all([ const [maintenanceEnabled, mediaBindings, siteSettings] = await Promise.all([
getMaintenanceMode(), getMaintenanceMode(),
getSiteSettingsMediaBindings(), getSiteSettingsMediaBindings(),
getSiteSettings(),
]); ]);
const authenticated = await isAdminAuthenticated(); const authenticated = await isAdminAuthenticated();
if (maintenanceEnabled && !authenticated) { if (maintenanceEnabled && !authenticated) {
redirect(getLocalizedPath(localeKey, "/coming-soon")); redirect(getLocalizedPath(localeKey, "/coming-soon", siteSettings.defaultLocale));
} }
return ( return (
@@ -40,9 +41,10 @@ export default async function SiteLayout({ children, params }: SiteLayoutProps)
<SiteHeader <SiteHeader
lightLogoUrl={mediaBindings.siteLogoLight?.url} lightLogoUrl={mediaBindings.siteLogoLight?.url}
darkLogoUrl={mediaBindings.siteLogoDark?.url} darkLogoUrl={mediaBindings.siteLogoDark?.url}
defaultLocale={siteSettings.defaultLocale}
/> />
<main className="flex-1">{children}</main> <main className="flex-1">{children}</main>
<SiteFooter /> <SiteFooter defaultLocale={siteSettings.defaultLocale} />
</div> </div>
); );
} }
+4 -3
View File
@@ -61,9 +61,10 @@ export async function generateMetadata({ params }: HomePageProps): Promise<Metad
export default async function HomePage({ params }: HomePageProps) { export default async function HomePage({ params }: HomePageProps) {
const { locale } = await params; const { locale } = await params;
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const [t, marqueeSettings] = await Promise.all([ const [t, marqueeSettings, siteSettings] = await Promise.all([
getTranslations({ locale: localeKey, namespace: "homepage" }), getTranslations({ locale: localeKey, namespace: "homepage" }),
getMarqueeSettings(), getMarqueeSettings(),
getSiteSettings(),
]); ]);
const serviceItems = [ const serviceItems = [
t("serviceBrand"), t("serviceBrand"),
@@ -108,7 +109,7 @@ export default async function HomePage({ params }: HomePageProps) {
</p> </p>
<div className="mt-8 flex flex-wrap gap-3"> <div className="mt-8 flex flex-wrap gap-3">
<Button asChild size="lg" className="min-w-[11rem]"> <Button asChild size="lg" className="min-w-[11rem]">
<Link href={getLocalizedPath(localeKey, "/contact")}> <Link href={getLocalizedPath(localeKey, "/contact", siteSettings.defaultLocale)}>
{t("servicesPrimaryCta")} {t("servicesPrimaryCta")}
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />
</Link> </Link>
@@ -119,7 +120,7 @@ export default async function HomePage({ params }: HomePageProps) {
variant="outline" variant="outline"
className="min-w-[11rem] border-border/80 bg-background/70 hover:bg-background" className="min-w-[11rem] border-border/80 bg-background/70 hover:bg-background"
> >
<Link href={getLocalizedPath(localeKey, "/portfolio")}> <Link href={getLocalizedPath(localeKey, "/portfolio", siteSettings.defaultLocale)}>
{t("servicesSecondaryCta")} {t("servicesSecondaryCta")}
</Link> </Link>
</Button> </Button>
+7 -3
View File
@@ -6,6 +6,7 @@ import { getTranslations } from "next-intl/server";
import { Container } from "@/components/layout/container"; import { Container } from "@/components/layout/container";
import { MotionFade } from "@/components/motion-fade"; import { MotionFade } from "@/components/motion-fade";
import { HeroShell, HeroTitle } from "@/components/layout/site-hero"; import { HeroShell, HeroTitle } from "@/components/layout/site-hero";
import { getSiteSettings } from "@/lib/app-config";
import { buildLocalizedMetadata } from "@/lib/metadata"; import { buildLocalizedMetadata } from "@/lib/metadata";
import { getLocalizedPath, resolveLocale } from "@/lib/locale"; import { getLocalizedPath, resolveLocale } from "@/lib/locale";
import { AppCard } from "@/components/ui/app-card"; import { AppCard } from "@/components/ui/app-card";
@@ -34,7 +35,10 @@ export async function generateMetadata({ params }: SuccessPageProps): Promise<Me
export default async function SuccessPage({ params }: SuccessPageProps) { export default async function SuccessPage({ params }: SuccessPageProps) {
const { locale } = await params; const { locale } = await params;
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const t = await getTranslations({ locale: localeKey, namespace: "successPage" }); const [t, siteSettings] = await Promise.all([
getTranslations({ locale: localeKey, namespace: "successPage" }),
getSiteSettings(),
]);
return ( return (
<> <>
@@ -51,10 +55,10 @@ export default async function SuccessPage({ params }: SuccessPageProps) {
</p> </p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-3"> <div className="mt-8 flex flex-wrap items-center justify-center gap-3">
<Button asChild> <Button asChild>
<Link href={getLocalizedPath(localeKey)}>{t("home")}</Link> <Link href={getLocalizedPath(localeKey, "/", siteSettings.defaultLocale)}>{t("home")}</Link>
</Button> </Button>
<Button asChild variant="outline"> <Button asChild variant="outline">
<Link href={getLocalizedPath(localeKey, "/contact")}>{t("contact")}</Link> <Link href={getLocalizedPath(localeKey, "/contact", siteSettings.defaultLocale)}>{t("contact")}</Link>
</Button> </Button>
</div> </div>
</div> </div>
+5 -2
View File
@@ -35,7 +35,10 @@ export default async function ComingSoonPage({
}: ComingSoonPageProps) { }: ComingSoonPageProps) {
const { locale } = await params; const { locale } = await params;
const localeKey = resolveLocale(locale); const localeKey = resolveLocale(locale);
const t = await getTranslations({ locale: localeKey, namespace: "comingSoon" }); const [t, siteSettings] = await Promise.all([
getTranslations({ locale: localeKey, namespace: "comingSoon" }),
getSiteSettings(),
]);
const isArabic = localeKey === "ar"; const isArabic = localeKey === "ar";
const lines = [ const lines = [
{ {
@@ -57,7 +60,7 @@ export default async function ComingSoonPage({
return ( return (
<div className="relative min-h-screen overflow-hidden"> <div className="relative min-h-screen overflow-hidden">
<FloatingPreferences locale={localeKey} /> <FloatingPreferences locale={localeKey} defaultLocale={siteSettings.defaultLocale} />
<HeroShell className="min-h-screen" showBridges> <HeroShell className="min-h-screen" showBridges>
<MotionFade className="relative z-10 w-full"> <MotionFade className="relative z-10 w-full">
+1 -1
View File
@@ -38,7 +38,7 @@ export default async function LocaleLayout({
const localeClassName = locale === "ar" ? "font-arabic" : "font-latin"; const localeClassName = locale === "ar" ? "font-arabic" : "font-latin";
return ( return (
<NextIntlClientProvider messages={messages}> <NextIntlClientProvider locale={locale} messages={messages}>
<div lang={locale} dir={getDirection(locale)} className={localeClassName}> <div lang={locale} dir={getDirection(locale)} className={localeClassName}>
{children} {children}
</div> </div>
@@ -9,11 +9,13 @@ import { cn } from "@/lib/utils";
type FloatingPreferencesProps = { type FloatingPreferencesProps = {
locale: string; locale: string;
defaultLocale: "de" | "en" | "ar";
className?: string; className?: string;
}; };
export function FloatingPreferences({ export function FloatingPreferences({
locale, locale,
defaultLocale,
className, className,
}: FloatingPreferencesProps) { }: FloatingPreferencesProps) {
const t = useTranslations("navigation"); const t = useTranslations("navigation");
@@ -39,6 +41,7 @@ export function FloatingPreferences({
/> />
<LocaleToggle <LocaleToggle
locale={locale} locale={locale}
defaultLocale={defaultLocale}
className="h-9 w-9 rounded-pill border border-transparent bg-transparent p-0 text-foreground/80 hover:bg-accent hover:text-foreground" className="h-9 w-9 rounded-pill border border-transparent bg-transparent p-0 text-foreground/80 hover:bg-accent hover:text-foreground"
/> />
</div> </div>
+3 -1
View File
@@ -17,6 +17,7 @@ const PENDING_TOAST_STORAGE_KEY = "mohfarawati-pending-toast";
type LocaleToggleProps = { type LocaleToggleProps = {
locale: string; locale: string;
defaultLocale?: AppLocale;
className?: string; className?: string;
showLabel?: boolean; showLabel?: boolean;
}; };
@@ -29,6 +30,7 @@ const localeChangedMessages: Record<AppLocale, string> = {
export function LocaleToggle({ export function LocaleToggle({
locale, locale,
defaultLocale = "de",
className, className,
showLabel = false, showLabel = false,
}: LocaleToggleProps) { }: LocaleToggleProps) {
@@ -82,7 +84,7 @@ export function LocaleToggle({
)} )}
> >
<a <a
href={getLocalizedPath(targetLocale, currentPath)} href={getLocalizedPath(targetLocale, currentPath, defaultLocale)}
onClick={() => { onClick={() => {
const message = localeChangedMessages[targetLocale]; const message = localeChangedMessages[targetLocale];
+7 -3
View File
@@ -2,7 +2,7 @@ import Link from "next/link";
import { useLocale, useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
import { Container } from "@/components/layout/container"; import { Container } from "@/components/layout/container";
import { getLocalizedPath } from "@/lib/locale"; import { AppLocale, getLocalizedPath } from "@/lib/locale";
const navItems = [ const navItems = [
{ key: "home", path: "" }, { key: "home", path: "" },
@@ -11,7 +11,11 @@ const navItems = [
{ key: "contact", path: "/contact" }, { key: "contact", path: "/contact" },
]; ];
export function SiteFooter() { type SiteFooterProps = {
defaultLocale: AppLocale;
};
export function SiteFooter({ defaultLocale }: SiteFooterProps) {
const locale = useLocale(); const locale = useLocale();
const tNav = useTranslations("navigation"); const tNav = useTranslations("navigation");
const tFooter = useTranslations("footer"); const tFooter = useTranslations("footer");
@@ -23,7 +27,7 @@ export function SiteFooter() {
{navItems.map((item) => ( {navItems.map((item) => (
<Link <Link
key={item.key} key={item.key}
href={getLocalizedPath(locale, item.path || "/")} href={getLocalizedPath(locale, item.path || "/", defaultLocale)}
className="text-muted-foreground hover:text-foreground" className="text-muted-foreground hover:text-foreground"
> >
{tNav(item.key)} {tNav(item.key)}
+10 -4
View File
@@ -34,6 +34,7 @@ const disabledMobileNavItemClassName =
type SiteHeaderProps = { type SiteHeaderProps = {
lightLogoUrl?: string | null; lightLogoUrl?: string | null;
darkLogoUrl?: string | null; darkLogoUrl?: string | null;
defaultLocale: "de" | "en" | "ar";
}; };
function getDirectionalReveal(direction: "rtl" | "ltr") { function getDirectionalReveal(direction: "rtl" | "ltr") {
@@ -57,9 +58,11 @@ function isNavItemActive(currentPath: string, itemPath: string) {
} }
function NavLinks({ function NavLinks({
defaultLocale,
onNavigate, onNavigate,
mobile = false, mobile = false,
}: { }: {
defaultLocale: "de" | "en" | "ar";
onNavigate?: () => void; onNavigate?: () => void;
mobile?: boolean; mobile?: boolean;
}) { }) {
@@ -113,7 +116,7 @@ function NavLinks({
return ( return (
<Link <Link
key={item.key} key={item.key}
href={getLocalizedPath(locale, itemPath)} href={getLocalizedPath(locale, itemPath, defaultLocale)}
className={cn( className={cn(
"relative z-10 rounded-pill transition-colors", "relative z-10 rounded-pill transition-colors",
mobile ? "px-4 py-3 text-sm font-medium" : "px-3.5 py-2 text-sm font-medium xl:px-4", mobile ? "px-4 py-3 text-sm font-medium" : "px-3.5 py-2 text-sm font-medium xl:px-4",
@@ -142,6 +145,7 @@ function NavLinks({
export function SiteHeader({ export function SiteHeader({
lightLogoUrl, lightLogoUrl,
darkLogoUrl, darkLogoUrl,
defaultLocale,
}: SiteHeaderProps) { }: SiteHeaderProps) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false); const [isScrolled, setIsScrolled] = useState(false);
@@ -210,7 +214,7 @@ export function SiteHeader({
)} )}
> >
<Link <Link
href={getLocalizedPath(locale)} href={getLocalizedPath(locale, "/", defaultLocale)}
className="inline-flex items-center gap-2.5 rounded-pill px-1 py-1 sm:gap-3 lg:py-2" className="inline-flex items-center gap-2.5 rounded-pill px-1 py-1 sm:gap-3 lg:py-2"
onClick={handleLogoClick} onClick={handleLogoClick}
> >
@@ -277,7 +281,7 @@ export function SiteHeader({
isArabic ? "w-auto max-w-none" : "min-w-[27rem] max-w-[42rem]", isArabic ? "w-auto max-w-none" : "min-w-[27rem] max-w-[42rem]",
)} )}
> >
<NavLinks /> <NavLinks defaultLocale={defaultLocale} />
</motion.nav> </motion.nav>
</div> </div>
@@ -293,6 +297,7 @@ export function SiteHeader({
<div className={desktopControlRailClassName}> <div className={desktopControlRailClassName}>
<LocaleToggle <LocaleToggle
locale={locale} locale={locale}
defaultLocale={defaultLocale}
className={desktopControlButtonClassName} className={desktopControlButtonClassName}
/> />
<SoundToggle <SoundToggle
@@ -405,7 +410,7 @@ export function SiteHeader({
</div> </div>
) : ( ) : (
<Link <Link
href={getLocalizedPath(locale, itemPath)} href={getLocalizedPath(locale, itemPath, defaultLocale)}
className={cn( className={cn(
"flex items-center justify-between rounded-nested px-4 py-3 text-sm font-medium transition-colors", "flex items-center justify-between rounded-nested px-4 py-3 text-sm font-medium transition-colors",
isActive isActive
@@ -435,6 +440,7 @@ export function SiteHeader({
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<LocaleToggle <LocaleToggle
locale={locale} locale={locale}
defaultLocale={defaultLocale}
className={mobileControlButtonClassName} className={mobileControlButtonClassName}
/> />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
+6 -2
View File
@@ -28,8 +28,12 @@ export function stripLocalePrefix(pathname: string): string {
return pathname || "/"; return pathname || "/";
} }
export function getLocalizedPath(locale: string, pathname = "/"): string { export function getLocalizedPath(
return getLocalizedPathWithDefault(locale, pathname, routing.defaultLocale); locale: string,
pathname = "/",
defaultLocale: AppLocale = routing.defaultLocale,
): string {
return getLocalizedPathWithDefault(locale, pathname, defaultLocale);
} }
export function getLocalizedPathWithDefault( export function getLocalizedPathWithDefault(
+7 -3
View File
@@ -196,9 +196,13 @@ export default async function middleware(request: NextRequest) {
configuredDefaultLocale !== routing.defaultLocale && configuredDefaultLocale !== routing.defaultLocale &&
!hasLocalePrefix(pathname) !hasLocalePrefix(pathname)
) { ) {
const redirectUrl = request.nextUrl.clone(); const rewriteUrl = request.nextUrl.clone();
redirectUrl.pathname = getLocalizedPathWithDefault(configuredDefaultLocale, pathname, routing.defaultLocale); rewriteUrl.pathname = getLocalizedPathWithDefault(
return NextResponse.redirect(redirectUrl, 307); configuredDefaultLocale,
pathname,
routing.defaultLocale,
);
return NextResponse.rewrite(rewriteUrl);
} }
if ( if (