From 3eb361f57f4adad702df1ac7d11b4e0931440db4 Mon Sep 17 00:00:00 2001 From: belviskhoremk Date: Wed, 12 Aug 2026 18:51:53 +0200 Subject: [PATCH] feat: translate admin panel to French Translates all user-visible text in the admin panel (login, nav, products, services, schedule, bookings, inquiries, and the shared friendlyError helper) to plain, direct French for Mira's internal use. Database CHECK-constraint enum values (product category, booking/inquiry status) and column names are left untouched in code; French display labels are added via lookup maps instead. Corresponding test assertions updated to match. --- src/lib/friendlyError.test.ts | 2 +- src/lib/friendlyError.ts | 2 +- src/pages/admin/AdminApp.test.tsx | 10 +++--- src/pages/admin/AdminBookings.test.tsx | 8 ++--- src/pages/admin/AdminBookings.tsx | 22 +++++++----- src/pages/admin/AdminInquiries.test.tsx | 2 +- src/pages/admin/AdminInquiries.tsx | 15 ++++++--- src/pages/admin/AdminLayout.test.tsx | 12 +++---- src/pages/admin/AdminLayout.tsx | 10 +++--- src/pages/admin/AdminLogin.test.tsx | 14 ++++---- src/pages/admin/AdminLogin.tsx | 10 +++--- src/pages/admin/AdminProducts.test.tsx | 8 ++--- src/pages/admin/AdminProducts.tsx | 45 ++++++++++++++----------- src/pages/admin/AdminSchedule.test.tsx | 6 ++-- src/pages/admin/AdminSchedule.tsx | 26 +++++++------- src/pages/admin/AdminServices.test.tsx | 10 +++--- src/pages/admin/AdminServices.tsx | 26 +++++++------- 17 files changed, 123 insertions(+), 105 deletions(-) diff --git a/src/lib/friendlyError.test.ts b/src/lib/friendlyError.test.ts index fa0f368..e0ae685 100644 --- a/src/lib/friendlyError.test.ts +++ b/src/lib/friendlyError.test.ts @@ -9,7 +9,7 @@ describe('friendlyError', () => { message: 'update or delete on table "products" violates foreign key constraint "inquiries_product_id_fkey" on table "inquiries"', }) - ).toBe("This can't be deleted because it's referenced by an existing booking or inquiry.") + ).toBe('Impossible de supprimer cet élément : il est référencé par une réservation ou une demande existante.') }) it('falls back to the raw message for any other error', () => { diff --git a/src/lib/friendlyError.ts b/src/lib/friendlyError.ts index 5d7adda..40e96f4 100644 --- a/src/lib/friendlyError.ts +++ b/src/lib/friendlyError.ts @@ -8,7 +8,7 @@ const FOREIGN_KEY_VIOLATION = '23503' export function friendlyError(error: { code?: string; message: string }): string { if (error.code === FOREIGN_KEY_VIOLATION) { - return "This can't be deleted because it's referenced by an existing booking or inquiry." + return "Impossible de supprimer cet élément : il est référencé par une réservation ou une demande existante." } return error.message } diff --git a/src/pages/admin/AdminApp.test.tsx b/src/pages/admin/AdminApp.test.tsx index ed09032..c164bad 100644 --- a/src/pages/admin/AdminApp.test.tsx +++ b/src/pages/admin/AdminApp.test.tsx @@ -25,7 +25,7 @@ describe('AdminApp routing', () => { , ) - expect(screen.getByRole('heading', { name: /admin login/i })).toBeInTheDocument() + expect(screen.getByRole('heading', { name: /connexion administrateur/i })).toBeInTheDocument() }) it('renders the products page (with layout) at /admin/products when authenticated', async () => { @@ -36,8 +36,8 @@ describe('AdminApp routing', () => { , ) - expect(await screen.findByRole('heading', { name: /^products$/i })).toBeInTheDocument() - expect(screen.getByRole('link', { name: /bookings/i })).toBeInTheDocument() + expect(await screen.findByRole('heading', { name: /^produits$/i })).toBeInTheDocument() + expect(screen.getByRole('link', { name: /réservations/i })).toBeInTheDocument() }) it('redirects /admin to /admin/products when authenticated', async () => { @@ -48,7 +48,7 @@ describe('AdminApp routing', () => { , ) - expect(await screen.findByRole('heading', { name: /^products$/i })).toBeInTheDocument() + expect(await screen.findByRole('heading', { name: /^produits$/i })).toBeInTheDocument() }) it('redirects an unmatched /admin/* path to /admin/products when authenticated', async () => { @@ -59,6 +59,6 @@ describe('AdminApp routing', () => { , ) - expect(await screen.findByRole('heading', { name: /^products$/i })).toBeInTheDocument() + expect(await screen.findByRole('heading', { name: /^produits$/i })).toBeInTheDocument() }) }) diff --git a/src/pages/admin/AdminBookings.test.tsx b/src/pages/admin/AdminBookings.test.tsx index 14f8fc8..f8185c8 100644 --- a/src/pages/admin/AdminBookings.test.tsx +++ b/src/pages/admin/AdminBookings.test.tsx @@ -47,7 +47,7 @@ describe('AdminBookings', () => { render() await screen.findByText('Jane Doe') - await userEvent.click(screen.getByRole('button', { name: /cancel/i })) + await userEvent.click(screen.getByRole('button', { name: /annuler/i })) expect(window.confirm).toHaveBeenCalled() await waitFor(() => expect(updateMock).toHaveBeenCalledWith({ status: 'cancelled' })) @@ -59,7 +59,7 @@ describe('AdminBookings', () => { render() await screen.findByText('Jane Doe') - await userEvent.click(screen.getByRole('button', { name: /cancel/i })) + await userEvent.click(screen.getByRole('button', { name: /annuler/i })) expect(window.confirm).toHaveBeenCalled() expect(updateMock).not.toHaveBeenCalled() @@ -69,7 +69,7 @@ describe('AdminBookings', () => { render() await screen.findByText('Jane Doe') - await userEvent.click(screen.getByRole('button', { name: /mark completed/i })) + await userEvent.click(screen.getByRole('button', { name: /marquer comme terminée/i })) expect(window.confirm).toHaveBeenCalled() await waitFor(() => expect(updateMock).toHaveBeenCalledWith({ status: 'completed' })) @@ -80,7 +80,7 @@ describe('AdminBookings', () => { render() await screen.findByText('Jane Doe') - await userEvent.click(screen.getByRole('button', { name: /mark completed/i })) + await userEvent.click(screen.getByRole('button', { name: /marquer comme terminée/i })) expect(window.confirm).toHaveBeenCalled() expect(updateMock).not.toHaveBeenCalled() diff --git a/src/pages/admin/AdminBookings.tsx b/src/pages/admin/AdminBookings.tsx index c140306..585bd1b 100644 --- a/src/pages/admin/AdminBookings.tsx +++ b/src/pages/admin/AdminBookings.tsx @@ -14,6 +14,12 @@ interface AdminBooking { services: { name: string } | null } +const STATUS_LABELS: Record = { + confirmed: 'Confirmée', + cancelled: 'Annulée', + completed: 'Terminée', +} + function AdminBookings() { const [bookings, setBookings] = useState([]) const [error, setError] = useState('') @@ -56,16 +62,16 @@ function AdminBookings() { return (
-

Bookings

+

Réservations

{error &&

{error}

} - + - + @@ -81,29 +87,29 @@ function AdminBookings() {
{booking.contact_email}
{booking.contact_notes &&
{booking.contact_notes}
} - + - + @@ -180,10 +180,10 @@ function AdminSchedule() { {exceptions.map((exception) => ( - + ))} @@ -192,7 +192,7 @@ function AdminSchedule() { - + diff --git a/src/pages/admin/AdminServices.test.tsx b/src/pages/admin/AdminServices.test.tsx index c4be681..8053059 100644 --- a/src/pages/admin/AdminServices.test.tsx +++ b/src/pages/admin/AdminServices.test.tsx @@ -55,11 +55,11 @@ describe('AdminServices', () => { await screen.findByText('Knotless Braids') await userEvent.type(screen.getByLabelText(/^id$/i), 'cornrows') - await userEvent.type(screen.getByLabelText(/^name$/i), 'Cornrows') - await userEvent.type(screen.getByLabelText(/duration/i), '120') - await userEvent.type(screen.getByLabelText(/price from/i), '90') + await userEvent.type(screen.getByLabelText(/^nom$/i), 'Cornrows') + await userEvent.type(screen.getByLabelText(/durée en minutes/i), '120') + await userEvent.type(screen.getByLabelText(/prix à partir de/i), '90') await userEvent.type(screen.getByLabelText(/^description$/i), 'Classic style.') - await userEvent.click(screen.getByRole('button', { name: /save service/i })) + await userEvent.click(screen.getByRole('button', { name: /enregistrer le service/i })) await waitFor(() => expect(upsertMock).toHaveBeenCalledWith( @@ -73,7 +73,7 @@ describe('AdminServices', () => { render() await screen.findByText('Knotless Braids') - await userEvent.click(screen.getByRole('button', { name: /delete/i })) + await userEvent.click(screen.getByRole('button', { name: /supprimer/i })) expect(deleteMock).toHaveBeenCalled() expect(eqMock).toHaveBeenCalledWith('id', 'knotless') diff --git a/src/pages/admin/AdminServices.tsx b/src/pages/admin/AdminServices.tsx index 940d784..b14aea7 100644 --- a/src/pages/admin/AdminServices.tsx +++ b/src/pages/admin/AdminServices.tsx @@ -55,7 +55,7 @@ function AdminServices() { try { image = await uploadSalonImage(file, `styles/${form.id}-${file.name}`) } catch { - setError('Image upload failed.') + setError("Échec du téléversement de l'image.") setSaving(false) return } @@ -73,7 +73,7 @@ function AdminServices() { } async function handleDelete(id: string) { - if (!window.confirm('Delete this service?')) return + if (!window.confirm('Supprimer ce service ?')) return const { error: deleteError } = await supabase.from('services').delete().eq('id', id) if (deleteError) { setError(friendlyError(deleteError)) @@ -90,9 +90,9 @@ function AdminServices() {
DateTimeHeure Service ContactStatusStatut
{booking.status}{STATUS_LABELS[booking.status]} {booking.status === 'confirmed' && ( <> )} diff --git a/src/pages/admin/AdminInquiries.test.tsx b/src/pages/admin/AdminInquiries.test.tsx index 547eade..5a386b8 100644 --- a/src/pages/admin/AdminInquiries.test.tsx +++ b/src/pages/admin/AdminInquiries.test.tsx @@ -43,7 +43,7 @@ describe('AdminInquiries', () => { render() await screen.findByText('Nadia') - await userEvent.click(screen.getByRole('button', { name: /mark as read/i })) + await userEvent.click(screen.getByRole('button', { name: /marquer comme lue/i })) await waitFor(() => expect(updateMock).toHaveBeenCalledWith({ status: 'read' })) expect(eqUpdateMock).toHaveBeenCalledWith('id', 'inquiry-1') diff --git a/src/pages/admin/AdminInquiries.tsx b/src/pages/admin/AdminInquiries.tsx index 8d8b229..4aaeaad 100644 --- a/src/pages/admin/AdminInquiries.tsx +++ b/src/pages/admin/AdminInquiries.tsx @@ -11,6 +11,11 @@ interface AdminInquiry { products: { name: string } | null } +const STATUS_LABELS: Record = { + new: 'Nouvelle', + read: 'Lue', +} + function AdminInquiries() { const [inquiries, setInquiries] = useState([]) const [error, setError] = useState('') @@ -53,15 +58,15 @@ function AdminInquiries() { return (
-

Inquiries

+

Demandes

{error &&

{error}

} - + - + @@ -74,10 +79,10 @@ function AdminInquiries() {
{inquiry.contact_email}
- + diff --git a/src/pages/admin/AdminLayout.test.tsx b/src/pages/admin/AdminLayout.test.tsx index 9847691..5aa4d9b 100644 --- a/src/pages/admin/AdminLayout.test.tsx +++ b/src/pages/admin/AdminLayout.test.tsx @@ -37,17 +37,17 @@ describe('AdminLayout', () => { it('renders nav links for every admin section and the outlet content', () => { renderLayout() - expect(screen.getByRole('link', { name: /products/i })).toBeInTheDocument() + expect(screen.getByRole('link', { name: /produits/i })).toBeInTheDocument() expect(screen.getByRole('link', { name: /services/i })).toBeInTheDocument() - expect(screen.getByRole('link', { name: /schedule/i })).toBeInTheDocument() - expect(screen.getByRole('link', { name: /bookings/i })).toBeInTheDocument() - expect(screen.getByRole('link', { name: /inquiries/i })).toBeInTheDocument() + expect(screen.getByRole('link', { name: /horaire/i })).toBeInTheDocument() + expect(screen.getByRole('link', { name: /réservations/i })).toBeInTheDocument() + expect(screen.getByRole('link', { name: /demandes/i })).toBeInTheDocument() expect(screen.getByText('Products content')).toBeInTheDocument() }) it('signs out and navigates to the login page', async () => { renderLayout() - await userEvent.click(screen.getByRole('button', { name: /sign out/i })) + await userEvent.click(screen.getByRole('button', { name: /déconnexion/i })) expect(signOutMock).toHaveBeenCalled() expect(navigateMock).toHaveBeenCalledWith('/admin/login') }) @@ -55,7 +55,7 @@ describe('AdminLayout', () => { it('does not navigate if sign out fails', async () => { signOutMock.mockResolvedValue({ error: new Error('Sign out failed') }) renderLayout() - await userEvent.click(screen.getByRole('button', { name: /sign out/i })) + await userEvent.click(screen.getByRole('button', { name: /déconnexion/i })) expect(signOutMock).toHaveBeenCalled() expect(navigateMock).not.toHaveBeenCalled() }) diff --git a/src/pages/admin/AdminLayout.tsx b/src/pages/admin/AdminLayout.tsx index ab60049..8006c74 100644 --- a/src/pages/admin/AdminLayout.tsx +++ b/src/pages/admin/AdminLayout.tsx @@ -3,11 +3,11 @@ import { supabase } from '../../lib/supabase' import styles from './AdminLayout.module.css' const NAV_ITEMS = [ - { to: '/admin/products', label: 'Products' }, + { to: '/admin/products', label: 'Produits' }, { to: '/admin/services', label: 'Services' }, - { to: '/admin/schedule', label: 'Schedule' }, - { to: '/admin/bookings', label: 'Bookings' }, - { to: '/admin/inquiries', label: 'Inquiries' }, + { to: '/admin/schedule', label: 'Horaire' }, + { to: '/admin/bookings', label: 'Réservations' }, + { to: '/admin/inquiries', label: 'Demandes' }, ] function AdminLayout() { @@ -34,7 +34,7 @@ function AdminLayout() { ))}
diff --git a/src/pages/admin/AdminLogin.test.tsx b/src/pages/admin/AdminLogin.test.tsx index 8a2b558..89d4ee6 100644 --- a/src/pages/admin/AdminLogin.test.tsx +++ b/src/pages/admin/AdminLogin.test.tsx @@ -27,9 +27,9 @@ describe('AdminLogin', () => { signInMock.mockResolvedValue({ error: null }) render() - await userEvent.type(screen.getByLabelText(/email/i), 'mira@example.com') - await userEvent.type(screen.getByLabelText(/password/i), 'correct-password') - await userEvent.click(screen.getByRole('button', { name: /sign in/i })) + await userEvent.type(screen.getByLabelText(/e-mail/i), 'mira@example.com') + await userEvent.type(screen.getByLabelText(/mot de passe/i), 'correct-password') + await userEvent.click(screen.getByRole('button', { name: /se connecter/i })) expect(signInMock).toHaveBeenCalledWith({ email: 'mira@example.com', password: 'correct-password' }) expect(navigateMock).toHaveBeenCalledWith('/admin/products') @@ -39,11 +39,11 @@ describe('AdminLogin', () => { signInMock.mockResolvedValue({ error: { message: 'Invalid login credentials' } }) render() - await userEvent.type(screen.getByLabelText(/email/i), 'mira@example.com') - await userEvent.type(screen.getByLabelText(/password/i), 'wrong-password') - await userEvent.click(screen.getByRole('button', { name: /sign in/i })) + await userEvent.type(screen.getByLabelText(/e-mail/i), 'mira@example.com') + await userEvent.type(screen.getByLabelText(/mot de passe/i), 'wrong-password') + await userEvent.click(screen.getByRole('button', { name: /se connecter/i })) - expect(await screen.findByText(/incorrect email or password/i)).toBeInTheDocument() + expect(await screen.findByText(/adresse e-mail ou mot de passe incorrect/i)).toBeInTheDocument() expect(navigateMock).not.toHaveBeenCalled() }) }) diff --git a/src/pages/admin/AdminLogin.tsx b/src/pages/admin/AdminLogin.tsx index 1861b16..5b0f815 100644 --- a/src/pages/admin/AdminLogin.tsx +++ b/src/pages/admin/AdminLogin.tsx @@ -17,7 +17,7 @@ function AdminLogin() { const { error: signInError } = await supabase.auth.signInWithPassword({ email, password }) setSubmitting(false) if (signInError) { - setError('Incorrect email or password.') + setError('Adresse e-mail ou mot de passe incorrect.') return } navigate('/admin/products') @@ -26,9 +26,9 @@ function AdminLogin() { return (
-

Admin Login

+

Connexion administrateur

- +
- + {error &&

{error}

}
diff --git a/src/pages/admin/AdminProducts.test.tsx b/src/pages/admin/AdminProducts.test.tsx index 3380a94..efcb67b 100644 --- a/src/pages/admin/AdminProducts.test.tsx +++ b/src/pages/admin/AdminProducts.test.tsx @@ -58,10 +58,10 @@ describe('AdminProducts', () => { await screen.findByText('Silk Edge Control') await userEvent.type(screen.getByLabelText(/^id$/i), 'braid-oil') - await userEvent.type(screen.getByLabelText(/^name$/i), 'Nourishing Braid Oil') - await userEvent.type(screen.getByLabelText(/^price$/i), '16') + await userEvent.type(screen.getByLabelText(/^nom$/i), 'Nourishing Braid Oil') + await userEvent.type(screen.getByLabelText(/^prix$/i), '16') await userEvent.type(screen.getByLabelText(/^description$/i), 'Lightweight scalp oil.') - await userEvent.click(screen.getByRole('button', { name: /save product/i })) + await userEvent.click(screen.getByRole('button', { name: /enregistrer le produit/i })) await waitFor(() => expect(upsertMock).toHaveBeenCalledWith( @@ -75,7 +75,7 @@ describe('AdminProducts', () => { render() await screen.findByText('Silk Edge Control') - await userEvent.click(screen.getByRole('button', { name: /delete/i })) + await userEvent.click(screen.getByRole('button', { name: /supprimer/i })) expect(deleteMock).toHaveBeenCalled() expect(eqMock).toHaveBeenCalledWith('id', 'edge-control') diff --git a/src/pages/admin/AdminProducts.tsx b/src/pages/admin/AdminProducts.tsx index 4969962..bc939fa 100644 --- a/src/pages/admin/AdminProducts.tsx +++ b/src/pages/admin/AdminProducts.tsx @@ -16,6 +16,13 @@ interface AdminProduct { const CATEGORIES: AdminProduct['category'][] = ['hair-care', 'extensions', 'tools', 'accessories'] +const CATEGORY_LABELS: Record = { + 'hair-care': 'Soins capillaires', + extensions: 'Extensions', + tools: 'Outils', + accessories: 'Accessoires', +} + function emptyForm(): AdminProduct { return { id: '', name: '', category: 'hair-care', price: 0, image: '', description: '', in_stock: true } } @@ -58,7 +65,7 @@ function AdminProducts() { try { image = await uploadSalonImage(file, `products/${form.id}-${file.name}`) } catch { - setError('Image upload failed.') + setError("Échec du téléversement de l'image.") setSaving(false) return } @@ -76,7 +83,7 @@ function AdminProducts() { } async function handleDelete(id: string) { - if (!window.confirm('Delete this product?')) return + if (!window.confirm('Supprimer ce produit ?')) return const { error: deleteError } = await supabase.from('products').delete().eq('id', id) if (deleteError) { setError(friendlyError(deleteError)) @@ -89,14 +96,14 @@ function AdminProducts() { return (
-

Products

+

Produits

ProductProduit Contact NotesStatusStatut
{inquiry.contact_notes}{inquiry.status}{STATUS_LABELS[inquiry.status]} {inquiry.status === 'new' && ( - + )}
- - - - + + + + @@ -104,12 +111,12 @@ function AdminProducts() { {products.map((product) => ( - - - + + + ))} @@ -117,7 +124,7 @@ function AdminProducts() {
NameCategoryPriceIn stockNomCatégoriePrixEn stock
{product.name}{product.category}${product.price}{product.in_stock ? 'Yes' : 'No'}{CATEGORY_LABELS[product.category]}{product.price} ${product.in_stock ? 'Oui' : 'Non'} - - + +
-

{isEditing ? 'Edit product' : 'New product'}

+

{isEditing ? 'Modifier le produit' : 'Nouveau produit'}

{error &&

{error}

} - +
) diff --git a/src/pages/admin/AdminSchedule.test.tsx b/src/pages/admin/AdminSchedule.test.tsx index a5b2043..79d9520 100644 --- a/src/pages/admin/AdminSchedule.test.tsx +++ b/src/pages/admin/AdminSchedule.test.tsx @@ -67,7 +67,7 @@ describe('AdminSchedule', () => { render() await screen.findByDisplayValue('09:00:00') - await userEvent.click(screen.getByRole('button', { name: /save schedule/i })) + await userEvent.click(screen.getByRole('button', { name: /enregistrer l'horaire/i })) await waitFor(() => expect(templateUpdateMock).toHaveBeenCalled()) expect(templateEqUpdateMock).toHaveBeenCalledWith('id', 'default') @@ -77,9 +77,9 @@ describe('AdminSchedule', () => { render() await screen.findByDisplayValue('09:00:00') - await userEvent.type(screen.getByLabelText(/exception date/i), '2026-11-01') + await userEvent.type(screen.getByLabelText(/date d'exception/i), '2026-11-01') await userEvent.type(screen.getByLabelText(/^note$/i), 'Closed for training') - await userEvent.click(screen.getByRole('button', { name: /add exception/i })) + await userEvent.click(screen.getByRole('button', { name: /ajouter une exception/i })) await waitFor(() => expect(exceptionsInsertMock).toHaveBeenCalledWith( diff --git a/src/pages/admin/AdminSchedule.tsx b/src/pages/admin/AdminSchedule.tsx index 5e84af9..7d6b7dc 100644 --- a/src/pages/admin/AdminSchedule.tsx +++ b/src/pages/admin/AdminSchedule.tsx @@ -16,7 +16,7 @@ interface ScheduleException { note: string | null } -const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] +const DAY_LABELS = ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'] function AdminSchedule() { const [template, setTemplate] = useState(null) @@ -115,11 +115,11 @@ function AdminSchedule() { return (
-

Schedule

+

Horaire

{template && (
-

Weekly hours

+

Heures hebdomadaires

{DAY_LABELS.map((label, day) => (
- +
)} @@ -171,7 +171,7 @@ function AdminSchedule() {
DateClosedFermé Note
{exception.date}{exception.closed ? 'Yes' : 'No'}{exception.closed ? 'Oui' : 'Non'} {exception.note} - +
- - - + + + @@ -101,10 +101,10 @@ function AdminServices() { - + ))} @@ -112,7 +112,7 @@ function AdminServices() {
NameDuration (min)Price fromNomDurée (min)Prix à partir de
{service.name} {service.duration_minutes}${service.price_from}{service.price_from} $ - - + +
-

{isEditing ? 'Edit service' : 'New service'}

+

{isEditing ? 'Modifier le service' : 'Nouveau service'}

{error &&

{error}

} - +
)