From fc9189433628467be00cc543b49441921e784771 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Sun, 8 Mar 2026 23:08:37 -0400 Subject: [PATCH] Refactor localization handling across components - Updated LocalizationContext to consolidate date and time formatting functions into a single `fmt` object. - Modified ChoreCardHelpers to accept a time format parameter for due date text. - Adjusted ChoreView, RepeatSection, TimePassedCard, and other components to utilize the new `fmt` object for date and time formatting. - Enhanced HistoryCard and TimerEditModal to use the new localization functions for consistent date and time display. - Refined Settings and CircleSettings to leverage the updated localization methods for subscription and member date displays. - Improved ThingsHistory and UserActivities to format dates and times using the new localization structure. - Ensured CalendarCard and CalendarDual components utilize the new formatting functions for better readability. --- public/locales/fr/chores.json | 75 ++++++ public/locales/fr/settings.json | 174 ++++++++++++ public/locales/nl/chores.json | 75 ++++++ public/locales/nl/settings.json | 174 ++++++++++++ src/contexts/LocalizationContext.jsx | 36 ++- src/utils/ChoreCardHelpers.jsx | 19 +- src/views/ChoreEdit/ChoreView.jsx | 4 +- src/views/ChoreEdit/RepeatSection.jsx | 8 +- src/views/ChoreEdit/TimePassedCard.jsx | 17 +- src/views/Chores/ChoreCard.jsx | 4 +- src/views/Chores/CompactChoreCard.jsx | 4 +- src/views/History/ChoreHistory.jsx | 4 +- src/views/History/HistoryCard.jsx | 8 +- src/views/Modals/Inputs/TimerEditModal.jsx | 8 +- src/views/Settings/APITokenSettings.jsx | 4 +- src/views/Settings/AccountSettings.jsx | 14 +- src/views/Settings/CircleSettings.jsx | 8 +- src/views/Settings/LocalizationSettings.jsx | 280 ++++++++++---------- src/views/Settings/Settings.jsx | 14 +- src/views/Settings/SettingsOverview.jsx | 45 +++- src/views/Things/ThingsHistory.jsx | 12 +- src/views/Timer/TimerDetails.jsx | 12 +- src/views/User/UserActivities.jsx | 14 +- src/views/components/CalendarCard.jsx | 6 +- src/views/components/CalendarDual.jsx | 7 +- src/views/components/SubTask.jsx | 4 +- 26 files changed, 785 insertions(+), 245 deletions(-) create mode 100644 public/locales/fr/chores.json create mode 100644 public/locales/fr/settings.json create mode 100644 public/locales/nl/chores.json create mode 100644 public/locales/nl/settings.json diff --git a/public/locales/fr/chores.json b/public/locales/fr/chores.json new file mode 100644 index 0000000..38a6911 --- /dev/null +++ b/public/locales/fr/chores.json @@ -0,0 +1,75 @@ +{ + "title": "Tâches", + "myChores": "Mes Tâches", + "allChores": "Toutes les Tâches", + "addChore": "Ajouter une Tâche", + "editChore": "Modifier la Tâche", + "deleteChore": "Supprimer la Tâche", + "completeChore": "Terminer la Tâche", + "dueDate": "Date d'Échéance", + "assignedTo": "Assigné à", + "priority": "Priorité", + "status": "Statut", + "description": "Description", + "choreView": { + "assignment": "Attribution", + "assigned": "Assigné", + "last": "Dernier", + "schedule": "Calendrier", + "due": "Échéance", + "statistics": "Statistiques", + "completed": "Terminé", + "times": "fois", + "details": "Détails", + "createdBy": "Créé par", + "na": "N/A", + "taskCompleted": "Tâche Terminée", + "taskCompletedMessage": "Votre tâche a été marquée comme terminée", + "taskCompletionUndone": "La complétion de la tâche a été annulée.", + "taskSkipUndone": "Le saut de tâche a été annulé.", + "undoSuccessful": "Annulation Réussie", + "undoFailed": "Annulation Échouée", + "undoFailedMessage": "Impossible d'annuler l'action. Veuillez réessayer.", + "resetTimer": "Réinitialiser le Minuteur", + "resetTimerConfirmation": "Êtes-vous sûr de vouloir réinitialiser le minuteur ? Cela effacera tous les enregistrements de temps depuis le début de la tâche.", + "clearAllTimeRecords": "Effacer Tous les Enregistrements de Temps", + "clearAllTimeConfirmation": "Cela supprimera définitivement tous les minuteurs de cette tâche et la remettra à l'état \"non démarrée\".", + "descriptionTitle": "Description", + "description": "Description :", + "previousNote": "Note Précédente", + "previousNoteLabel": "Note précédente :", + "subtasksLabel": "Sous-tâches :", + "taskActions": "Actions de Tâche", + "addNote": "Ajouter une note", + "additionalNotes": "Notes Supplémentaires :", + "notePlaceholder": "Ajouter une note sur la complétion...", + "setCustomCompletionTime": "Définir une heure de complétion personnalisée", + "skipTask": "Passer la Tâche", + "skipTaskConfirmation": "Êtes-vous sûr de vouloir passer cette tâche ?", + "markComplete": "Marquer comme Terminée", + "markAsDone": "Marquer comme fait", + "edit": "Modifier", + "archive": "Archiver", + "unarchive": "Désarchiver", + "viewHistory": "Voir l'Historique", + "history": "Historique", + "startTimer": "Démarrer le Minuteur", + "start": "Démarrer", + "pauseTimer": "Mettre en Pause le Minuteur", + "approve": "Approuver", + "reject": "Rejeter", + "pendingApproval": "En Attente d'Approbation", + "undo": "Annuler", + "skip": "Passer", + "cancel": "Annuler", + "noPriority": "Aucune Priorité", + "subtasks": "Sous-tâches", + "noDescription": "Aucune description disponible", + "timer": { + "active": "Minuteur Actif", + "paused": "Minuteur en Pause", + "reset": "Réinitialiser le Minuteur", + "delete": "Supprimer la Session" + } + } +} diff --git a/public/locales/fr/settings.json b/public/locales/fr/settings.json new file mode 100644 index 0000000..b747bd1 --- /dev/null +++ b/public/locales/fr/settings.json @@ -0,0 +1,174 @@ +{ + "title": "Paramètres", + "circleSettings": { + "title": "Paramètres du cercle", + "description": "Votre compte est automatiquement connecté à un Cercle lorsque vous en créez un ou en rejoignez un. Invitez facilement des amis en partageant le code unique du Cercle ou le lien ci-dessous.", + "circleCode": "Code du Cercle", + "copyCode": "Copier le Code", + "copyLink": "Copier le Lien", + "codeCopied": "Code du cercle copié !", + "linkCopied": "Lien copié !", + "joinCircle": "Rejoindre un Cercle", + "joinCirclePlaceholder": "Entrer le Code du Cercle", + "join": "Rejoindre", + "leave": "Quitter le Cercle", + "leaveConfirmTitle": "Quitter le Cercle", + "leaveConfirmMessage": "Êtes-vous sûr de vouloir quitter ce cercle ?", + "circleMembers": "Membres du Cercle", + "circleMemberRequests": "Demandes de Membres du Cercle", + "admin": "Administrateur", + "member": "Membre", + "pending": "En attente", + "accept": "Accepter", + "reject": "Rejeter", + "makeAdmin": "Nommer Administrateur", + "makeMember": "Nommer Membre", + "remove": "Supprimer", + "webhookURL": "URL du Webhook", + "webhookDescription": "Entrez une URL de webhook pour recevoir des notifications pour les événements du cercle", + "webhookPlaceholder": "https://votre-url-webhook.com" + }, + "accountSettings": { + "title": "Paramètres du Compte", + "subscription": "Abonnement", + "subscriptionStatus": "Plan Actuel", + "free": "Gratuit", + "plus": "Plus", + "upgrade": "Mettre à Niveau", + "cancel": "Annuler", + "changePassword": "Changer le Mot de Passe", + "password": "Mot de passe", + "dangerZone": "Zone Dangereuse", + "dangerZoneDescription": "Une fois votre compte supprimé, il n'y a pas de retour en arrière. Veuillez être certain.", + "deleteAccount": "Supprimer le Compte" + }, + "localization": { + "title": "Localisation", + "description": "Personnalisez la langue, le format de date et les préférences régionales pour votre compte.", + "language": "Langue", + "languageDescription": "Sélectionnez votre langue préférée", + "dateFormat": "Format de Date", + "dateFormatDescription": "Choisissez comment les dates doivent être affichées dans l'application", + "timeFormat": "Format de l'Heure", + "timeFormatDescription": "Sélectionnez le format 12 heures ou 24 heures", + "12hour": "12 heures (AM/PM)", + "24hour": "24 heures", + "firstDayOfWeek": "Premier Jour de la Semaine", + "firstDayOfWeekDescription": "Sélectionnez quel jour commence votre semaine", + "sunday": "Dimanche", + "monday": "Lundi", + "saturday": "Samedi", + "formats": { + "mdy": "MM/JJ/AAAA (États-Unis)", + "dmy": "JJ/MM/AAAA (Europe)", + "ymd": "AAAA-MM-JJ (ISO)", + "long": "Format long (ex., 1 janvier 2024)", + "short": "Format court (ex., 1 janv. 2024)" + } + }, + "sidepanel": { + "title": "Personnalisation du Panneau Latéral", + "description": "Personnalisez la disposition et la visibilité des cartes dans le panneau latéral. Cette section n'est disponible que sur les grands écrans comme les tablettes et les ordinateurs de bureau." + }, + "theme": { + "title": "Préférences de thème", + "description": "Choisissez comment le site vous apparaît. Sélectionnez un thème unique ou synchronisez avec votre système pour basculer automatiquement entre les thèmes jour et nuit.", + "themeMode": "Mode de thème", + "light": "Clair", + "dark": "Sombre", + "system": "Système" + }, + "notifications": { + "settingsSaved": "Paramètres enregistrés avec succès", + "settingsSaveFailed": "Échec de l'enregistrement des paramètres", + "invalidWebhook": "URL de webhook invalide" + }, + "profile": { + "title": "Paramètres du Profil", + "description": "Mettez à jour votre nom d'affichage et votre photo de profil.", + "photoUpdated": "Photo Mise à Jour", + "photoUpdatedMessage": "Votre photo de profil a été mise à jour avec succès !", + "uploadFailed": "Échec du Téléchargement", + "uploadFailedMessage": "Échec du téléchargement de votre photo. Veuillez réessayer.", + "profileUpdated": "Profil Mis à Jour", + "profileUpdatedMessage": "Vos informations de profil ont été enregistrées avec succès !", + "updateFailed": "Échec de la Mise à Jour", + "updateFailedMessage": "Impossible de mettre à jour votre profil. Veuillez vérifier votre connexion et réessayer.", + "changePhoto": "Changer la Photo", + "displayName": "Nom d'Affichage", + "displayNamePlaceholder": "Entrez votre nom d'affichage", + "timezone": "Fuseau Horaire", + "timezonePlaceholder": "Sélectionnez votre fuseau horaire", + "save": "Enregistrer", + "cancel": "Annuler" + }, + "overview": { + "title": "Paramètres", + "subtitle": "Personnalisez votre expérience et gérez les préférences de votre compte", + "upgrade": { + "title": "Passer à Plus", + "description": "Débloquez des fonctionnalités puissantes pour améliorer votre productivité", + "button": "Mettre à Niveau Maintenant", + "features": { + "richText": "Descriptions en texte enrichi", + "notifications": "Notifications de tâches", + "apiIntegrations": "Intégrations API", + "advancedAutomation": "Automatisation avancée" + } + }, + "sections": { + "profile": { + "title": "Paramètres du Profil", + "description": "Mettez à jour vos informations de profil, photo, nom d'affichage et préférences de fuseau horaire." + }, + "circle": { + "title": "Paramètres du Cercle", + "description": "Gérez votre cercle, invitez des membres et gérez les demandes d'adhésion." + }, + "account": { + "title": "Paramètres du Compte", + "description": "Gérez votre abonnement, changez le mot de passe et les options de suppression de compte." + }, + "subaccounts": { + "title": "Comptes Gérés", + "description": "Créez et gérez des sous-comptes pour vous connecter et accomplir les tâches assignées." + }, + "notifications": { + "title": "Notifications", + "description": "Configurez les notifications push, les alertes par e-mail et les cibles de notification pour les tâches." + }, + "mfa": { + "title": "Authentification Multi-Facteurs", + "description": "Ajoutez une couche de sécurité supplémentaire avec MFA en utilisant des applications d'authentification." + }, + "apitokens": { + "title": "Jetons API", + "description": "Générez et gérez des jetons d'accès pour les intégrations tierces et l'accès à l'API." + }, + "storage": { + "title": "Paramètres de Stockage", + "description": "Sauvegardez et restaurez vos données, gérez le stockage local et les préférences de synchronisation." + }, + "sidepanel": { + "title": "Personnalisation du Panneau Latéral", + "description": "Personnalisez la disposition et la visibilité des cartes dans l'interface du panneau latéral." + }, + "theme": { + "title": "Préférences de Thème", + "description": "Choisissez votre thème préféré et configurez les paramètres de mode sombre/clair." + }, + "localization": { + "title": "Localisation", + "description": "Personnalisez la langue, le format de date, le format de l'heure et les préférences régionales." + }, + "advanced": { + "title": "Paramètres Avancés", + "description": "Configurez les webhooks, les mises à jour en temps réel et d'autres fonctionnalités avancées pour améliorer la productivité." + }, + "developer": { + "title": "Paramètres Développeur", + "description": "Consultez les informations techniques sur les jetons d'authentification, les connexions SSE et les données de débogage." + } + } + } +} diff --git a/public/locales/nl/chores.json b/public/locales/nl/chores.json new file mode 100644 index 0000000..04569df --- /dev/null +++ b/public/locales/nl/chores.json @@ -0,0 +1,75 @@ +{ + "title": "Taken", + "myChores": "Mijn Taken", + "allChores": "Alle Taken", + "addChore": "Taak Toevoegen", + "editChore": "Taak Bewerken", + "deleteChore": "Taak Verwijderen", + "completeChore": "Taak Voltooien", + "dueDate": "Vervaldatum", + "assignedTo": "Toegewezen aan", + "priority": "Prioriteit", + "status": "Status", + "description": "Beschrijving", + "choreView": { + "assignment": "Toewijzing", + "assigned": "Toegewezen", + "last": "Laatste", + "schedule": "Schema", + "due": "Vervaldatum", + "statistics": "Statistieken", + "completed": "Voltooid", + "times": "keer", + "details": "Details", + "createdBy": "Aangemaakt door", + "na": "N/B", + "taskCompleted": "Taak Voltooid", + "taskCompletedMessage": "Uw taak is gemarkeerd als voltooid", + "taskCompletionUndone": "De voltooiing van de taak is ongedaan gemaakt.", + "taskSkipUndone": "Het overslaan van de taak is ongedaan gemaakt.", + "undoSuccessful": "Ongedaan Maken Geslaagd", + "undoFailed": "Ongedaan Maken Mislukt", + "undoFailedMessage": "Kan de actie niet ongedaan maken. Probeer het opnieuw.", + "resetTimer": "Timer Resetten", + "resetTimerConfirmation": "Weet u zeker dat u de timer wilt resetten? Dit wist alle tijdregistraties sinds u de taak bent begonnen.", + "clearAllTimeRecords": "Alle Tijdregistraties Wissen", + "clearAllTimeConfirmation": "Dit verwijdert permanent alle timers voor deze taak en zet hem terug naar \"niet gestart\".", + "descriptionTitle": "Beschrijving", + "description": "Beschrijving :", + "previousNote": "Vorige Notitie", + "previousNoteLabel": "Vorige notitie:", + "subtasksLabel": "Subtaken :", + "taskActions": "Taakacties", + "addNote": "Een notitie toevoegen", + "additionalNotes": "Aanvullende Notities:", + "notePlaceholder": "Voeg een notitie toe over de voltooiing...", + "setCustomCompletionTime": "Aangepaste voltooitijd instellen", + "skipTask": "Taak Overslaan", + "skipTaskConfirmation": "Weet u zeker dat u deze taak wilt overslaan?", + "markComplete": "Markeren als Voltooid", + "markAsDone": "Markeren als gedaan", + "edit": "Bewerken", + "archive": "Archiveren", + "unarchive": "Dearchiveren", + "viewHistory": "Geschiedenis Bekijken", + "history": "Geschiedenis", + "startTimer": "Timer Starten", + "start": "Starten", + "pauseTimer": "Timer Pauzeren", + "approve": "Goedkeuren", + "reject": "Afwijzen", + "pendingApproval": "In Afwachting van Goedkeuring", + "undo": "Ongedaan Maken", + "skip": "Overslaan", + "cancel": "Annuleren", + "noPriority": "Geen Prioriteit", + "subtasks": "Subtaken", + "noDescription": "Geen beschrijving beschikbaar", + "timer": { + "active": "Timer Actief", + "paused": "Timer Gepauzeerd", + "reset": "Timer Resetten", + "delete": "Sessie Verwijderen" + } + } +} diff --git a/public/locales/nl/settings.json b/public/locales/nl/settings.json new file mode 100644 index 0000000..20f6ba2 --- /dev/null +++ b/public/locales/nl/settings.json @@ -0,0 +1,174 @@ +{ + "title": "Instellingen", + "circleSettings": { + "title": "Cirkelinstellingen", + "description": "Uw account wordt automatisch verbonden met een Cirkel wanneer u er een aanmaakt of lid wordt. Nodig eenvoudig vrienden uit door de unieke Cirkelcode of onderstaande link te delen.", + "circleCode": "Cirkelcode", + "copyCode": "Code Kopiëren", + "copyLink": "Link Kopiëren", + "codeCopied": "Cirkelcode gekopieerd!", + "linkCopied": "Link gekopieerd!", + "joinCircle": "Lid Worden van een Cirkel", + "joinCirclePlaceholder": "Voer Cirkelcode in", + "join": "Lid Worden", + "leave": "Cirkel Verlaten", + "leaveConfirmTitle": "Cirkel Verlaten", + "leaveConfirmMessage": "Weet u zeker dat u deze cirkel wilt verlaten?", + "circleMembers": "Cirkelleden", + "circleMemberRequests": "Aanvragen voor Cirkellidmaatschap", + "admin": "Beheerder", + "member": "Lid", + "pending": "In behandeling", + "accept": "Accepteren", + "reject": "Afwijzen", + "makeAdmin": "Beheerder Maken", + "makeMember": "Lid Maken", + "remove": "Verwijderen", + "webhookURL": "Webhook URL", + "webhookDescription": "Voer een webhook URL in om meldingen te ontvangen voor cirkelgebeurtenissen", + "webhookPlaceholder": "https://uw-webhook-url.com" + }, + "accountSettings": { + "title": "Accountinstellingen", + "subscription": "Abonnement", + "subscriptionStatus": "Huidig Plan", + "free": "Gratis", + "plus": "Plus", + "upgrade": "Upgraden", + "cancel": "Annuleren", + "changePassword": "Wachtwoord Wijzigen", + "password": "Wachtwoord", + "dangerZone": "Gevarenzone", + "dangerZoneDescription": "Zodra u uw account verwijdert, is er geen weg terug. Wees zeker van uw beslissing.", + "deleteAccount": "Account Verwijderen" + }, + "localization": { + "title": "Lokalisatie", + "description": "Pas de taal, datumnotatie en regionale voorkeuren voor uw account aan.", + "language": "Taal", + "languageDescription": "Selecteer uw voorkeurstaal", + "dateFormat": "Datumnotatie", + "dateFormatDescription": "Kies hoe datums in de applicatie worden weergegeven", + "timeFormat": "Tijdnotatie", + "timeFormatDescription": "Selecteer 12-uurs of 24-uurs tijdnotatie", + "12hour": "12-uurs (AM/PM)", + "24hour": "24-uurs", + "firstDayOfWeek": "Eerste Dag van de Week", + "firstDayOfWeekDescription": "Selecteer welke dag uw week begint", + "sunday": "Zondag", + "monday": "Maandag", + "saturday": "Zaterdag", + "formats": { + "mdy": "MM/DD/JJJJ (VS)", + "dmy": "DD/MM/JJJJ (Europa)", + "ymd": "JJJJ-MM-DD (ISO)", + "long": "Lang formaat (bijv. 1 januari 2024)", + "short": "Kort formaat (bijv. 1 jan 2024)" + } + }, + "sidepanel": { + "title": "Zijpaneel Aanpassen", + "description": "Pas de indeling en zichtbaarheid van kaarten in het zijpaneel aan. Deze sectie is alleen beschikbaar op grote schermapparaten zoals tablets en desktops." + }, + "theme": { + "title": "Themavoorkeuren", + "description": "Kies hoe de site er voor u uitziet. Selecteer een enkel thema of synchroniseer met uw systeem en schakel automatisch tussen dag- en nachtthema's.", + "themeMode": "Thamamodus", + "light": "Licht", + "dark": "Donker", + "system": "Systeem" + }, + "notifications": { + "settingsSaved": "Instellingen succesvol opgeslagen", + "settingsSaveFailed": "Opslaan van instellingen mislukt", + "invalidWebhook": "Ongeldige webhook URL" + }, + "profile": { + "title": "Profielinstellingen", + "description": "Werk uw weergavenaam en profielfoto bij.", + "photoUpdated": "Foto Bijgewerkt", + "photoUpdatedMessage": "Uw profielfoto is succesvol bijgewerkt!", + "uploadFailed": "Uploaden Mislukt", + "uploadFailedMessage": "Het uploaden van uw foto is mislukt. Probeer het opnieuw.", + "profileUpdated": "Profiel Bijgewerkt", + "profileUpdatedMessage": "Uw profielgegevens zijn succesvol opgeslagen!", + "updateFailed": "Bijwerken Mislukt", + "updateFailedMessage": "Kan uw profiel niet bijwerken. Controleer uw verbinding en probeer het opnieuw.", + "changePhoto": "Foto Wijzigen", + "displayName": "Weergavenaam", + "displayNamePlaceholder": "Voer uw weergavenaam in", + "timezone": "Tijdzone", + "timezonePlaceholder": "Selecteer uw tijdzone", + "save": "Opslaan", + "cancel": "Annuleren" + }, + "overview": { + "title": "Instellingen", + "subtitle": "Pas uw ervaring aan en beheer uw accountvoorkeuren", + "upgrade": { + "title": "Upgraden naar Plus", + "description": "Ontgrendel krachtige functies om uw productiviteit te verbeteren", + "button": "Nu Upgraden", + "features": { + "richText": "Rijke tekstbeschrijvingen", + "notifications": "Taakmeldingen", + "apiIntegrations": "API-integraties", + "advancedAutomation": "Geavanceerde automatisering" + } + }, + "sections": { + "profile": { + "title": "Profielinstellingen", + "description": "Werk uw profielgegevens, foto, weergavenaam en tijdzonevoorkeuren bij." + }, + "circle": { + "title": "Cirkelinstellingen", + "description": "Beheer uw cirkel, nodig leden uit en behandel lidmaatschapsverzoeken." + }, + "account": { + "title": "Accountinstellingen", + "description": "Beheer uw abonnement, wijzig het wachtwoord en opties voor accountverwijdering." + }, + "subaccounts": { + "title": "Beheerde Accounts", + "description": "Maak subaccounts aan en beheer ze om in te loggen en toegewezen taken te voltooien." + }, + "notifications": { + "title": "Meldingen", + "description": "Configureer pushmeldingen, e-mailwaarschuwingen en meldingsdoelen voor taken." + }, + "mfa": { + "title": "Multi-Factor Authenticatie", + "description": "Voeg een extra beveiligingslaag toe met MFA via authenticatie-apps." + }, + "apitokens": { + "title": "API-tokens", + "description": "Genereer en beheer toegangstokens voor integraties van derden en API-toegang." + }, + "storage": { + "title": "Opslaginstellingen", + "description": "Maak een back-up van uw gegevens en herstel ze, beheer lokale opslag en synchronisatievoorkeuren." + }, + "sidepanel": { + "title": "Zijpaneel Aanpassen", + "description": "Pas de indeling en zichtbaarheid van kaarten in de zijpaneelinterface aan." + }, + "theme": { + "title": "Themavoorkeuren", + "description": "Kies uw voorkeursthema en configureer de instellingen voor donker/licht modus." + }, + "localization": { + "title": "Lokalisatie", + "description": "Pas de taal, datumnotatie, tijdnotatie en regionale voorkeuren aan." + }, + "advanced": { + "title": "Geavanceerde Instellingen", + "description": "Configureer webhooks, real-time updates en andere geavanceerde functies voor verbeterde productiviteit." + }, + "developer": { + "title": "Ontwikkelaarsinstellingen", + "description": "Bekijk technische informatie over authenticatietokens, SSE-verbindingen en foutopsporingsgegevens." + } + } + } +} diff --git a/src/contexts/LocalizationContext.jsx b/src/contexts/LocalizationContext.jsx index 24ce673..bfd5016 100644 --- a/src/contexts/LocalizationContext.jsx +++ b/src/contexts/LocalizationContext.jsx @@ -24,13 +24,7 @@ export const AVAILABLE_LANGUAGES = [ { code: 'en', name: 'English', nativeName: 'English' }, { code: 'es', name: 'Spanish', nativeName: 'Español' }, { code: 'fr', name: 'French', nativeName: 'Français' }, - { code: 'de', name: 'German', nativeName: 'Deutsch' }, - { code: 'ar', name: 'Arabic', nativeName: 'العربية' }, - { code: 'he', name: 'Hebrew', nativeName: 'עברית' }, - { code: 'zh', name: 'Chinese', nativeName: '中文' }, - { code: 'ja', name: 'Japanese', nativeName: '日本語' }, - { code: 'pt', name: 'Portuguese', nativeName: 'Português' }, - { code: 'ru', name: 'Russian', nativeName: 'Русский' }, + { code: 'nl', name: 'Dutch', nativeName: 'Nederlands' }, ] export const LocalizationProvider = ({ children }) => { @@ -43,7 +37,10 @@ export const LocalizationProvider = ({ children }) => { TIME_FORMATS.HOUR_12, 'timeFormat', ) - const [firstDayOfWeek, setFirstDayOfWeek] = useStickyState(0, 'firstDayOfWeek') // 0 = Sunday, 1 = Monday + const [firstDayOfWeek, setFirstDayOfWeek] = useStickyState( + 0, + 'firstDayOfWeek', + ) // 0 = Sunday, 1 = Monday const [language, setLanguage] = useStickyState('en', 'language') useEffect(() => { @@ -80,11 +77,26 @@ export const LocalizationProvider = ({ children }) => { const formatCalendar = date => { if (!date) return '' - return moment(date).calendar() + return moment(date).calendar(null, { + sameDay: `[Today] ${timeFormat}`, + nextDay: `[Tomorrow] ${timeFormat}`, + nextWeek: `dddd ${timeFormat}`, + lastDay: `[Yesterday] ${timeFormat}`, + lastWeek: `[Last] dddd ${timeFormat}`, + sameElse: `${dateFormat} ${timeFormat}`, + }) } const isRTL = RTL_LANGUAGES.includes(language) + const fmt = { + date: formatDate, + dateTime: formatDateTime, + time: formatTime, + relative: formatRelative, + calendar: formatCalendar, + } + const value = { dateFormat, setDateFormat, @@ -95,11 +107,7 @@ export const LocalizationProvider = ({ children }) => { language, setLanguage, isRTL, - formatDate, - formatDateTime, - formatTime, - formatRelative, - formatCalendar, + fmt, availableLanguages: AVAILABLE_LANGUAGES, } diff --git a/src/utils/ChoreCardHelpers.jsx b/src/utils/ChoreCardHelpers.jsx index 013d5e5..4153ba0 100644 --- a/src/utils/ChoreCardHelpers.jsx +++ b/src/utils/ChoreCardHelpers.jsx @@ -19,12 +19,21 @@ const allMonths = [ * @param {Object} chore - The chore object (needed for nextDueDate null check) * @returns {string} The formatted due date text */ -export const getDueDateChipText = (nextDueDate, chore) => { +export const getDueDateChipText = (nextDueDate, chore, timeFormat = 'h:mm A') => { if (chore?.nextDueDate === null || nextDueDate === null) return 'No Due Date' const dueDate = moment(nextDueDate) const diff = moment(nextDueDate).diff(moment(), 'hours') + const calendarFormat = { + sameDay: `[Today] ${timeFormat}`, + nextDay: `[Tomorrow] ${timeFormat}`, + nextWeek: `dddd ${timeFormat}`, + lastDay: `[Yesterday] ${timeFormat}`, + lastWeek: `[Last] dddd ${timeFormat}`, + sameElse: `MMM D ${timeFormat}`, + } + // if seconds and minutes set to 59, treat as no time (date only) if (dueDate.seconds() === 59 && dueDate.minutes() === 59) { if (diff < 0) { @@ -33,22 +42,22 @@ export const getDueDateChipText = (nextDueDate, chore) => { if (absDiff <= 48) { return ( 'Overdue ' + - moment(nextDueDate).calendar().split(' at ')[0].toLowerCase() + moment(nextDueDate).calendar(null, calendarFormat).split(' ')[0].toLowerCase() ) } return 'Overdue ' + dueDate.fromNow() } // if due in next 48 hours, show calendar format without time (e.g., "Tomorrow") if (diff < 48 && diff > 0) { - return moment(nextDueDate).calendar().split(' at ')[0] + return moment(nextDueDate).calendar(null, calendarFormat).split(' ')[0] } // if due date is after 48 hours, show it in format: Due in 3 days return 'Due ' + dueDate.fromNow() } - // if due in next 48 hours, we should show it in this format: Tomorrow 11:00 AM + // if due in next 48 hours, we should show it in this format: Tomorrow 11:00 if (diff < 48 && diff > 0) { - return moment(nextDueDate).calendar().replace(' at', '') + return moment(nextDueDate).calendar(null, calendarFormat) } return 'Due ' + moment(nextDueDate).fromNow() } diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx index a2fe056..fdbc12e 100644 --- a/src/views/ChoreEdit/ChoreView.jsx +++ b/src/views/ChoreEdit/ChoreView.jsx @@ -78,7 +78,7 @@ import TimerSplitButton from './TimerSplitButton.jsx' const ChoreView = () => { const { t } = useTranslation('chores') - const { formatDate, formatDateTime } = useLocalization() + const { fmt } = useLocalization() const [chore, setChore] = useState({}) const navigate = useNavigate() const [performers, setPerformers] = useState([]) @@ -475,7 +475,7 @@ const ChoreView = () => { )} } size='md' sx={{ mb: 1 }}> {chore.nextDueDate - ? `${t('choreView.due')} ${formatDateTime(chore.nextDueDate)}` + ? `${t('choreView.due')} ${fmt.dateTime(chore.nextDueDate)}` : t('choreView.na')} { +const generateSchedulePreview = (metadata, formatTimeFn) => { if (!metadata?.days?.length) return '' const dayNames = metadata.days @@ -84,7 +85,7 @@ const generateSchedulePreview = metadata => { .join(', ') const timeStr = metadata.time - ? moment(metadata.time).format('h:mm A') + ? formatTimeFn(metadata.time) : '6:00 PM' if (metadata.weekPattern === 'every_week' || !metadata.weekPattern) { @@ -114,6 +115,7 @@ const RepeatOnSections = ({ frequencyMetadata, onFrequencyMetadataUpdate, }) => { + const { fmt } = useLocalization() // if time on frequencyMetadata is not set, try to set it to the nextDueDate if available, // otherwise set it to 18:00 of the current day useEffect(() => { @@ -400,7 +402,7 @@ const RepeatOnSections = ({ {frequencyMetadata?.days?.length > 0 && ( - {generateSchedulePreview(frequencyMetadata)} + {generateSchedulePreview(frequencyMetadata, fmt.time)} )} diff --git a/src/views/ChoreEdit/TimePassedCard.jsx b/src/views/ChoreEdit/TimePassedCard.jsx index e1d5a0c..bed91bf 100644 --- a/src/views/ChoreEdit/TimePassedCard.jsx +++ b/src/views/ChoreEdit/TimePassedCard.jsx @@ -8,9 +8,11 @@ import { import { Box, Card, Chip, Typography } from '@mui/joy' import { useEffect, useRef, useState } from 'react' import { useNavigate } from 'react-router-dom' +import { useLocalization } from '../../contexts/LocalizationContext' const TimePassedCard = ({ chore, handleAction, onShowDetails }) => { const navigate = useNavigate() + const { fmt } = useLocalization() const [time, setTime] = useState(0) const [shouldAnimate, setShouldAnimate] = useState(false) const [prevStatus, setPrevStatus] = useState(null) // Initialize as null @@ -187,10 +189,7 @@ const TimePassedCard = ({ chore, handleAction, onShowDetails }) => { size='md' startDecorator={} > - {new Date(chore.startTime).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - })} + {fmt.time(chore.startTime)} )} @@ -202,10 +201,7 @@ const TimePassedCard = ({ chore, handleAction, onShowDetails }) => { size='md' startDecorator={} > - {new Date(chore.timerUpdatedAt).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - })} + {fmt.time(chore.timerUpdatedAt)} )} @@ -219,10 +215,7 @@ const TimePassedCard = ({ chore, handleAction, onShowDetails }) => { size='md' startDecorator={} > - {new Date(chore.timerUpdatedAt).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - })} + {fmt.time(chore.timerUpdatedAt)} )} diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx index 8ed5fcf..12592f5 100644 --- a/src/views/Chores/ChoreCard.jsx +++ b/src/views/Chores/ChoreCard.jsx @@ -21,6 +21,7 @@ import { Typography, } from '@mui/joy' import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx' +import { useLocalization } from '../../contexts/LocalizationContext' import { useUserProfile } from '../../queries/UserQueries.jsx' import { getDueDateChipColor, @@ -45,6 +46,7 @@ const ChoreCard = ({ onSelectionToggle, }) => { const { data: userProfile } = useUserProfile() + const { timeFormat } = useLocalization() const { impersonatedUser } = useImpersonateUser() @@ -95,7 +97,7 @@ const ChoreCard = ({ }} color={getDueDateChipColor(chore.nextDueDate, chore)} > - {getDueDateChipText(chore.nextDueDate, chore)} + {getDueDateChipText(chore.nextDueDate, chore, timeFormat)} - {getDueDateChipText(chore.nextDueDate, chore)} + {getDueDateChipText(chore.nextDueDate, chore, timeFormat)} diff --git a/src/views/History/ChoreHistory.jsx b/src/views/History/ChoreHistory.jsx index 506faf0..263f969 100644 --- a/src/views/History/ChoreHistory.jsx +++ b/src/views/History/ChoreHistory.jsx @@ -22,6 +22,7 @@ import { Box, Button, Card, Container, Grid, Sheet, Typography } from '@mui/joy' import moment from 'moment' import { useEffect, useState } from 'react' import { Link, useParams } from 'react-router-dom' +import { useLocalization } from '../../contexts/LocalizationContext' import useConfirmationModal from '../../hooks/useConfirmationModal' import { useChoreHistory, @@ -44,6 +45,7 @@ const ChoreHistory = () => { const [isEditModalOpen, setIsEditModalOpen] = useState(false) const [editHistory, setEditHistory] = useState(null) const { confirmModalConfig, showConfirmation } = useConfirmationModal() + const { fmt } = useLocalization() const [showMoreInfoId, setShowMoreInfoId] = useState(null) const [noteViewerConfig, setNoteViewerConfig] = useState({ isOpen: false }) const { showSuccess, showError } = useNotification() @@ -367,7 +369,7 @@ const ChoreHistory = () => { onViewNote={notes => { setNoteViewerConfig({ isOpen: true, - title: `Updated at ${moment(historyEntry.updatedAt).format('LLLL')}`, + title: `Updated at ${fmt.dateTime(historyEntry.updatedAt)}`, content: notes, onClose: () => setNoteViewerConfig({ isOpen: false }), }) diff --git a/src/views/History/HistoryCard.jsx b/src/views/History/HistoryCard.jsx index a4dbe28..b2a7308 100644 --- a/src/views/History/HistoryCard.jsx +++ b/src/views/History/HistoryCard.jsx @@ -13,6 +13,7 @@ import { } from '@mui/icons-material' import { Avatar, Box, Chip, Grid, IconButton, Typography } from '@mui/joy' import moment from 'moment' +import { useLocalization } from '../../contexts/LocalizationContext' import { TASK_COLOR } from '../../utils/Colors.jsx' const getCompletedChip = historyEntry => { @@ -93,6 +94,7 @@ const HistoryCard = ({ onToggleActions, onViewNote, }) => { + const { fmt } = useLocalization() const performer = performers.find(p => p.userId === historyEntry.completedBy) const assignedTo = performers.find(p => p.userId === historyEntry.assignedTo) @@ -191,9 +193,9 @@ const HistoryCard = ({ }> - {moment( + {fmt.dateTime( historyEntry.performedAt || historyEntry.updatedAt, - ).format('MMM DD, h:mm A')} + )} @@ -214,7 +216,7 @@ const HistoryCard = ({ > {historyEntry.dueDate && ( }> - {moment(historyEntry.dueDate).format('MMM DD h:mm A')} + {fmt.dateTime(historyEntry.dueDate)} )} diff --git a/src/views/Modals/Inputs/TimerEditModal.jsx b/src/views/Modals/Inputs/TimerEditModal.jsx index d6b4b2e..8cbc91e 100644 --- a/src/views/Modals/Inputs/TimerEditModal.jsx +++ b/src/views/Modals/Inputs/TimerEditModal.jsx @@ -12,6 +12,7 @@ import { } from '@mui/joy' import moment from 'moment' import { useEffect, useState } from 'react' +import { useLocalization } from '../../../contexts/LocalizationContext' import { useResponsiveModal } from '../../../hooks/useResponsiveModal' import { useNotification } from '../../../service/NotificationProvider' import { @@ -23,6 +24,7 @@ import ConfirmationModal from './ConfirmationModal' const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => { const { ResponsiveModal } = useResponsiveModal() + const { fmt } = useLocalization() const [timerData, setTimerData] = useState(null) const [loading, setLoading] = useState(false) @@ -632,11 +634,9 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => { const sessionDate = moment(pause.start).format( 'MMM DD', ) - const startTime = moment(pause.start).format( - 'HH:mm', - ) + const startTime = fmt.time(pause.start) const endTime = pause.end - ? moment(pause.end).format('HH:mm') + ? fmt.time(pause.end) : null const realTimeDuration = isOngoing diff --git a/src/views/Settings/APITokenSettings.jsx b/src/views/Settings/APITokenSettings.jsx index 603d9ce..2998f5d 100644 --- a/src/views/Settings/APITokenSettings.jsx +++ b/src/views/Settings/APITokenSettings.jsx @@ -12,6 +12,7 @@ import { import moment from 'moment' import { useEffect, useState } from 'react' +import { useLocalization } from '../../contexts/LocalizationContext' import { useUserProfile } from '../../queries/UserQueries' import { useNotification } from '../../service/NotificationProvider' import { @@ -27,6 +28,7 @@ import SettingsLayout from './SettingsLayout' const APITokenSettings = () => { const { data: userProfile } = useUserProfile() const { showNotification } = useNotification() + const { fmt } = useLocalization() const [tokens, setTokens] = useState([]) const [isGetTokenNameModalOpen, setIsGetTokenNameModalOpen] = useState(false) const [showTokenId, setShowTokenId] = useState(null) @@ -106,7 +108,7 @@ const APITokenSettings = () => { {token.name} {moment(token.createdAt).fromNow()}( - {moment(token.createdAt).format('lll')}) + {fmt.dateTime(token.createdAt)}) diff --git a/src/views/Settings/AccountSettings.jsx b/src/views/Settings/AccountSettings.jsx index e498b21..b1c08fa 100644 --- a/src/views/Settings/AccountSettings.jsx +++ b/src/views/Settings/AccountSettings.jsx @@ -5,6 +5,7 @@ import { useQueryClient } from '@tanstack/react-query' import moment from 'moment' import { useEffect, useState } from 'react' import SubscriptionModal from '../../components/SubscriptionModal' +import { useLocalization } from '../../contexts/LocalizationContext' import { useUserProfile } from '../../queries/UserQueries' import { useNotification } from '../../service/NotificationProvider' import { CancelSubscription, UpdatePassword } from '../../utils/Fetcher' @@ -17,6 +18,7 @@ const AccountSettings = () => { const { data: userProfile } = useUserProfile() const queryClient = useQueryClient() const { showNotification } = useNotification() + const { fmt } = useLocalization() const [changePasswordModal, setChangePasswordModal] = useState(false) const [subscriptionModal, setSubscriptionModal] = useState(false) @@ -37,13 +39,9 @@ const AccountSettings = () => { const getSubscriptionDetails = () => { if (userProfile?.subscription === 'active') { - return `You are currently subscribed to the Plus plan. Your subscription will renew on ${moment( - userProfile?.expiration, - ).format('MMM DD, YYYY')}.` + return `You are currently subscribed to the Plus plan. Your subscription will renew on ${fmt.date(userProfile?.expiration)}.` } else if (userProfile?.subscription === 'cancelled') { - return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${moment( - userProfile?.expiration, - ).format('MMM DD, YYYY')}.` + return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${fmt.date(userProfile?.expiration)}.` } else { return `You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.` } @@ -54,9 +52,7 @@ const AccountSettings = () => { return `Plus` } else if (userProfile?.subscription === 'cancelled') { if (moment().isBefore(userProfile?.expiration)) { - return `Plus(until ${moment(userProfile?.expiration).format( - 'MMM DD, YYYY', - )})` + return `Plus(until ${fmt.date(userProfile?.expiration)})` } return `Free` } else { diff --git a/src/views/Settings/CircleSettings.jsx b/src/views/Settings/CircleSettings.jsx index 577287c..b83885f 100644 --- a/src/views/Settings/CircleSettings.jsx +++ b/src/views/Settings/CircleSettings.jsx @@ -15,6 +15,7 @@ import { useQueryClient } from '@tanstack/react-query' import moment from 'moment' import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' +import { useLocalization } from '../../contexts/LocalizationContext' import { useUserProfile } from '../../queries/UserQueries' import { useNotification } from '../../service/NotificationProvider' import { @@ -36,6 +37,7 @@ const CircleSettings = () => { const queryClient = useQueryClient() const { showNotification } = useNotification() const navigate = useNavigate() + const { fmt } = useLocalization() const [userCircles, setUserCircles] = useState([]) const [circleMemberRequests, setCircleMemberRequests] = useState([]) @@ -221,12 +223,12 @@ const CircleSettings = () => { {member.isActive ? ( - Joined on {moment(member.createdAt).format('MMM DD, YYYY')} + Joined on {fmt.date(member.createdAt)} ) : ( Request to join{' '} - {moment(member.updatedAt).format('MMM DD, YYYY')} + {fmt.date(member.updatedAt)} )} @@ -365,7 +367,7 @@ const CircleSettings = () => { {lastRefresh && ( - Last updated: {moment(lastRefresh).format('MMM DD, HH:mm')} + Last updated: {fmt.dateTime(lastRefresh)} )} - - + {t('localization.12hour')} + + {sampleDate.format(TIME_FORMATS.HOUR_12)} + - - - + + + + + Preview: {sampleDate.format(timeFormat)} + + + + {t('localization.firstDayOfWeek')} + + + {t('localization.firstDayOfWeekDescription')} + + + + + + + + ) diff --git a/src/views/Settings/Settings.jsx b/src/views/Settings/Settings.jsx index 52c8172..c1e66bc 100644 --- a/src/views/Settings/Settings.jsx +++ b/src/views/Settings/Settings.jsx @@ -60,7 +60,7 @@ const Settings = () => { const queryClient = useQueryClient() const { showNotification } = useNotification() const navigate = useNavigate() - const { formatDate } = useLocalization() + const { fmt } = useLocalization() const [userCircles, setUserCircles] = useState([]) const [circleMemberRequests, setCircleMemberRequests] = useState([]) @@ -191,11 +191,11 @@ const Settings = () => { const getSubscriptionDetails = () => { if (userProfile?.subscription === 'active') { - return `You are currently subscribed to the Plus plan. Your subscription will renew on ${formatDate( + return `You are currently subscribed to the Plus plan. Your subscription will renew on ${fmt.date( userProfile?.expiration, )}.` } else if (userProfile?.subscription === 'cancelled') { - return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${formatDate( + return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${fmt.date( userProfile?.expiration, )}.` } else { @@ -207,7 +207,7 @@ const Settings = () => { return `Plus` } else if (userProfile?.subscription === 'cancelled') { if (moment().isBefore(userProfile?.expiration)) { - return `Plus(until ${formatDate(userProfile?.expiration)})` + return `Plus(until ${fmt.date(userProfile?.expiration)})` } return `Free` } else { @@ -340,12 +340,12 @@ const Settings = () => { {member.isActive ? ( - Joined on {moment(member.createdAt).format('MMM DD, YYYY')} + Joined on {fmt.date(member.createdAt)} ) : ( Request to join{' '} - {moment(member.updatedAt).format('MMM DD, YYYY')} + {fmt.date(member.updatedAt)} )} @@ -486,7 +486,7 @@ const Settings = () => { {lastRefresh && ( - Last updated: {moment(lastRefresh).format('MMM DD, HH:mm')} + Last updated: {fmt.dateTime(lastRefresh)} )}