diff --git a/src/App.test.tsx b/src/App.test.tsx
index 0818bef..f1bdf0a 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -5,21 +5,21 @@ import App from './App'
describe('App routing', () => {
it('renders the Home page at /', async () => {
render(
Braids by Mira
-Your hair, your crown
+Vos cheveux, votre couronne
Navigate
+Navigation
© {new Date().getFullYear()} Braids by Mira. All rights reserved.
+© {new Date().getFullYear()} Braids by Mira. Tous droits réservés.
) } diff --git a/src/components/InquiryModal.test.tsx b/src/components/InquiryModal.test.tsx index 85d7103..dc65bc4 100644 --- a/src/components/InquiryModal.test.tsx +++ b/src/components/InquiryModal.test.tsx @@ -12,24 +12,24 @@ const product: Product = { describe('InquiryModal', () => { it('shows a validation error when submitting without a name or valid email', async () => { render(Thanks — Mira will get back to you about {product.name} soon.
- +Merci — Mira vous recontactera bientôt au sujet de {product.name}.
+ > ) : ( )} diff --git a/src/components/Nav.test.tsx b/src/components/Nav.test.tsx index f614513..b6f8c12 100644 --- a/src/components/Nav.test.tsx +++ b/src/components/Nav.test.tsx @@ -6,10 +6,10 @@ import Nav from './Nav' describe('Nav', () => { it('renders links to all four routes', () => { render(${product.price}
+{product.price} $
{product.description}
- {!product.inStock &&Currently unavailable
} + {!product.inStock &&Actuellement indisponible
}{formatDuration(service.durationMinutes)}
-From ${service.priceFrom}
+À partir de {service.priceFrom} $
) } diff --git a/src/components/StepProgress.tsx b/src/components/StepProgress.tsx index ef9a145..4241df3 100644 --- a/src/components/StepProgress.tsx +++ b/src/components/StepProgress.tsx @@ -3,9 +3,9 @@ import styles from './StepProgress.module.css' const STEPS = [ { key: 'service', label: 'Service' }, - { key: 'date', label: 'Date & Time' }, + { key: 'date', label: 'Date et heure' }, { key: 'contact', label: 'Contact' }, - { key: 'confirmed', label: 'Confirmed' }, + { key: 'confirmed', label: 'Confirmé' }, ] as const interface StepProgressProps { diff --git a/src/data/products.ts b/src/data/products.ts index a5ba0d0..b52796b 100644 --- a/src/data/products.ts +++ b/src/data/products.ts @@ -9,16 +9,16 @@ export interface Product { } export const products: Product[] = [ - { id: 'edge-control', name: 'Silk Edge Control', category: 'hair-care', price: 12, image: '/products/edge-control.jpg', description: 'Long-lasting hold with a natural shine, no flaking.', inStock: true }, - { id: 'braid-oil', name: 'Nourishing Braid Oil', category: 'hair-care', price: 16, image: '/products/braid-oil.jpg', description: 'Lightweight scalp oil that soothes and adds shine between washes.', inStock: true }, - { id: 'leave-in', name: 'Detangling Leave-In', category: 'hair-care', price: 14, image: '/products/leave-in.jpg', description: 'Slip-rich leave-in conditioner for gentle detangling.', inStock: true }, - { id: 'kanekalon-black', name: 'Kanekalon Braiding Hair — Jet Black', category: 'extensions', price: 8, image: '/products/kanekalon-black.jpg', description: 'Premium synthetic braiding hair, soft texture, low shedding.', inStock: true }, - { id: 'kanekalon-brown', name: 'Kanekalon Braiding Hair — Chestnut', category: 'extensions', price: 8, image: '/products/kanekalon-brown.jpg', description: 'Warm brown tone, blends seamlessly for natural styles.', inStock: false }, - { id: 'locs-extension', name: 'Faux Locs Extensions', category: 'extensions', price: 22, image: '/products/locs-extension.jpg', description: 'Pre-looped faux locs for a natural, lightweight install.', inStock: true }, - { id: 'rat-tail-comb', name: 'Steel Rat-Tail Comb', category: 'tools', price: 6, image: '/products/rat-tail-comb.jpg', description: 'Precision parting comb with a fine metal tail.', inStock: true }, - { id: 'edge-brush', name: 'Mini Edge Brush', category: 'tools', price: 5, image: '/products/edge-brush.jpg', description: 'Dual-sided brush for laying edges and smoothing baby hairs.', inStock: true }, - { id: 'gold-cuffs', name: 'Gold Hair Cuffs (Set of 6)', category: 'accessories', price: 10, image: '/products/gold-cuffs.jpg', description: 'Adjustable cuffs to accessorize braids and locs.', inStock: true }, - { id: 'satin-bonnet', name: 'Satin Sleep Bonnet', category: 'accessories', price: 9, image: '/products/satin-bonnet.jpg', description: 'Protects braids and locs overnight, reduces frizz.', inStock: true }, + { id: 'edge-control', name: 'Gel Contour Soyeux', category: 'hair-care', price: 12, image: '/products/edge-control.jpg', description: 'Tenue longue durée avec un éclat naturel, sans effet blanc.', inStock: true }, + { id: 'braid-oil', name: 'Huile Nourrissante pour Tresses', category: 'hair-care', price: 16, image: '/products/braid-oil.jpg', description: "Huile légère pour le cuir chevelu qui apaise et ajoute de l'éclat entre les lavages.", inStock: true }, + { id: 'leave-in', name: 'Soin Démêlant Sans Rinçage', category: 'hair-care', price: 14, image: '/products/leave-in.jpg', description: "Après-shampooing sans rinçage riche en glisse pour un démêlage tout en douceur.", inStock: true }, + { id: 'kanekalon-black', name: 'Fibre à Tresser Kanekalon — Noir Jais', category: 'extensions', price: 8, image: '/products/kanekalon-black.jpg', description: 'Fibre synthétique haut de gamme pour tresses, texture douce, faible chute.', inStock: true }, + { id: 'kanekalon-brown', name: 'Fibre à Tresser Kanekalon — Châtain', category: 'extensions', price: 8, image: '/products/kanekalon-brown.jpg', description: 'Teinte brune chaude qui se fond parfaitement pour un rendu naturel.', inStock: false }, + { id: 'locs-extension', name: 'Extensions de Faux Locs', category: 'extensions', price: 22, image: '/products/locs-extension.jpg', description: 'Faux locs pré-bouclées pour une pose naturelle et légère.', inStock: true }, + { id: 'rat-tail-comb', name: 'Peigne à Tige en Acier', category: 'tools', price: 6, image: '/products/rat-tail-comb.jpg', description: 'Peigne de précision pour les raies, avec une fine tige métallique.', inStock: true }, + { id: 'edge-brush', name: 'Mini Brosse à Contours', category: 'tools', price: 5, image: '/products/edge-brush.jpg', description: 'Brosse double face pour lisser les contours et discipliner les cheveux follets.', inStock: true }, + { id: 'gold-cuffs', name: 'Anneaux Dorés pour Cheveux (Lot de 6)', category: 'accessories', price: 10, image: '/products/gold-cuffs.jpg', description: 'Anneaux ajustables pour accessoiriser tresses et locs.', inStock: true }, + { id: 'satin-bonnet', name: 'Bonnet de Nuit en Satin', category: 'accessories', price: 9, image: '/products/satin-bonnet.jpg', description: 'Protège tresses et locs pendant la nuit et réduit les frisottis.', inStock: true }, ] export function getProductsByCategory(category: Product['category'] | 'all'): Product[] { diff --git a/src/data/services.ts b/src/data/services.ts index 5808dbb..2c94e90 100644 --- a/src/data/services.ts +++ b/src/data/services.ts @@ -8,8 +8,8 @@ export interface Service { } export const services: Service[] = [ - { id: 'knotless', name: 'Knotless Braids', durationMinutes: 240, priceFrom: 150, description: 'Tension-free braids with a natural, seamless root.', image: '/styles/knotless.jpg' }, - { id: 'boho', name: 'Boho Braids', durationMinutes: 270, priceFrom: 180, description: 'Knotless braids with curly pieces woven in for a soft, romantic look.', image: '/styles/boho.jpg' }, - { id: 'locs', name: 'Faux Locs', durationMinutes: 300, priceFrom: 200, description: 'Lightweight, natural-looking locs installed with care.', image: '/styles/locs.jpg' }, - { id: 'cornrows', name: 'Cornrows', durationMinutes: 120, priceFrom: 90, description: 'Classic sleek cornrows, straight-back or with a custom design.', image: '/styles/cornrows.jpg' }, + { id: 'knotless', name: 'Tresses sans nœuds', durationMinutes: 240, priceFrom: 150, description: 'Tresses sans tension avec une racine naturelle et invisible.', image: '/styles/knotless.jpg' }, + { id: 'boho', name: 'Tresses Boho', durationMinutes: 270, priceFrom: 180, description: 'Tresses sans nœuds agrémentées de mèches bouclées pour un look doux et romantique.', image: '/styles/boho.jpg' }, + { id: 'locs', name: 'Faux Locs', durationMinutes: 300, priceFrom: 200, description: 'Locs légères à l\'allure naturelle, installées avec soin.', image: '/styles/locs.jpg' }, + { id: 'cornrows', name: 'Nattes Collées', durationMinutes: 120, priceFrom: 90, description: 'Nattes collées classiques et nettes, droites vers l\'arrière ou avec un motif personnalisé.', image: '/styles/cornrows.jpg' }, ] diff --git a/src/hooks/useBooking.ts b/src/hooks/useBooking.ts index 4c95d51..d88c859 100644 --- a/src/hooks/useBooking.ts +++ b/src/hooks/useBooking.ts @@ -53,9 +53,9 @@ export default function useBooking(initialServiceId?: string) { const submit = useCallback((): boolean => { const nextErrors: BookingErrors = {} - if (!state.contact.name.trim()) nextErrors.name = 'Name is required.' - if (!isValidEmail(state.contact.email)) nextErrors.email = 'A valid email is required.' - if (!state.contact.phone.trim()) nextErrors.phone = 'Phone is required.' + if (!state.contact.name.trim()) nextErrors.name = 'Le nom est requis.' + if (!isValidEmail(state.contact.email)) nextErrors.email = 'Une adresse courriel valide est requise.' + if (!state.contact.phone.trim()) nextErrors.phone = 'Le téléphone est requis.' setErrors(nextErrors) if (Object.keys(nextErrors).length > 0) { return false diff --git a/src/pages/Book.test.tsx b/src/pages/Book.test.tsx index 4aff6ea..1486d66 100644 --- a/src/pages/Book.test.tsx +++ b/src/pages/Book.test.tsx @@ -17,35 +17,35 @@ function renderBook(initialEntries: { pathname: string; state?: unknown }[]) { describe('Book page', () => { it('starts on the service step and advances to date after selecting a service', async () => { renderBook([{ pathname: '/book' }]) - expect(screen.getByText(/knotless braids/i)).toBeInTheDocument() - await userEvent.click(screen.getByRole('button', { name: /knotless braids/i })) - expect(await screen.findByLabelText(/choose a date/i)).toBeInTheDocument() + expect(screen.getByText(/tresses sans nœuds/i)).toBeInTheDocument() + await userEvent.click(screen.getByRole('button', { name: /tresses sans nœuds/i })) + expect(await screen.findByLabelText(/choisissez une date/i)).toBeInTheDocument() }) it('preselects the service from route state and skips to the date step', () => { renderBook([{ pathname: '/book', state: { serviceId: 'cornrows' } }]) - expect(screen.getByLabelText(/choose a date/i)).toBeInTheDocument() + expect(screen.getByLabelText(/choisissez une date/i)).toBeInTheDocument() }) it('walks through date -> slot -> contact -> confirmation', async () => { renderBook([{ pathname: '/book', state: { serviceId: 'cornrows' } }]) - await userEvent.type(screen.getByLabelText(/choose a date/i), '2026-08-11') + await userEvent.type(screen.getByLabelText(/choisissez une date/i), '2026-08-11') const slotButton = (await screen.findAllByRole('button', { name: /am|pm/i }))[0] await userEvent.click(slotButton) - await userEvent.type(await screen.findByLabelText(/^name$/i), 'Jane Doe') - await userEvent.type(screen.getByLabelText(/^email$/i), 'jane@example.com') - await userEvent.type(screen.getByLabelText(/^phone$/i), '5551234567') - await userEvent.click(screen.getByRole('button', { name: /confirm/i })) + await userEvent.type(await screen.findByLabelText(/^nom$/i), 'Jane Doe') + await userEvent.type(screen.getByLabelText(/^courriel$/i), 'jane@example.com') + await userEvent.type(screen.getByLabelText(/^téléphone$/i), '5551234567') + await userEvent.click(screen.getByRole('button', { name: /confirmer/i })) - expect(await screen.findByText(/mira will see you then/i)).toBeInTheDocument() + expect(await screen.findByText(/mira vous attend/i)).toBeInTheDocument() }) it('shows a friendly empty state with the next available date when a chosen date has none', async () => { renderBook([{ pathname: '/book', state: { serviceId: 'cornrows' } }]) // Sundays are fully closed in the mock availability generator - await userEvent.type(screen.getByLabelText(/choose a date/i), '2026-08-09') - expect(await screen.findByText(/no times available/i)).toBeInTheDocument() - expect(screen.getByRole('button', { name: /try .* instead/i })).toBeInTheDocument() + await userEvent.type(screen.getByLabelText(/choisissez une date/i), '2026-08-09') + expect(await screen.findByText(/aucun créneau disponible/i)).toBeInTheDocument() + expect(screen.getByRole('button', { name: /essayez .* à la place/i })).toBeInTheDocument() }) }) diff --git a/src/pages/Book.tsx b/src/pages/Book.tsx index 85b6e4b..5789ada 100644 --- a/src/pages/Book.tsx +++ b/src/pages/Book.tsx @@ -26,7 +26,7 @@ function Book() { return (- {selectedService?.name} on {booking.state.date} at {booking.state.slot?.time}. - Mira will see you then. + {selectedService?.name} le {booking.state.date} à {booking.state.slot?.time}. + Mira vous attend.
)} @@ -55,7 +55,7 @@ function Book() { {(booking.state.step === 'date' || booking.state.step === 'slot') && ( <>No times available on this date.
+Aucun créneau disponible à cette date.
{nextAvailable && ( )}Protective Styles by Mira
+Coiffures protectrices par Mira
- With over a decade behind the chair, Mira treats every install as an art form — - gentle on the scalp, precise at the root, and finished with a crown you'll want - to show off. + Avec plus d'une décennie d'expérience derrière le fauteuil, Mira aborde chaque + installation comme une œuvre d'art — douce pour le cuir chevelu, précise à la + racine, et achevée pour une couronne que vous aurez envie d'afficher.
Shop All
+Voir toute la boutique
@@ -96,9 +96,9 @@ function Home() {- Before You Book
+Avant de réserver
- Please take a moment to read the salon rules before booking your appointment. + Merci de prendre un instant pour lire les règlements du salon avant de réserver votre rendez-vous.
Please read through before booking your appointment.
+Merci de les lire avant de réserver votre rendez-vous.
{rules.map((rule, index) => (By booking, you agree to these terms.
- Book Now +En réservant, vous acceptez ces conditions.
+ Réserver maintenant