Update homepage routes to include language and mode
Build & Deploy / Build & Push Docker Image (push) Successful in 2m51s
Test / Type Check (all packages) (push) Successful in 52s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Successful in 49s
Test / Homepage Unit Tests (push) Successful in 44s
Test / Storefront Unit Tests (push) Successful in 39s
Test / Admin Unit Tests (push) Successful in 40s
Test / Dashboard Unit Tests (push) Successful in 40s
Test / API Integration Tests (push) Successful in 1m0s

This commit is contained in:
root
2026-07-02 14:10:17 -04:00
parent 56c3bcf666
commit 781512399b
51 changed files with 792 additions and 344 deletions
+64 -8
View File
@@ -1,4 +1,9 @@
import rawManifest from '@/contracts/phase9/route-manifest.json';
import {
isThemePreference,
themePreferences,
type ThemePreference,
} from '@/lib/theme/config';
import { z } from 'zod';
export const locales = ['en', 'fr', 'ar'] as const;
@@ -7,6 +12,7 @@ export type Direction = 'ltr' | 'rtl';
export type RouteId = 'home' | 'sign-in' | 'forgot-password' | 'reset-password' | 'privacy' | 'terms' | 'accessibility';
export const defaultLocale: Locale = 'en';
export const defaultMode: ThemePreference = 'system';
export const localeCookie = 'hpc-locale';
export const localeStorageKey = 'hpc.locale';
export const localeCookieMaxAgeSeconds = 31_536_000;
@@ -53,12 +59,20 @@ export function getDirection(locale: Locale): Direction {
}
export function localizedPath(routeId: RouteId, locale: Locale): string {
return localizedModePath(routeId, locale, defaultMode);
}
export function localizedModePath(
routeId: RouteId,
locale: Locale,
mode: ThemePreference,
): string {
const route = routeManifest.routes.find((item) => item.id === routeId);
if (!route) {
throw new Error(`Unknown route ID: ${routeId}`);
}
const slug = route.slugs[locale];
return slug.length > 0 ? `/${locale}/${slug}` : `/${locale}`;
return slug.length > 0 ? `/${locale}/${mode}/${slug}` : `/${locale}/${mode}`;
}
export function routeById(routeId: RouteId) {
@@ -90,7 +104,10 @@ export function localeFromAcceptLanguage(headerValue: string | null): Locale {
}
export function localeSwitchUrl(current: URL, targetLocale: Locale, routeId: RouteId): URL {
const target = new URL(localizedPath(routeId, targetLocale), current.origin);
const target = new URL(
localizedModePath(routeId, targetLocale, modeFromPathname(current.pathname)),
current.origin,
);
for (const key of campaignQueryAllowlist) {
const value = current.searchParams.get(key);
if (value !== null) target.searchParams.set(key, value);
@@ -104,6 +121,19 @@ export function localeSwitchUrl(current: URL, targetLocale: Locale, routeId: Rou
return target;
}
export function modeSwitchUrl(current: URL, targetMode: ThemePreference, routeId: RouteId): URL {
const locale = localeFromPathname(current.pathname) ?? defaultLocale;
const target = new URL(localizedModePath(routeId, locale, targetMode), current.origin);
target.search = current.search;
const route = routeById(routeId);
const hash = current.hash.replace(/^#/, '');
if (routeManifest.switching.preserveValidHash && route.hashes.includes(hash)) {
target.hash = hash;
}
return target;
}
export function routeIdFromSlug(locale: Locale, slug: string): RouteId | null {
let decoded: string;
try {
@@ -131,24 +161,50 @@ export function routeIdFromAnyLocaleSlug(slug: string): RouteId | null {
}
export function routeIdFromPathname(pathname: string): RouteId | null {
const [localeValue, ...segments] = pathname.split('/').filter(Boolean);
const [localeValue, modeValue, ...segments] = pathname.split('/').filter(Boolean);
if (!isLocale(localeValue)) return null;
if (!isThemePreference(modeValue)) return null;
if (segments.length === 0) return 'home';
return routeIdFromSlug(localeValue, segments.join('/'));
}
export function localizedUrl(origin: URL, routeId: RouteId, locale: Locale): URL {
return new URL(localizedPath(routeId, locale), origin);
export function localeFromPathname(pathname: string): Locale | null {
const [localeValue] = pathname.split('/').filter(Boolean);
return isLocale(localeValue) ? localeValue : null;
}
export function hreflangMap(origin: URL, routeId: RouteId): Record<string, string> {
export function modeFromPathname(pathname: string): ThemePreference {
const [, modeValue] = pathname.split('/').filter(Boolean);
return isThemePreference(modeValue) ? modeValue : defaultMode;
}
export function isMode(value: unknown): value is ThemePreference {
return isThemePreference(value);
}
export function localizedUrl(
origin: URL,
routeId: RouteId,
locale: Locale,
mode: ThemePreference = defaultMode,
): URL {
return new URL(localizedModePath(routeId, locale, mode), origin);
}
export function hreflangMap(
origin: URL,
routeId: RouteId,
mode: ThemePreference = defaultMode,
): Record<string, string> {
const entries = locales.map((locale) => [
locale,
new URL(localizedPath(routeId, locale), origin).href,
new URL(localizedModePath(routeId, locale, mode), origin).href,
]);
entries.push([
'x-default',
new URL(localizedPath(routeId, routeManifest.hreflang.xDefaultLocale), origin).href,
new URL(localizedModePath(routeId, routeManifest.hreflang.xDefaultLocale, mode), origin).href,
]);
return Object.fromEntries(entries);
}
export { themePreferences as modes };
@@ -1,15 +1,18 @@
import {
hreflangMap,
localizedPath,
localizedModePath,
locales,
defaultMode,
type Locale,
type RouteId,
} from '@/lib/localization/config';
import type { ThemePreference } from '@/lib/theme/config';
import { getSiteOrigin } from '@/lib/localization/site-origin';
import type { Metadata } from 'next';
interface LocalizedMetadataInput {
locale: Locale;
mode?: ThemePreference;
routeId: RouteId;
title: string;
description: string;
@@ -18,6 +21,7 @@ interface LocalizedMetadataInput {
export function buildLocalizedMetadata({
locale,
mode = defaultMode,
routeId,
title,
description,
@@ -26,7 +30,7 @@ export function buildLocalizedMetadata({
const origin = getSiteOrigin();
const publicReleaseApproved = process.env.PUBLIC_RELEASE_APPROVED === 'true';
const canIndex = publicReleaseApproved && indexable;
const canonicalPath = localizedPath(routeId, locale);
const canonicalPath = localizedModePath(routeId, locale, mode);
return {
metadataBase: origin,
@@ -34,7 +38,7 @@ export function buildLocalizedMetadata({
description,
alternates: {
canonical: canonicalPath,
languages: hreflangMap(origin, routeId),
languages: hreflangMap(origin, routeId, mode),
},
openGraph: {
type: 'website',