Finalize multilingual routing and translations
CI / quality (push) Waiting to run

This commit is contained in:
MOH
2026-03-07 14:10:30 +01:00
parent 1ad9ae9629
commit 7f4277d1d7
53 changed files with 2805 additions and 1382 deletions
+41
View File
@@ -0,0 +1,41 @@
import { routing } from "@/i18n/routing";
export type AppLocale = (typeof routing.locales)[number];
export function resolveLocale(locale: string): AppLocale {
if (locale === "en" || locale === "ar") {
return locale;
}
return routing.defaultLocale;
}
export function getDirection(locale: string): "ltr" | "rtl" {
return resolveLocale(locale) === "ar" ? "rtl" : "ltr";
}
export function stripLocalePrefix(pathname: string): string {
for (const locale of routing.locales) {
if (pathname === `/${locale}`) {
return "/";
}
if (pathname.startsWith(`/${locale}/`)) {
return pathname.slice(locale.length + 1);
}
}
return pathname || "/";
}
export function getLocalizedPath(locale: string, pathname = "/"): string {
const localeKey = resolveLocale(locale);
const normalizedPath = pathname === "" ? "/" : pathname;
const strippedPath = stripLocalePrefix(normalizedPath);
if (localeKey === routing.defaultLocale) {
return strippedPath;
}
return strippedPath === "/" ? `/${localeKey}` : `/${localeKey}${strippedPath}`;
}
+56
View File
@@ -0,0 +1,56 @@
import type { Metadata } from "next";
import { routing } from "@/i18n/routing";
import { AppLocale, getLocalizedPath, resolveLocale } from "@/lib/locale";
function getSiteUrl(): URL {
return new URL(process.env.NEXT_PUBLIC_SITE_URL ?? "https://mohfarawati.de");
}
function toAbsoluteUrl(pathname: string): string {
return new URL(pathname, getSiteUrl()).toString();
}
export function buildLocaleAlternates(pathname: string) {
const languages = Object.fromEntries(
routing.locales.map((locale) => [locale, toAbsoluteUrl(getLocalizedPath(locale, pathname))]),
) as Record<AppLocale, string>;
return {
canonical: toAbsoluteUrl(getLocalizedPath(routing.defaultLocale, pathname)),
languages: {
...languages,
"x-default": toAbsoluteUrl(getLocalizedPath(routing.defaultLocale, pathname)),
},
};
}
type LocalizedMetadataInput = {
locale: string;
pathname: string;
title: string;
description: string;
};
export function buildLocalizedMetadata({
locale,
pathname,
title,
description,
}: LocalizedMetadataInput): Metadata {
const localeKey = resolveLocale(locale);
return {
title,
description,
alternates: buildLocaleAlternates(pathname),
openGraph: {
title,
description,
url: toAbsoluteUrl(getLocalizedPath(localeKey, pathname)),
siteName: "moh-sass",
locale: localeKey,
type: "website",
},
};
}
+40
View File
@@ -0,0 +1,40 @@
import { LayoutDashboard, ShieldAlert, SwatchBook, type LucideIcon } from "lucide-react";
type RootNavigationCopy = {
overview: string;
maintenance: string;
uiKit: string;
};
export type RootNavItem = {
label: string;
href: string;
icon: LucideIcon;
active?: boolean;
};
export function getRootNavigation(
copy: RootNavigationCopy,
active: "overview" | "maintenance" | "ui-kit",
): RootNavItem[] {
return [
{
label: copy.overview,
href: "/root",
icon: LayoutDashboard,
active: active === "overview",
},
{
label: copy.maintenance,
href: "/root/maintenance",
icon: ShieldAlert,
active: active === "maintenance",
},
{
label: copy.uiKit,
href: "/root/ui-kit",
icon: SwatchBook,
active: active === "ui-kit",
},
];
}
+25 -5
View File
@@ -1,4 +1,4 @@
export type AppLocale = "de" | "en";
import type { AppLocale } from "@/lib/locale";
type LocalizedText = Record<AppLocale, string>;
@@ -24,14 +24,17 @@ export const portfolioItems: PortfolioItem[] = [
title: {
de: "Brand Redesign",
en: "Brand Redesign",
ar: "إعادة تصميم الهوية",
},
summary: {
de: "Modernes Redesign fuer eine digitale Marke mit klarer Struktur.",
en: "Modern redesign for a digital brand with a clear system.",
ar: "إعادة تصميم حديثة لعلامة رقمية مع بنية واضحة.",
},
category: {
de: "Branding",
en: "Branding",
ar: "الهوية",
},
year: "2025",
},
@@ -40,14 +43,17 @@ export const portfolioItems: PortfolioItem[] = [
title: {
de: "Commerce Relaunch",
en: "Commerce Relaunch",
ar: "إعادة إطلاق المتجر",
},
summary: {
de: "Relaunch eines Shops mit Fokus auf Performance und Conversion.",
en: "Store relaunch focused on performance and conversion.",
ar: "إعادة إطلاق متجر مع تركيز على الأداء والتحويل.",
},
category: {
de: "E-Commerce",
en: "E-Commerce",
ar: "التجارة الإلكترونية",
},
year: "2024",
},
@@ -56,14 +62,17 @@ export const portfolioItems: PortfolioItem[] = [
title: {
de: "SaaS Dashboard",
en: "SaaS Dashboard",
ar: "لوحة تحكم SaaS",
},
summary: {
de: "Admin Dashboard fuer Teams mit klaren KPIs und Reports.",
en: "Admin dashboard for teams with clear KPIs and reports.",
ar: "لوحة تحكم إدارية للفرق مع مؤشرات وتقارير واضحة.",
},
category: {
de: "Web App",
en: "Web App",
ar: "تطبيق ويب",
},
year: "2024",
},
@@ -72,14 +81,17 @@ export const portfolioItems: PortfolioItem[] = [
title: {
de: "Campaign Site",
en: "Campaign Site",
ar: "موقع حملة",
},
summary: {
de: "Landing Seite fuer Produktkampagnen mit schneller Iteration.",
en: "Landing experience for product campaigns and quick iteration.",
ar: "صفحة هبوط لحملات المنتجات مع تنفيذ سريع.",
},
category: {
de: "Marketing",
en: "Marketing",
ar: "التسويق",
},
year: "2023",
},
@@ -91,18 +103,22 @@ export const productItems: ProductItem[] = [
name: {
de: "Starter Kit",
en: "Starter Kit",
ar: "Starter Kit",
},
summary: {
de: "Basis Paket fuer den schnellen Start von neuen Projekten.",
en: "Base package for launching new projects quickly.",
ar: "باقة أساسية لبدء المشاريع الجديدة بسرعة.",
},
segment: {
de: "Small Teams",
en: "Small Teams",
ar: "الفرق الصغيرة",
},
price: {
de: "ab 990 EUR",
en: "from 990 EUR",
ar: "ابتداءً من 990 EUR",
},
},
{
@@ -110,18 +126,22 @@ export const productItems: ProductItem[] = [
name: {
de: "Growth Kit",
en: "Growth Kit",
ar: "Growth Kit",
},
summary: {
de: "Skalierbares Paket fuer wachsende Produkte und Prozesse.",
en: "Scalable package for growing products and processes.",
ar: "باقة قابلة للتوسع للمنتجات والعمليات المتنامية.",
},
segment: {
de: "Scaleups",
en: "Scaleups",
ar: "الشركات المتوسعة",
},
price: {
de: "ab 2490 EUR",
en: "from 2490 EUR",
ar: "ابتداءً من 2490 EUR",
},
},
{
@@ -129,26 +149,26 @@ export const productItems: ProductItem[] = [
name: {
de: "Enterprise Kit",
en: "Enterprise Kit",
ar: "Enterprise Kit",
},
summary: {
de: "Massgeschneiderte Loesung fuer grosse Teams und komplexe Systeme.",
en: "Tailored solution for large teams and complex systems.",
ar: "حل مخصص للفرق الكبيرة والأنظمة المعقدة.",
},
segment: {
de: "Enterprise",
en: "Enterprise",
ar: "المؤسسات",
},
price: {
de: "auf Anfrage",
en: "on request",
ar: "عند الطلب",
},
},
];
export function resolveLocale(locale: string): AppLocale {
return locale === "en" ? "en" : "de";
}
export function pickText(text: LocalizedText, locale: AppLocale): string {
return text[locale];
}