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
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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user