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.
This commit is contained in:
Mo Tarbin
2026-03-08 23:08:37 -04:00
parent 176f66b437
commit fc91894336
26 changed files with 785 additions and 245 deletions

View File

@@ -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"
}
}
}

View File

@@ -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."
}
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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."
}
}
}
}

View File

@@ -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,
}

View File

@@ -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()
}

View File

@@ -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 = () => {
)}
<Chip startDecorator={<CalendarMonth />} size='md' sx={{ mb: 1 }}>
{chore.nextDueDate
? `${t('choreView.due')} ${formatDateTime(chore.nextDueDate)}`
? `${t('choreView.due')} ${fmt.dateTime(chore.nextDueDate)}`
: t('choreView.na')}
</Chip>
<Box

View File

@@ -19,6 +19,7 @@ import {
import moment from 'moment'
import { useEffect } from 'react'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useUserProfile } from '../../queries/UserQueries'
import { isPlusAccount } from '../../utils/Helpers'
import ThingTriggerSection from './ThingTriggerSection'
@@ -76,7 +77,7 @@ const DAY_OCCURRENCE_OPTIONS = [
{ value: -1, label: 'Last occurrence' },
]
// Helper function to generate schedule preview text
const generateSchedulePreview = metadata => {
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 && (
<Card mt={2} p={2}>
<Typography level='body-sm' color='primary'>
{generateSchedulePreview(frequencyMetadata)}
{generateSchedulePreview(frequencyMetadata, fmt.time)}
</Typography>
</Card>
)}

View File

@@ -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={<Flag sx={{ fontSize: 14 }} />}
>
{new Date(chore.startTime).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
{fmt.time(chore.startTime)}
</Chip>
)}
@@ -202,10 +201,7 @@ const TimePassedCard = ({ chore, handleAction, onShowDetails }) => {
size='md'
startDecorator={<Schedule sx={{ fontSize: 14 }} />}
>
{new Date(chore.timerUpdatedAt).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
{fmt.time(chore.timerUpdatedAt)}
</Chip>
)}
</>
@@ -219,10 +215,7 @@ const TimePassedCard = ({ chore, handleAction, onShowDetails }) => {
size='md'
startDecorator={<Schedule sx={{ fontSize: 14 }} />}
>
{new Date(chore.timerUpdatedAt).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
{fmt.time(chore.timerUpdatedAt)}
</Chip>
)}
</Box>

View File

@@ -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)}
</Chip>
<Chip

View File

@@ -11,6 +11,7 @@ import {
import { Box, Checkbox, Chip, IconButton, Typography } from '@mui/joy'
import { useNavigate } from 'react-router-dom'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
import {
getDueDateChipColor,
@@ -41,6 +42,7 @@ const CompactChoreCard = ({
const navigate = useNavigate()
const { data: userProfile } = useUserProfile()
const { timeFormat } = useLocalization()
const { data: circleMembersData } = useCircleMembers()
const { impersonatedUser } = useImpersonateUser()
@@ -395,7 +397,7 @@ const CompactChoreCard = ({
ml: 1,
}}
>
{getDueDateChipText(chore.nextDueDate, chore)}
{getDueDateChipText(chore.nextDueDate, chore, timeFormat)}
</Chip>
</Box>

View File

@@ -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 }),
})

View File

@@ -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 = ({
</Typography>
<Chip size='sm' startDecorator={<EventNote />}>
{moment(
{fmt.dateTime(
historyEntry.performedAt || historyEntry.updatedAt,
).format('MMM DD, h:mm A')}
)}
</Chip>
<Box sx={{ display: 'flex', gap: 0.5 }}>
@@ -214,7 +216,7 @@ const HistoryCard = ({
>
{historyEntry.dueDate && (
<Chip size='sm' startDecorator={<CalendarMonth />}>
{moment(historyEntry.dueDate).format('MMM DD h:mm A')}
{fmt.dateTime(historyEntry.dueDate)}
</Chip>
)}
</Box>

View File

@@ -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

View File

@@ -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 = () => {
<Typography level='body-md'>{token.name}</Typography>
<Typography level='body-xs'>
{moment(token.createdAt).fromNow()}(
{moment(token.createdAt).format('lll')})
{fmt.dateTime(token.createdAt)})
</Typography>
</Box>
<Box>

View File

@@ -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 {

View File

@@ -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 = () => {
</Typography>
{member.isActive ? (
<Typography level='body-sm'>
Joined on {moment(member.createdAt).format('MMM DD, YYYY')}
Joined on {fmt.date(member.createdAt)}
</Typography>
) : (
<Typography level='body-sm' color='danger'>
Request to join{' '}
{moment(member.updatedAt).format('MMM DD, YYYY')}
{fmt.date(member.updatedAt)}
</Typography>
)}
</Box>
@@ -365,7 +367,7 @@ const CircleSettings = () => {
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{lastRefresh && (
<Typography level='body-sm' color='neutral'>
Last updated: {moment(lastRefresh).format('MMM DD, HH:mm')}
Last updated: {fmt.dateTime(lastRefresh)}
</Typography>
)}
<Button

View File

@@ -1,13 +1,12 @@
import {
useLocalization,
DATE_FORMATS,
TIME_FORMATS,
useLocalization,
} from '@/contexts/LocalizationContext'
import { LanguageOutlined } from '@mui/icons-material'
import {
Box,
Button,
Card,
ButtonGroup,
Divider,
FormControl,
FormHelperText,
@@ -45,151 +44,150 @@ const LocalizationSettings = () => {
]
return (
<SettingsLayout title="Localization">
<div className='grid gap-4'>
<SettingsLayout title='Localization'>
<div className='grid gap-4 py-4'>
<Typography level='body-md'>{t('localization.description')}</Typography>
<Typography level='h3'>{t('localization.language')}</Typography>
<Divider />
<Typography level='body-md'>
{t('localization.description')}
{t('localization.languageDescription')}
</Typography>
<Card variant='outlined' sx={{ p: 3, mb: 2 }}>
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<LanguageOutlined />
<Typography level='title-md'>
{t('localization.language')}
</Typography>
</Box>
<Typography level='body-sm' sx={{ mb: 2 }}>
{t('localization.languageDescription')}
</Typography>
<FormControl>
<Select
value={language}
onChange={(_, value) => setLanguage(value)}
sx={{ minWidth: 250 }}
>
{availableLanguages.map(lang => (
<Option key={lang.code} value={lang.code}>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<Typography>{lang.nativeName}</Typography>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
({lang.name})
</Typography>
</Box>
</Option>
))}
</Select>
{isRTL && (
<FormHelperText>
This language uses right-to-left (RTL) text direction
</FormHelperText>
<FormControl>
<Select
value={language}
onChange={(_, value) => setLanguage(value)}
sx={{ maxWidth: '300px' }}
>
{availableLanguages.map(lang => (
<Option key={lang.code} value={lang.code}>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<Typography>{lang.nativeName}</Typography>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
({lang.name})
</Typography>
</Box>
</Option>
))}
</Select>
{isRTL && (
<FormHelperText>
{t(
'localization.rtlNotice',
'This language uses right-to-left (RTL) text direction',
)}
</FormControl>
</Box>
</FormHelperText>
)}
</FormControl>
<Divider sx={{ my: 3 }} />
<Typography level='h3'>{t('localization.dateFormat')}</Typography>
<Divider />
<Typography level='body-md'>
{t('localization.dateFormatDescription')}
</Typography>
<FormControl>
<Select
value={dateFormat}
onChange={(_, value) => setDateFormat(value)}
sx={{ maxWidth: '300px' }}
>
{dateFormatOptions.map(option => (
<Option key={option.value} value={option.value}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
width: '100%',
gap: 2,
}}
>
<Typography>{option.label}</Typography>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
{sampleDate.format(option.value)}
</Typography>
</Box>
</Option>
))}
</Select>
<FormHelperText>
Preview: {sampleDate.format(dateFormat)}
</FormHelperText>
</FormControl>
<Box sx={{ mb: 3 }}>
<Typography level='title-md' sx={{ mb: 1 }}>
{t('localization.dateFormat')}
</Typography>
<Typography level='body-sm' sx={{ mb: 2 }}>
{t('localization.dateFormatDescription')}
</Typography>
<FormControl>
<Select
value={dateFormat}
onChange={(_, value) => setDateFormat(value)}
sx={{ minWidth: 250 }}
<Typography level='h3'>{t('localization.timeFormat')}</Typography>
<Divider />
<Typography level='body-md'>
{t('localization.timeFormatDescription')}
</Typography>
<FormControl>
<Select
value={timeFormat}
onChange={(_, value) => setTimeFormat(value)}
sx={{ maxWidth: '300px' }}
>
<Option value={TIME_FORMATS.HOUR_12}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
width: '100%',
gap: 2,
}}
>
{dateFormatOptions.map(option => (
<Option key={option.value} value={option.value}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', width: '100%', gap: 2 }}>
<Typography>{option.label}</Typography>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
{sampleDate.format(option.value)}
</Typography>
</Box>
</Option>
))}
</Select>
<FormHelperText>
Preview: {sampleDate.format(dateFormat)}
</FormHelperText>
</FormControl>
</Box>
<Divider sx={{ my: 3 }} />
<Box sx={{ mb: 3 }}>
<Typography level='title-md' sx={{ mb: 1 }}>
{t('localization.timeFormat')}
</Typography>
<Typography level='body-sm' sx={{ mb: 2 }}>
{t('localization.timeFormatDescription')}
</Typography>
<FormControl>
<Select
value={timeFormat}
onChange={(_, value) => setTimeFormat(value)}
sx={{ minWidth: 250 }}
>
<Option value={TIME_FORMATS.HOUR_12}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', width: '100%', gap: 2 }}>
<Typography>{t('localization.12hour')}</Typography>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
{sampleDate.format(TIME_FORMATS.HOUR_12)}
</Typography>
</Box>
</Option>
<Option value={TIME_FORMATS.HOUR_24}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', width: '100%', gap: 2 }}>
<Typography>{t('localization.24hour')}</Typography>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
{sampleDate.format(TIME_FORMATS.HOUR_24)}
</Typography>
</Box>
</Option>
</Select>
<FormHelperText>
Preview: {sampleDate.format(timeFormat)}
</FormHelperText>
</FormControl>
</Box>
<Divider sx={{ my: 3 }} />
<Box>
<Typography level='title-md' sx={{ mb: 1 }}>
{t('localization.firstDayOfWeek')}
</Typography>
<Typography level='body-sm' sx={{ mb: 2 }}>
{t('localization.firstDayOfWeekDescription')}
</Typography>
<FormControl>
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
<Button
variant={firstDayOfWeek === 0 ? 'solid' : 'outlined'}
onClick={() => setFirstDayOfWeek(0)}
>
{t('localization.sunday')}
</Button>
<Button
variant={firstDayOfWeek === 1 ? 'solid' : 'outlined'}
onClick={() => setFirstDayOfWeek(1)}
>
{t('localization.monday')}
</Button>
<Button
variant={firstDayOfWeek === 6 ? 'solid' : 'outlined'}
onClick={() => setFirstDayOfWeek(6)}
>
{t('localization.saturday')}
</Button>
<Typography>{t('localization.12hour')}</Typography>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
{sampleDate.format(TIME_FORMATS.HOUR_12)}
</Typography>
</Box>
</FormControl>
</Box>
</Card>
</Option>
<Option value={TIME_FORMATS.HOUR_24}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
width: '100%',
gap: 2,
}}
>
<Typography>{t('localization.24hour')}</Typography>
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
{sampleDate.format(TIME_FORMATS.HOUR_24)}
</Typography>
</Box>
</Option>
</Select>
<FormHelperText>
Preview: {sampleDate.format(timeFormat)}
</FormHelperText>
</FormControl>
<Typography level='h3'>{t('localization.firstDayOfWeek')}</Typography>
<Divider />
<Typography level='body-md'>
{t('localization.firstDayOfWeekDescription')}
</Typography>
<FormControl>
<ButtonGroup variant='outlined'>
<Button
variant={firstDayOfWeek === 0 ? 'solid' : 'outlined'}
onClick={() => setFirstDayOfWeek(0)}
>
{t('localization.sunday')}
</Button>
<Button
variant={firstDayOfWeek === 1 ? 'solid' : 'outlined'}
onClick={() => setFirstDayOfWeek(1)}
>
{t('localization.monday')}
</Button>
<Button
variant={firstDayOfWeek === 6 ? 'solid' : 'outlined'}
onClick={() => setFirstDayOfWeek(6)}
>
{t('localization.saturday')}
</Button>
</ButtonGroup>
</FormControl>
</div>
</SettingsLayout>
)

View File

@@ -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 = () => {
</Typography>
{member.isActive ? (
<Typography level='body-sm'>
Joined on {moment(member.createdAt).format('MMM DD, YYYY')}
Joined on {fmt.date(member.createdAt)}
</Typography>
) : (
<Typography level='body-sm' color='danger'>
Request to join{' '}
{moment(member.updatedAt).format('MMM DD, YYYY')}
{fmt.date(member.updatedAt)}
</Typography>
)}
</Box>
@@ -486,7 +486,7 @@ const Settings = () => {
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{lastRefresh && (
<Typography level='body-sm' color='neutral'>
Last updated: {moment(lastRefresh).format('MMM DD, HH:mm')}
Last updated: {fmt.dateTime(lastRefresh)}
</Typography>
)}
<Button

View File

@@ -21,6 +21,7 @@ import {
Button,
Card,
CardContent,
Chip,
Container,
List,
ListItem,
@@ -107,6 +108,7 @@ const SettingsOverview = () => {
title: t('overview.sections.localization.title'),
description: t('overview.sections.localization.description'),
icon: <Language />,
isBeta: true,
},
{
id: 'advanced',
@@ -246,9 +248,15 @@ const SettingsOverview = () => {
}}
>
<span> {t('overview.upgrade.features.richText')}</span>
<span> {t('overview.upgrade.features.notifications')}</span>
<span> {t('overview.upgrade.features.apiIntegrations')}</span>
<span> {t('overview.upgrade.features.advancedAutomation')}</span>
<span>
{t('overview.upgrade.features.notifications')}
</span>
<span>
{t('overview.upgrade.features.apiIntegrations')}
</span>
<span>
{t('overview.upgrade.features.advancedAutomation')}
</span>
</Box>
</Box>
</Box>
@@ -316,12 +324,33 @@ const SettingsOverview = () => {
</Avatar>
</ListItemDecorator>
<ListItemContent sx={{ ml: 2 }}>
<Typography
level='title-md'
sx={{ mb: 0.5, fontWeight: 'lg' }}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mb: 0.5,
}}
>
{setting.title}
</Typography>
<Typography level='title-md' sx={{ fontWeight: 'lg' }}>
{setting.title}
</Typography>
{setting.isBeta && (
<Chip
variant='outlined'
size='sm'
sx={{
height: '20px',
fontSize: '0.65rem',
fontWeight: 'bold',
color: 'warning.main',
borderColor: 'warning.main',
}}
>
Early Access
</Chip>
)}
</Box>
<Typography
level='body-sm'
color='neutral'

View File

@@ -26,6 +26,7 @@ import {
import { useTheme } from '@mui/joy/styles'
import moment from 'moment'
import { Link, useParams } from 'react-router-dom'
import { useLocalization } from '../../contexts/LocalizationContext'
import {
Line,
LineChart,
@@ -40,6 +41,7 @@ import LoadingComponent from '../components/Loading'
const ThingsHistory = () => {
const { id } = useParams()
const theme = useTheme()
const { fmt } = useLocalization()
const {
data,
error,
@@ -294,9 +296,7 @@ const ThingsHistory = () => {
tick='false'
tickLine='false'
axisLine='false'
tickFormatter={tick =>
moment(tick).format('ddd MM/DD/yyyy HH:mm:ss')
}
tickFormatter={tick => fmt.dateTime(tick)}
/>
<YAxis
hide='true'
@@ -306,9 +306,7 @@ const ThingsHistory = () => {
axisLine='false'
/>
<Tooltip
labelFormatter={label =>
moment(label).format('ddd MM/DD/yyyy HH:mm:ss')
}
labelFormatter={label => fmt.dateTime(label)}
/>
<Line
@@ -399,7 +397,7 @@ const ThingsHistory = () => {
color='primary'
startDecorator={<Schedule />}
>
{moment(history.updatedAt).format('MMM DD, h:mm A')}
{fmt.dateTime(history.updatedAt)}
</Chip>
</Box>
</Grid>

View File

@@ -38,6 +38,7 @@ import {
import moment from 'moment'
import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { useLocalization } from '../../contexts/LocalizationContext'
import {
useChoreTimer,
usePauseChore,
@@ -52,6 +53,7 @@ import LoadingComponent from '../components/Loading'
const TimerDetails = () => {
const { choreId } = useParams()
const { fmt } = useLocalization()
const [timerData, setTimerData] = useState(null)
const [loading, setLoading] = useState(false)
const [editingSessions, setEditingSessions] = useState({})
@@ -819,14 +821,14 @@ const TimerDetails = () => {
level='body-xs'
sx={{ color: 'text.tertiary' }}
>
Started: {moment(timerData.startTime).format('HH:mm')}
Started: {fmt.time(timerData.startTime)}
</Typography>
{timerData.endTime && (
<Typography
level='body-xs'
sx={{ color: 'text.tertiary' }}
>
Ended: {moment(timerData.endTime).format('HH:mm')}
Ended: {fmt.time(timerData.endTime)}
</Typography>
)}
{!timerData.endTime && (
@@ -834,7 +836,7 @@ const TimerDetails = () => {
level='body-xs'
sx={{ color: 'success.500' }}
>
Now: {moment(currentTime).format('HH:mm')}
Now: {fmt.time(currentTime)}
</Typography>
)}
<Typography
@@ -925,9 +927,9 @@ const TimerDetails = () => {
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

View File

@@ -32,6 +32,7 @@ import {
} from '@mui/joy'
import React, { useEffect, useState } from 'react'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
import { ChoresGrouper } from '../../utils/Chores'
@@ -119,6 +120,7 @@ const ChoreHistoryItem = ({ time, name, points, status, performer }) => {
}
const ChoreHistoryTimeline = ({ history }) => {
const { fmt } = useLocalization()
const groupedHistory = groupByDate(history)
return (
@@ -133,12 +135,7 @@ const ChoreHistoryTimeline = ({ history }) => {
{Object.entries(groupedHistory).map(([date, items]) => (
<Box key={date} sx={{ mb: 4 }}>
<Typography level='title-sm' sx={{ mb: 0.5 }}>
{new Date(date).toLocaleDateString([], {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric',
})}
{fmt.date(date)}
</Typography>
<Divider />
<Stack spacing={1}>
@@ -146,10 +143,7 @@ const ChoreHistoryTimeline = ({ history }) => {
<>
<ChoreHistoryItem
key={record.id}
time={new Date(record.performedAt).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
time={fmt.time(record.performedAt)}
name={record.choreName}
points={record.points}
status={record.status}

View File

@@ -3,6 +3,7 @@ import { Avatar, Box, Chip, Grid, Typography } from '@mui/joy'
import moment from 'moment'
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
import { getPriorityColor, TASK_COLOR } from '../../utils/Colors'
@@ -15,6 +16,7 @@ const getAssigneeColor = (assignee, userProfile) => {
}
const CalendarCard = ({ chores }) => {
const { data: userProfile } = useUserProfile()
const { fmt } = useLocalization()
const [selectedDate, setSeletedDate] = useState(null)
const Navigate = useNavigate()
@@ -197,7 +199,7 @@ const CalendarCard = ({ chores }) => {
}}
>
<Typography level='title-md'>
{moment(selectedDate).format('MMMM D, YYYY')}
{fmt.date(selectedDate)}
</Typography>
<Chip variant='soft' color='primary' size='md'>
{(() => {
@@ -291,7 +293,7 @@ const CalendarCard = ({ chores }) => {
color: 'neutral.500',
}}
>
{moment(chore.nextDueDate).format('h:mm A')}
{fmt.time(chore.nextDueDate)}
</Typography>
{/* <Typography
level='body-xs'

View File

@@ -14,7 +14,7 @@ const getAssigneeColor = (assignee, userProfile) => {
}
const CalendarDual = ({ chores, onDateChange }) => {
const { data: userProfile } = useUserProfile()
const { firstDayOfWeek } = useLocalization()
const { firstDayOfWeek, fmt } = useLocalization()
const calendarType =
firstDayOfWeek === 1 ? 'iso8601' : firstDayOfWeek === 6 ? 'islamic' : 'gregory'
@@ -100,10 +100,7 @@ const CalendarDual = ({ chores, onDateChange }) => {
{isSecondary && (
<div className={styles.secondaryCalendarHeader}>
<Typography level='title-md' sx={{ textAlign: 'center', mb: 1 }}>
{date.toLocaleDateString('en-US', {
month: 'long',
year: 'numeric',
})}
{fmt.date(date, 'MMMM YYYY')}
</Typography>
</div>
)}

View File

@@ -32,6 +32,7 @@ import {
Typography,
} from '@mui/joy'
import { useState } from 'react'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext'
import { useUserProfile } from '../../queries/UserQueries'
import { CompleteSubTask } from '../../utils/Fetcher'
@@ -48,6 +49,7 @@ function SortableItem({
editMode,
performers = [],
}) {
const { fmt } = useLocalization()
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({
id: task.id,
@@ -210,7 +212,7 @@ function SortableItem({
fontSize: 'sm',
}}
>
{new Date(task.completedAt).toLocaleString()}
{fmt.dateTime(task.completedAt)}
{performers.find(p => p.userId === task.completedBy) ? (
<Chip>
{