feat(i18n): migrate route structure under [locale] parameter prefix

This commit is contained in:
Poyraz
2026-06-27 14:42:42 +03:00
parent 968ebb33c5
commit d3c56713fb
3 changed files with 163 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { NextIntlClientProvider } from "next-intl";
import { getMessages, getTranslations } from "next-intl/server";
import { routing } from "@/i18n/routing";
import "../globals.css";
import { AppShell } from "@/components/app-shell";
import { GoogleAnalytics } from "@/components/google-analytics";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Metadata" });
const title = t("title");
const titleTemplate = t("titleTemplate");
const description = t("description");
return {
metadataBase: new URL("https://poyrazavsever.com"),
title: {
default: title,
template: titleTemplate,
},
description: description,
applicationName: "Poyraz Avsever Portfolyo",
alternates: {
canonical: "/",
},
icons: {
icon: [
{ url: "/favicon.ico", sizes: "any" },
{ url: "/logo/logo.jpeg", type: "image/jpeg" },
],
shortcut: "/favicon.ico",
apple: "/logo/logo.jpeg",
},
authors: [{ name: "Poyraz Avsever" }],
creator: "Poyraz Avsever",
publisher: "Poyraz Avsever",
openGraph: {
type: "website",
locale: locale === "tr" ? "tr_TR" : "en_US",
url: "https://poyrazavsever.com",
siteName: title,
title: title,
description: description,
images: [
{
url: "/logo/logo.jpeg",
width: 1200,
height: 1200,
alt: "Poyraz Avsever Logo",
},
],
},
twitter: {
card: "summary_large_image",
title: title,
description: description,
images: ["/logo/logo.jpeg"],
creator: "@poyrazavsever",
},
keywords: [
"Poyraz Avsever",
"Portfolio",
"Freelancer",
"Fullstack Developer",
"Web Developer",
"Projects",
"Skills",
"Services",
"Coding",
"Technology",
"Personal Website",
"Software Engineer",
"Frontend Developer",
"Backend Developer",
"JavaScript",
"React",
"Node.js",
"Next.js",
"Web Development",
"Programming",
"Tech Enthusiast",
"Developer Portfolio",
],
};
}
export default async function LocaleLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
// Validate that the incoming `locale` parameter is valid
if (!routing.locales.includes(locale as "tr" | "en")) {
notFound();
}
// Provide messages for NextIntlClientProvider
const messages = await getMessages();
return (
<html lang={locale}>
<body className="min-h-dvh bg-background text-foreground antialiased">
<NextIntlClientProvider messages={messages}>
<GoogleAnalytics />
<AppShell>{children}</AppShell>
</NextIntlClientProvider>
</body>
</html>
);
}
+26
View File
@@ -0,0 +1,26 @@
"use client";
import { useLocale } from "next-intl";
import { useRouter, usePathname } from "@/i18n/routing";
export function LanguageSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const toggleLanguage = () => {
const nextLocale = locale === "tr" ? "en" : "tr";
router.replace(pathname, { locale: nextLocale });
};
return (
<button
onClick={toggleLanguage}
className="inline-flex h-8 min-w-[36px] cursor-pointer items-center justify-center rounded-sm border border-border px-2 text-xs font-semibold text-muted-foreground transition-colors hover:text-foreground"
aria-label={locale === "tr" ? "Switch to English" : "Türkçe'ye geç"}
title={locale === "tr" ? "Switch to English" : "Türkçe'ye geç"}
>
{locale === "tr" ? "EN" : "TR"}
</button>
);
}
+15
View File
@@ -0,0 +1,15 @@
export type Localized<T = string> = {
tr: T;
en: T;
};
export function getLocalizedValue<T>(
value: Localized<T> | T | string,
locale: string
): T {
if (value && typeof value === "object" && ("tr" in value || "en" in value)) {
const localizedObj = value as Record<string, T>;
return localizedObj[locale] || localizedObj["tr"];
}
return value as T;
}