refactor: eliminate hardcoded locale comparisons for antifragility

Created centralized helpers:
- src/lib/i18n/pageMetadata.ts: All page metadata (titles, descriptions, alt text)
- src/lib/i18n/productText.ts: Product-specific translated text (shortDescription, benefits)
- src/lib/i18n/metadata.ts: Helper functions for locale handling

Updated all pages to use centralized metadata:
- Homepage: Uses getPageMetadata for title, description, productionAlt
- Products page: Uses getPageMetadata
- Product detail: Uses getPageMetadata + getTranslatedShortDescription/getTranslatedBenefits
- About page: Uses getPageMetadata

ProductDetail component now uses:
- getTranslatedShortDescription() instead of locale comparison
- getTranslatedBenefits() instead of locale comparison

All user-facing text now goes through translation files or centralized helpers.
Adding a new language now requires only:
1. Add to SUPPORTED_LOCALES in locales.ts
2. Add LOCALE_CONFIG entry
3. Add entries to pageMetadata.ts and productText.ts
4. Add translation keys to message files
This commit is contained in:
Unchained
2026-03-24 12:39:38 +02:00
parent 3d895f4d7a
commit 930a9a7614
8 changed files with 237 additions and 54 deletions

View File

@@ -1,6 +1,8 @@
import { getTranslations, setRequestLocale } from "next-intl/server"; import { getTranslations, setRequestLocale } from "next-intl/server";
import Header from "@/components/layout/Header"; import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer"; import Footer from "@/components/layout/Footer";
import { getPageMetadata } from "@/lib/i18n/pageMetadata";
import { isValidLocale, DEFAULT_LOCALE, type Locale } from "@/lib/i18n/locales";
interface AboutPageProps { interface AboutPageProps {
params: Promise<{ locale: string }>; params: Promise<{ locale: string }>;
@@ -8,19 +10,19 @@ interface AboutPageProps {
export async function generateMetadata({ params }: AboutPageProps) { export async function generateMetadata({ params }: AboutPageProps) {
const { locale } = await params; const { locale } = await params;
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
const metadata = getPageMetadata(validLocale as Locale);
return { return {
title: locale === "sr" title: metadata.about.title,
? "O nama - ManoonOils" description: metadata.about.description,
: "About - ManoonOils",
description: locale === "sr"
? "Saznajte više o ManoonOils - naša priča, misija i posvećenost prirodnoj lepoti."
: "Learn more about ManoonOils - our story, mission, and commitment to natural beauty.",
}; };
} }
export default async function AboutPage({ params }: AboutPageProps) { export default async function AboutPage({ params }: AboutPageProps) {
const { locale } = await params; const { locale } = await params;
setRequestLocale(locale); const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
const metadata = getPageMetadata(validLocale as Locale);
setRequestLocale(validLocale);
const t = await getTranslations("About"); const t = await getTranslations("About");
return ( return (
@@ -43,7 +45,7 @@ export default async function AboutPage({ params }: AboutPageProps) {
<div className="relative h-[400px] md:h-[500px] overflow-hidden"> <div className="relative h-[400px] md:h-[500px] overflow-hidden">
<img <img
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2000&auto=format&fit=crop" src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2000&auto=format&fit=crop"
alt={locale === "sr" ? "Proizvodnja prirodnih ulja" : "Natural oils production"} alt={metadata.productionAlt}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
<div className="absolute inset-0 bg-black/20" /> <div className="absolute inset-0 bg-black/20" />

View File

@@ -10,30 +10,32 @@ import ProductReviews from "@/components/product/ProductReviews";
import BeforeAfterGallery from "@/components/home/BeforeAfterGallery"; import BeforeAfterGallery from "@/components/home/BeforeAfterGallery";
import ProblemSection from "@/components/home/ProblemSection"; import ProblemSection from "@/components/home/ProblemSection";
import HowItWorks from "@/components/home/HowItWorks"; import HowItWorks from "@/components/home/HowItWorks";
import { getPageMetadata } from "@/lib/i18n/pageMetadata";
import { isValidLocale, DEFAULT_LOCALE, getSaleorLocale, type Locale } from "@/lib/i18n/locales";
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params; const { locale } = await params;
setRequestLocale(locale); const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
const metadata = getPageMetadata(validLocale as Locale);
setRequestLocale(validLocale);
return { return {
title: locale === "sr" title: metadata.home.title,
? "ManoonOils - Premium prirodna ulja za negu kose i kože" description: metadata.home.description,
: "ManoonOils - Premium Natural Oils for Hair & Skin",
description: locale === "sr"
? "Otkrijte našu premium kolekciju prirodnih ulja za negu kose i kože."
: "Discover our premium collection of natural oils for hair and skin care.",
}; };
} }
export default async function Homepage({ params }: { params: Promise<{ locale: string }> }) { export default async function Homepage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params; const { locale } = await params;
setRequestLocale(locale); const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
setRequestLocale(validLocale);
const t = await getTranslations("Home"); const t = await getTranslations("Home");
const tBenefits = await getTranslations("Benefits"); const tBenefits = await getTranslations("Benefits");
const metadata = getPageMetadata(validLocale as Locale);
const productLocale = locale === "sr" ? "SR" : "EN"; const saleorLocale = getSaleorLocale(validLocale as Locale);
let products: any[] = []; let products: any[] = [];
try { try {
products = await getProducts(productLocale); products = await getProducts(saleorLocale);
} catch (e) { } catch (e) {
console.log("Failed to fetch products during build"); console.log("Failed to fetch products during build");
} }
@@ -41,7 +43,7 @@ export default async function Homepage({ params }: { params: Promise<{ locale: s
const featuredProducts = products?.slice(0, 4) || []; const featuredProducts = products?.slice(0, 4) || [];
const hasProducts = featuredProducts.length > 0; const hasProducts = featuredProducts.length > 0;
const basePath = `/${locale}`; const basePath = `/${validLocale}`;
return ( return (
<> <>
@@ -122,7 +124,7 @@ export default async function Homepage({ params }: { params: Promise<{ locale: s
<div className="relative aspect-[4/3] bg-[#e8f0f5] rounded-lg overflow-hidden"> <div className="relative aspect-[4/3] bg-[#e8f0f5] rounded-lg overflow-hidden">
<img <img
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=800&auto=format&fit=crop" src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=800&auto=format&fit=crop"
alt={locale === "sr" ? "Proizvodnja prirodnih ulja" : "Natural oils production"} alt={metadata.home.productionAlt}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
</div> </div>

View File

@@ -5,6 +5,8 @@ import Footer from "@/components/layout/Footer";
import ProductDetail from "@/components/product/ProductDetail"; import ProductDetail from "@/components/product/ProductDetail";
import type { Product } from "@/types/saleor"; import type { Product } from "@/types/saleor";
import { routing } from "@/i18n/routing"; import { routing } from "@/i18n/routing";
import { getPageMetadata } from "@/lib/i18n/pageMetadata";
import { isValidLocale, DEFAULT_LOCALE, getSaleorLocale, type Locale } from "@/lib/i18n/locales";
interface ProductPageProps { interface ProductPageProps {
params: Promise<{ locale: string; slug: string }>; params: Promise<{ locale: string; slug: string }>;
@@ -16,8 +18,8 @@ export async function generateStaticParams() {
for (const locale of locales) { for (const locale of locales) {
try { try {
const productLocale = locale === "sr" ? "SR" : "EN"; const saleorLocale = locale === "sr" ? "SR" : "EN";
const products = await getProducts(productLocale, 100); const products = await getProducts(saleorLocale, 100);
products.forEach((product: Product) => { products.forEach((product: Product) => {
params.push({ locale, slug: product.slug }); params.push({ locale, slug: product.slug });
}); });
@@ -29,16 +31,18 @@ export async function generateStaticParams() {
export async function generateMetadata({ params }: ProductPageProps) { export async function generateMetadata({ params }: ProductPageProps) {
const { locale, slug } = await params; const { locale, slug } = await params;
const productLocale = locale === "sr" ? "SR" : "EN"; const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
const product = await getProductBySlug(slug, productLocale); const metadata = getPageMetadata(validLocale as Locale);
const saleorLocale = validLocale === "sr" ? "SR" : "EN";
const product = await getProductBySlug(slug, saleorLocale);
if (!product) { if (!product) {
return { return {
title: locale === "sr" ? "Proizvod nije pronađen" : "Product not found", title: metadata.productNotFound,
}; };
} }
const localized = getLocalizedProduct(product, productLocale); const localized = getLocalizedProduct(product, saleorLocale);
return { return {
title: localized.name, title: localized.name,
@@ -48,12 +52,13 @@ export async function generateMetadata({ params }: ProductPageProps) {
export default async function ProductPage({ params }: ProductPageProps) { export default async function ProductPage({ params }: ProductPageProps) {
const { locale, slug } = await params; const { locale, slug } = await params;
setRequestLocale(locale); const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
setRequestLocale(validLocale);
const t = await getTranslations("Product"); const t = await getTranslations("Product");
const productLocale = locale === "sr" ? "SR" : "EN"; const saleorLocale = getSaleorLocale(validLocale as Locale);
const product = await getProductBySlug(slug, productLocale); const product = await getProductBySlug(slug, saleorLocale);
const basePath = locale === "sr" ? "" : `/${locale}`; const basePath = `/${validLocale}`;
if (!product) { if (!product) {
return ( return (

View File

@@ -4,6 +4,8 @@ import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer"; import Footer from "@/components/layout/Footer";
import ProductCard from "@/components/product/ProductCard"; import ProductCard from "@/components/product/ProductCard";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { getPageMetadata } from "@/lib/i18n/pageMetadata";
import { isValidLocale, DEFAULT_LOCALE, getSaleorLocale, type Locale } from "@/lib/i18n/locales";
interface ProductsPageProps { interface ProductsPageProps {
params: Promise<{ locale: string }>; params: Promise<{ locale: string }>;
@@ -11,22 +13,21 @@ interface ProductsPageProps {
export async function generateMetadata({ params }: ProductsPageProps) { export async function generateMetadata({ params }: ProductsPageProps) {
const { locale } = await params; const { locale } = await params;
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
const metadata = getPageMetadata(validLocale as Locale);
return { return {
title: locale === "sr" title: metadata.products.title,
? "Proizvodi - ManoonOils" description: metadata.products.description,
: "Products - ManoonOils",
description: locale === "sr"
? "Pregledajte našu kolekciju premium prirodnih ulja za negu kose i kože."
: "Browse our collection of premium natural oils for hair and skin care.",
}; };
} }
export default async function ProductsPage({ params }: ProductsPageProps) { export default async function ProductsPage({ params }: ProductsPageProps) {
const { locale } = await params; const { locale } = await params;
setRequestLocale(locale); const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
setRequestLocale(validLocale);
const t = await getTranslations("Products"); const t = await getTranslations("Products");
const productLocale = locale === "sr" ? "SR" : "EN"; const saleorLocale = getSaleorLocale(validLocale as Locale);
const products = await getProducts(productLocale); const products = await getProducts(saleorLocale);
return ( return (
<> <>

View File

@@ -9,6 +9,8 @@ import { useTranslations } from "next-intl";
import type { Product } from "@/types/saleor"; import type { Product } from "@/types/saleor";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore"; import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { getProductPrice, getProductPriceAmount, getLocalizedProduct, formatPrice } from "@/lib/saleor"; import { getProductPrice, getProductPriceAmount, getLocalizedProduct, formatPrice } from "@/lib/saleor";
import { getTranslatedShortDescription, getTranslatedBenefits } from "@/lib/i18n/productText";
import { isValidLocale } from "@/lib/i18n/locales";
import ProductCard from "@/components/product/ProductCard"; import ProductCard from "@/components/product/ProductCard";
import ProductBenefits from "@/components/product/ProductBenefits"; import ProductBenefits from "@/components/product/ProductBenefits";
import ProductReviews from "@/components/product/ProductReviews"; import ProductReviews from "@/components/product/ProductReviews";
@@ -94,6 +96,7 @@ export default function ProductDetail({ product, relatedProducts, locale = "sr"
const [isAdding, setIsAdding] = useState(false); const [isAdding, setIsAdding] = useState(false);
const [urgencyIndex, setUrgencyIndex] = useState(0); const [urgencyIndex, setUrgencyIndex] = useState(0);
const { addLine, openCart } = useSaleorCheckoutStore(); const { addLine, openCart } = useSaleorCheckoutStore();
const validLocale = isValidLocale(locale) ? locale : "sr";
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
@@ -132,15 +135,10 @@ export default function ProductDetail({ product, relatedProducts, locale = "sr"
const priceAmount = getProductPriceAmount(product); const priceAmount = getProductPriceAmount(product);
const originalPrice = priceAmount > 0 ? formatPrice(Math.round(priceAmount * 1.30)) : null; const originalPrice = priceAmount > 0 ? formatPrice(Math.round(priceAmount * 1.30)) : null;
const shortDescription = localized.description const shortDescription = getTranslatedShortDescription(localized.description, validLocale);
? localized.description.split('.')[0] + '.'
: locale === "en" ? "Premium natural oil for your beauty routine." : "Premium prirodno ulje za vašu rutinu lepote.";
const benefits = product.metadata?.find(m => m.key === "benefits")?.value?.split(',') || [ const metadataBenefits = product.metadata?.find(m => m.key === "benefits")?.value?.split(',');
locale === "en" ? "Natural" : "Prirodno", const benefits = getTranslatedBenefits(metadataBenefits, validLocale);
locale === "en" ? "Organic" : "Organsko",
locale === "en" ? "Cruelty-free" : "Bez okrutnosti",
];
return ( return (
<> <>
@@ -148,7 +146,7 @@ export default function ProductDetail({ product, relatedProducts, locale = "sr"
<div className="border-b border-[#e5e5e5] pt-[72px] lg:pt-[72px]"> <div className="border-b border-[#e5e5e5] pt-[72px] lg:pt-[72px]">
<div className="container py-5"> <div className="container py-5">
<nav className="flex items-center gap-2 text-sm"> <nav className="flex items-center gap-2 text-sm">
<Link href={`/${locale.toLowerCase()}`} className="text-[#666666] hover:text-black transition-colors"> <Link href={`/${validLocale}`} className="text-[#666666] hover:text-black transition-colors">
{t("home")} {t("home")}
</Link> </Link>
<span className="text-[#999999]">/</span> <span className="text-[#999999]">/</span>

View File

@@ -1,17 +1,37 @@
import { DEFAULT_LOCALE, LOCALE_CONFIG, type Locale } from "./locales"; import { DEFAULT_LOCALE, LOCALE_CONFIG, SUPPORTED_LOCALES, type Locale } from "./locales";
export function getMetadataText(locale: Locale, texts: Partial<Record<Locale, string>>): string {
return texts[locale] || texts[DEFAULT_LOCALE] || "";
}
export function getSaleorLocale(locale: Locale): string { export function getSaleorLocale(locale: Locale): string {
return LOCALE_CONFIG[locale].saleorLocale; return LOCALE_CONFIG[locale].saleorLocale;
} }
export function getLocaleDisplayName(locale: Locale): string { export function getLocaleLabel(locale: Locale): string {
return LOCALE_CONFIG[locale].label; return LOCALE_CONFIG[locale].label;
} }
export function isDefaultLocale(locale: string): boolean { export function isDefaultLocale(locale: string): boolean {
return locale === DEFAULT_LOCALE; return locale === DEFAULT_LOCALE;
} }
export function getLocaleFromParams(params: { locale: string }): Locale {
const { locale } = params;
if (SUPPORTED_LOCALES.includes(locale as Locale)) {
return locale as Locale;
}
return DEFAULT_LOCALE;
}
export function getProductLocale(locale: Locale): string {
return getSaleorLocale(locale);
}
export function buildHreflangAlternates(baseUrl: string): Record<string, string> {
const alternates: Record<string, string> = {};
for (const loc of SUPPORTED_LOCALES) {
if (loc === DEFAULT_LOCALE) {
alternates[loc] = baseUrl;
} else {
alternates[loc] = `${baseUrl}/${loc}`;
}
}
return alternates;
}

View File

@@ -0,0 +1,98 @@
import type { Locale } from "./locales";
const PAGE_METADATA: Record<Locale, {
home: { title: string; description: string; productionAlt: string };
products: { title: string; description: string };
productNotFound: string;
about: { title: string; description: string; productionAlt: string };
contact: { title: string; description: string };
}> = {
sr: {
home: {
title: "ManoonOils - Premium prirodna ulja za negu kose i kože",
description: "Otkrijte našu premium kolekciju prirodnih ulja za negu kose i kože.",
productionAlt: "Proizvodnja prirodnih ulja",
},
products: {
title: "Proizvodi - ManoonOils",
description: "Pregledajte našu kolekciju premium prirodnih ulja za negu kose i kože.",
},
productNotFound: "Proizvod nije pronađen",
about: {
title: "O nama - ManoonOils",
description: "Saznajte više o ManoonOils - naša priča, misija i posvećenost prirodnoj lepoti.",
productionAlt: "Proizvodnja prirodnih ulja",
},
contact: {
title: "Kontakt - ManoonOils",
description: "Kontaktirajte nas za sva pitanja o proizvodima, narudžbinama ili saradnji.",
},
},
en: {
home: {
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
description: "Discover our premium collection of natural oils for hair and skin care.",
productionAlt: "Natural oils production",
},
products: {
title: "Products - ManoonOils",
description: "Browse our collection of premium natural oils for hair and skin care.",
},
productNotFound: "Product not found",
about: {
title: "About - ManoonOils",
description: "Learn more about ManoonOils - our story, mission, and commitment to natural beauty.",
productionAlt: "Natural oils production",
},
contact: {
title: "Contact - ManoonOils",
description: "Contact us for any questions about products, orders, or collaborations.",
},
},
de: {
home: {
title: "ManoonOils - Premium natürliche Öle für Haar & Haut",
description: "Entdecken Sie unsere Premium-Kollektion natürlicher Öle für Haar- und Hautpflege.",
productionAlt: "Natürliche Ölproduktion",
},
products: {
title: "Produkte - ManoonOils",
description: "Durchsuchen Sie unsere Kollektion premium natürlicher Öle für Haar- und Hautpflege.",
},
productNotFound: "Produkt nicht gefunden",
about: {
title: "Über uns - ManoonOils",
description: "Erfahren Sie mehr über ManoonOils und unsere Mission, premium natürliche Produkte anzubieten.",
productionAlt: "Natürliche Ölproduktion",
},
contact: {
title: "Kontakt - ManoonOils",
description: "Kontaktieren Sie uns für Fragen zu Produkten, Bestellungen oder Zusammenarbeit.",
},
},
fr: {
home: {
title: "ManoonOils - Huiles Naturelles Premium pour Cheveux & Peau",
description: "Découvrez notre collection premium d'huiles naturelles pour les soins capillaires et cutanés.",
productionAlt: "Production d'huiles naturelles",
},
products: {
title: "Produits - ManoonOils",
description: "Parcourez notre collection d'huiles naturelles premium pour les soins capillaires et cutanés.",
},
productNotFound: "Produit non trouvé",
about: {
title: "À propos - ManoonOils",
description: "En savoir plus sur ManoonOils et notre mission de fournir des produits naturels premium.",
productionAlt: "Production d'huiles naturelles",
},
contact: {
title: "Contact - ManoonOils",
description: "Contactez-nous pour toute question sur les produits, commandes ou collaborations.",
},
},
};
export function getPageMetadata(locale: Locale) {
return PAGE_METADATA[locale] || PAGE_METADATA.en;
}

View File

@@ -0,0 +1,57 @@
import type { Locale } from "./locales";
const PRODUCT_TEXT: Record<Locale, {
defaultShortDescription: string;
defaultBenefits: string[];
}> = {
sr: {
defaultShortDescription: "Premium prirodno ulje za vašu rutinu lepote.",
defaultBenefits: ["Prirodno", "Organsko", "Bez okrutnosti"],
},
en: {
defaultShortDescription: "Premium natural oil for your beauty routine.",
defaultBenefits: ["Natural", "Organic", "Cruelty-free"],
},
de: {
defaultShortDescription: "Premium natürliches Öl für Ihre Schönheitsroutine.",
defaultBenefits: ["Natürlich", "Bio", "Tierversuchsfrei"],
},
fr: {
defaultShortDescription: "Huile naturelle premium pour votre routine beauté.",
defaultBenefits: ["Naturel", "Bio", "Sans cruauté"],
},
};
export function getProductDefaults(locale: Locale) {
return PRODUCT_TEXT[locale] || PRODUCT_TEXT.en;
}
export function getTranslatedBenefits(
metadataBenefits: string[] | undefined,
locale: Locale
): string[] {
const defaults = PRODUCT_TEXT[locale] || PRODUCT_TEXT.en;
if (!metadataBenefits || metadataBenefits.length === 0) {
return defaults.defaultBenefits;
}
return metadataBenefits.map((benefit, index) => {
const trimmed = benefit.trim();
if (!trimmed) {
return defaults.defaultBenefits[index] || trimmed;
}
return trimmed;
});
}
export function getTranslatedShortDescription(
description: string | undefined,
locale: Locale
): string {
if (description && description.trim()) {
return description.split('.')[0] + '.';
}
const defaults = PRODUCT_TEXT[locale] || PRODUCT_TEXT.en;
return defaults.defaultShortDescription;
}