- {t("dashboard.kpi.toCollect")}
+ Rubis ce mois
@@ -148,8 +146,8 @@ export function AppSidebar({ rubisThisMonth = 0 }: { rubisThisMonth?: number })
diff --git a/apps/web/src/components/settings/LanguageSwitcher.tsx b/apps/web/src/components/settings/LanguageSwitcher.tsx
deleted file mode 100644
index b27399a..0000000
--- a/apps/web/src/components/settings/LanguageSwitcher.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { useTranslation } from "react-i18next";
-import { toast } from "sonner";
-import { Globe } from "lucide-react";
-
-import { Card } from "@rubis/ui";
-import { Eyebrow } from "@rubis/ui";
-import { setLocale, type Locale } from "@/i18n";
-
-const OPTIONS: Array<{ value: Locale; flag: string; labelKey: string }> = [
- { value: "fr", flag: "🇫🇷", labelKey: "parametres.language.fr" },
- { value: "en", flag: "🇬🇧", labelKey: "parametres.language.en" },
-];
-
-export function LanguageSwitcher() {
- const { t, i18n } = useTranslation();
- const current = (i18n.language?.slice(0, 2) ?? "fr") as Locale;
-
- const handleChange = (locale: Locale) => {
- if (locale === current) return;
- setLocale(locale);
- toast.success(t("parametres.language.saved"));
- };
-
- return (
-
-
-
-
-
-
-
{t("parametres.language.title")}
-
- {t("parametres.language.description")}
-
-
-
- {OPTIONS.map((opt) => {
- const active = opt.value === current;
- return (
-
- );
- })}
-
-
-
-
- );
-}
diff --git a/apps/web/src/i18n/en.ts b/apps/web/src/i18n/en.ts
deleted file mode 100644
index ca8fd33..0000000
--- a/apps/web/src/i18n/en.ts
+++ /dev/null
@@ -1,380 +0,0 @@
-import type { Dict } from "./fr";
-
-export const en: Dict = {
- common: {
- save: "Save",
- saving: "Saving…",
- cancel: "Cancel",
- delete: "Delete",
- edit: "Edit",
- confirm: "Confirm",
- close: "Close",
- back: "Back",
- next: "Next",
- previous: "Previous",
- submit: "Submit",
- loading: "Loading…",
- error: "Error",
- success: "Success",
- yes: "Yes",
- no: "No",
- optional: "optional",
- required: "required",
- search: "Search",
- filter: "Filter",
- add: "Add",
- create: "Create",
- update: "Update",
- open: "Open",
- copy: "Copy",
- copied: "Copied",
- export: "Export",
- import: "Import",
- download: "Download",
- refresh: "Refresh",
- retry: "Retry",
- moreActions: "More actions",
- select: "Select",
- seeAll: "See all",
- seeMore: "See more",
- seeLess: "See less",
- },
- errors: {
- fallbackTitle: "Something went wrong.",
- fallbackBody: "We've logged it. Reload the page to try again.",
- fallbackCta: "Reload",
- networkError: "Network error. Check your connection.",
- unknownError: "An unexpected error occurred.",
- notFoundTitle: "Page not found",
- notFoundBody: "This page doesn't exist or no longer exists.",
- notFoundCta: "Back to dashboard",
- },
- nav: {
- dashboard: "Dashboard",
- factures: "Invoices",
- clients: "Clients",
- plans: "Chase plans",
- insights: "Stats",
- parametres: "Settings",
- logout: "Sign out",
- admin: "Admin",
- adminBlog: "Blog admin",
- skipToContent: "Skip to content",
- },
- account: {
- badge: "Account",
- plan: "Plan",
- upgradeCta: "Upgrade plan",
- settingsLink: "Settings",
- helpLink: "Help",
- },
- auth: {
- login: {
- title: "Welcome back.",
- subtitle: "Sign in to pick up where you left off.",
- emailLabel: "Email",
- emailPlaceholder: "you@company.com",
- passwordLabel: "Password",
- passwordPlaceholder: "•••••••••",
- forgotPassword: "Forgot password?",
- submit: "Sign in",
- submitting: "Signing in…",
- withGoogle: "Continue with Google",
- withMicrosoft: "Continue with Microsoft",
- orDivider: "or",
- noAccount: "Don't have an account yet?",
- signupLink: "Create an account",
- sso: {
- cancelled: "{{provider}} sign-in cancelled.",
- expired: "Session expired. Sign in again.",
- invalidState: "A verification failed. Try again.",
- accountExists: "An account already exists with this email. Sign in first to link your account.",
- unknown: "{{provider}} sign-in failed. Try again.",
- },
- providers: {
- google: "Google",
- microsoft: "Microsoft",
- },
- invalidCredentials: "Email or password incorrect.",
- validation: {
- emailRequired: "Email is required.",
- emailInvalid: "Invalid email.",
- passwordRequired: "Password is required.",
- },
- },
- signup: {
- title: "Create your account.",
- subtitle: "30 days free, no credit card.",
- emailLabel: "Work email",
- emailPlaceholder: "you@company.com",
- passwordLabel: "Password",
- passwordPlaceholder: "At least 10 characters",
- passwordHint: "At least 10 characters. Mix letters and numbers.",
- submit: "Create my account",
- submitting: "Creating…",
- withGoogle: "Sign up with Google",
- withMicrosoft: "Sign up with Microsoft",
- orDivider: "or",
- hasAccount: "Already have an account?",
- loginLink: "Sign in",
- terms: "By creating an account, you accept our <1>Terms of Service1> and <2>Privacy Policy2>.",
- emailTaken: "An account already exists with this email.",
- validation: {
- emailRequired: "Email is required.",
- emailInvalid: "Invalid email.",
- passwordTooShort: "Password must be at least 10 characters.",
- },
- },
- onboarding: {
- stepLabel: "Step {{current}} of {{total}}",
- account: {
- title: "Welcome. A bit about you.",
- subtitle: "To personalise your email signature and your dashboard.",
- firstNameLabel: "First name",
- lastNameLabel: "Last name",
- nextCta: "Continue",
- },
- company: {
- title: "Your company.",
- subtitle: "This info will appear on your reminders. You can edit it later.",
- nameLabel: "Company name",
- sirenLabel: "SIREN (optional)",
- addressLabel: "Address",
- nextCta: "Continue",
- },
- signature: {
- title: "Your email signature.",
- subtitle: "Will appear at the bottom of every reminder. Edit freely.",
- signatureLabel: "Signature",
- signaturePlaceholder: "Best regards,\nYour name",
- finishCta: "Finish",
- finishing: "Setting up…",
- },
- },
- },
- dashboard: {
- title: "Dashboard",
- welcome: "Hi {{name}}.",
- welcomeAnon: "Hi.",
- rubisCount_one: "{{count}} ruby earned",
- rubisCount_other: "{{count}} rubies earned",
- rubisExplain: "≈ <1>{{hours}}1> you didn't spend chasing invoices.",
- kpi: {
- toCollect: "To collect",
- overdue: "Overdue",
- collected: "Collected this month",
- dso: "DSO",
- dsoUnit: "days",
- },
- cta: {
- uploadInvoice: "Upload an invoice",
- createInvoice: "Create an invoice",
- seeAllInvoices: "See all invoices",
- },
- activity: {
- title: "Recent activity",
- empty: "No activity yet. Upload your first invoice to get started.",
- },
- overdue: {
- title: "Overdue invoices",
- empty: "All caught up. Nice work.",
- seeAll: "See all",
- },
- },
- factures: {
- title: "Invoices",
- subtitle_zero: "No invoices yet.",
- subtitle_one: "1 invoice",
- subtitle_other: "{{count}} invoices",
- newCta: "New invoice",
- importCta: "Import",
- empty: {
- title: "No invoices yet.",
- body: "Upload your first invoice to start chasing automatically.",
- uploadCta: "Upload an invoice",
- createCta: "Create a native invoice",
- },
- filters: {
- all: "All",
- pending: "Pending",
- chasing: "Chasing",
- overdue: "Overdue",
- paid: "Paid",
- cancelled: "Cancelled",
- status: "Status",
- search: "Search (number, client, amount)",
- },
- columns: {
- number: "Number",
- client: "Client",
- amount: "Amount",
- dueDate: "Due date",
- status: "Status",
- plan: "Plan",
- nextAction: "Next action",
- },
- status: {
- draft: "Draft",
- pending: "Pending",
- chasing: "Chasing",
- overdue: "Overdue",
- paid: "Paid",
- cancelled: "Cancelled",
- },
- },
- clients: {
- title: "Clients",
- subtitle_zero: "No clients yet.",
- subtitle_one: "1 record",
- subtitle_other: "{{count}} records",
- newCta: "New client",
- empty: {
- title: "All quiet on the client front.",
- body: "Your clients will appear here as soon as you import or create your first invoice.",
- },
- columns: {
- name: "Name",
- email: "Email",
- pendingInvoices: "Open invoices",
- overdueInvoices: "Overdue",
- lastReminder: "Last reminder",
- },
- create: {
- title: "New client",
- nameLabel: "Client name",
- namePlaceholder: "ACME Inc.",
- contactLabel: "Contact name",
- contactPlaceholder: "Jane Doe",
- emailLabel: "Email",
- emailPlaceholder: "accounts@acme.com",
- phoneLabel: "Phone (optional)",
- addressLabel: "Address (optional)",
- submit: "Create client",
- submitting: "Creating…",
- },
- overdueLabel_one: "1 overdue invoice",
- overdueLabel_other: "{{count}} overdue invoices",
- },
- plans: {
- title: "Chase plans",
- subtitle: "Your sending cadences — to chase politely, firmly, or anywhere in between.",
- newCta: "New plan",
- empty: {
- title: "No custom plans yet.",
- body: "You're using the default plans for now. Build a custom one when you're ready.",
- },
- defaultPlans: {
- label: "Default plans",
- },
- customPlans: {
- label: "Your plans",
- },
- columns: {
- name: "Name",
- steps: "Steps",
- tone: "Tone",
- activeInvoices: "Active invoices",
- },
- tone: {
- polite: "Courteous",
- firm: "Firm",
- mixed: "Progressive",
- },
- },
- parametres: {
- title: "Settings",
- subtitle: "Configure your account, your company, your preferences.",
- sections: {
- account: {
- title: "Account",
- description: "Email, password, active sessions.",
- },
- profile: {
- title: "Profile",
- description: "Your name, your email signature.",
- },
- company: {
- title: "Company",
- description: "Name, SIREN, address, bank details.",
- },
- billing: {
- title: "Billing",
- description: "Issuer identity, legal mentions, numbering.",
- },
- branding: {
- title: "Brand",
- description: "Logo and colour on your reminders.",
- },
- subscription: {
- title: "Subscription",
- description: "Plan, payment, Rubis invoices.",
- },
- bank: {
- title: "Bank",
- description: "Bank connection to auto-detect payments (coming soon).",
- },
- preferences: {
- title: "Preferences",
- description: "Language, notifications, time zone.",
- },
- data: {
- title: "Data",
- description: "Export, delete account.",
- },
- },
- language: {
- title: "Interface language",
- description: "Choose the language used throughout the app.",
- label: "Language",
- fr: "Français",
- en: "English",
- saved: "Language updated.",
- },
- profile: {
- firstNameLabel: "First name",
- lastNameLabel: "Last name",
- signatureLabel: "Email signature",
- signatureHint: "Appears at the bottom of every reminder.",
- },
- account: {
- emailLabel: "Email",
- emailHint: "Used for sign-in and notifications.",
- passwordTitle: "Password",
- changePassword: "Change password",
- currentPassword: "Current password",
- newPassword: "New password",
- confirmPassword: "Confirm new password",
- sessionsTitle: "Active sessions",
- logoutAll: "Sign out all sessions",
- },
- dangerZone: {
- title: "Danger zone",
- deleteAccount: "Delete my account",
- deleteConfirm: "This action is irreversible. All your invoices, clients and data will be deleted.",
- },
- },
- insights: {
- title: "Stats",
- subtitle: "Track Rubis's impact on your cash flow.",
- period: {
- label: "Period",
- week: "Week",
- month: "Month",
- quarter: "Quarter",
- year: "Year",
- all: "All",
- },
- kpi: {
- timeSaved: "Time freed up",
- remindersSent: "Reminders sent",
- paidInvoices: "Invoices collected",
- avgDelay: "Average payment delay",
- },
- },
- toasts: {
- saved: "Changes saved.",
- deleted: "Deleted.",
- copied: "Copied to clipboard.",
- error: "Something went wrong. Try again.",
- },
-};
diff --git a/apps/web/src/i18n/fr.ts b/apps/web/src/i18n/fr.ts
deleted file mode 100644
index 37996ae..0000000
--- a/apps/web/src/i18n/fr.ts
+++ /dev/null
@@ -1,386 +0,0 @@
-/**
- * Dictionnaire FR — source de vérité pour les types du module i18n SPA.
- *
- * Convention : clés en kebab-case ou camelCase, regroupées par surface
- * (auth, nav, dashboard, factures, etc.). Placeholders i18next entre {{}}.
- */
-export const fr = {
- common: {
- save: "Enregistrer",
- saving: "Enregistrement…",
- cancel: "Annuler",
- delete: "Supprimer",
- edit: "Modifier",
- confirm: "Confirmer",
- close: "Fermer",
- back: "Retour",
- next: "Suivant",
- previous: "Précédent",
- submit: "Valider",
- loading: "Chargement…",
- error: "Erreur",
- success: "Succès",
- yes: "Oui",
- no: "Non",
- optional: "facultatif",
- required: "obligatoire",
- search: "Rechercher",
- filter: "Filtrer",
- add: "Ajouter",
- create: "Créer",
- update: "Mettre à jour",
- open: "Ouvrir",
- copy: "Copier",
- copied: "Copié",
- export: "Exporter",
- import: "Importer",
- download: "Télécharger",
- refresh: "Rafraîchir",
- retry: "Réessayer",
- moreActions: "Plus d'actions",
- select: "Sélectionner",
- seeAll: "Tout voir",
- seeMore: "Voir plus",
- seeLess: "Voir moins",
- },
- errors: {
- fallbackTitle: "Quelque chose a coincé.",
- fallbackBody: "On a noté, on regarde. Rechargez la page pour réessayer.",
- fallbackCta: "Recharger",
- networkError: "Erreur réseau. Vérifiez votre connexion.",
- unknownError: "Une erreur inattendue s'est produite.",
- notFoundTitle: "Page introuvable",
- notFoundBody: "Cette page n'existe pas ou plus.",
- notFoundCta: "Retour au tableau de bord",
- },
- nav: {
- dashboard: "Tableau de bord",
- factures: "Factures",
- clients: "Clients",
- plans: "Plans de relance",
- insights: "Statistiques",
- parametres: "Paramètres",
- logout: "Se déconnecter",
- admin: "Admin",
- adminBlog: "Blog admin",
- skipToContent: "Aller au contenu",
- },
- account: {
- badge: "Compte",
- plan: "Plan",
- upgradeCta: "Passer au plan supérieur",
- settingsLink: "Paramètres",
- helpLink: "Aide",
- },
- auth: {
- login: {
- title: "Bon retour.",
- subtitle: "Connectez-vous pour reprendre où vous en étiez.",
- emailLabel: "Email",
- emailPlaceholder: "vous@entreprise.fr",
- passwordLabel: "Mot de passe",
- passwordPlaceholder: "•••••••••",
- forgotPassword: "Mot de passe oublié ?",
- submit: "Se connecter",
- submitting: "Connexion…",
- withGoogle: "Continuer avec Google",
- withMicrosoft: "Continuer avec Microsoft",
- orDivider: "ou",
- noAccount: "Pas encore de compte ?",
- signupLink: "Créer un compte",
- sso: {
- cancelled: "Connexion {{provider}} annulée.",
- expired: "Session expirée. Reconnectez-vous.",
- invalidState: "Une vérification a échoué. Réessayez.",
- accountExists: "Un compte existe déjà avec cet email. Connectez-vous d'abord pour lier votre compte.",
- unknown: "La connexion {{provider}} a échoué. Réessayez.",
- },
- providers: {
- google: "Google",
- microsoft: "Microsoft",
- },
- invalidCredentials: "Email ou mot de passe incorrect.",
- validation: {
- emailRequired: "L'email est obligatoire.",
- emailInvalid: "Email invalide.",
- passwordRequired: "Le mot de passe est obligatoire.",
- },
- },
- signup: {
- title: "Créer votre compte.",
- subtitle: "30 jours gratuits, sans carte bancaire.",
- emailLabel: "Email professionnel",
- emailPlaceholder: "vous@entreprise.fr",
- passwordLabel: "Mot de passe",
- passwordPlaceholder: "Au moins 10 caractères",
- passwordHint: "Au moins 10 caractères, mélangez lettres et chiffres.",
- submit: "Créer mon compte",
- submitting: "Création…",
- withGoogle: "S'inscrire avec Google",
- withMicrosoft: "S'inscrire avec Microsoft",
- orDivider: "ou",
- hasAccount: "Déjà un compte ?",
- loginLink: "Se connecter",
- terms: "En créant un compte, vous acceptez nos <1>Conditions générales1> et notre <2>politique de confidentialité2>.",
- emailTaken: "Un compte existe déjà avec cet email.",
- validation: {
- emailRequired: "L'email est obligatoire.",
- emailInvalid: "Email invalide.",
- passwordTooShort: "Le mot de passe doit faire au moins 10 caractères.",
- },
- },
- onboarding: {
- stepLabel: "Étape {{current}} sur {{total}}",
- account: {
- title: "Bienvenue. Quelques infos sur vous.",
- subtitle: "Pour personnaliser votre signature email et votre dashboard.",
- firstNameLabel: "Prénom",
- lastNameLabel: "Nom",
- nextCta: "Continuer",
- },
- company: {
- title: "Votre entreprise.",
- subtitle: "Ces infos apparaîtront sur vos relances. Vous pourrez les modifier plus tard.",
- nameLabel: "Nom de l'entreprise",
- sirenLabel: "SIREN (facultatif)",
- addressLabel: "Adresse",
- nextCta: "Continuer",
- },
- signature: {
- title: "Votre signature d'email.",
- subtitle: "Apparaîtra en bas de chaque relance. Vous pouvez l'éditer librement.",
- signatureLabel: "Signature",
- signaturePlaceholder: "Cordialement,\nVotre nom",
- finishCta: "Terminer",
- finishing: "Configuration…",
- },
- },
- },
- dashboard: {
- title: "Tableau de bord",
- welcome: "Bonjour {{name}}.",
- welcomeAnon: "Bonjour.",
- rubisCount_one: "{{count}} rubis gagné",
- rubisCount_other: "{{count}} rubis gagnés",
- rubisExplain: "≈ <1>{{hours}}1> que vous n'avez pas passées à relancer.",
- kpi: {
- toCollect: "À encaisser",
- overdue: "En retard",
- collected: "Encaissé ce mois",
- dso: "DSO",
- dsoUnit: "jours",
- },
- cta: {
- uploadInvoice: "Importer une facture",
- createInvoice: "Créer une facture",
- seeAllInvoices: "Voir toutes les factures",
- },
- activity: {
- title: "Activité récente",
- empty: "Pas encore d'activité. Importez votre première facture pour démarrer.",
- },
- overdue: {
- title: "Factures en retard",
- empty: "Tout est à jour. Bravo.",
- seeAll: "Voir tout",
- },
- },
- factures: {
- title: "Factures",
- subtitle_zero: "Aucune facture pour l'instant.",
- subtitle_one: "1 facture",
- subtitle_other: "{{count}} factures",
- newCta: "Nouvelle facture",
- importCta: "Importer",
- empty: {
- title: "Aucune facture pour l'instant.",
- body: "Importez votre première facture pour commencer à relancer automatiquement.",
- uploadCta: "Importer une facture",
- createCta: "Créer une facture native",
- },
- filters: {
- all: "Toutes",
- pending: "En attente",
- chasing: "En relance",
- overdue: "En retard",
- paid: "Payées",
- cancelled: "Annulées",
- status: "Statut",
- search: "Rechercher (numéro, client, montant)",
- },
- columns: {
- number: "Numéro",
- client: "Client",
- amount: "Montant",
- dueDate: "Échéance",
- status: "Statut",
- plan: "Plan",
- nextAction: "Prochaine action",
- },
- status: {
- draft: "Brouillon",
- pending: "En attente",
- chasing: "En relance",
- overdue: "En retard",
- paid: "Payée",
- cancelled: "Annulée",
- },
- },
- clients: {
- title: "Clients",
- subtitle_zero: "Aucun client pour l'instant.",
- subtitle_one: "1 fiche",
- subtitle_other: "{{count}} fiches",
- newCta: "Nouveau client",
- empty: {
- title: "Tout est calme côté clients.",
- body: "Vos clients apparaîtront ici dès que vous importerez ou créerez votre première facture.",
- },
- columns: {
- name: "Nom",
- email: "Email",
- pendingInvoices: "Factures en cours",
- overdueInvoices: "En retard",
- lastReminder: "Dernière relance",
- },
- create: {
- title: "Nouveau client",
- nameLabel: "Nom du client",
- namePlaceholder: "ACME SAS",
- contactLabel: "Nom du contact",
- contactPlaceholder: "Jean Dupont",
- emailLabel: "Email",
- emailPlaceholder: "compta@acme.fr",
- phoneLabel: "Téléphone (facultatif)",
- addressLabel: "Adresse (facultatif)",
- submit: "Créer le client",
- submitting: "Création…",
- },
- overdueLabel_one: "1 facture en retard",
- overdueLabel_other: "{{count}} factures en retard",
- },
- plans: {
- title: "Plans de relance",
- subtitle: "Vos cadences d'envoi pour relancer poliment, fermement, ou tout entre les deux.",
- newCta: "Nouveau plan",
- empty: {
- title: "Aucun plan personnalisé.",
- body: "Vous utilisez encore les plans fournis par défaut. Créez-en un sur mesure quand vous serez prêt.",
- },
- defaultPlans: {
- label: "Plans par défaut",
- },
- customPlans: {
- label: "Vos plans",
- },
- columns: {
- name: "Nom",
- steps: "Étapes",
- tone: "Ton",
- activeInvoices: "Factures actives",
- },
- tone: {
- polite: "Courtois",
- firm: "Ferme",
- mixed: "Progressif",
- },
- },
- parametres: {
- title: "Paramètres",
- subtitle: "Configurez votre compte, votre entreprise, vos préférences.",
- sections: {
- account: {
- title: "Compte",
- description: "Email, mot de passe, sessions actives.",
- },
- profile: {
- title: "Profil",
- description: "Votre nom, votre signature email.",
- },
- company: {
- title: "Entreprise",
- description: "Nom, SIREN, adresse, RIB.",
- },
- billing: {
- title: "Facturation",
- description: "Identité émetteur, mentions légales, numérotation.",
- },
- branding: {
- title: "Marque",
- description: "Logo et couleur sur vos relances.",
- },
- subscription: {
- title: "Abonnement",
- description: "Plan, paiement, factures Rubis.",
- },
- bank: {
- title: "Banque",
- description: "Connexion bancaire pour détecter les paiements (bientôt).",
- },
- preferences: {
- title: "Préférences",
- description: "Langue, notifications, fuseau horaire.",
- },
- data: {
- title: "Données",
- description: "Export, suppression de compte.",
- },
- },
- language: {
- title: "Langue de l'interface",
- description: "Choisissez la langue utilisée dans toute l'application.",
- label: "Langue",
- fr: "Français",
- en: "English",
- saved: "Langue mise à jour.",
- },
- profile: {
- firstNameLabel: "Prénom",
- lastNameLabel: "Nom",
- signatureLabel: "Signature email",
- signatureHint: "Apparaît en bas de chaque relance.",
- },
- account: {
- emailLabel: "Email",
- emailHint: "Utilisé pour la connexion et les notifications.",
- passwordTitle: "Mot de passe",
- changePassword: "Changer le mot de passe",
- currentPassword: "Mot de passe actuel",
- newPassword: "Nouveau mot de passe",
- confirmPassword: "Confirmer le nouveau mot de passe",
- sessionsTitle: "Sessions actives",
- logoutAll: "Déconnecter toutes les sessions",
- },
- dangerZone: {
- title: "Zone dangereuse",
- deleteAccount: "Supprimer mon compte",
- deleteConfirm: "Cette action est irréversible. Toutes vos factures, clients et données seront supprimés.",
- },
- },
- insights: {
- title: "Statistiques",
- subtitle: "Suivez l'impact de Rubis sur votre trésorerie.",
- period: {
- label: "Période",
- week: "Semaine",
- month: "Mois",
- quarter: "Trimestre",
- year: "Année",
- all: "Tout",
- },
- kpi: {
- timeSaved: "Temps libéré",
- remindersSent: "Relances envoyées",
- paidInvoices: "Factures encaissées",
- avgDelay: "Délai moyen de paiement",
- },
- },
- toasts: {
- saved: "Modifications enregistrées.",
- deleted: "Supprimé.",
- copied: "Copié dans le presse-papiers.",
- error: "Une erreur s'est produite. Réessayez.",
- },
-};
-
-export type Dict = typeof fr;
diff --git a/apps/web/src/i18n/index.ts b/apps/web/src/i18n/index.ts
deleted file mode 100644
index a5949b3..0000000
--- a/apps/web/src/i18n/index.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * Setup i18next + react-i18next pour l'app SPA.
- *
- * Locale détectée dans l'ordre :
- * 1. localStorage["rubis:locale"] (préférence explicite de l'user)
- * 2. navigator.language (fallback intelligent au 1er load)
- * 3. DEFAULT_LOCALE = "fr"
- *
- * Le module exporte `i18n` (instance) et `setLocale(locale)` qui combine
- * `i18n.changeLanguage` + persistance localStorage + mise à jour ``.
- */
-import i18n from "i18next";
-import { initReactI18next } from "react-i18next";
-
-import { fr } from "./fr";
-import { en } from "./en";
-import { DEFAULT_LOCALE, LOCALES, STORAGE_KEY, isLocale, type Locale } from "./types";
-
-function detectInitialLocale(): Locale {
- if (typeof window === "undefined") return DEFAULT_LOCALE;
- const stored = window.localStorage.getItem(STORAGE_KEY);
- if (isLocale(stored)) return stored;
- const browser = window.navigator.language?.slice(0, 2).toLowerCase();
- if (isLocale(browser)) return browser;
- return DEFAULT_LOCALE;
-}
-
-const initialLocale = detectInitialLocale();
-
-void i18n.use(initReactI18next).init({
- resources: {
- fr: { translation: fr },
- en: { translation: en },
- },
- lng: initialLocale,
- fallbackLng: DEFAULT_LOCALE,
- supportedLngs: [...LOCALES],
- interpolation: {
- escapeValue: false,
- },
- returnNull: false,
-});
-
-if (typeof document !== "undefined") {
- document.documentElement.lang = initialLocale;
-}
-
-export function setLocale(locale: Locale): void {
- void i18n.changeLanguage(locale);
- if (typeof window !== "undefined") {
- window.localStorage.setItem(STORAGE_KEY, locale);
- }
- if (typeof document !== "undefined") {
- document.documentElement.lang = locale;
- }
-}
-
-export function getCurrentLocale(): Locale {
- return isLocale(i18n.language) ? i18n.language : DEFAULT_LOCALE;
-}
-
-export { i18n, DEFAULT_LOCALE, LOCALES };
-export type { Locale };
diff --git a/apps/web/src/i18n/types.ts b/apps/web/src/i18n/types.ts
deleted file mode 100644
index 6b34dcf..0000000
--- a/apps/web/src/i18n/types.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * Type-safe i18n pour l'app SPA Rubis.
- *
- * Convention identique à apps/landing : FR fait foi (`Dict` inféré de fr.ts),
- * EN doit matcher la même shape (TS le force via la signature de `en`).
- */
-export type Locale = "fr" | "en";
-
-export const LOCALES: readonly Locale[] = ["fr", "en"] as const;
-
-export const DEFAULT_LOCALE: Locale = "fr";
-
-export const STORAGE_KEY = "rubis:locale";
-
-export function isLocale(value: unknown): value is Locale {
- return typeof value === "string" && (LOCALES as readonly string[]).includes(value);
-}
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
index e4df1d5..7627029 100644
--- a/apps/web/src/main.tsx
+++ b/apps/web/src/main.tsx
@@ -1,14 +1,11 @@
// Sentry init AVANT tout autre import non-essentiel pour capturer même
// les erreurs de bootstrap (cf. apps/web/src/lib/sentry.ts).
import "./lib/sentry";
-// i18n init AVANT le 1er render pour que les chaînes soient résolues dès le bootstrap.
-import "./i18n";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { useTranslation } from "react-i18next";
import * as Sentry from "@sentry/react";
import { routeTree } from "./routeTree.gen";
@@ -88,17 +85,16 @@ async function bootstrapSession(): Promise
{
}
function FallbackError() {
- const { t } = useTranslation();
return (
-
{t("errors.fallbackTitle")}
-
{t("errors.fallbackBody")}
+
Quelque chose a coincé.
+
On a noté, on regarde. Rechargez la page pour réessayer.
diff --git a/apps/web/src/routes/_app/index.tsx b/apps/web/src/routes/_app/index.tsx
index 65a3bf8..d145385 100644
--- a/apps/web/src/routes/_app/index.tsx
+++ b/apps/web/src/routes/_app/index.tsx
@@ -1,6 +1,5 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
-import { useTranslation } from "react-i18next";
import { Camera, Plus, ArrowDownRight, ArrowRight } from "lucide-react";
import { api } from "@/lib/api";
@@ -75,7 +74,6 @@ export const Route = createFileRoute("/_app/")({
function DashboardPage() {
const manual = useManualInvoice();
- const { t } = useTranslation();
const { data: kpis } = useQuery({
queryKey: queryKeys.dashboard.kpis(),
queryFn: () => api.get("/api/v1/dashboard/kpis"),
@@ -102,7 +100,7 @@ function DashboardPage() {
@@ -122,29 +120,29 @@ function DashboardPage() {
/>
{t("dashboard.kpi.dso")}
+ DSO
}
- value={`${kpis?.dsoDays ?? 0} ${t("dashboard.kpi.dsoUnit")}`}
+ value={`${kpis?.dsoDays ?? 0} jours`}
intent={kpis && kpis.dsoDeltaDays < 0 ? "positive" : "neutral"}
/>
@@ -164,7 +162,7 @@ function DashboardPage() {
to="/insights"
className="text-[12px] text-rubis hover:underline underline-offset-4"
>
- {t("common.seeMore")} →
+ Voir plus →