refactor(i18n): replace hrefFor usages with astro:i18n getRelativeLocaleUrl; update config and docs

This commit is contained in:
Ruidy 2025-09-06 14:23:58 -04:00
parent 2e9aa1dc31
commit 7613249b56
16 changed files with 110 additions and 114 deletions

View file

@ -20,7 +20,7 @@
- UI: Tailwind CSS v4 (brand tokens: `--color-brand`, `--color-brand-600`). - UI: Tailwind CSS v4 (brand tokens: `--color-brand`, `--color-brand-600`).
- Icons: `lucide-astro` inline SVGs; use `text-brand` for accent. - Icons: `lucide-astro` inline SVGs; use `text-brand` for accent.
- JS: vanilla only (no jQuery). Prefer lightweight patterns (e.g., scrollBy carousels). - JS: vanilla only (no jQuery). Prefer lightweight patterns (e.g., scrollBy carousels).
- i18n: use `hrefFor()` and `siblingPath()` for all internal links and language toggles. - i18n: prefer `getRelativeLocaleUrl()` from `astro:i18n` for links; use `siblingPath()` for toggles when slugs differ.
- Content slugs differ by locale (e.g., FR `avis` ↔ EN `reviews`) — never hardcode. - Content slugs differ by locale (e.g., FR `avis` ↔ EN `reviews`) — never hardcode.
## Testing Guidelines ## Testing Guidelines

View file

@ -1,11 +1,19 @@
// @ts-check // @ts-check
import { defineConfig } from 'astro/config'; import { defineConfig } from "astro/config";
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from "@tailwindcss/vite";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
i18n: {
locales: ["fr", "en"],
defaultLocale: "fr",
routing: {
prefixDefaultLocale: true,
redirectToDefaultLocale: true,
},
},
vite: { vite: {
plugins: [tailwindcss()] plugins: [tailwindcss()],
} },
}); });

1
dist/index.html vendored
View file

@ -0,0 +1 @@
<!doctype html><title>Redirecting to: /fr</title><meta http-equiv="refresh" content="0;url=/fr"><meta name="robots" content="noindex"><link rel="canonical" href="/fr"><body> <a href="/fr">Redirecting from <code>/</code> to <code>/fr</code></a></body>

View file

@ -1,78 +1,67 @@
export type Locale = 'fr' | 'en'; export type Locale = "fr" | "en";
export type RouteKey = export type RouteKey =
| 'home' | "home"
| 'apartments' | "apartments"
| 'apartment_t2' | "apartment_t2"
| 'apartment_t3' | "apartment_t3"
| 'reviews' | "reviews"
| 'location' | "location"
| 'rates' | "rates"
| 'contact' | "contact"
| 'thank_you' | "thank_you"
| 'terms' | "terms"
| 'privacy' | "privacy"
| 'cancellation' | "cancellation"
| 'house_rules'; | "house_rules";
export const routes: Record<RouteKey, Record<Locale, string>> = { export const routes: Record<RouteKey, Record<Locale, string>> = {
home: { fr: '/fr/', en: '/en/' }, home: { fr: "/fr/", en: "/en/" },
apartments: { fr: '/fr/appartements/', en: '/en/apartments/' }, apartments: { fr: "/fr/appartements/", en: "/en/apartments/" },
apartment_t2: { fr: '/fr/appartements/t2-corail/', en: '/en/apartments/t2-corail/' }, apartment_t2: {
apartment_t3: { fr: '/fr/appartements/t3-azur/', en: '/en/apartments/t3-azur/' }, fr: "/fr/appartements/t2-corail/",
reviews: { fr: '/fr/avis/', en: '/en/reviews/' }, en: "/en/apartments/t2-corail/",
location: { fr: '/fr/acces/', en: '/en/location-access/' }, },
rates: { fr: '/fr/tarifs-disponibilites/', en: '/en/rates-availability/' }, apartment_t3: {
contact: { fr: '/fr/contact/', en: '/en/contact/' }, fr: "/fr/appartements/t3-azur/",
thank_you: { fr: '/fr/merci/', en: '/en/thank-you/' }, en: "/en/apartments/t3-azur/",
terms: { fr: '/fr/politiques/conditions/', en: '/en/policies/terms/' }, },
privacy: { fr: '/fr/politiques/confidentialite/', en: '/en/policies/privacy/' }, reviews: { fr: "/fr/avis/", en: "/en/reviews/" },
cancellation: { fr: '/fr/politiques/annulation/', en: '/en/policies/cancellation/' }, location: { fr: "/fr/acces/", en: "/en/location-access/" },
house_rules: { fr: '/fr/politiques/reglement/', en: '/en/policies/house-rules/' }, rates: { fr: "/fr/tarifs-disponibilites/", en: "/en/rates-availability/" },
contact: { fr: "/fr/contact/", en: "/en/contact/" },
thank_you: { fr: "/fr/merci/", en: "/en/thank-you/" },
terms: { fr: "/fr/politiques/conditions/", en: "/en/policies/terms/" },
privacy: {
fr: "/fr/politiques/confidentialite/",
en: "/en/policies/privacy/",
},
cancellation: {
fr: "/fr/politiques/annulation/",
en: "/en/policies/cancellation/",
},
house_rules: {
fr: "/fr/politiques/reglement/",
en: "/en/policies/house-rules/",
},
}; };
const normalize = (p: string) => {
if (!p) return '/';
let out = p.startsWith('/') ? p : `/${p}`;
if (!out.endsWith('/')) out = `${out}/`;
return out;
};
export function keyForPath(pathname: string): RouteKey | null {
const n = normalize(pathname);
for (const key of Object.keys(routes) as RouteKey[]) {
const localizations = routes[key];
if (n === localizations.fr || n === localizations.en) return key;
}
return null;
}
export function siblingPath(pathname: string, locale: Locale): string {
const key = keyForPath(pathname);
if (key) return routes[key][locale];
return routes.home[locale];
}
export function hrefFor(key: RouteKey, locale: Locale): string {
return routes[key][locale];
}
export function navFor(locale: Locale): Array<{ label: string; href: string }> { export function navFor(locale: Locale): Array<{ label: string; href: string }> {
return locale === 'en' return locale === "en"
? [ ? [
{ label: 'Apartments', href: routes.apartments.en }, { label: "Apartments", href: routes.apartments.en },
{ label: 'Reviews', href: routes.reviews.en }, { label: "Reviews", href: routes.reviews.en },
{ label: 'Location & Access', href: routes.location.en }, { label: "Location & Access", href: routes.location.en },
{ label: 'Rates', href: routes.rates.en }, { label: "Rates", href: routes.rates.en },
] ]
: [ : [
{ label: 'Appartements', href: routes.apartments.fr }, { label: "Appartements", href: routes.apartments.fr },
{ label: 'Avis', href: routes.reviews.fr }, { label: "Avis", href: routes.reviews.fr },
{ label: 'Accès', href: routes.location.fr }, { label: "Accès", href: routes.location.fr },
{ label: 'Tarifs', href: routes.rates.fr }, { label: "Tarifs", href: routes.rates.fr },
]; ];
} }
export function ctaLabelFor(locale: Locale): string { export function ctaLabelFor(locale: Locale): string {
return locale === 'en' ? 'Send a Request' : 'Envoyer une demande'; return locale === "en" ? "Send a Request" : "Envoyer une demande";
} }

View file

@ -1,13 +1,13 @@
--- ---
import '../styles/global.css'; import '../styles/global.css';
import { siblingPath, hrefFor, navFor, ctaLabelFor } from '../i18n/routes'; import { navFor, ctaLabelFor } from '../i18n/routes';
import { getRelativeLocaleUrl } from 'astro:i18n';
const { title = 'VillaFleurie', lang = 'fr', description = 'Séjours confortables au Gosier pour couples et petites familles' } = Astro.props; const { title = 'VillaFleurie', lang = 'fr', description = 'Séjours confortables au Gosier pour couples et petites familles' } = Astro.props;
const pathname = Astro.url.pathname; const altFr = "/fr/"
const altFr = siblingPath(pathname, 'fr'); const altEn = "/en/"
const altEn = siblingPath(pathname, 'en');
const nav = navFor(lang); const nav = navFor(lang);
const ctaLabel = ctaLabelFor(lang); const ctaLabel = ctaLabelFor(lang);
const homeHref = hrefFor('home', lang); const homeHref = getRelativeLocaleUrl(lang, '/');
--- ---
<!doctype html> <!doctype html>
<html lang={lang}> <html lang={lang}>
@ -36,7 +36,7 @@ const homeHref = hrefFor('home', lang);
</button> </button>
<div class="hidden md:flex items-center gap-4"> <div class="hidden md:flex items-center gap-4">
{nav.map((n) => <a href={n.href} class="text-sm hover:underline">{n.label}</a>)} {nav.map((n) => <a href={n.href} class="text-sm hover:underline">{n.label}</a>)}
<a href={hrefFor('contact', lang)} class="inline-flex items-center rounded-lg bg-brand px-3 py-2 text-white hover:bg-brand-600">{ctaLabel}</a> <a href={getRelativeLocaleUrl(lang, '/contact/')} class="inline-flex items-center rounded-lg bg-brand px-3 py-2 text-white hover:bg-brand-600">{ctaLabel}</a>
<div class="ml-2 flex items-center gap-2 text-xs"> <div class="ml-2 flex items-center gap-2 text-xs">
<a href={altFr} class={"hover:underline " + (lang==='fr' ? 'font-semibold text-brand-600' : '')}>FR</a> <a href={altFr} class={"hover:underline " + (lang==='fr' ? 'font-semibold text-brand-600' : '')}>FR</a>
<span class="text-slate-400">|</span> <span class="text-slate-400">|</span>
@ -48,7 +48,7 @@ const homeHref = hrefFor('home', lang);
<div id="mobile-menu" class="mt-3 hidden md:hidden border-t border-slate-200 pt-3"> <div id="mobile-menu" class="mt-3 hidden md:hidden border-t border-slate-200 pt-3">
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
{nav.map((n) => <a href={n.href} class="text-base">{n.label}</a>)} {nav.map((n) => <a href={n.href} class="text-base">{n.label}</a>)}
<a href={hrefFor('contact', lang)} class="inline-flex items-center justify-center rounded-lg bg-brand px-4 py-2 text-white hover:bg-brand-600">{ctaLabel}</a> <a href={getRelativeLocaleUrl(lang, '/contact/')} class="inline-flex items-center justify-center rounded-lg bg-brand px-4 py-2 text-white hover:bg-brand-600">{ctaLabel}</a>
<div class="flex items-center gap-2 text-sm"> <div class="flex items-center gap-2 text-sm">
<span class="text-slate-500">Lang:</span> <span class="text-slate-500">Lang:</span>
<a href={altFr} class={"hover:underline " + (lang==='fr' ? 'font-semibold text-brand-600' : '')}>FR</a> <a href={altFr} class={"hover:underline " + (lang==='fr' ? 'font-semibold text-brand-600' : '')}>FR</a>
@ -71,17 +71,17 @@ const homeHref = hrefFor('home', lang);
<ul class="flex gap-4"> <ul class="flex gap-4">
{lang === 'en' ? ( {lang === 'en' ? (
<> <>
<li><a class="underline" href={hrefFor('terms','en')}>Terms</a></li> <li><a class="underline" href={getRelativeLocaleUrl('en','/policies/terms/')}>Terms</a></li>
<li><a class="underline" href={hrefFor('privacy','en')}>Privacy</a></li> <li><a class="underline" href={getRelativeLocaleUrl('en','/policies/privacy/')}>Privacy</a></li>
<li><a class="underline" href={hrefFor('cancellation','en')}>Cancellation</a></li> <li><a class="underline" href={getRelativeLocaleUrl('en','/policies/cancellation/')}>Cancellation</a></li>
<li><a class="underline" href={hrefFor('house_rules','en')}>House Rules</a></li> <li><a class="underline" href={getRelativeLocaleUrl('en','/policies/house-rules/')}>House Rules</a></li>
</> </>
) : ( ) : (
<> <>
<li><a class="underline" href={hrefFor('terms','fr')}>Conditions</a></li> <li><a class="underline" href={getRelativeLocaleUrl('fr','/politiques/conditions/')}>Conditions</a></li>
<li><a class="underline" href={hrefFor('privacy','fr')}>Confidentialité</a></li> <li><a class="underline" href={getRelativeLocaleUrl('fr','/politiques/confidentialite/')}>Confidentialité</a></li>
<li><a class="underline" href={hrefFor('cancellation','fr')}>Annulation</a></li> <li><a class="underline" href={getRelativeLocaleUrl('fr','/politiques/annulation/')}>Annulation</a></li>
<li><a class="underline" href={hrefFor('house_rules','fr')}>Règlement intérieur</a></li> <li><a class="underline" href={getRelativeLocaleUrl('fr','/politiques/reglement/')}>Règlement intérieur</a></li>
</> </>
)} )}
</ul> </ul>

View file

@ -1,8 +1,8 @@
--- ---
import BaseLayout from '../../../layouts/BaseLayout.astro'; import BaseLayout from '../../../layouts/BaseLayout.astro';
import { hrefFor } from '../../../i18n/routes'; import { getRelativeLocaleUrl } from 'astro:i18n';
const t2Href = hrefFor('apartment_t2', 'en'); const t2Href = getRelativeLocaleUrl('en','/apartments/t2-corail/');
const t3Href = hrefFor('apartment_t3', 'en'); const t3Href = getRelativeLocaleUrl('en','/apartments/t3-azur/');
--- ---
<BaseLayout title="Apartments" lang="en"> <BaseLayout title="Apartments" lang="en">
<div class="mx-auto max-w-6xl px-6 py-10"> <div class="mx-auto max-w-6xl px-6 py-10">

View file

@ -1,7 +1,7 @@
--- ---
import BaseLayout from '../../../layouts/BaseLayout.astro'; import BaseLayout from '../../../layouts/BaseLayout.astro';
import { hrefFor } from '../../../i18n/routes'; import { getRelativeLocaleUrl } from 'astro:i18n';
const contactHref = hrefFor('contact', 'en'); const contactHref = getRelativeLocaleUrl('en','/contact/');
--- ---
<BaseLayout title="T2 Corail" lang="en"> <BaseLayout title="T2 Corail" lang="en">
<div class="mx-auto max-w-6xl px-6 py-10"> <div class="mx-auto max-w-6xl px-6 py-10">

View file

@ -1,7 +1,7 @@
--- ---
import BaseLayout from '../../../layouts/BaseLayout.astro'; import BaseLayout from '../../../layouts/BaseLayout.astro';
import { hrefFor } from '../../../i18n/routes'; import { getRelativeLocaleUrl } from 'astro:i18n';
const contactHref = hrefFor('contact', 'en'); const contactHref = getRelativeLocaleUrl('en','/contact/');
--- ---
<BaseLayout title="T3 Azur" lang="en"> <BaseLayout title="T3 Azur" lang="en">
<div class="mx-auto max-w-6xl px-6 py-10"> <div class="mx-auto max-w-6xl px-6 py-10">

View file

@ -1,7 +1,7 @@
--- ---
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import { hrefFor } from '../../i18n/routes'; import { getRelativeLocaleUrl } from 'astro:i18n';
const thankHref = hrefFor('thank_you', 'en'); const thankHref = getRelativeLocaleUrl('en', '/thank-you/');
--- ---
<BaseLayout title="Contact" lang="en"> <BaseLayout title="Contact" lang="en">
<div class="mx-auto max-w-6xl px-6 py-10"> <div class="mx-auto max-w-6xl px-6 py-10">
@ -46,7 +46,7 @@ const thankHref = hrefFor('thank_you', 'en');
</label> </label>
<label class="md:col-span-2 flex items-start gap-2 text-sm"> <label class="md:col-span-2 flex items-start gap-2 text-sm">
<input type="checkbox" required name="consent_privacy" /> <input type="checkbox" required name="consent_privacy" />
<span>I agree to the use of my data to process my request in accordance with the <a class="underline" href={hrefFor('privacy','en')}>Privacy Policy</a>.</span> <span>I agree to the use of my data to process my request in accordance with the <a class="underline" href={getRelativeLocaleUrl('en','/policies/privacy/')}>Privacy Policy</a>.</span>
</label> </label>
<div class="md:col-span-2"> <div class="md:col-span-2">
<button class="rounded-lg bg-brand px-4 py-2 text-white hover:bg-brand-600">Send</button> <button class="rounded-lg bg-brand px-4 py-2 text-white hover:bg-brand-600">Send</button>

View file

@ -1,11 +1,11 @@
--- ---
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import { hrefFor } from '../../i18n/routes'; import { getRelativeLocaleUrl } from 'astro:i18n';
import { Umbrella, Car, Wifi, Snowflake, Utensils, Home as HomeIcon, Users, Baby } from 'lucide-astro'; import { Umbrella, Car, Wifi, Snowflake, Utensils, Home as HomeIcon, Users, Baby } from 'lucide-astro';
const title = 'Home'; const title = 'Home';
const contactHref = hrefFor('contact', 'en'); const contactHref = getRelativeLocaleUrl('en', '/contact/');
const t2Href = hrefFor('apartment_t2', 'en'); const t2Href = getRelativeLocaleUrl('en','/apartments/t2-corail/');
const t3Href = hrefFor('apartment_t3', 'en'); const t3Href = getRelativeLocaleUrl('en','/apartments/t3-azur/');
--- ---
<BaseLayout title={title} lang="en" description="Comfortable stays in Le Gosier for couples and small families"> <BaseLayout title={title} lang="en" description="Comfortable stays in Le Gosier for couples and small families">
<section class="relative h-[70vh] md:h-[80vh]"> <section class="relative h-[70vh] md:h-[80vh]">

View file

@ -1,8 +1,8 @@
--- ---
import BaseLayout from '../../../layouts/BaseLayout.astro'; import BaseLayout from '../../../layouts/BaseLayout.astro';
import { hrefFor } from '../../../i18n/routes'; import { getRelativeLocaleUrl } from 'astro:i18n';
const t2Href = hrefFor('apartment_t2', 'fr'); const t2Href = getRelativeLocaleUrl('fr','/appartements/t2-corail/');
const t3Href = hrefFor('apartment_t3', 'fr'); const t3Href = getRelativeLocaleUrl('fr','/appartements/t3-azur/');
--- ---
<BaseLayout title="Appartements" lang="fr"> <BaseLayout title="Appartements" lang="fr">
<div class="mx-auto max-w-6xl px-6 py-10"> <div class="mx-auto max-w-6xl px-6 py-10">

View file

@ -1,7 +1,7 @@
--- ---
import BaseLayout from '../../../layouts/BaseLayout.astro'; import BaseLayout from '../../../layouts/BaseLayout.astro';
import { hrefFor } from '../../../i18n/routes'; import { getRelativeLocaleUrl } from 'astro:i18n';
const contactHref = hrefFor('contact', 'fr'); const contactHref = getRelativeLocaleUrl('fr','/contact/');
--- ---
<BaseLayout title="T2 Corail" lang="fr"> <BaseLayout title="T2 Corail" lang="fr">
<div class="mx-auto max-w-6xl px-6 py-10"> <div class="mx-auto max-w-6xl px-6 py-10">

View file

@ -1,7 +1,7 @@
--- ---
import BaseLayout from '../../../layouts/BaseLayout.astro'; import BaseLayout from '../../../layouts/BaseLayout.astro';
import { hrefFor } from '../../../i18n/routes'; import { getRelativeLocaleUrl } from 'astro:i18n';
const contactHref = hrefFor('contact', 'fr'); const contactHref = getRelativeLocaleUrl('fr','/contact/');
--- ---
<BaseLayout title="T3 Azur" lang="fr"> <BaseLayout title="T3 Azur" lang="fr">
<div class="mx-auto max-w-6xl px-6 py-10"> <div class="mx-auto max-w-6xl px-6 py-10">

View file

@ -1,7 +1,7 @@
--- ---
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import { hrefFor } from '../../i18n/routes'; import { getRelativeLocaleUrl } from 'astro:i18n';
const thankHref = hrefFor('thank_you', 'fr'); const thankHref = getRelativeLocaleUrl('fr', '/merci/');
--- ---
<BaseLayout title="Contact" lang="fr"> <BaseLayout title="Contact" lang="fr">
<div class="mx-auto max-w-6xl px-6 py-10"> <div class="mx-auto max-w-6xl px-6 py-10">
@ -46,7 +46,7 @@ const thankHref = hrefFor('thank_you', 'fr');
</label> </label>
<label class="md:col-span-2 flex items-start gap-2 text-sm"> <label class="md:col-span-2 flex items-start gap-2 text-sm">
<input type="checkbox" required name="consent_privacy" /> <input type="checkbox" required name="consent_privacy" />
<span>Jaccepte que mes données soient utilisées pour traiter ma demande conformément à la <a class="underline" href={hrefFor('privacy','fr')}>Politique de Confidentialité</a>.</span> <span>Jaccepte que mes données soient utilisées pour traiter ma demande conformément à la <a class="underline" href={getRelativeLocaleUrl('fr','/politiques/confidentialite/')}>Politique de Confidentialité</a>.</span>
</label> </label>
<div class="md:col-span-2"> <div class="md:col-span-2">
<button class="rounded-lg bg-brand px-4 py-2 text-white hover:bg-brand-600">Envoyer</button> <button class="rounded-lg bg-brand px-4 py-2 text-white hover:bg-brand-600">Envoyer</button>

View file

@ -1,11 +1,11 @@
--- ---
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import { hrefFor } from '../../i18n/routes'; import { getRelativeLocaleUrl } from 'astro:i18n';
import { Umbrella, Car, Wifi, Snowflake, Utensils, Home as HomeIcon, Users, Baby } from 'lucide-astro'; import { Umbrella, Car, Wifi, Snowflake, Utensils, Home as HomeIcon, Users, Baby } from 'lucide-astro';
const title = 'Accueil'; const title = 'Accueil';
const contactHref = hrefFor('contact', 'fr'); const contactHref = getRelativeLocaleUrl('fr', '/contact/');
const t2Href = hrefFor('apartment_t2', 'fr'); const t2Href = getRelativeLocaleUrl('fr','/appartements/t2-corail/');
const t3Href = hrefFor('apartment_t3', 'fr'); const t3Href = getRelativeLocaleUrl('fr','/appartements/t3-azur/');
--- ---
<BaseLayout title={title} lang="fr" description="Séjours confortables au Gosier pour couples et petites familles"> <BaseLayout title={title} lang="fr" description="Séjours confortables au Gosier pour couples et petites familles">
<section class="relative h-[70vh] md:h-[80vh]"> <section class="relative h-[70vh] md:h-[80vh]">

View file

@ -1,5 +1,3 @@
--- ---
// Redirect root to FR default
Astro.redirect('/fr/');
--- ---