From abf283c9ab13929895e41fd35e8c02b278b63472 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Tue, 20 Jan 2026 23:43:01 +0100 Subject: [PATCH] slug i18n --- app/[locale]/products/[...slug]/page.tsx | 35 +++++---- app/[locale]/products/page.tsx | 38 ++++++---- components/RelatedProducts.tsx | 7 +- lib/blog.ts | 7 +- lib/mdx.ts | 15 ++-- lib/pages.ts | 19 ++++- lib/slugs.ts | 96 ++++++++++++++++++++++++ messages/de.json | 46 ++++++++++++ messages/en.json | 46 ++++++++++++ 9 files changed, 267 insertions(+), 42 deletions(-) create mode 100644 lib/slugs.ts diff --git a/app/[locale]/products/[...slug]/page.tsx b/app/[locale]/products/[...slug]/page.tsx index dd2d6e49..d85cf664 100644 --- a/app/[locale]/products/[...slug]/page.tsx +++ b/app/[locale]/products/[...slug]/page.tsx @@ -6,6 +6,7 @@ import RelatedProducts from '@/components/RelatedProducts'; import { Badge, Container, Section } from '@/components/ui'; import { getDatasheetPath } from '@/lib/datasheets'; import { getAllProducts, getProductBySlug } from '@/lib/mdx'; +import { mapFileSlugToTranslated } from '@/lib/slugs'; import { Metadata } from 'next'; import { getTranslations } from 'next-intl/server'; import { MDXRemote } from 'next-mdx-remote/rsc'; @@ -38,9 +39,9 @@ export async function generateMetadata({ params }: ProductPageProps): Promise - p.frontmatter.categories.some(cat => - cat.toLowerCase().replace(/\s+/g, '-') === productSlug || + const filteredProducts = allProducts.filter(p => + p.frontmatter.categories.some(cat => + cat.toLowerCase().replace(/\s+/g, '-') === productSlug || cat === categoryTitle ) ); + // Get translated product slugs + const productsWithTranslatedSlugs = await Promise.all( + filteredProducts.map(async (p) => ({ + ...p, + translatedSlug: await mapFileSlugToTranslated(p.slug, locale) + })) + ); + return (
@@ -161,10 +170,10 @@ export default async function ProductPage({ params }: ProductPageProps) {
- {filteredProducts.map((product) => ( - ( +
diff --git a/app/[locale]/products/page.tsx b/app/[locale]/products/page.tsx index 117092b6..5d14cf0d 100644 --- a/app/[locale]/products/page.tsx +++ b/app/[locale]/products/page.tsx @@ -1,11 +1,11 @@ import Reveal from '@/components/Reveal'; import Scribble from '@/components/Scribble'; import { Badge, Button, Card, Container, Section } from '@/components/ui'; -import { useTranslations } from 'next-intl'; import { getTranslations } from 'next-intl/server'; import { Metadata } from 'next'; import Image from 'next/image'; import Link from 'next/link'; +import { mapFileSlugToTranslated } from '@/lib/slugs'; interface ProductsPageProps { params: { @@ -39,37 +39,43 @@ export async function generateMetadata({ params: { locale } }: ProductsPageProps }; } -export default function ProductsPage({ params }: ProductsPageProps) { - const t = useTranslations('Products'); +export default async function ProductsPage({ params }: ProductsPageProps) { + const t = await getTranslations('Products'); + + // Get translated category slugs + const lowVoltageSlug = await mapFileSlugToTranslated('low-voltage-cables', params.locale); + const mediumVoltageSlug = await mapFileSlugToTranslated('medium-voltage-cables', params.locale); + const highVoltageSlug = await mapFileSlugToTranslated('high-voltage-cables', params.locale); + const solarSlug = await mapFileSlugToTranslated('solar-cables', params.locale); const categories = [ - { - title: t('categories.lowVoltage.title'), + { + title: t('categories.lowVoltage.title'), desc: t('categories.lowVoltage.description'), img: '/uploads/2024/11/low-voltage-category.webp', icon: '/uploads/2024/11/Low-Voltage.svg', - href: `/${params.locale}/products/low-voltage-cables` + href: `/${params.locale}/products/${lowVoltageSlug}` }, - { - title: t('categories.mediumVoltage.title'), + { + title: t('categories.mediumVoltage.title'), desc: t('categories.mediumVoltage.description'), img: '/uploads/2024/11/medium-voltage-category.webp', icon: '/uploads/2024/11/Medium-Voltage.svg', - href: `/${params.locale}/products/medium-voltage-cables` + href: `/${params.locale}/products/${mediumVoltageSlug}` }, - { - title: t('categories.highVoltage.title'), + { + title: t('categories.highVoltage.title'), desc: t('categories.highVoltage.description'), img: '/uploads/2024/11/high-voltage-category.webp', icon: '/uploads/2024/11/High-Voltage.svg', - href: `/${params.locale}/products/high-voltage-cables` + href: `/${params.locale}/products/${highVoltageSlug}` }, - { - title: t('categories.solar.title'), + { + title: t('categories.solar.title'), desc: t('categories.solar.description'), img: '/uploads/2024/11/solar-category.webp', icon: '/uploads/2024/11/Solar.svg', - href: `/${params.locale}/products/solar-cables` + href: `/${params.locale}/products/${solarSlug}` } ]; @@ -124,7 +130,7 @@ export default function ProductsPage({ params }: ProductsPageProps) {
- +
diff --git a/components/RelatedProducts.tsx b/components/RelatedProducts.tsx index 60bc2095..ac6d65cf 100644 --- a/components/RelatedProducts.tsx +++ b/components/RelatedProducts.tsx @@ -1,4 +1,5 @@ import { getAllProducts } from '@/lib/mdx'; +import { mapFileSlugToTranslated } from '@/lib/slugs'; import { getTranslations } from 'next-intl/server'; import Image from 'next/image'; import Link from 'next/link'; @@ -35,7 +36,7 @@ export default async function RelatedProducts({ currentSlug, categories, locale
- {related.map((product) => { + {related.map(async (product) => { // Find the category slug for the link const categorySlugs = ['low-voltage-cables', 'medium-voltage-cables', 'high-voltage-cables', 'solar-cables']; const catSlug = categorySlugs.find(slug => { @@ -46,10 +47,12 @@ export default async function RelatedProducts({ currentSlug, categories, locale ); }) || 'low-voltage-cables'; + const translatedProductSlug = await mapFileSlugToTranslated(product.slug, locale); + return (
diff --git a/lib/blog.ts b/lib/blog.ts index fb383c2b..f79e4a33 100644 --- a/lib/blog.ts +++ b/lib/blog.ts @@ -1,6 +1,7 @@ import fs from 'fs'; import path from 'path'; import matter from 'gray-matter'; +import { mapSlugToFileSlug } from './slugs'; export interface PostFrontmatter { title: string; @@ -18,8 +19,10 @@ export interface PostMdx { } export async function getPostBySlug(slug: string, locale: string): Promise { + // Map translated slug to file slug + const fileSlug = await mapSlugToFileSlug(slug, locale); const postsDir = path.join(process.cwd(), 'data', 'blog', locale); - const filePath = path.join(postsDir, `${slug}.mdx`); + const filePath = path.join(postsDir, `${fileSlug}.mdx`); if (!fs.existsSync(filePath)) { return null; @@ -29,7 +32,7 @@ export async function getPostBySlug(slug: string, locale: string): Promise { + // Map translated slug to file slug + const fileSlug = await mapSlugToFileSlug(slug, locale); const productsDir = path.join(process.cwd(), 'data', 'products', locale); // Try exact slug first - let filePath = path.join(productsDir, `${slug}.mdx`); + let filePath = path.join(productsDir, `${fileSlug}.mdx`); if (!fs.existsSync(filePath)) { // Try with -2 suffix (common in the dumped files) - filePath = path.join(productsDir, `${slug}-2.mdx`); + filePath = path.join(productsDir, `${fileSlug}-2.mdx`); } if (!fs.existsSync(filePath)) { // Fallback to English if locale is not 'en' if (locale !== 'en') { const enProductsDir = path.join(process.cwd(), 'data', 'products', 'en'); - let enFilePath = path.join(enProductsDir, `${slug}.mdx`); + let enFilePath = path.join(enProductsDir, `${fileSlug}.mdx`); if (!fs.existsSync(enFilePath)) { - enFilePath = path.join(enProductsDir, `${slug}-2.mdx`); + enFilePath = path.join(enProductsDir, `${fileSlug}-2.mdx`); } if (fs.existsSync(enFilePath)) { const fileContent = fs.readFileSync(enFilePath, 'utf8'); const { data, content } = matter(fileContent); return { - slug, + slug: fileSlug, frontmatter: { ...data, isFallback: true @@ -57,7 +60,7 @@ export async function getProductBySlug(slug: string, locale: string): Promise { + // Map translated slug to file slug + const fileSlug = await mapSlugToFileSlug(slug, locale); const pagesDir = path.join(process.cwd(), 'data', 'pages', locale); - const filePath = path.join(pagesDir, `${slug}.mdx`); + const filePath = path.join(pagesDir, `${fileSlug}.mdx`); if (!fs.existsSync(filePath)) { return null; @@ -27,7 +30,7 @@ export async function getPageBySlug(slug: string, locale: string): Promise { const pages = await Promise.all( files .filter(file => file.endsWith('.mdx')) - .map(file => getPageBySlug(file.replace(/\.mdx$/, ''), locale)) + .map(file => { + const fileSlug = file.replace(/\.mdx$/, ''); + const filePath = path.join(pagesDir, file); + const fileContent = { content: fs.readFileSync(filePath, 'utf8') }; + const { data, content } = matter(fileContent.content); + return { + slug: fileSlug, + frontmatter: data as PageFrontmatter, + content, + }; + }) ); return pages.filter((p): p is PageMdx => p !== null); diff --git a/lib/slugs.ts b/lib/slugs.ts new file mode 100644 index 00000000..46c2cf56 --- /dev/null +++ b/lib/slugs.ts @@ -0,0 +1,96 @@ +import { getTranslations } from 'next-intl/server'; + +/** + * Maps a translated slug to original file slug + * @param translatedSlug - The slug from URL (translated) + * @param _locale - The current locale (unused, kept for API consistency) + * @returns The original file slug, or input slug if no mapping exists + */ +export async function mapSlugToFileSlug(translatedSlug: string, _locale: string): Promise { + const t = await getTranslations('Slugs'); + + // Check pages + try { + const pageSlug = t.raw(`pages.${translatedSlug}`); + if (pageSlug && typeof pageSlug === 'string') { + return pageSlug; + } + } catch { + // Key doesn't exist, continue + } + + // Check products + try { + const productSlug = t.raw(`products.${translatedSlug}`); + if (productSlug && typeof productSlug === 'string') { + return productSlug; + } + } catch { + // Key doesn't exist, continue + } + + // Check categories + try { + const categorySlug = t.raw(`categories.${translatedSlug}`); + if (categorySlug && typeof categorySlug === 'string') { + return categorySlug; + } + } catch { + // Key doesn't exist, continue + } + + // Return original slug if no mapping found + return translatedSlug; +} + +/** + * Maps an original file slug to translated slug for a locale + * @param fileSlug - The original file slug + * @param _locale - The target locale (unused, kept for API consistency) + * @returns The translated slug, or input slug if no mapping exists + */ +export async function mapFileSlugToTranslated(fileSlug: string, _locale: string): Promise { + const t = await getTranslations('Slugs'); + + // Find the key that maps to this file slug + const sections = ['pages', 'products', 'categories']; + + for (const section of sections) { + try { + const sectionData = t.raw(section); + if (sectionData && typeof sectionData === 'object') { + for (const [translatedSlug, mappedSlug] of Object.entries(sectionData)) { + if (mappedSlug === fileSlug) { + return translatedSlug; + } + } + } + } catch { + // Section doesn't exist, continue + } + } + + // Return original slug if no mapping found + return fileSlug; +} + +/** + * Gets all translated slugs for a section + * @param section - The section name (pages, products, categories) + * @param _locale - The current locale (unused, kept for API consistency) + * @returns Object mapping translated slugs to file slugs + */ +export async function getSlugMappings(section: 'pages' | 'products' | 'categories', _locale: string): Promise> { + const t = await getTranslations('Slugs'); + + try { + const sectionData = t.raw(section); + if (sectionData && typeof (sectionData as any) === 'object') { + return sectionData as Record; + } + } catch { + // Section doesn't exist + } + + return {}; +} diff --git a/messages/de.json b/messages/de.json index d3929b4a..d13321e6 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1,4 +1,50 @@ { + "Slugs": { + "pages": { + "impressum": "legal-notice", + "datenschutz": "privacy-policy", + "agbs": "terms", + "kontakt": "contact", + "team": "team", + "blog": "blog", + "produkte": "products", + "start": "start", + "danke": "thanks" + }, + "products": { + "n2x2y": "n2x2y", + "n2xfk2y": "n2xfk2y", + "n2xfkld2y": "n2xfkld2y", + "n2xs2y": "n2xs2y", + "n2xsf2y": "n2xsf2y", + "n2xsfl2y-hv": "n2xsfl2y-hv", + "n2xsfl2y-mv": "n2xsfl2y-mv", + "n2xsy": "n2xsy", + "n2xy": "n2xy", + "na2x2y": "na2x2y", + "na2xfk2y": "na2xfk2y", + "na2xfkld2y": "na2xfkld2y", + "na2xs2y": "na2xs2y", + "na2xsf2y": "na2xsf2y", + "na2xsfl2y-hv": "na2xsfl2y-hv", + "na2xsfl2y-mv": "na2xsfl2y-mv", + "na2xsy": "na2xsy", + "na2xy": "na2xy", + "nay2y": "nay2y", + "naycwy": "naycwy", + "nayy": "nayy", + "ny2y": "ny2y", + "nycwy": "nycwy", + "nyy": "nyy", + "h1z2z2-k": "h1z2z2-k" + }, + "categories": { + "niederspannungskabel": "low-voltage-cables", + "mittelspannungskabel": "medium-voltage-cables", + "hochspannungskabel": "high-voltage-cables", + "solarkabel": "solar-cables" + } + }, "Index": { "title": "KLZ Cables - Hochwertige Kabel", "description": "Ihr Partner für hochwertige Kabel.", diff --git a/messages/en.json b/messages/en.json index 7b56229d..a8bdaf27 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,4 +1,50 @@ { + "Slugs": { + "pages": { + "legal-notice": "legal-notice", + "privacy-policy": "privacy-policy", + "terms": "terms", + "contact": "contact", + "team": "team", + "blog": "blog", + "products": "products", + "start": "start", + "thanks": "thanks" + }, + "products": { + "n2x2y": "n2x2y", + "n2xfk2y": "n2xfk2y", + "n2xfkld2y": "n2xfkld2y", + "n2xs2y": "n2xs2y", + "n2xsf2y": "n2xsf2y", + "n2xsfl2y-hv": "n2xsfl2y-hv", + "n2xsfl2y-mv": "n2xsfl2y-mv", + "n2xsy": "n2xsy", + "n2xy": "n2xy", + "na2x2y": "na2x2y", + "na2xfk2y": "na2xfk2y", + "na2xfkld2y": "na2xfkld2y", + "na2xs2y": "na2xs2y", + "na2xsf2y": "na2xsf2y", + "na2xsfl2y-hv": "na2xsfl2y-hv", + "na2xsfl2y-mv": "na2xsfl2y-mv", + "na2xsy": "na2xsy", + "na2xy": "na2xy", + "nay2y": "nay2y", + "naycwy": "naycwy", + "nayy": "nayy", + "ny2y": "ny2y", + "nycwy": "nycwy", + "nyy": "nyy", + "h1z2z2-k": "h1z2z2-k" + }, + "categories": { + "low-voltage-cables": "low-voltage-cables", + "medium-voltage-cables": "medium-voltage-cables", + "high-voltage-cables": "high-voltage-cables", + "solar-cables": "solar-cables" + } + }, "Index": { "title": "KLZ Cables - High Quality Cables", "description": "Your partner for high quality cables.",