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`).
- Icons: `lucide-astro` inline SVGs; use `text-brand` for accent.
- 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.
## Testing Guidelines

View file

@ -1,11 +1,19 @@
// @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
export default defineConfig({
i18n: {
locales: ["fr", "en"],
defaultLocale: "fr",
routing: {
prefixDefaultLocale: true,
redirectToDefaultLocale: true,
},
},
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 =
| 'home'
| 'apartments'
| 'apartment_t2'
| 'apartment_t3'
| 'reviews'
| 'location'
| 'rates'
| 'contact'
| 'thank_you'
| 'terms'
| 'privacy'
| 'cancellation'
| 'house_rules';
| "home"
| "apartments"
| "apartment_t2"
| "apartment_t3"
| "reviews"
| "location"
| "rates"
| "contact"
| "thank_you"
| "terms"
| "privacy"
| "cancellation"
| "house_rules";
export const routes: Record<RouteKey, Record<Locale, string>> = {
home: { fr: '/fr/', en: '/en/' },
apartments: { fr: '/fr/appartements/', en: '/en/apartments/' },
apartment_t2: { fr: '/fr/appartements/t2-corail/', en: '/en/apartments/t2-corail/' },
apartment_t3: { fr: '/fr/appartements/t3-azur/', en: '/en/apartments/t3-azur/' },
reviews: { fr: '/fr/avis/', en: '/en/reviews/' },
location: { fr: '/fr/acces/', en: '/en/location-access/' },
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/' },
home: { fr: "/fr/", en: "/en/" },
apartments: { fr: "/fr/appartements/", en: "/en/apartments/" },
apartment_t2: {
fr: "/fr/appartements/t2-corail/",
en: "/en/apartments/t2-corail/",
},
apartment_t3: {
fr: "/fr/appartements/t3-azur/",
en: "/en/apartments/t3-azur/",
},
reviews: { fr: "/fr/avis/", en: "/en/reviews/" },
location: { fr: "/fr/acces/", en: "/en/location-access/" },
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 }> {
return locale === 'en'
return locale === "en"
? [
{ label: 'Apartments', href: routes.apartments.en },
{ label: 'Reviews', href: routes.reviews.en },
{ label: 'Location & Access', href: routes.location.en },
{ label: 'Rates', href: routes.rates.en },
{ label: "Apartments", href: routes.apartments.en },
{ label: "Reviews", href: routes.reviews.en },
{ label: "Location & Access", href: routes.location.en },
{ label: "Rates", href: routes.rates.en },
]
: [
{ label: 'Appartements', href: routes.apartments.fr },
{ label: 'Avis', href: routes.reviews.fr },
{ label: 'Accès', href: routes.location.fr },
{ label: 'Tarifs', href: routes.rates.fr },
{ label: "Appartements", href: routes.apartments.fr },
{ label: "Avis", href: routes.reviews.fr },
{ label: "Accès", href: routes.location.fr },
{ label: "Tarifs", href: routes.rates.fr },
];
}
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 { 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 pathname = Astro.url.pathname;
const altFr = siblingPath(pathname, 'fr');
const altEn = siblingPath(pathname, 'en');
const altFr = "/fr/"
const altEn = "/en/"
const nav = navFor(lang);
const ctaLabel = ctaLabelFor(lang);
const homeHref = hrefFor('home', lang);
const homeHref = getRelativeLocaleUrl(lang, '/');
---
<!doctype html>
<html lang={lang}>
@ -36,7 +36,7 @@ const homeHref = hrefFor('home', lang);
</button>
<div class="hidden md:flex items-center gap-4">
{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">
<a href={altFr} class={"hover:underline " + (lang==='fr' ? 'font-semibold text-brand-600' : '')}>FR</a>
<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 class="flex flex-col gap-3">
{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">
<span class="text-slate-500">Lang:</span>
<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">
{lang === 'en' ? (
<>
<li><a class="underline" href={hrefFor('terms','en')}>Terms</a></li>
<li><a class="underline" href={hrefFor('privacy','en')}>Privacy</a></li>
<li><a class="underline" href={hrefFor('cancellation','en')}>Cancellation</a></li>
<li><a class="underline" href={hrefFor('house_rules','en')}>House Rules</a></li>
<li><a class="underline" href={getRelativeLocaleUrl('en','/policies/terms/')}>Terms</a></li>
<li><a class="underline" href={getRelativeLocaleUrl('en','/policies/privacy/')}>Privacy</a></li>
<li><a class="underline" href={getRelativeLocaleUrl('en','/policies/cancellation/')}>Cancellation</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={hrefFor('privacy','fr')}>Confidentialité</a></li>
<li><a class="underline" href={hrefFor('cancellation','fr')}>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/conditions/')}>Conditions</a></li>
<li><a class="underline" href={getRelativeLocaleUrl('fr','/politiques/confidentialite/')}>Confidentialité</a></li>
<li><a class="underline" href={getRelativeLocaleUrl('fr','/politiques/annulation/')}>Annulation</a></li>
<li><a class="underline" href={getRelativeLocaleUrl('fr','/politiques/reglement/')}>Règlement intérieur</a></li>
</>
)}
</ul>

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { hrefFor } from '../../i18n/routes';
const thankHref = hrefFor('thank_you', 'en');
import { getRelativeLocaleUrl } from 'astro:i18n';
const thankHref = getRelativeLocaleUrl('en', '/thank-you/');
---
<BaseLayout title="Contact" lang="en">
<div class="mx-auto max-w-6xl px-6 py-10">
@ -46,7 +46,7 @@ const thankHref = hrefFor('thank_you', 'en');
</label>
<label class="md:col-span-2 flex items-start gap-2 text-sm">
<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>
<div class="md:col-span-2">
<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 { hrefFor } from '../../i18n/routes';
import { getRelativeLocaleUrl } from 'astro:i18n';
import { Umbrella, Car, Wifi, Snowflake, Utensils, Home as HomeIcon, Users, Baby } from 'lucide-astro';
const title = 'Home';
const contactHref = hrefFor('contact', 'en');
const t2Href = hrefFor('apartment_t2', 'en');
const t3Href = hrefFor('apartment_t3', 'en');
const contactHref = getRelativeLocaleUrl('en', '/contact/');
const t2Href = getRelativeLocaleUrl('en','/apartments/t2-corail/');
const t3Href = getRelativeLocaleUrl('en','/apartments/t3-azur/');
---
<BaseLayout title={title} lang="en" description="Comfortable stays in Le Gosier for couples and small families">
<section class="relative h-[70vh] md:h-[80vh]">

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { hrefFor } from '../../i18n/routes';
const thankHref = hrefFor('thank_you', 'fr');
import { getRelativeLocaleUrl } from 'astro:i18n';
const thankHref = getRelativeLocaleUrl('fr', '/merci/');
---
<BaseLayout title="Contact" lang="fr">
<div class="mx-auto max-w-6xl px-6 py-10">
@ -46,7 +46,7 @@ const thankHref = hrefFor('thank_you', 'fr');
</label>
<label class="md:col-span-2 flex items-start gap-2 text-sm">
<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>
<div class="md:col-span-2">
<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 { hrefFor } from '../../i18n/routes';
import { getRelativeLocaleUrl } from 'astro:i18n';
import { Umbrella, Car, Wifi, Snowflake, Utensils, Home as HomeIcon, Users, Baby } from 'lucide-astro';
const title = 'Accueil';
const contactHref = hrefFor('contact', 'fr');
const t2Href = hrefFor('apartment_t2', 'fr');
const t3Href = hrefFor('apartment_t3', 'fr');
const contactHref = getRelativeLocaleUrl('fr', '/contact/');
const t2Href = getRelativeLocaleUrl('fr','/appartements/t2-corail/');
const t3Href = getRelativeLocaleUrl('fr','/appartements/t3-azur/');
---
<BaseLayout title={title} lang="fr" description="Séjours confortables au Gosier pour couples et petites familles">
<section class="relative h-[70vh] md:h-[80vh]">

View file

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