diff --git a/middleware.ts b/middleware.ts index 35af576..bca233b 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,7 +1,8 @@ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; +import { SUPPORTED_LOCALES, DEFAULT_LOCALE, LOCALE_COOKIE, getLocaleFromPath } from "@/lib/i18n/locales"; -const LOCALE_COOKIE = "NEXT_LOCALE"; +const OLD_SERBIAN_PATHS = ["products", "about", "contact", "checkout"]; export default function middleware(request: NextRequest) { const pathname = request.nextUrl.pathname; @@ -9,9 +10,9 @@ export default function middleware(request: NextRequest) { const acceptLanguage = request.headers.get("accept-language") || ""; if (pathname === "/" || pathname === "") { - let locale = "sr"; + let locale = DEFAULT_LOCALE; - if (cookieLocale && ["sr", "en", "de", "fr"].includes(cookieLocale)) { + if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale as typeof SUPPORTED_LOCALES[number])) { locale = cookieLocale; } else if (acceptLanguage.includes("en")) { locale = "en"; @@ -22,15 +23,14 @@ export default function middleware(request: NextRequest) { return NextResponse.redirect(url, 301); } - const oldSerbianPaths = ["products", "about", "contact", "checkout"]; - const isOldSerbianPath = oldSerbianPaths.some( + const isOldSerbianPath = OLD_SERBIAN_PATHS.some( (path) => pathname === `/${path}` || pathname.startsWith(`/${path}/`) ); if (isOldSerbianPath) { - let locale = "sr"; + let locale = DEFAULT_LOCALE; - if (cookieLocale && ["sr", "en", "de", "fr"].includes(cookieLocale)) { + if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale as typeof SUPPORTED_LOCALES[number])) { locale = cookieLocale; } else if (acceptLanguage.includes("en")) { locale = "en"; diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 31f42ee..326e078 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,12 +1,12 @@ import { Metadata } from "next"; import { NextIntlClientProvider } from "next-intl"; import { getMessages, setRequestLocale } from "next-intl/server"; -import { routing } from "@/i18n/routing"; +import { SUPPORTED_LOCALES } from "@/lib/i18n/locales"; const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com"; export function generateStaticParams() { - return routing.locales.map((locale) => ({ locale })); + return SUPPORTED_LOCALES.map((locale) => ({ locale })); } export async function generateMetadata({ @@ -18,7 +18,7 @@ export async function generateMetadata({ const localePrefix = locale === "sr" ? "" : `/${locale}`; const languages: Record = {}; - for (const loc of routing.locales) { + for (const loc of SUPPORTED_LOCALES) { const prefix = loc === "sr" ? "" : `/${loc}`; languages[loc] = `${baseUrl}${prefix}`; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 606dc94..e8ffdcd 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import "./globals.css"; import type { Metadata, Viewport } from "next"; import ErrorBoundary from "@/components/providers/ErrorBoundary"; +import { SUPPORTED_LOCALES } from "@/lib/i18n/locales"; const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com"; @@ -13,12 +14,9 @@ export const metadata: Metadata = { robots: "index, follow", alternates: { canonical: baseUrl, - languages: { - sr: baseUrl, - en: `${baseUrl}/en`, - de: `${baseUrl}/de`, - fr: `${baseUrl}/fr`, - }, + languages: Object.fromEntries( + SUPPORTED_LOCALES.map((locale) => [locale, locale === "sr" ? baseUrl : `${baseUrl}/${locale}`]) + ), }, openGraph: { title: "ManoonOils - Premium Natural Oils for Hair & Skin", @@ -48,4 +46,4 @@ export default async function RootLayout({ ); -} \ No newline at end of file +} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index f58c6ec..a509bfc 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,7 +1,7 @@ import { MetadataRoute } from "next"; import { getProducts } from "@/lib/saleor"; +import { SUPPORTED_LOCALES, type Locale } from "@/lib/i18n/locales"; -const LOCALES = ["sr", "en", "de", "fr"] as const; const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com"; interface SitemapEntry { @@ -29,12 +29,9 @@ export default async function sitemap(): Promise { changeFrequency: "daily", priority: 1, alternates: { - languages: { - sr: `${baseUrl}`, - en: `${baseUrl}/en`, - de: `${baseUrl}/de`, - fr: `${baseUrl}/fr`, - }, + languages: Object.fromEntries( + SUPPORTED_LOCALES.map((locale) => [locale, locale === "sr" ? baseUrl : `${baseUrl}/${locale}`]) + ), }, }, { @@ -43,12 +40,9 @@ export default async function sitemap(): Promise { changeFrequency: "daily", priority: 0.9, alternates: { - languages: { - sr: `${baseUrl}/products`, - en: `${baseUrl}/en/products`, - de: `${baseUrl}/de/products`, - fr: `${baseUrl}/fr/products`, - }, + languages: Object.fromEntries( + SUPPORTED_LOCALES.map((locale) => [locale, locale === "sr" ? `${baseUrl}/products` : `${baseUrl}/${locale}/products`]) + ), }, }, { @@ -57,12 +51,9 @@ export default async function sitemap(): Promise { changeFrequency: "monthly", priority: 0.6, alternates: { - languages: { - sr: `${baseUrl}/about`, - en: `${baseUrl}/en/about`, - de: `${baseUrl}/de/about`, - fr: `${baseUrl}/fr/about`, - }, + languages: Object.fromEntries( + SUPPORTED_LOCALES.map((locale) => [locale, locale === "sr" ? `${baseUrl}/about` : `${baseUrl}/${locale}/about`]) + ), }, }, { @@ -71,12 +62,9 @@ export default async function sitemap(): Promise { changeFrequency: "monthly", priority: 0.6, alternates: { - languages: { - sr: `${baseUrl}/contact`, - en: `${baseUrl}/en/contact`, - de: `${baseUrl}/de/contact`, - fr: `${baseUrl}/fr/contact`, - }, + languages: Object.fromEntries( + SUPPORTED_LOCALES.map((locale) => [locale, locale === "sr" ? `${baseUrl}/contact` : `${baseUrl}/${locale}/contact`]) + ), }, }, { @@ -85,12 +73,9 @@ export default async function sitemap(): Promise { changeFrequency: "monthly", priority: 0.5, alternates: { - languages: { - sr: `${baseUrl}/checkout`, - en: `${baseUrl}/en/checkout`, - de: `${baseUrl}/de/checkout`, - fr: `${baseUrl}/fr/checkout`, - }, + languages: Object.fromEntries( + SUPPORTED_LOCALES.map((locale) => [locale, locale === "sr" ? `${baseUrl}/checkout` : `${baseUrl}/${locale}/checkout`]) + ), }, }, ]; @@ -98,7 +83,13 @@ export default async function sitemap(): Promise { const productUrls: SitemapEntry[] = []; for (const product of products) { - for (const locale of LOCALES) { + const hreflangs: Record = {}; + for (const locale of SUPPORTED_LOCALES) { + const path = locale === "sr" ? `/products/${product.slug}` : `/${locale}/products/${product.slug}`; + hreflangs[locale] = `${baseUrl}${path}`; + } + + for (const locale of SUPPORTED_LOCALES) { const localePrefix = locale === "sr" ? "" : `/${locale}`; productUrls.push({ url: `${baseUrl}${localePrefix}/products/${product.slug}`, @@ -106,12 +97,7 @@ export default async function sitemap(): Promise { changeFrequency: "weekly", priority: 0.8, alternates: { - languages: { - sr: `${baseUrl}/products/${product.slug}`, - en: `${baseUrl}/en/products/${product.slug}`, - de: `${baseUrl}/de/products/${product.slug}`, - fr: `${baseUrl}/fr/products/${product.slug}`, - }, + languages: hreflangs, }, }); } diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index a076765..8b14527 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -9,11 +9,8 @@ import { useTranslations } from "next-intl"; import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore"; import { User, ShoppingBag, Menu, X, Globe } from "lucide-react"; import CartDrawer from "@/components/cart/CartDrawer"; - -const LOCALES = [ - { code: "sr", label: "Srpski", flag: "πŸ‡·πŸ‡Έ" }, - { code: "en", label: "English", flag: "πŸ‡¬πŸ‡§" }, -] as const; +import { SUPPORTED_LOCALES, LOCALE_COOKIE, LOCALE_CONFIG, isValidLocale } from "@/lib/i18n/locales"; +import type { Locale } from "@/lib/i18n/locales"; interface HeaderProps { locale?: string; @@ -29,9 +26,7 @@ export default function Header({ locale = "sr" }: HeaderProps) { const { getLineCount, toggleCart, initCheckout } = useSaleorCheckoutStore(); const itemCount = getLineCount(); - const localePath = `/${locale}`; - - const currentLocale = LOCALES.find((l) => l.code === locale) || LOCALES[0]; + const currentLocale = isValidLocale(locale) ? LOCALE_CONFIG[locale] : LOCALE_CONFIG.sr; useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -48,7 +43,11 @@ export default function Header({ locale = "sr" }: HeaderProps) { setLangDropdownOpen(false); return; } - document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000`; + if (!isValidLocale(newLocale)) { + setLangDropdownOpen(false); + return; + } + document.cookie = `${LOCALE_COOKIE}=${newLocale}; path=/; max-age=31536000`; const pathWithoutLocale = pathname.replace(/^\/(sr|en|de|fr)/, "") || "/"; const newPath = `/${newLocale}${pathWithoutLocale}`; window.location.replace(newPath); @@ -115,7 +114,7 @@ export default function Header({ locale = "sr" }: HeaderProps) { ))} - + ManoonOils - {LOCALES.map((loc) => ( + {SUPPORTED_LOCALES.map((loc) => ( ))} @@ -195,7 +194,7 @@ export default function Header({ locale = "sr" }: HeaderProps) { >
- setMobileMenuOpen(false)}> + setMobileMenuOpen(false)}> ManoonOils ); -} \ No newline at end of file +} diff --git a/src/components/product/ProductCard.tsx b/src/components/product/ProductCard.tsx index 42e90ae..c49b3e2 100644 --- a/src/components/product/ProductCard.tsx +++ b/src/components/product/ProductCard.tsx @@ -6,6 +6,7 @@ import Link from "next/link"; import { useTranslations } from "next-intl"; import type { Product } from "@/types/saleor"; import { getProductPrice, getProductImage, getLocalizedProduct } from "@/lib/saleor"; +import { isValidLocale, getSaleorLocale } from "@/lib/i18n/locales"; interface ProductCardProps { product: Product; @@ -13,13 +14,13 @@ interface ProductCardProps { locale?: string; } -export default function ProductCard({ product, index = 0, locale = "SR" }: ProductCardProps) { +export default function ProductCard({ product, index = 0, locale = "sr" }: ProductCardProps) { const t = useTranslations("ProductCard"); const image = getProductImage(product); const price = getProductPrice(product); - const localized = getLocalizedProduct(product, locale); + const saleorLocale = isValidLocale(locale) ? getSaleorLocale(locale) : "SR"; + const localized = getLocalizedProduct(product, saleorLocale); const isAvailable = product.variants?.[0]?.quantityAvailable > 0; - const urlLocale = locale === "SR" ? "sr" : "en"; return ( - +
{image ? ( ); -} \ No newline at end of file +} diff --git a/src/i18n/routing.ts b/src/i18n/routing.ts index 1d94d71..5ed74f2 100644 --- a/src/i18n/routing.ts +++ b/src/i18n/routing.ts @@ -1,7 +1,8 @@ import { defineRouting } from "next-intl/routing"; +import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from "@/lib/i18n/locales"; export const routing = defineRouting({ - locales: ["sr", "en", "de", "fr"], - defaultLocale: "sr", + locales: SUPPORTED_LOCALES, + defaultLocale: DEFAULT_LOCALE, localePrefix: "as-needed", -}); \ No newline at end of file +}); diff --git a/src/lib/i18n/locales.ts b/src/lib/i18n/locales.ts new file mode 100644 index 0000000..094b26a --- /dev/null +++ b/src/lib/i18n/locales.ts @@ -0,0 +1,26 @@ +export const SUPPORTED_LOCALES = ["sr", "en", "de", "fr"] as const; +export type Locale = (typeof SUPPORTED_LOCALES)[number]; + +export const DEFAULT_LOCALE: Locale = "sr"; + +export const LOCALE_COOKIE = "NEXT_LOCALE"; + +export const LOCALE_CONFIG: Record = { + sr: { label: "Srpski", flag: "πŸ‡·πŸ‡Έ", saleorLocale: "SR" }, + en: { label: "English", flag: "πŸ‡¬πŸ‡§", saleorLocale: "EN" }, + de: { label: "Deutsch", flag: "πŸ‡©πŸ‡ͺ", saleorLocale: "EN" }, + fr: { label: "FranΓ§ais", flag: "πŸ‡«πŸ‡·", saleorLocale: "EN" }, +}; + +export function isValidLocale(locale: string): locale is Locale { + return SUPPORTED_LOCALES.includes(locale as Locale); +} + +export function getSaleorLocale(locale: Locale): string { + return LOCALE_CONFIG[locale].saleorLocale; +} + +export function getLocaleFromPath(pathname: string): string { + const match = pathname.match(/^\/(sr|en|de|fr)/); + return match ? match[1] : DEFAULT_LOCALE; +}