refactor: theme from lovable
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import type { Locale, Localized } from "@/types/sections";
|
||||
import { detectLocaleFromPath } from "./routes";
|
||||
|
||||
interface LocaleContextValue {
|
||||
locale: Locale;
|
||||
setLocale: (l: Locale) => void;
|
||||
t: <T>(value: Localized<T>) => T;
|
||||
}
|
||||
|
||||
const LocaleContext = createContext<LocaleContextValue | null>(null);
|
||||
|
||||
export function LocaleProvider({ children }: { children: ReactNode }) {
|
||||
const location = useLocation();
|
||||
const pathLocale = detectLocaleFromPath(location.pathname);
|
||||
|
||||
const [locale, setLocaleState] = useState<Locale>(pathLocale);
|
||||
|
||||
// Keep locale in sync with the URL (URL is the source of truth).
|
||||
useEffect(() => {
|
||||
if (pathLocale !== locale) setLocaleState(pathLocale);
|
||||
}, [pathLocale, locale]);
|
||||
|
||||
// Keep <html lang> attribute in sync.
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = locale === "fr" ? "fr-CA" : "en-CA";
|
||||
}, [locale]);
|
||||
|
||||
const value = useMemo<LocaleContextValue>(
|
||||
() => ({
|
||||
locale,
|
||||
setLocale: setLocaleState,
|
||||
t: <T,>(v: Localized<T>) => v[locale],
|
||||
}),
|
||||
[locale],
|
||||
);
|
||||
|
||||
return <LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>;
|
||||
}
|
||||
|
||||
export function useLocale() {
|
||||
const ctx = useContext(LocaleContext);
|
||||
if (!ctx) throw new Error("useLocale must be used within <LocaleProvider>");
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { Locale } from "@/types/sections";
|
||||
|
||||
/**
|
||||
* Page keys used internally. Each maps to FR and EN URL slugs.
|
||||
* The FR slug is the canonical one (no language prefix).
|
||||
* EN routes live under /en/<slug>.
|
||||
*/
|
||||
export type PageKey =
|
||||
| "home"
|
||||
| "municipality"
|
||||
| "mayor"
|
||||
| "council"
|
||||
| "services"
|
||||
| "documents"
|
||||
| "community"
|
||||
| "communityPhotos"
|
||||
| "activitiesPhotos"
|
||||
| "developmentPlan"
|
||||
| "townMap"
|
||||
| "businesses"
|
||||
| "events"
|
||||
| "publicNotices"
|
||||
| "minutes"
|
||||
| "newsletters";
|
||||
|
||||
export const ROUTES: Record<PageKey, { fr: string; en: string }> = {
|
||||
home: { fr: "/", en: "/en" },
|
||||
municipality: { fr: "/municipalite", en: "/en/municipality" },
|
||||
mayor: { fr: "/mot-du-maire", en: "/en/word-from-the-mayor" },
|
||||
council: { fr: "/conseil-municipal", en: "/en/our-council" },
|
||||
services: { fr: "/services-municipaux-et-publics", en: "/en/our-services" },
|
||||
documents: { fr: "/documents-importants", en: "/en/important-documents" },
|
||||
community: { fr: "/notre-communaute", en: "/en/our-community" },
|
||||
communityPhotos: { fr: "/album-photos-communaute", en: "/en/community-photo-album" },
|
||||
activitiesPhotos: { fr: "/album-photos-activites", en: "/en/activities-photo-album" },
|
||||
developmentPlan: { fr: "/plan-de-developpement", en: "/en/development-plan" },
|
||||
townMap: { fr: "/carte-de-la-ville", en: "/en/town-map" },
|
||||
businesses: { fr: "/entreprises-locales", en: "/en/local-businesses" },
|
||||
events: { fr: "/evenements", en: "/en/events" },
|
||||
publicNotices: { fr: "/avis-publics", en: "/en/public-notices" },
|
||||
minutes: { fr: "/proces-verbaux", en: "/en/minutes" },
|
||||
newsletters: { fr: "/bulletins", en: "/en/newsletters" },
|
||||
};
|
||||
|
||||
export function getPath(page: PageKey, locale: Locale): string {
|
||||
return ROUTES[page][locale];
|
||||
}
|
||||
|
||||
/** Given the current pathname, return the equivalent path in the target locale. */
|
||||
export function switchLocalePath(pathname: string, target: Locale): string {
|
||||
const normalized = pathname.replace(/\/+$/, "") || "/";
|
||||
for (const key of Object.keys(ROUTES) as PageKey[]) {
|
||||
const fr = ROUTES[key].fr;
|
||||
const en = ROUTES[key].en;
|
||||
if (normalized === fr || normalized === en) {
|
||||
return ROUTES[key][target];
|
||||
}
|
||||
}
|
||||
return target === "en" ? "/en" : "/";
|
||||
}
|
||||
|
||||
/** Detect the locale implied by a pathname (anything under /en/* or /en is EN). */
|
||||
export function detectLocaleFromPath(pathname: string): Locale {
|
||||
return pathname === "/en" || pathname.startsWith("/en/") ? "en" : "fr";
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { Locale } from "@/types/sections";
|
||||
|
||||
/**
|
||||
* UI strings (chrome): nav labels, footer, buttons. Page content lives in src/content/*.
|
||||
*/
|
||||
export const UI = {
|
||||
nav: {
|
||||
home: { fr: "Accueil", en: "Home" },
|
||||
municipality: { fr: "Municipalité", en: "Municipality" },
|
||||
mayor: { fr: "Mot du maire", en: "A Word from the Mayor" },
|
||||
council: { fr: "Conseil municipal", en: "Our Council" },
|
||||
services: { fr: "Services municipaux et publics",en: "Our Services" },
|
||||
documents: { fr: "Documents importants", en: "Important Documents" },
|
||||
community: { fr: "Notre communauté", en: "Our Community" },
|
||||
communityPhotos: { fr: "Album photos communauté", en: "Community Photo Album" },
|
||||
activitiesPhotos: { fr: "Album photos activités", en: "Activities Photo Album" },
|
||||
developmentPlan: { fr: "Plan de développement", en: "Development Plan" },
|
||||
townMap: { fr: "Carte de la ville", en: "Town Map" },
|
||||
businesses: { fr: "Entreprises locales", en: "Local Businesses" },
|
||||
events: { fr: "Événements", en: "Events" },
|
||||
publicNotices: { fr: "Avis publics", en: "Public Notices" },
|
||||
minutes: { fr: "Procès-verbaux", en: "Minutes" },
|
||||
newsletters: { fr: "Bulletins", en: "Newsletters" },
|
||||
},
|
||||
topbar: {
|
||||
home: { fr: "Accueil", en: "Home" },
|
||||
hours: { fr: "Mar. - Ven. 8 h à 16 h", en: "Tues. - Fri. 8:00 a.m. to 4:00 p.m." },
|
||||
},
|
||||
cta: {
|
||||
learnMore: { fr: "En savoir plus", en: "Learn more" },
|
||||
contact: { fr: "Nous contacter", en: "Contact us" },
|
||||
viewAll: { fr: "Tout voir", en: "View all" },
|
||||
send: { fr: "Envoyer", en: "Send" },
|
||||
download: { fr: "Télécharger", en: "Download" },
|
||||
openPdf: { fr: "Ouvrir le PDF", en: "Open PDF" },
|
||||
backHome: { fr: "Retour à l'accueil", en: "Back to home" },
|
||||
},
|
||||
form: {
|
||||
name: { fr: "Nom", en: "Name" },
|
||||
email: { fr: "Courriel", en: "Email" },
|
||||
subject: { fr: "Sujet", en: "Subject" },
|
||||
message: { fr: "Message", en: "Message" },
|
||||
soon: { fr: "L’envoi du formulaire sera activé prochainement. Vous pouvez nous joindre par courriel ou téléphone.",
|
||||
en: "Form submission will be enabled soon. You can reach us by email or phone in the meantime." },
|
||||
},
|
||||
footer: {
|
||||
address: { fr: "Adresse", en: "Address" },
|
||||
contact: { fr: "Coordonnées", en: "Contact" },
|
||||
hours: { fr: "Heures d’ouverture", en: "Office hours" },
|
||||
quick: { fr: "Liens rapides", en: "Quick links" },
|
||||
rights: { fr: "Tous droits réservés.", en: "All rights reserved." },
|
||||
},
|
||||
events: {
|
||||
upcoming: { fr: "À venir", en: "Upcoming" },
|
||||
},
|
||||
documents: {
|
||||
pdf: { fr: "Document PDF", en: "PDF document" },
|
||||
requestOnDemand: {
|
||||
fr: "Document disponible sur demande à la municipalité.",
|
||||
en: "Document available on request from the municipality.",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function t<T extends { fr: string; en: string }>(value: T, locale: Locale): string {
|
||||
return value[locale];
|
||||
}
|
||||
Reference in New Issue
Block a user