Files
carmanagement/apps/dashboard/src/components/I18nProvider.tsx
T
root dcd62f35ac fix: remove medium theme, fix navbar re-render, fix Node.js env loading
- Remove 'medium' theme from dashboard and marketplace; keep only light/dark
- Marketplace: move public pages into (public)/ route group so header/footer
  persist across navigations without re-rendering (eliminates usePathname)
- MarketplaceShell is now a pure context provider; chrome lives in
  (public)/layout.tsx which Next.js holds stable across navigations
- WorkspaceFrame: remove history.replaceState and standalone-route logic
- Docker API: bypass npm run dev to avoid node --env-file in containers
- Add scripts/run-with-env-file.cjs for Node.js < 20.6 compatibility;
  update apps/api/package.json dev script to use it

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 02:41:02 -04:00

1576 lines
61 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'
import { SHARED_LANGUAGE_COOKIE, SHARED_LANGUAGE_KEY, SHARED_THEME_KEY, readCurrentUserScopedPreference, readScopedPreference, writeScopedPreference } from '@/lib/preferences'
import { apiFetch } from '@/lib/api'
export type DashboardLanguage = 'en' | 'fr' | 'ar'
export type DashboardTheme = 'light' | 'dark'
type FleetDict = {
statusLabels: Record<'AVAILABLE' | 'RENTED' | 'MAINTENANCE' | 'OUT_OF_SERVICE', string>
logMaintenance: string
maintenanceSubtitle: (name: string) => string
maintenanceTypeRequired: string
failedToSave: string
typeLabel: string
typePlaceholder: string
description: string
descriptionPlaceholder: string
date: string
nextDueDate: string
cost: string
odometer: string
skip: string
saveLog: string
saving: string
addNewVehicle: string
makeMissing: string
modelMissing: string
plateMissing: string
rateMissing: string
failedToAdd: string
make: string
selectMake: string
other: string
typeMake: string
model: string
selectModel: string
typeModel: string
year: string
category: string
dailyRate: string
licensePlate: string
color: string
select: string
typeColor: string
seats: string
transmission: string
manual: string
automatic: string
fuelType: string
photosLabel: string
clickToAddPhotos: string
addMorePhotos: string
cancel: string
adding: string
addVehicle: string
heading: string
vehicleCount: (n: number) => string
searchPlaceholder: string
categoryLabels: Record<'ECONOMY' | 'COMPACT' | 'MIDSIZE' | 'FULLSIZE' | 'SUV' | 'LUXURY' | 'VAN' | 'TRUCK', string>
fuelTypeLabels: Record<'GASOLINE' | 'DIESEL' | 'ELECTRIC' | 'HYBRID', string>
allStatuses: string
allCategories: string
allVisibility: string
published: string
unpublished: string
colVehicle: string
colCategory: string
colStatus: string
colDailyRate: string
colPublished: string
colActions: string
noPhoto: string
perDay: string
noVehicles: string
noMatch: string
}
type ReservationsDict = {
heading: string
subtitle: string
colCustomer: string
colVehicle: string
colDates: string
colSource: string
colStatus: string
colTotal: string
noReservations: string
loadingReservation: string
failedToLoad: string
confirmReservation: string
checkInVehicle: string
checkOutVehicle: string
working: string
failedConfirm: string
failedCheckIn: string
failedCheckOut: string
sectionCustomer: string
noPhone: string
licenseLabel: string
notCaptured: string
flaggedBadge: string
sectionVehicle: string
sectionCharges: string
chargeDiscount: string
chargeInsurance: string
chargeAdditionalDrivers: string
chargePricingAdjustments: string
chargeGrandTotal: string
appliedInsurance: string
pricingRulesApplied: string
sectionAdditionalDrivers: string
driverLicenseLabel: string
driverChargeLabel: string
approveBtn: string
approvedBadge: string
noApprovalNeeded: string
noAdditionalDrivers: string
failedApproveDriver: string
sectionInspectionSummary: string
checkInInspectionLabel: string
checkOutInspectionLabel: string
savedBadge: string
pendingBadge: string
inspectionCardTitle: (type: 'CHECKIN' | 'CHECKOUT') => string
inspectionSubtitle: string
saveInspection: string
savingInspection: string
failedSaveInspection: string
diagramHelp: string
mileageLabel: string
fuelLevelLabel: string
fuelLevels: Record<'FULL' | 'SEVEN_EIGHTHS' | 'THREE_QUARTERS' | 'FIVE_EIGHTHS' | 'HALF' | 'THREE_EIGHTHS' | 'QUARTER' | 'ONE_EIGHTH' | 'EMPTY', string>
generalConditionLabel: string
employeeNotesLabel: string
customerAcknowledged: string
damageMarkersHeading: string
removeMarker: string
noMarkers: string
damageTypeLabels: Record<'SCRATCH' | 'DENT' | 'CRACK' | 'CHIP' | 'MISSING' | 'STAIN' | 'OTHER', string>
severityLabels: Record<'MINOR' | 'MODERATE' | 'MAJOR', string>
}
type DashboardPageDict = {
failedToLoad: string
forbidden: string
kpiTotalBookings: string
kpiActiveVehicles: string
kpiMonthlyRevenue: string
kpiTotalCustomers: string
vsLastMonth: string
bookingSources: string
barBookings: string
barRevenue: string
noBookingData: string
quickStats: string
noData: string
recentReservations: string
viewAll: string
colBookingNum: string
colCustomer: string
colVehicle: string
colDates: string
colStatus: string
colTotal: string
noReservations: string
trialEnds: (date: string) => string
trialActive: string
pastDueMsg: string
suspendedMsg: string
manageBilling: string
statusLabels: Record<'CONFIRMED' | 'PENDING' | 'ACTIVE' | 'COMPLETED' | 'CANCELLED', string>
}
type CalendarDict = {
heading: string
blockDates: string
clickSecondDay: (selected: string) => string
cancelSelection: string
legendReserved: string
legendMaintenance: string
legendBlocked: string
legendAvailable: string
failedLoad: string
moreDays: (n: number) => string
reservationsThisMonth: string
blocksAndMaintenance: string
removeBlockConfirm: string
failedDeleteBlock: string
modalTitle: string
blockTypeLabel: string
manualBlock: string
maintenanceBlock: string
startDate: string
endDate: string
reasonLabel: string
optional: string
reasonPlaceholderManual: string
reasonPlaceholderMaintenance: string
datesRequired: string
endAfterStart: string
failedCreateBlock: string
cancel: string
saveBlock: string
saving: string
removeBlock: string
dayHeaders: [string, string, string, string, string, string, string]
}
type VehicleDetailDict = {
tabDetails: string
tabMaintenance: string
tabCalendar: string
editBtn: string
cancelBtn: string
saveBtn: string
savingBtn: string
uploadingPhotosBtn: string
loadingVehicle: string
photosHeading: string
noPhotosYet: string
clickToAddPhotos: string
addMorePhotos: (current: number) => string
maxPhotosReached: string
newPhotoBadge: string
vehicleDetailsHeading: string
labelCategory: string
labelDailyRate: string
labelStatus: string
labelSeats: string
labelTransmission: string
labelFuelType: string
labelColor: string
labelOdometer: string
labelNotes: string
makeLabel: string
selectMake: string
other: string
typeMake: string
modelLabel: string
selectModel: string
typeModel: string
yearLabel: string
dailyRateLabel: string
licensePlateLabel: string
statusLabel: string
colorLabel: string
selectColor: string
typeColor: string
seatsLabel: string
transmissionLabel: string
fuelTypeLabel: string
odometerLabel: string
notesLabel: string
optionalNotes: string
optional: string
makeRequired: string
modelRequired: string
plateRequired: string
rateInvalid: string
failedSave: string
maintenanceIntro: string
logBtn: string
statusNone: string
statusOverdue: string
statusDueSoon: string
statusOk: string
noRecord: string
lastPrefix: string
atKm: (km: string) => string
duePrefix: string
orSep: string
kmLeft: (km: string) => string
overdueByKm: string
serviceDateLabel: string
odometerAtService: string
nextDueDateLabel: string
nextDueKmLabel: string
costLabel: string
maintenanceNotesLabel: string
saveLabel: string
savingLabel: string
dateRequired: string
routineTypes: Record<string, string>
}
type DashboardDictionary = {
nav: Record<string, string>
titles: Record<string, string>
notifications: string
noNewNotifications: string
unreadNotifications: (count: number) => string
signOut: string
workspaceUser: string
localAuth: string
language: string
theme: string
light: string
dark: string
fleet: FleetDict
vehicleDetail: VehicleDetailDict
calendar: CalendarDict
dashboardPage: DashboardPageDict
reservations: ReservationsDict
}
const dictionaries: Record<DashboardLanguage, DashboardDictionary> = {
en: {
nav: {
dashboard: 'Dashboard',
fleet: 'Fleet',
reservations: 'Booking',
onlineReservations: 'Online Reservations',
customers: 'Customers',
offers: 'Offers',
team: 'Team',
reports: 'Reports',
subscription: 'Subscription',
billing: 'Customer Billing',
contracts: 'Contracts',
notifications: 'Notifications',
settings: 'Settings',
},
titles: {
'/dashboard': 'Dashboard',
'/dashboard/fleet': 'Fleet Management',
'/dashboard/reservations': 'Booking',
'/dashboard/reservations/new': 'Book Car',
'/dashboard/online-reservations': 'Online Reservations',
'/dashboard/customers': 'Customers',
'/dashboard/offers': 'Offers',
'/dashboard/team': 'Team',
'/dashboard/reports': 'Reports',
'/dashboard/subscription': 'Subscription',
'/dashboard/billing': 'Customer Billing',
'/dashboard/contracts': 'Contracts',
'/dashboard/notifications': 'Notifications',
'/dashboard/settings': 'Settings',
},
notifications: 'Notifications',
noNewNotifications: 'No new notifications',
unreadNotifications: (count) => `${count} unread notification${count > 1 ? 's' : ''}`,
signOut: 'Sign out',
workspaceUser: 'Workspace user',
localAuth: 'Local authentication',
language: 'Language',
theme: 'Theme',
light: 'Light',
dark: 'Dark',
fleet: {
statusLabels: { AVAILABLE: 'Available', RENTED: 'Rented', MAINTENANCE: 'Maintenance', OUT_OF_SERVICE: 'Out of Service' },
logMaintenance: 'Log Maintenance',
maintenanceSubtitle: (name) => `${name} has been set to maintenance. Add details below.`,
maintenanceTypeRequired: 'Please enter a maintenance type.',
failedToSave: 'Failed to save maintenance log.',
typeLabel: 'Type *',
typePlaceholder: 'e.g. Oil change, Tire rotation, Brake service…',
description: 'Description',
descriptionPlaceholder: 'Optional details…',
date: 'Date *',
nextDueDate: 'Next Due Date',
cost: 'Cost (MAD)',
odometer: 'Odometer (km)',
skip: 'Skip',
saveLog: 'Save Log',
saving: 'Saving…',
addNewVehicle: 'Add New Vehicle',
makeMissing: 'Please select or enter a make.',
modelMissing: 'Please select or enter a model.',
plateMissing: 'Please enter a license plate.',
rateMissing: 'Please enter a valid daily rate.',
failedToAdd: 'Failed to add vehicle. Please check all fields and try again.',
make: 'Make *',
selectMake: 'Select make…',
other: 'Other…',
typeMake: 'Type make…',
model: 'Model *',
selectModel: 'Select model…',
typeModel: 'Type model…',
year: 'Year *',
category: 'Category',
dailyRate: 'Daily Rate (MAD) *',
licensePlate: 'License Plate *',
color: 'Color',
select: 'Select…',
typeColor: 'Type color…',
seats: 'Seats',
transmission: 'Transmission',
manual: 'Manual',
automatic: 'Automatic',
fuelType: 'Fuel Type',
photosLabel: 'Photos (up to 10)',
clickToAddPhotos: 'Click to add photos',
addMorePhotos: 'Add more photos',
cancel: 'Cancel',
adding: 'Adding…',
addVehicle: 'Add Vehicle',
heading: 'Fleet',
vehicleCount: (n) => `${n} vehicle${n !== 1 ? 's' : ''} total`,
searchPlaceholder: 'Search make, model, plate…',
categoryLabels: {
ECONOMY: 'Economy',
COMPACT: 'Compact',
MIDSIZE: 'Midsize',
FULLSIZE: 'Full-size',
SUV: 'SUV',
LUXURY: 'Luxury',
VAN: 'Van',
TRUCK: 'Truck',
},
fuelTypeLabels: {
GASOLINE: 'Gasoline',
DIESEL: 'Diesel',
ELECTRIC: 'Electric',
HYBRID: 'Hybrid',
},
allStatuses: 'All statuses',
allCategories: 'All categories',
allVisibility: 'All visibility',
published: 'Published',
unpublished: 'Unpublished',
colVehicle: 'Vehicle',
colCategory: 'Category',
colStatus: 'Status',
colDailyRate: 'Daily Rate',
colPublished: 'Published',
colActions: 'Actions',
noPhoto: 'No photo',
perDay: '/day',
noVehicles: 'No vehicles yet. Add your first vehicle to get started.',
noMatch: 'No vehicles match your filters.',
},
vehicleDetail: {
tabDetails: 'Details',
tabMaintenance: 'Maintenance',
tabCalendar: 'Calendar',
editBtn: 'Edit',
cancelBtn: 'Cancel',
saveBtn: 'Save',
savingBtn: 'Saving…',
uploadingPhotosBtn: 'Uploading photos…',
loadingVehicle: 'Loading vehicle…',
photosHeading: 'Photos',
noPhotosYet: 'No photos uploaded yet.',
clickToAddPhotos: 'Click to add photos',
addMorePhotos: (n) => `Add more photos (${n}/10)`,
maxPhotosReached: 'Maximum 10 photos reached.',
newPhotoBadge: 'New',
vehicleDetailsHeading: 'Vehicle details',
labelCategory: 'Category',
labelDailyRate: 'Daily rate',
labelStatus: 'Status',
labelSeats: 'Seats',
labelTransmission: 'Transmission',
labelFuelType: 'Fuel type',
labelColor: 'Color',
labelOdometer: 'Odometer',
labelNotes: 'Notes',
makeLabel: 'Make *',
selectMake: 'Select make…',
other: 'Other…',
typeMake: 'Type make…',
modelLabel: 'Model *',
selectModel: 'Select model…',
typeModel: 'Type model…',
yearLabel: 'Year *',
dailyRateLabel: 'Daily Rate (MAD) *',
licensePlateLabel: 'License Plate *',
statusLabel: 'Status',
colorLabel: 'Color',
selectColor: 'Select color…',
typeColor: 'Type color…',
seatsLabel: 'Seats',
transmissionLabel: 'Transmission',
fuelTypeLabel: 'Fuel Type',
odometerLabel: 'Odometer (km)',
notesLabel: 'Notes',
optionalNotes: 'Optional notes…',
optional: 'Optional…',
makeRequired: 'Make is required.',
modelRequired: 'Model is required.',
plateRequired: 'License plate is required.',
rateInvalid: 'Please enter a valid daily rate.',
failedSave: 'Failed to save changes.',
maintenanceIntro: 'Track routine maintenance for this vehicle. Click Log on any item to record a service.',
logBtn: 'Log',
statusNone: 'Not logged',
statusOverdue: 'Overdue',
statusDueSoon: 'Due soon',
statusOk: 'Up to date',
noRecord: 'No record yet',
lastPrefix: 'Last:',
atKm: (km) => `at ${km} km`,
duePrefix: '· Due:',
orSep: 'or',
kmLeft: (km) => `${km} km left`,
overdueByKm: 'overdue by km',
serviceDateLabel: 'Service Date *',
odometerAtService: 'Odometer at service (km)',
nextDueDateLabel: 'Next due date',
nextDueKmLabel: 'Next due at (km)',
costLabel: 'Cost (MAD)',
maintenanceNotesLabel: 'Notes',
saveLabel: 'Save',
savingLabel: 'Saving…',
dateRequired: 'Service date is required.',
routineTypes: {
'Oil Change': 'Oil Change',
'Technical Inspection': 'Technical Inspection',
'Vehicle Registration': 'Vehicle Registration',
'Car Tax': 'Car Tax',
'Tire Rotation': 'Tire Rotation',
'Brake Inspection': 'Brake Inspection',
'Air Filter': 'Air Filter',
'Cabin Filter': 'Cabin Filter',
'Battery Check': 'Battery Check',
'Belt / Chain Service': 'Belt / Chain Service',
'Coolant Flush': 'Coolant Flush',
'Transmission Service': 'Transmission Service',
},
},
calendar: {
heading: 'Availability Calendar',
blockDates: 'Block dates',
clickSecondDay: (selected) => `Click a second day to set the block end date (selected: ${selected})`,
cancelSelection: 'cancel',
legendReserved: 'Reserved',
legendMaintenance: 'Maintenance',
legendBlocked: 'Blocked',
legendAvailable: 'Available',
failedLoad: 'Failed to load calendar',
moreDays: (n) => `+${n} more`,
reservationsThisMonth: 'Reservations this month',
blocksAndMaintenance: 'Blocks & maintenance',
removeBlockConfirm: 'Remove this block?',
failedDeleteBlock: 'Failed to delete block',
modalTitle: 'Block vehicle dates',
blockTypeLabel: 'Block type',
manualBlock: 'Manual block',
maintenanceBlock: 'Maintenance',
startDate: 'Start date',
endDate: 'End date (exclusive)',
reasonLabel: 'Reason',
optional: '(optional)',
reasonPlaceholderManual: 'e.g. Reserved for VIP, company event…',
reasonPlaceholderMaintenance: 'e.g. Oil change, tire rotation…',
datesRequired: 'Start and end dates are required.',
endAfterStart: 'End date must be after start date.',
failedCreateBlock: 'Failed to create block',
cancel: 'Cancel',
saveBlock: 'Save block',
saving: 'Saving…',
removeBlock: 'Remove block',
dayHeaders: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
},
dashboardPage: {
failedToLoad: 'Failed to load dashboard',
forbidden: 'You do not have permission to access this page. A Manager role or higher is required.',
kpiTotalBookings: 'Total Bookings',
kpiActiveVehicles: 'Active Vehicles',
kpiMonthlyRevenue: 'Monthly Revenue',
kpiTotalCustomers: 'Total Customers',
vsLastMonth: 'vs last month',
bookingSources: 'Booking Sources',
barBookings: 'Bookings',
barRevenue: 'Revenue (MAD)',
noBookingData: 'No booking data available yet',
quickStats: 'Quick Stats',
noData: 'No data yet',
recentReservations: 'Recent Reservations',
viewAll: 'View all →',
colBookingNum: 'Booking #',
colCustomer: 'Customer',
colVehicle: 'Vehicle',
colDates: 'Dates',
colStatus: 'Status',
colTotal: 'Total',
noReservations: "No reservations yet. Once customers book vehicles, they'll appear here.",
trialEnds: (date) => `You're on a free trial. Trial ends ${date}.`,
trialActive: "You're on a free trial.",
pastDueMsg: 'Your payment is past due. Please update your billing information.',
suspendedMsg: 'Your account has been suspended. Please contact support or renew your subscription.',
manageBilling: 'Manage subscription →',
statusLabels: { CONFIRMED: 'Confirmed', PENDING: 'Pending', ACTIVE: 'Active', COMPLETED: 'Completed', CANCELLED: 'Cancelled' },
},
reservations: {
heading: 'Booking',
subtitle: 'All booking sources, including dashboard, public site, and marketplace.',
colCustomer: 'Customer',
colVehicle: 'Vehicle',
colDates: 'Dates',
colSource: 'Source',
colStatus: 'Status',
colTotal: 'Total',
noReservations: 'No reservations yet.',
loadingReservation: 'Loading reservation…',
failedToLoad: 'Failed to load reservation',
confirmReservation: 'Confirm reservation',
checkInVehicle: 'Check in vehicle',
checkOutVehicle: 'Check out vehicle',
working: 'Working…',
failedConfirm: 'Failed to confirm reservation',
failedCheckIn: 'Failed to check in reservation',
failedCheckOut: 'Failed to check out reservation',
sectionCustomer: 'Customer',
noPhone: 'No phone provided',
licenseLabel: 'License:',
notCaptured: 'Not captured',
flaggedBadge: 'Flagged',
sectionVehicle: 'Vehicle',
sectionCharges: 'Charges',
chargeDiscount: 'Discount',
chargeInsurance: 'Insurance',
chargeAdditionalDrivers: 'Additional drivers',
chargePricingAdjustments: 'Pricing adjustments',
chargeGrandTotal: 'Grand total',
appliedInsurance: 'Applied insurance',
pricingRulesApplied: 'Pricing rules applied',
sectionAdditionalDrivers: 'Additional drivers',
driverLicenseLabel: 'License:',
driverChargeLabel: 'Charge:',
approveBtn: 'Approve',
approvedBadge: 'Approved',
noApprovalNeeded: 'No approval needed',
noAdditionalDrivers: 'No additional drivers were added to this reservation.',
failedApproveDriver: 'Failed to approve additional driver',
sectionInspectionSummary: 'Inspection summary',
checkInInspectionLabel: 'Check-in inspection',
checkOutInspectionLabel: 'Check-out inspection',
savedBadge: 'Saved',
pendingBadge: 'Pending',
inspectionCardTitle: (type) => type === 'CHECKIN' ? 'Check-in inspection' : 'Check-out inspection',
inspectionSubtitle: 'Mark existing and newly discovered damage directly on the vehicle diagram.',
saveInspection: 'Save inspection',
savingInspection: 'Saving…',
failedSaveInspection: 'Failed to save inspection',
diagramHelp: 'Click anywhere on the diagram to add a damage marker with the selected type and severity.',
mileageLabel: 'Mileage',
fuelLevelLabel: 'Fuel level',
fuelLevels: { FULL: 'Full', SEVEN_EIGHTHS: '7/8', THREE_QUARTERS: '3/4', FIVE_EIGHTHS: '5/8', HALF: '1/2', THREE_EIGHTHS: '3/8', QUARTER: '1/4', ONE_EIGHTH: '1/8', EMPTY: 'Empty' },
generalConditionLabel: 'General condition',
employeeNotesLabel: 'Employee notes',
customerAcknowledged: 'Customer acknowledged this inspection',
damageMarkersHeading: 'Damage markers',
removeMarker: 'Remove',
noMarkers: 'No damage markers saved yet.',
damageTypeLabels: { SCRATCH: 'Scratch', DENT: 'Dent', CRACK: 'Crack', CHIP: 'Chip', MISSING: 'Missing', STAIN: 'Stain', OTHER: 'Other' },
severityLabels: { MINOR: 'Minor', MODERATE: 'Moderate', MAJOR: 'Major' },
},
},
fr: {
nav: {
dashboard: 'Tableau de bord',
fleet: 'Flotte',
reservations: 'Réservations',
onlineReservations: 'Réservations en ligne',
customers: 'Clients',
offers: 'Offres',
team: 'Équipe',
reports: 'Rapports',
subscription: 'Abonnement',
billing: 'Facturation clients',
contracts: 'Contrats',
notifications: 'Notifications',
settings: 'Paramètres',
},
titles: {
'/dashboard': 'Tableau de bord',
'/dashboard/fleet': 'Gestion de flotte',
'/dashboard/reservations': 'Réservations',
'/dashboard/reservations/new': 'Réserver une voiture',
'/dashboard/online-reservations': 'Réservations en ligne',
'/dashboard/customers': 'Clients',
'/dashboard/offers': 'Offres',
'/dashboard/team': 'Équipe',
'/dashboard/reports': 'Rapports',
'/dashboard/subscription': 'Abonnement',
'/dashboard/billing': 'Facturation clients',
'/dashboard/contracts': 'Contrats',
'/dashboard/notifications': 'Notifications',
'/dashboard/settings': 'Paramètres',
},
notifications: 'Notifications',
noNewNotifications: 'Aucune nouvelle notification',
unreadNotifications: (count) => `${count} notification${count > 1 ? 's' : ''} non lue${count > 1 ? 's' : ''}`,
signOut: 'Déconnexion',
workspaceUser: 'Utilisateur espace',
localAuth: 'Authentification locale',
language: 'Langue',
theme: 'Mode',
light: 'Clair',
dark: 'Sombre',
fleet: {
statusLabels: { AVAILABLE: 'Disponible', RENTED: 'Loué', MAINTENANCE: 'Maintenance', OUT_OF_SERVICE: 'Hors service' },
logMaintenance: 'Journal de maintenance',
maintenanceSubtitle: (name) => `${name} a été mis en maintenance. Ajoutez les détails ci-dessous.`,
maintenanceTypeRequired: 'Veuillez saisir un type de maintenance.',
failedToSave: 'Échec de la sauvegarde du journal de maintenance.',
typeLabel: 'Type *',
typePlaceholder: 'ex. Vidange, Rotation des pneus, Freins…',
description: 'Description',
descriptionPlaceholder: 'Détails optionnels…',
date: 'Date *',
nextDueDate: 'Prochaine échéance',
cost: 'Coût (MAD)',
odometer: 'Kilométrage (km)',
skip: 'Ignorer',
saveLog: 'Enregistrer',
saving: 'Enregistrement…',
addNewVehicle: 'Ajouter un véhicule',
makeMissing: 'Veuillez sélectionner ou saisir une marque.',
modelMissing: 'Veuillez sélectionner ou saisir un modèle.',
plateMissing: 'Veuillez saisir un numéro de plaque.',
rateMissing: 'Veuillez saisir un tarif journalier valide.',
failedToAdd: "Échec de l'ajout du véhicule. Vérifiez tous les champs.",
make: 'Marque *',
selectMake: 'Sélectionner la marque…',
other: 'Autre…',
typeMake: 'Saisir la marque…',
model: 'Modèle *',
selectModel: 'Sélectionner le modèle…',
typeModel: 'Saisir le modèle…',
year: 'Année *',
category: 'Catégorie',
dailyRate: 'Tarif journalier (MAD) *',
licensePlate: "Plaque d'immatriculation *",
color: 'Couleur',
select: 'Sélectionner…',
typeColor: 'Saisir la couleur…',
seats: 'Places',
transmission: 'Transmission',
manual: 'Manuelle',
automatic: 'Automatique',
fuelType: 'Carburant',
photosLabel: "Photos (jusqu'à 10)",
clickToAddPhotos: 'Cliquer pour ajouter des photos',
addMorePhotos: "Ajouter d'autres photos",
cancel: 'Annuler',
adding: 'Ajout…',
addVehicle: 'Ajouter le véhicule',
heading: 'Flotte',
vehicleCount: (n) => `${n} véhicule${n !== 1 ? 's' : ''} au total`,
searchPlaceholder: 'Rechercher marque, modèle, plaque…',
categoryLabels: {
ECONOMY: 'Économique',
COMPACT: 'Compacte',
MIDSIZE: 'Intermédiaire',
FULLSIZE: 'Grande berline',
SUV: 'SUV',
LUXURY: 'Luxe',
VAN: 'Van',
TRUCK: 'Camionnette',
},
fuelTypeLabels: {
GASOLINE: 'Essence',
DIESEL: 'Diesel',
ELECTRIC: 'Électrique',
HYBRID: 'Hybride',
},
allStatuses: 'Tous les statuts',
allCategories: 'Toutes les catégories',
allVisibility: 'Toute visibilité',
published: 'Publié',
unpublished: 'Non publié',
colVehicle: 'Véhicule',
colCategory: 'Catégorie',
colStatus: 'Statut',
colDailyRate: 'Tarif journalier',
colPublished: 'Publié',
colActions: 'Actions',
noPhoto: 'Pas de photo',
perDay: '/jour',
noVehicles: 'Aucun véhicule. Ajoutez votre premier véhicule pour commencer.',
noMatch: 'Aucun véhicule ne correspond aux filtres.',
},
vehicleDetail: {
tabDetails: 'Détails',
tabMaintenance: 'Entretien',
tabCalendar: 'Calendrier',
editBtn: 'Modifier',
cancelBtn: 'Annuler',
saveBtn: 'Enregistrer',
savingBtn: 'Enregistrement…',
uploadingPhotosBtn: 'Envoi des photos…',
loadingVehicle: 'Chargement du véhicule…',
photosHeading: 'Photos',
noPhotosYet: 'Aucune photo ajoutée.',
clickToAddPhotos: 'Cliquer pour ajouter des photos',
addMorePhotos: (n) => `Ajouter des photos (${n}/10)`,
maxPhotosReached: '10 photos maximum atteintes.',
newPhotoBadge: 'Nouveau',
vehicleDetailsHeading: 'Détails du véhicule',
labelCategory: 'Catégorie',
labelDailyRate: 'Tarif journalier',
labelStatus: 'Statut',
labelSeats: 'Places',
labelTransmission: 'Transmission',
labelFuelType: 'Carburant',
labelColor: 'Couleur',
labelOdometer: 'Kilométrage',
labelNotes: 'Notes',
makeLabel: 'Marque *',
selectMake: 'Sélectionner la marque…',
other: 'Autre…',
typeMake: 'Saisir la marque…',
modelLabel: 'Modèle *',
selectModel: 'Sélectionner le modèle…',
typeModel: 'Saisir le modèle…',
yearLabel: 'Année *',
dailyRateLabel: 'Tarif journalier (MAD) *',
licensePlateLabel: "Plaque d'immatriculation *",
statusLabel: 'Statut',
colorLabel: 'Couleur',
selectColor: 'Sélectionner la couleur…',
typeColor: 'Saisir la couleur…',
seatsLabel: 'Places',
transmissionLabel: 'Transmission',
fuelTypeLabel: 'Carburant',
odometerLabel: 'Kilométrage (km)',
notesLabel: 'Notes',
optionalNotes: 'Notes optionnelles…',
optional: 'Optionnel…',
makeRequired: 'La marque est requise.',
modelRequired: 'Le modèle est requis.',
plateRequired: "La plaque d'immatriculation est requise.",
rateInvalid: 'Veuillez saisir un tarif journalier valide.',
failedSave: "Échec de l'enregistrement.",
maintenanceIntro: "Suivez l'entretien de ce véhicule. Cliquez sur Saisir pour enregistrer une intervention.",
logBtn: 'Saisir',
statusNone: 'Non enregistré',
statusOverdue: 'En retard',
statusDueSoon: 'Bientôt dû',
statusOk: 'À jour',
noRecord: 'Aucune intervention',
lastPrefix: 'Dernier :',
atKm: (km) => `à ${km} km`,
duePrefix: '· Échéance :',
orSep: 'ou',
kmLeft: (km) => `${km} km restants`,
overdueByKm: 'dépassé (km)',
serviceDateLabel: 'Date du service *',
odometerAtService: 'Kilométrage lors du service (km)',
nextDueDateLabel: 'Prochaine échéance (date)',
nextDueKmLabel: 'Prochaine échéance (km)',
costLabel: 'Coût (MAD)',
maintenanceNotesLabel: 'Notes',
saveLabel: 'Enregistrer',
savingLabel: 'Enregistrement…',
dateRequired: 'La date du service est requise.',
routineTypes: {
'Oil Change': "Vidange d'huile",
'Technical Inspection': 'Contrôle technique',
'Vehicle Registration': 'Carte grise',
'Car Tax': 'Vignette automobile',
'Tire Rotation': 'Rotation des pneus',
'Brake Inspection': 'Contrôle des freins',
'Air Filter': 'Filtre à air',
'Cabin Filter': 'Filtre habitacle',
'Battery Check': 'Vérification batterie',
'Belt / Chain Service': 'Courroie / Chaîne',
'Coolant Flush': 'Liquide de refroidissement',
'Transmission Service': 'Entretien transmission',
},
},
calendar: {
heading: 'Calendrier de disponibilité',
blockDates: 'Bloquer des dates',
clickSecondDay: (selected) => `Cliquez sur un second jour pour définir la fin du blocage (sélectionné : ${selected})`,
cancelSelection: 'annuler',
legendReserved: 'Réservé',
legendMaintenance: 'Maintenance',
legendBlocked: 'Bloqué',
legendAvailable: 'Disponible',
failedLoad: 'Échec du chargement du calendrier',
moreDays: (n) => `+${n} de plus`,
reservationsThisMonth: 'Réservations ce mois-ci',
blocksAndMaintenance: 'Blocages & maintenance',
removeBlockConfirm: 'Supprimer ce blocage ?',
failedDeleteBlock: 'Échec de la suppression du blocage',
modalTitle: 'Bloquer les dates du véhicule',
blockTypeLabel: 'Type de blocage',
manualBlock: 'Blocage manuel',
maintenanceBlock: 'Maintenance',
startDate: 'Date de début',
endDate: 'Date de fin (exclusive)',
reasonLabel: 'Raison',
optional: '(optionnel)',
reasonPlaceholderManual: 'ex. Réservé pour VIP, événement…',
reasonPlaceholderMaintenance: 'ex. Vidange, rotation des pneus…',
datesRequired: 'Les dates de début et de fin sont requises.',
endAfterStart: 'La date de fin doit être après la date de début.',
failedCreateBlock: 'Échec de la création du blocage',
cancel: 'Annuler',
saveBlock: 'Enregistrer',
saving: 'Enregistrement…',
removeBlock: 'Supprimer le blocage',
dayHeaders: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
},
dashboardPage: {
failedToLoad: 'Échec du chargement du tableau de bord',
forbidden: "Vous n'avez pas la permission d'accéder à cette page. Un rôle Gestionnaire ou supérieur est requis.",
kpiTotalBookings: 'Total des réservations',
kpiActiveVehicles: 'Véhicules actifs',
kpiMonthlyRevenue: 'Revenus du mois',
kpiTotalCustomers: 'Total des clients',
vsLastMonth: 'vs mois dernier',
bookingSources: 'Sources de réservation',
barBookings: 'Réservations',
barRevenue: 'Revenus (MAD)',
noBookingData: 'Aucune donnée de réservation disponible',
quickStats: 'Statistiques rapides',
noData: 'Aucune donnée',
recentReservations: 'Réservations récentes',
viewAll: 'Voir tout →',
colBookingNum: 'Réservation #',
colCustomer: 'Client',
colVehicle: 'Véhicule',
colDates: 'Dates',
colStatus: 'Statut',
colTotal: 'Total',
noReservations: "Aucune réservation pour l'instant. Les réservations clients apparaîtront ici.",
trialEnds: (date) => `Vous êtes en période d'essai. Fin de l'essai le ${date}.`,
trialActive: "Vous êtes en période d'essai.",
pastDueMsg: 'Votre paiement est en retard. Veuillez mettre à jour vos informations de facturation.',
suspendedMsg: 'Votre compte a été suspendu. Veuillez contacter le support ou renouveler votre abonnement.',
manageBilling: 'Gérer labonnement →',
statusLabels: { CONFIRMED: 'Confirmé', PENDING: 'En attente', ACTIVE: 'Actif', COMPLETED: 'Terminé', CANCELLED: 'Annulé' },
},
reservations: {
heading: 'Réservations',
subtitle: 'Toutes les sources de réservation : tableau de bord, site public et marketplace.',
colCustomer: 'Client',
colVehicle: 'Véhicule',
colDates: 'Dates',
colSource: 'Source',
colStatus: 'Statut',
colTotal: 'Total',
noReservations: 'Aucune réservation pour le moment.',
loadingReservation: 'Chargement de la réservation…',
failedToLoad: 'Échec du chargement de la réservation',
confirmReservation: 'Confirmer la réservation',
checkInVehicle: 'Enregistrer le départ',
checkOutVehicle: 'Enregistrer le retour',
working: 'En cours…',
failedConfirm: 'Échec de la confirmation de la réservation',
failedCheckIn: 'Échec de l\'enregistrement du départ',
failedCheckOut: 'Échec de l\'enregistrement du retour',
sectionCustomer: 'Client',
noPhone: 'Aucun téléphone renseigné',
licenseLabel: 'Permis :',
notCaptured: 'Non saisi',
flaggedBadge: 'Signalé',
sectionVehicle: 'Véhicule',
sectionCharges: 'Facturation',
chargeDiscount: 'Remise',
chargeInsurance: 'Assurance',
chargeAdditionalDrivers: 'Conducteurs supplémentaires',
chargePricingAdjustments: 'Ajustements tarifaires',
chargeGrandTotal: 'Total général',
appliedInsurance: 'Assurance appliquée',
pricingRulesApplied: 'Règles tarifaires appliquées',
sectionAdditionalDrivers: 'Conducteurs supplémentaires',
driverLicenseLabel: 'Permis :',
driverChargeLabel: 'Frais :',
approveBtn: 'Approuver',
approvedBadge: 'Approuvé',
noApprovalNeeded: 'Pas d\'approbation requise',
noAdditionalDrivers: 'Aucun conducteur supplémentaire ajouté à cette réservation.',
failedApproveDriver: 'Échec de l\'approbation du conducteur supplémentaire',
sectionInspectionSummary: 'Résumé des inspections',
checkInInspectionLabel: 'Inspection de départ',
checkOutInspectionLabel: 'Inspection de retour',
savedBadge: 'Enregistré',
pendingBadge: 'En attente',
inspectionCardTitle: (type) => type === 'CHECKIN' ? 'Inspection de départ' : 'Inspection de retour',
inspectionSubtitle: 'Signalez les dommages existants et nouvellement découverts sur le schéma du véhicule.',
saveInspection: 'Enregistrer l\'inspection',
savingInspection: 'Enregistrement…',
failedSaveInspection: 'Échec de l\'enregistrement de l\'inspection',
diagramHelp: 'Cliquez n\'importe où sur le schéma pour ajouter un marqueur de dommage avec le type et la gravité sélectionnés.',
mileageLabel: 'Kilométrage',
fuelLevelLabel: 'Niveau de carburant',
fuelLevels: { FULL: 'Plein', SEVEN_EIGHTHS: '7/8', THREE_QUARTERS: '3/4', FIVE_EIGHTHS: '5/8', HALF: '1/2', THREE_EIGHTHS: '3/8', QUARTER: '1/4', ONE_EIGHTH: '1/8', EMPTY: 'Vide' },
generalConditionLabel: 'État général',
employeeNotesLabel: 'Notes de l\'employé',
customerAcknowledged: 'Le client a reconnu cette inspection',
damageMarkersHeading: 'Marqueurs de dommages',
removeMarker: 'Supprimer',
noMarkers: 'Aucun marqueur de dommage enregistré.',
damageTypeLabels: { SCRATCH: 'Rayure', DENT: 'Bosse', CRACK: 'Fissure', CHIP: 'Éclat', MISSING: 'Manquant', STAIN: 'Tache', OTHER: 'Autre' },
severityLabels: { MINOR: 'Léger', MODERATE: 'Modéré', MAJOR: 'Majeur' },
},
},
ar: {
nav: {
dashboard: 'لوحة التحكم',
fleet: 'الأسطول',
reservations: 'الحجوزات',
onlineReservations: 'الحجوزات الإلكترونية',
customers: 'العملاء',
offers: 'العروض',
team: 'الفريق',
reports: 'التقارير',
subscription: 'الاشتراك',
billing: 'فوترة العملاء',
contracts: 'العقود',
notifications: 'الإشعارات',
settings: 'الإعدادات',
},
titles: {
'/dashboard': 'لوحة التحكم',
'/dashboard/fleet': 'إدارة الأسطول',
'/dashboard/reservations': 'الحجوزات',
'/dashboard/reservations/new': 'حجز سيارة',
'/dashboard/online-reservations': 'الحجوزات الإلكترونية',
'/dashboard/customers': 'العملاء',
'/dashboard/offers': 'العروض',
'/dashboard/team': 'الفريق',
'/dashboard/reports': 'التقارير',
'/dashboard/subscription': 'الاشتراك',
'/dashboard/billing': 'فوترة العملاء',
'/dashboard/contracts': 'العقود',
'/dashboard/notifications': 'الإشعارات',
'/dashboard/settings': 'الإعدادات',
},
notifications: 'الإشعارات',
noNewNotifications: 'لا توجد إشعارات جديدة',
unreadNotifications: (count) => `${count} إشعار غير مقروء`,
signOut: 'تسجيل الخروج',
workspaceUser: 'مستخدم مساحة العمل',
localAuth: 'مصادقة محلية',
language: 'اللغة',
theme: 'الوضع',
light: 'فاتح',
dark: 'داكن',
fleet: {
statusLabels: { AVAILABLE: 'متاح', RENTED: 'مؤجر', MAINTENANCE: 'صيانة', OUT_OF_SERVICE: 'خارج الخدمة' },
logMaintenance: 'تسجيل الصيانة',
maintenanceSubtitle: (name) => `تم تعيين ${name} في وضع الصيانة. أضف التفاصيل أدناه.`,
maintenanceTypeRequired: 'الرجاء إدخال نوع الصيانة.',
failedToSave: 'فشل حفظ سجل الصيانة.',
typeLabel: 'النوع *',
typePlaceholder: 'مثال: تغيير الزيت، تدوير الإطارات، خدمة الفرامل…',
description: 'الوصف',
descriptionPlaceholder: 'تفاصيل اختيارية…',
date: 'التاريخ *',
nextDueDate: 'تاريخ الاستحقاق التالي',
cost: 'التكلفة (درهم)',
odometer: 'عداد المسافة (كم)',
skip: 'تخطي',
saveLog: 'حفظ السجل',
saving: 'جارٍ الحفظ…',
addNewVehicle: 'إضافة مركبة جديدة',
makeMissing: 'الرجاء اختيار أو إدخال الماركة.',
modelMissing: 'الرجاء اختيار أو إدخال الطراز.',
plateMissing: 'الرجاء إدخال رقم اللوحة.',
rateMissing: 'الرجاء إدخال سعر يومي صحيح.',
failedToAdd: 'فشل إضافة المركبة. يرجى التحقق من جميع الحقول.',
make: 'الماركة *',
selectMake: 'اختر الماركة…',
other: 'أخرى…',
typeMake: 'اكتب الماركة…',
model: 'الطراز *',
selectModel: 'اختر الطراز…',
typeModel: 'اكتب الطراز…',
year: 'السنة *',
category: 'الفئة',
dailyRate: 'السعر اليومي (درهم) *',
licensePlate: 'رقم اللوحة *',
color: 'اللون',
select: 'اختر…',
typeColor: 'اكتب اللون…',
seats: 'المقاعد',
transmission: 'ناقل الحركة',
manual: 'يدوي',
automatic: 'أوتوماتيكي',
fuelType: 'نوع الوقود',
photosLabel: 'الصور (حتى 10)',
clickToAddPhotos: 'انقر لإضافة صور',
addMorePhotos: 'إضافة المزيد من الصور',
cancel: 'إلغاء',
adding: 'جارٍ الإضافة…',
addVehicle: 'إضافة المركبة',
heading: 'الأسطول',
vehicleCount: (n) => `${n} مركبة إجمالاً`,
searchPlaceholder: 'بحث عن الماركة، الطراز، اللوحة…',
categoryLabels: {
ECONOMY: 'اقتصادية',
COMPACT: 'مدمجة',
MIDSIZE: 'متوسطة',
FULLSIZE: 'كبيرة',
SUV: 'دفع رباعي',
LUXURY: 'فاخرة',
VAN: 'فان',
TRUCK: 'شاحنة',
},
fuelTypeLabels: {
GASOLINE: 'بنزين',
DIESEL: 'ديزل',
ELECTRIC: 'كهربائي',
HYBRID: 'هجين',
},
allStatuses: 'جميع الحالات',
allCategories: 'جميع الفئات',
allVisibility: 'جميع الرؤية',
published: 'منشور',
unpublished: 'غير منشور',
colVehicle: 'المركبة',
colCategory: 'الفئة',
colStatus: 'الحالة',
colDailyRate: 'السعر اليومي',
colPublished: 'النشر',
colActions: 'الإجراءات',
noPhoto: 'لا توجد صورة',
perDay: '/يوم',
noVehicles: 'لا توجد مركبات. أضف مركبتك الأولى للبدء.',
noMatch: 'لا توجد مركبات تطابق المرشحات.',
},
vehicleDetail: {
tabDetails: 'التفاصيل',
tabMaintenance: 'الصيانة',
tabCalendar: 'التقويم',
editBtn: 'تعديل',
cancelBtn: 'إلغاء',
saveBtn: 'حفظ',
savingBtn: 'جارٍ الحفظ…',
uploadingPhotosBtn: 'جارٍ رفع الصور…',
loadingVehicle: 'جارٍ تحميل المركبة…',
photosHeading: 'الصور',
noPhotosYet: 'لم يتم إضافة أي صور.',
clickToAddPhotos: 'انقر لإضافة صور',
addMorePhotos: (n) => `إضافة المزيد من الصور (${n}/10)`,
maxPhotosReached: 'تم الوصول إلى الحد الأقصى (10 صور).',
newPhotoBadge: 'جديد',
vehicleDetailsHeading: 'تفاصيل المركبة',
labelCategory: 'الفئة',
labelDailyRate: 'السعر اليومي',
labelStatus: 'الحالة',
labelSeats: 'المقاعد',
labelTransmission: 'ناقل الحركة',
labelFuelType: 'نوع الوقود',
labelColor: 'اللون',
labelOdometer: 'عداد المسافة',
labelNotes: 'الملاحظات',
makeLabel: 'الماركة *',
selectMake: 'اختر الماركة…',
other: 'أخرى…',
typeMake: 'اكتب الماركة…',
modelLabel: 'الطراز *',
selectModel: 'اختر الطراز…',
typeModel: 'اكتب الطراز…',
yearLabel: 'السنة *',
dailyRateLabel: 'السعر اليومي (درهم) *',
licensePlateLabel: 'رقم اللوحة *',
statusLabel: 'الحالة',
colorLabel: 'اللون',
selectColor: 'اختر اللون…',
typeColor: 'اكتب اللون…',
seatsLabel: 'المقاعد',
transmissionLabel: 'ناقل الحركة',
fuelTypeLabel: 'نوع الوقود',
odometerLabel: 'عداد المسافة (كم)',
notesLabel: 'الملاحظات',
optionalNotes: 'ملاحظات اختيارية…',
optional: 'اختياري…',
makeRequired: 'الماركة مطلوبة.',
modelRequired: 'الطراز مطلوب.',
plateRequired: 'رقم اللوحة مطلوب.',
rateInvalid: 'يرجى إدخال سعر يومي صحيح.',
failedSave: 'فشل الحفظ.',
maintenanceIntro: 'تتبع صيانة هذه المركبة. انقر على سجّل لأي عنصر لتسجيل الخدمة.',
logBtn: 'سجّل',
statusNone: 'غير مسجل',
statusOverdue: 'متأخر',
statusDueSoon: 'قريباً',
statusOk: 'محدّث',
noRecord: 'لا يوجد سجل',
lastPrefix: 'آخر:',
atKm: (km) => `عند ${km} كم`,
duePrefix: '· الموعد:',
orSep: 'أو',
kmLeft: (km) => `${km} كم متبقية`,
overdueByKm: 'تجاوز المسافة',
serviceDateLabel: 'تاريخ الخدمة *',
odometerAtService: 'قراءة العداد عند الخدمة (كم)',
nextDueDateLabel: 'تاريخ الخدمة القادمة',
nextDueKmLabel: 'المسافة عند الخدمة القادمة (كم)',
costLabel: 'التكلفة (درهم)',
maintenanceNotesLabel: 'الملاحظات',
saveLabel: 'حفظ',
savingLabel: 'جارٍ الحفظ…',
dateRequired: 'تاريخ الخدمة مطلوب.',
routineTypes: {
'Oil Change': 'تغيير الزيت',
'Technical Inspection': 'الفحص التقني',
'Vehicle Registration': 'تسجيل المركبة',
'Car Tax': 'الضريبة السنوية',
'Tire Rotation': 'تدوير الإطارات',
'Brake Inspection': 'فحص الفرامل',
'Air Filter': 'فلتر الهواء',
'Cabin Filter': 'فلتر المقصورة',
'Battery Check': 'فحص البطارية',
'Belt / Chain Service': 'خدمة السير / السلسلة',
'Coolant Flush': 'تغيير سائل التبريد',
'Transmission Service': 'خدمة ناقل الحركة',
},
},
calendar: {
heading: 'تقويم التوفر',
blockDates: 'حجب التواريخ',
clickSecondDay: (selected) => `انقر على يوم ثانٍ لتحديد نهاية الحجب (المختار: ${selected})`,
cancelSelection: 'إلغاء',
legendReserved: 'محجوز',
legendMaintenance: 'صيانة',
legendBlocked: 'محجوب',
legendAvailable: 'متاح',
failedLoad: 'فشل تحميل التقويم',
moreDays: (n) => `+${n} أكثر`,
reservationsThisMonth: 'الحجوزات هذا الشهر',
blocksAndMaintenance: 'الحجب والصيانة',
removeBlockConfirm: 'حذف هذا الحجب؟',
failedDeleteBlock: 'فشل حذف الحجب',
modalTitle: 'حجب تواريخ المركبة',
blockTypeLabel: 'نوع الحجب',
manualBlock: 'حجب يدوي',
maintenanceBlock: 'صيانة',
startDate: 'تاريخ البداية',
endDate: 'تاريخ النهاية (غير شامل)',
reasonLabel: 'السبب',
optional: '(اختياري)',
reasonPlaceholderManual: 'مثال: محجوز لشخص مميز، فعالية…',
reasonPlaceholderMaintenance: 'مثال: تغيير الزيت، تدوير الإطارات…',
datesRequired: 'تواريخ البداية والنهاية مطلوبة.',
endAfterStart: 'يجب أن يكون تاريخ النهاية بعد تاريخ البداية.',
failedCreateBlock: 'فشل إنشاء الحجب',
cancel: 'إلغاء',
saveBlock: 'حفظ',
saving: 'جارٍ الحفظ…',
removeBlock: 'حذف الحجب',
dayHeaders: ['أحد', 'اثن', 'ثلا', 'أرب', 'خمي', 'جمع', 'سبت'],
},
dashboardPage: {
failedToLoad: 'فشل تحميل لوحة التحكم',
forbidden: 'ليس لديك صلاحية الوصول إلى هذه الصفحة. يلزم دور المدير أو أعلى.',
kpiTotalBookings: 'إجمالي الحجوزات',
kpiActiveVehicles: 'المركبات النشطة',
kpiMonthlyRevenue: 'الإيرادات الشهرية',
kpiTotalCustomers: 'إجمالي العملاء',
vsLastMonth: 'مقارنة بالشهر الماضي',
bookingSources: 'مصادر الحجز',
barBookings: 'الحجوزات',
barRevenue: 'الإيرادات (درهم)',
noBookingData: 'لا توجد بيانات حجز متاحة بعد',
quickStats: 'إحصائيات سريعة',
noData: 'لا توجد بيانات',
recentReservations: 'الحجوزات الأخيرة',
viewAll: 'عرض الكل ←',
colBookingNum: 'الحجز #',
colCustomer: 'العميل',
colVehicle: 'المركبة',
colDates: 'التواريخ',
colStatus: 'الحالة',
colTotal: 'الإجمالي',
noReservations: 'لا توجد حجوزات بعد. ستظهر الحجوزات هنا عند قيام العملاء بالحجز.',
trialEnds: (date) => `أنت في فترة تجريبية. تنتهي في ${date}.`,
trialActive: 'أنت في فترة تجريبية مجانية.',
pastDueMsg: 'دفعتك متأخرة. يرجى تحديث معلومات الفوترة.',
suspendedMsg: 'تم تعليق حسابك. يرجى التواصل مع الدعم أو تجديد اشتراكك.',
manageBilling: 'إدارة الاشتراك ←',
statusLabels: { CONFIRMED: 'مؤكد', PENDING: 'قيد الانتظار', ACTIVE: 'نشط', COMPLETED: 'مكتمل', CANCELLED: 'ملغى' },
},
reservations: {
heading: 'الحجوزات',
subtitle: 'جميع مصادر الحجز: لوحة التحكم، الموقع العام، والسوق الإلكتروني.',
colCustomer: 'العميل',
colVehicle: 'المركبة',
colDates: 'التواريخ',
colSource: 'المصدر',
colStatus: 'الحالة',
colTotal: 'الإجمالي',
noReservations: 'لا توجد حجوزات بعد.',
loadingReservation: 'جارٍ تحميل الحجز…',
failedToLoad: 'فشل تحميل الحجز',
confirmReservation: 'تأكيد الحجز',
checkInVehicle: 'تسجيل الاستلام',
checkOutVehicle: 'تسجيل الإرجاع',
working: 'جارٍ التنفيذ…',
failedConfirm: 'فشل تأكيد الحجز',
failedCheckIn: 'فشل تسجيل الاستلام',
failedCheckOut: 'فشل تسجيل الإرجاع',
sectionCustomer: 'العميل',
noPhone: 'لم يُدخل رقم هاتف',
licenseLabel: 'الرخصة:',
notCaptured: 'غير مُسجّل',
flaggedBadge: 'مُبلَّغ عنه',
sectionVehicle: 'المركبة',
sectionCharges: 'الرسوم',
chargeDiscount: 'الخصم',
chargeInsurance: 'التأمين',
chargeAdditionalDrivers: 'السائقون الإضافيون',
chargePricingAdjustments: 'تعديلات التسعير',
chargeGrandTotal: 'الإجمالي الكلي',
appliedInsurance: 'التأمين المطبق',
pricingRulesApplied: 'قواعد التسعير المطبقة',
sectionAdditionalDrivers: 'السائقون الإضافيون',
driverLicenseLabel: 'الرخصة:',
driverChargeLabel: 'الرسوم:',
approveBtn: 'موافقة',
approvedBadge: 'مُوافَق عليه',
noApprovalNeeded: 'لا يحتاج موافقة',
noAdditionalDrivers: 'لم يتم إضافة سائقين إضافيين لهذا الحجز.',
failedApproveDriver: 'فشلت الموافقة على السائق الإضافي',
sectionInspectionSummary: 'ملخص الفحص',
checkInInspectionLabel: 'فحص الاستلام',
checkOutInspectionLabel: 'فحص الإرجاع',
savedBadge: 'محفوظ',
pendingBadge: 'معلّق',
inspectionCardTitle: (type) => type === 'CHECKIN' ? 'فحص الاستلام' : 'فحص الإرجاع',
inspectionSubtitle: 'حدد الأضرار الموجودة والمكتشفة حديثاً مباشرة على مخطط المركبة.',
saveInspection: 'حفظ الفحص',
savingInspection: 'جارٍ الحفظ…',
failedSaveInspection: 'فشل حفظ الفحص',
diagramHelp: 'انقر في أي مكان على المخطط لإضافة علامة ضرر بالنوع والخطورة المحددين.',
mileageLabel: 'عداد المسافة',
fuelLevelLabel: 'مستوى الوقود',
fuelLevels: { FULL: 'ممتلئ', SEVEN_EIGHTHS: '7/8', THREE_QUARTERS: '3/4', FIVE_EIGHTHS: '5/8', HALF: '1/2', THREE_EIGHTHS: '3/8', QUARTER: '1/4', ONE_EIGHTH: '1/8', EMPTY: 'فارغ' },
generalConditionLabel: 'الحالة العامة',
employeeNotesLabel: 'ملاحظات الموظف',
customerAcknowledged: 'وافق العميل على هذا الفحص',
damageMarkersHeading: 'علامات الضرر',
removeMarker: 'حذف',
noMarkers: 'لا توجد علامات ضرر محفوظة.',
damageTypeLabels: { SCRATCH: 'خدش', DENT: 'انبعاج', CRACK: 'تشقق', CHIP: 'تقشر', MISSING: 'مفقود', STAIN: 'بقعة', OTHER: 'أخرى' },
severityLabels: { MINOR: 'طفيف', MODERATE: 'متوسط', MAJOR: 'شديد' },
},
},
}
type I18nContextValue = {
language: DashboardLanguage
setLanguage: (value: DashboardLanguage) => void
theme: DashboardTheme
setTheme: (value: DashboardTheme) => void
dict: DashboardDictionary
}
const I18nContext = createContext<I18nContextValue | null>(null)
const DASHBOARD_LANGUAGE_KEY = 'dashboard-language'
function normalizeLanguage(value: string | null | undefined): DashboardLanguage {
return value === 'fr' || value === 'ar' || value === 'en' ? value : 'en'
}
function detectBrowserLanguage(): DashboardLanguage | null {
if (typeof navigator === 'undefined') return null
const langs = Array.from(navigator.languages ?? [navigator.language])
for (const lang of langs) {
const code = lang.split('-')[0].toLowerCase()
if (code === 'fr' || code === 'ar' || code === 'en') return code as DashboardLanguage
}
return null
}
export function DashboardI18nProvider({
children,
initialLanguage = 'en',
}: {
children: React.ReactNode
initialLanguage?: DashboardLanguage
}) {
const [language, setLanguageState] = useState<DashboardLanguage>(initialLanguage)
const [theme, setThemeState] = useState<DashboardTheme>('light')
const themeInitialized = useRef(false)
// Skip the very first write so we don't overwrite a stored preference with the
// server-resolved initialLanguage before the read effect has a chance to apply it.
const skipFirstLangWrite = useRef(true)
function applyLanguage(nextLanguage: DashboardLanguage) {
if (nextLanguage === language) return
if (typeof document !== 'undefined') {
document.documentElement.lang = nextLanguage
document.documentElement.dir = nextLanguage === 'ar' ? 'rtl' : 'ltr'
writeScopedPreference(SHARED_LANGUAGE_KEY, nextLanguage, [DASHBOARD_LANGUAGE_KEY])
document.cookie = `${SHARED_LANGUAGE_COOKIE}=${nextLanguage}; path=/; max-age=31536000; samesite=lax`
document.cookie = `${DASHBOARD_LANGUAGE_KEY}=${nextLanguage}; path=/; max-age=31536000; samesite=lax`
}
setLanguageState(nextLanguage)
}
function applyTheme(nextTheme: DashboardTheme) {
if (nextTheme === theme) return
if (typeof document !== 'undefined') {
document.documentElement.classList.remove('light', 'dark')
document.documentElement.classList.add(nextTheme === 'light' ? 'light' : 'dark')
document.documentElement.style.colorScheme = nextTheme === 'light' ? 'light' : 'dark'
document.body.dataset.theme = nextTheme
writeScopedPreference(SHARED_THEME_KEY, nextTheme, ['dashboard-theme'])
}
setThemeState(nextTheme)
}
useEffect(() => {
const stored = readScopedPreference(SHARED_LANGUAGE_KEY, [DASHBOARD_LANGUAGE_KEY])
if (stored !== null) {
const next = normalizeLanguage(stored)
if (next !== language) setLanguageState(next)
} else {
const detected = detectBrowserLanguage()
if (detected && detected !== language) setLanguageState(detected)
}
}, [])
useEffect(() => {
const storedTheme = readScopedPreference(SHARED_THEME_KEY, ['dashboard-theme'])
if (storedTheme === 'light' || storedTheme === 'dark') {
if (storedTheme !== theme) setThemeState(storedTheme)
return
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches && theme !== 'dark') {
setThemeState('dark')
}
}, [])
useEffect(() => {
document.documentElement.lang = language
document.documentElement.dir = language === 'ar' ? 'rtl' : 'ltr'
if (skipFirstLangWrite.current) {
skipFirstLangWrite.current = false
return
}
writeScopedPreference(SHARED_LANGUAGE_KEY, language, [DASHBOARD_LANGUAGE_KEY])
document.cookie = `${SHARED_LANGUAGE_COOKIE}=${language}; path=/; max-age=31536000; samesite=lax`
document.cookie = `${DASHBOARD_LANGUAGE_KEY}=${language}; path=/; max-age=31536000; samesite=lax`
}, [language])
useEffect(() => {
if (!themeInitialized.current) {
// Skip first run: inline script already applied the correct class; writing 'light' here would flash and corrupt storage.
themeInitialized.current = true
return
}
document.documentElement.classList.remove('light', 'dark')
document.documentElement.classList.add(theme === 'light' ? 'light' : 'dark')
document.documentElement.style.colorScheme = theme === 'light' ? 'light' : 'dark'
document.body.dataset.theme = theme
writeScopedPreference(SHARED_THEME_KEY, theme, ['dashboard-theme'])
}, [theme])
useEffect(() => {
function handleAuthChanged() {
const scopedLanguage = readCurrentUserScopedPreference(SHARED_LANGUAGE_KEY)
const scopedTheme = readCurrentUserScopedPreference(SHARED_THEME_KEY)
if (scopedLanguage) {
// Employee has a stored preference — apply it.
const next = normalizeLanguage(scopedLanguage)
if (next !== language) setLanguageState(next)
} else {
// No scoped pref yet (new login). Promote any base preference that was set
// before login (e.g., language selected on the sign-up page) to the scoped key.
const baseLang = readScopedPreference(SHARED_LANGUAGE_KEY, [DASHBOARD_LANGUAGE_KEY])
const langToWrite = baseLang ? normalizeLanguage(baseLang) : language
if (langToWrite !== language) setLanguageState(langToWrite)
writeScopedPreference(SHARED_LANGUAGE_KEY, langToWrite, [DASHBOARD_LANGUAGE_KEY])
}
if (scopedTheme === 'light' || scopedTheme === 'dark') {
if (scopedTheme !== theme) setThemeState(scopedTheme)
} else {
writeScopedPreference(SHARED_THEME_KEY, theme, ['dashboard-theme'])
}
}
window.addEventListener('rentaldrivego:auth-changed', handleAuthChanged as EventListener)
return () => window.removeEventListener('rentaldrivego:auth-changed', handleAuthChanged as EventListener)
}, [language, theme])
const value = useMemo(
() => ({ language, setLanguage: applyLanguage, theme, setTheme: applyTheme, dict: dictionaries[language] }),
[language, theme],
)
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>
}
export function useDashboardI18n() {
const context = useContext(I18nContext)
if (!context) throw new Error('useDashboardI18n must be used within DashboardI18nProvider')
return context
}
export function DashboardLanguageSwitcher() {
const { language, setLanguage, dict } = useDashboardI18n()
const [embedded, setEmbedded] = useState(false)
useEffect(() => {
setEmbedded(window.self !== window.top)
}, [])
if (embedded) return null
function handleLanguageChange(value: DashboardLanguage) {
setLanguage(value)
apiFetch('/auth/employee/me/language', {
method: 'PATCH',
body: JSON.stringify({ language: value }),
}).catch(() => {})
}
return (
<div className="flex items-center gap-1 rounded-full border border-slate-200 bg-white px-2 py-1 shadow-sm transition-colors dark:border-slate-700 dark:bg-slate-900">
<span className="px-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">
{dict.language}
</span>
{(['en', 'fr', 'ar'] as DashboardLanguage[]).map((value) => {
const active = value === language
return (
<button
key={value}
type="button"
onClick={() => handleLanguageChange(value)}
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
active
? 'bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-950'
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
}`}
>
{value.toUpperCase()}
</button>
)
})}
</div>
)
}
export function DashboardThemeSwitcher() {
const { theme, setTheme, dict } = useDashboardI18n()
const [embedded, setEmbedded] = useState(false)
useEffect(() => {
setEmbedded(window.self !== window.top)
}, [])
if (embedded) return null
return (
<div className="flex items-center gap-1 rounded-full border border-slate-200 bg-white px-2 py-1 shadow-sm transition-colors dark:border-slate-700 dark:bg-slate-900">
<span className="px-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400">
{dict.theme}
</span>
{(['light', 'dark'] as DashboardTheme[]).map((value) => {
const active = value === theme
return (
<button
key={value}
type="button"
onClick={() => setTheme(value)}
className={`rounded-full px-3 py-1.5 text-xs font-semibold transition ${
active
? 'bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-950'
: 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800'
}`}
>
{value === 'light' ? dict.light : dict.dark}
</button>
)
})}
</div>
)
}