diff --git a/public/locales/de/auth.json b/public/locales/de/auth.json
new file mode 100644
index 00000000..43419bef
--- /dev/null
+++ b/public/locales/de/auth.json
@@ -0,0 +1,93 @@
+{
+ "subtitle": "Melde dich an, um fortzufahren",
+ "signupSubtitle": "Erstelle ein Konto, um loszulegen!",
+ "forgotPasswordSubtitle": "Gib deine E-Mail-Adresse ein und wir senden dir einen Link, um wieder auf dein Konto zuzugreifen.",
+ "forgotPasswordConfirmation": "Falls zu der eingegebenen E-Mail-Adresse ein Konto existiert, erhältst du eine E-Mail mit Anweisungen zum Zurücksetzen deines Passworts.",
+ "welcomeBack": "Willkommen zurück, {{name}}",
+ "subAccountBadge": "(Unterkonto)",
+ "tabs": {
+ "primary": "Hauptkonto",
+ "sub": "Unterkonto"
+ },
+ "fields": {
+ "primaryAccountUsername": "Benutzername des Hauptkontos",
+ "subAccountUsername": "Benutzername des Unterkontos"
+ },
+ "placeholders": {
+ "primaryAccountUsername": "Benutzername des Hauptkontos eingeben",
+ "subAccountUsername": "Name des Unterkontos eingeben",
+ "passwordRange": "Passwort eingeben (8-64 Zeichen)",
+ "enterEmailAddress": "E-Mail-Adresse eingeben",
+ "verificationCode": "6-stelligen Code eingeben",
+ "backupCode": "Backup-Code eingeben"
+ },
+ "actions": {
+ "signIn": "Anmelden",
+ "signInSubAccount": "Als Unterkonto anmelden",
+ "forgotPassword": "Passwort vergessen?",
+ "createAccount": "Neues Konto erstellen",
+ "continueWithGoogle": "Mit Google fortfahren",
+ "continueWithApple": "Mit Apple fortfahren",
+ "continueWithProvider": "Mit {{provider}} fortfahren",
+ "resetPassword": "Passwort zurücksetzen",
+ "goToLogin": "Zur Anmeldung",
+ "useBackupCode": "Kein Zugriff auf die Authenticator-App? Backup-Code verwenden",
+ "useAuthenticatorApp": "Stattdessen die Authenticator-App verwenden"
+ },
+ "messages": {
+ "agreement": "Mit der Registrierung stimmst du unseren Nutzungsbedingungen und unserer Datenschutzerklärung zu"
+ },
+ "errors": {
+ "validationTitle": "Validierungsfehler",
+ "loginFailedTitle": "Anmeldung fehlgeschlagen",
+ "loginFailedMessage": "Ein Fehler ist aufgetreten. Bitte versuche es erneut.",
+ "usernameRequired": "Benutzername ist erforderlich",
+ "passwordRequired": "Passwort ist erforderlich",
+ "primaryUsernameRequired": "Für die Anmeldung des Unterkontos ist der Benutzername des Hauptkontos erforderlich",
+ "subAccountNameRequired": "Für die Anmeldung des Unterkontos ist der Name des Unterkontos erforderlich",
+ "googleLoginFailedTitle": "Google-Anmeldung fehlgeschlagen",
+ "googleLoginFailedMessage": "Die Anmeldung mit Google war nicht erfolgreich. Bitte versuche es erneut.",
+ "appleLoginFailedTitle": "Apple-Anmeldung fehlgeschlagen",
+ "appleLoginFailedMessage": "Die Anmeldung mit Apple war nicht erfolgreich. Bitte versuche es erneut.",
+ "oauthErrorTitle": "OAuth-Fehler",
+ "oauthBrowserError": "Der Anmeldebrowser konnte nicht geöffnet werden",
+ "twoFactorFailedTitle": "Zwei-Faktor-Authentifizierung fehlgeschlagen",
+ "signupFailedTitle": "Registrierung fehlgeschlagen",
+ "signupDisabled": "Registrierung ist deaktiviert. Bitte kontaktiere den Admin.",
+ "signupGeneric": "Bei der Registrierung ist ein Fehler aufgetreten",
+ "usernameMin": "Der Benutzername muss mindestens 4 Zeichen lang sein",
+ "invalidEmail": "Ungültige E-Mail-Adresse",
+ "passwordLength": "Das Passwort muss zwischen 8 und 64 Zeichen lang sein",
+ "displayNameRequired": "Anzeigename ist erforderlich",
+ "displayNamePattern": "Der Anzeigename darf nur Buchstaben und Zahlen enthalten",
+ "usernamePattern": "Der Benutzername darf nur Kleinbuchstaben, Punkt und Bindestrich enthalten",
+ "resetEmailSentTitle": "E-Mail zum Zurücksetzen gesendet",
+ "resetEmailSentMessage": "Prüfe deine E-Mails für Anweisungen zum Zurücksetzen des Passworts",
+ "resetFailedTitle": "Zurücksetzen fehlgeschlagen",
+ "resetFailedMessage": "Die E-Mail zum Zurücksetzen konnte nicht gesendet werden. Bitte versuche es später erneut.",
+ "emailRequired": "E-Mail ist erforderlich",
+ "validEmail": "Bitte gib eine gültige E-Mail-Adresse ein"
+ },
+ "mfa": {
+ "title": "Zwei-Faktor-Authentifizierung",
+ "instruction": "Gib den Bestätigungscode aus deiner Authenticator-App ein",
+ "invalidCode": "Ungültiger Bestätigungscode. Bitte versuche es erneut.",
+ "missingCode": "Bitte gib einen Bestätigungscode ein",
+ "verifyFailed": "Der Code konnte nicht geprüft werden. Bitte versuche es erneut.",
+ "helpText": "Probleme? Stelle sicher, dass deine Authenticator-App synchronisiert ist, und versuche es erneut. Jeder Backup-Code kann nur einmal verwendet werden."
+ },
+ "status": {
+ "authenticating": "Authentifizierung läuft",
+ "pleaseWait": "Bitte warten",
+ "unknownProvider": "Unbekannter Authentifizierungsanbieter",
+ "contactSupport": "Bitte kontaktiere den Support",
+ "failed": "Authentifizierung fehlgeschlagen",
+ "stateMismatch": "Der Status stimmt nicht überein",
+ "tryAgain": "Bitte versuche es erneut",
+ "passwordUpdatedRedirect": "Dein Passwort wurde erfolgreich aktualisiert. Weiterleitung zur Anmeldung...",
+ "passwordUpdateFailed": "Passwortaktualisierung fehlgeschlagen",
+ "passwordUpdateFailedMessage": "Passwort konnte nicht aktualisiert werden. Bitte versuche es später erneut.",
+ "enterNewPassword": "Bitte gib unten dein neues Passwort ein",
+ "savePassword": "Passwort speichern"
+ }
+}
diff --git a/public/locales/de/chores.json b/public/locales/de/chores.json
index a8e283a1..cf3223cb 100644
--- a/public/locales/de/chores.json
+++ b/public/locales/de/chores.json
@@ -1,75 +1,641 @@
{
- "title": "Aufgaben",
- "myChores": "Meine Aufgaben",
- "allChores": "Alle Aufgaben",
- "addChore": "Aufgabe hinzufügen",
- "editChore": "Aufgabe bearbeiten",
- "deleteChore": "Aufgabe löschen",
- "completeChore": "Aufgabe abschließen",
- "dueDate": "Fälligkeitsdatum",
- "assignedTo": "Zugewiesen an",
- "priority": "Priorität",
- "status": "Status",
- "description": "Beschreibung",
+ "sidepanel": {
+ "summary": {
+ "title": "Übersicht",
+ "description": "Das ist eine Zusammenfassung deiner Aufgaben",
+ "dueToday": "Heute fällig",
+ "overdue": "Überfällig"
+ },
+ "activities": {
+ "title": "Letzte Aktivitäten",
+ "loading": "Aktivitäten werden geladen...",
+ "empty": "Keine aktuellen Aktivitäten",
+ "unknownChore": "Unbekannte Aufgabe",
+ "justNow": "Gerade eben",
+ "hoursAgo_one": "vor {{count}} Std.",
+ "hoursAgo_other": "vor {{count}} Std.",
+ "daysAgo_one": "vor {{count}} Tag",
+ "daysAgo_other": "vor {{count}} Tagen",
+ "by": "von",
+ "points_one": "{{count}} Punkt",
+ "points_other": "{{count}} Punkte",
+ "showMore": "Mehr anzeigen",
+ "noteTitle": "Notiz - {{name}}",
+ "status": {
+ "started": "Gestartet",
+ "done": "Erledigt",
+ "late": "Verspätet",
+ "skipped": "Übersprungen",
+ "pendingApproval": "Wartet auf Freigabe",
+ "rejected": "Abgelehnt",
+ "completed": "Abgeschlossen"
+ }
+ },
+ "assignees": {
+ "title": "Aufgaben nach Zuständigen",
+ "loading": "Aufgaben nach Zuständigen werden geladen...",
+ "empty": "Keine zugewiesenen Aufgaben gefunden",
+ "legend": {
+ "inProgress": "In Arbeit",
+ "overdue": "Überfällig",
+ "scheduled": "Geplant",
+ "pendingReview": "Wartet auf Prüfung"
+ }
+ },
+ "userSwitcher": {
+ "title": "Aufgaben ansehen als",
+ "switchTitle": "Zur Benutzeransicht wechseln",
+ "switchDescription": "Aufgaben werden so gefiltert, dass nur Zuweisungen für die ausgewählte Person angezeigt werden",
+ "chooseUser": "Benutzer wählen",
+ "changeUser": "Benutzer ändern"
+ },
+ "notifications": {
+ "title": "Benachrichtigungen aktivieren?",
+ "description": "Du musst die Berechtigung für Benachrichtigungen aktivieren. Möchtest du das jetzt tun?",
+ "enable": "Ja",
+ "keepDisabled": "Nein, deaktiviert lassen"
+ },
+ "insights": {
+ "title": "Smarte Einblicke",
+ "active": "Aktiv",
+ "clearHint": "Klicke auf den aktiven Filter, um ihn zu entfernen",
+ "description": "Schnellaktionen basierend auf deinen Aufgaben",
+ "items": {
+ "overdue": {
+ "title": "Überfällig",
+ "description_one": "{{count}} Aufgabe ist überfällig",
+ "description_other": "{{count}} Aufgaben sind überfällig"
+ },
+ "dueToday": {
+ "title": "Heute fällig",
+ "description_one": "{{count}} Aufgabe ist heute fällig",
+ "description_other": "{{count}} Aufgaben sind heute fällig"
+ },
+ "pendingApproval": {
+ "title": "Wartet auf Freigabe",
+ "description_one": "{{count}} Aufgabe wartet auf Freigabe",
+ "description_other": "{{count}} Aufgaben warten auf Freigabe"
+ },
+ "dueThisWeek": {
+ "title": "Diese Woche fällig",
+ "description_one": "{{count}} Aufgabe ist in den nächsten 7 Tagen fällig",
+ "description_other": "{{count}} Aufgaben sind in den nächsten 7 Tagen fällig"
+ },
+ "highPriority": {
+ "title": "Hohe Priorität",
+ "description_one": "{{count}} Aufgabe erfordert sofortige Aufmerksamkeit",
+ "description_other": "{{count}} Aufgaben erfordern sofortige Aufmerksamkeit"
+ },
+ "noDueDate": {
+ "title": "Kein Fälligkeitsdatum",
+ "description_one": "{{count}} Aufgabe braucht eine Frist",
+ "description_other": "{{count}} Aufgaben brauchen eine Frist"
+ }
+ }
+ },
+ "multiSelect": {
+ "showShortcuts": "Tastenkürzel anzeigen",
+ "title": "Mehrfachauswahl-Modus",
+ "description": "Nutze diese Tastenkürzel, um mit mehreren Aufgaben effizienter zu arbeiten:",
+ "sections": {
+ "selection": "Auswahl",
+ "actions": "Aktionen",
+ "interface": "Oberfläche"
+ },
+ "shortcuts": {
+ "selectAllVisible": "Alle sichtbaren Aufgaben auswählen",
+ "clearOrExit": "Auswahl leeren oder Mehrfachauswahl beenden",
+ "markCompleted": "Ausgewählte Aufgaben als erledigt markieren",
+ "deleteSelected": "Ausgewählte Aufgaben löschen",
+ "quickAdd": "Neue Aufgabe schnell hinzufügen"
+ },
+ "gotIt": "Verstanden!"
+ }
+ },
+ "due": {
+ "noDueDate": "Kein Fälligkeitsdatum",
+ "dueRelative": "Fällig {{when}}",
+ "overdueRelative": "Überfällig {{when}}",
+ "overdueDay": "Überfällig {{day}}"
+ },
+ "frequency": {
+ "once": "Einmal",
+ "trigger": "Auslöser",
+ "daily": "Täglich",
+ "adaptive": "Adaptiv",
+ "weekly": "Wöchentlich",
+ "monthly": "Monatlich",
+ "yearly": "Jährlich",
+ "dailyExcept": "Täglich außer {{days}}",
+ "monthlyExcept": "{{label}} außer {{months}}",
+ "dayOfMonths": "{{day}} von {{months}}",
+ "everyUnit": "Alle {{count}} {{unit}}",
+ "units": {
+ "hours": "Stunden",
+ "day": "Tag",
+ "days": "Tage",
+ "week": "Woche",
+ "weeks": "Wochen",
+ "month": "Monat",
+ "months": "Monate",
+ "year": "Jahr",
+ "years": "Jahre"
+ }
+ },
+ "actions": {
+ "completeWithNote": "Mit Notiz abschließen",
+ "completeInPast": "In der Vergangenheit abschließen",
+ "skipToNextDueDate": "Zum nächsten Fälligkeitsdatum überspringen",
+ "delegate": "An jemand anderen delegieren",
+ "selectPerformer": "Ausführende Person auswählen",
+ "addCompletionNote": "Notiz zu diesem Abschluss hinzufügen",
+ "sendNudge": "Erinnerung senden",
+ "history": "Verlauf",
+ "changeDueDate": "Fälligkeitsdatum ändern",
+ "writeToNfc": "Auf NFC schreiben"
+ },
+ "groups": {
+ "started": "Gestartet",
+ "pendingApproval": "Wartet auf Freigabe",
+ "overdue": "Überfällig",
+ "today": "Heute",
+ "tomorrow": "Morgen",
+ "next7Days": "Nächste 7 Tage",
+ "laterThisMonth": "Später in diesem Monat",
+ "future": "Später",
+ "anytime": "Jederzeit"
+ },
+ "labels": {
+ "calendarOverview": "Kalenderübersicht",
+ "highPriority": "Hohe Priorität",
+ "mediumPriority": "Mittlere Priorität",
+ "lowPriority": "Niedrige Priorität",
+ "lowestPriority": "Niedrigste Priorität",
+ "noPriority": "Keine Priorität",
+ "noTasksForDate": "Für dieses Datum sind keine Aufgaben geplant"
+ },
+ "messages": {
+ "serverUnavailableTitle": "Keine Verbindung zum Server möglich",
+ "serverUnavailableDescription": "Der Server ist aktuell nicht erreichbar. Bitte prüfe deine Verbindung und versuche es erneut."
+ },
+ "edit": {
+ "nameQuestion": "Wie heißt diese Aufgabe?",
+ "descriptionQuestion": "Worum geht es bei dieser Aufgabe?",
+ "priorityQuestion": "Wie wichtig ist diese Aufgabe?",
+ "projectQuestion": "Zu welchem Projekt gehört diese Aufgabe?",
+ "labelsQuestion": "Dinge, die man sich zu dieser Aufgabe merken oder mit ihr verknüpfen möchte",
+ "addNewLabel": "Neues Label hinzufügen",
+ "whoCanDoTask": "Wer kann diese Aufgabe erledigen?",
+ "whoIsAssignedNext": "Wer ist als Nächstes zugewiesen?",
+ "noAssigneesAvailable": "Noch keine Zuständigen können diese Aufgabe erledigen",
+ "selectAssignee": "Wähle eine zuständige Person für diese Aufgabe",
+ "assignmentStrategyQuestion": "Wie soll die nächste zuständige Person ausgewählt werden?",
+ "triggerDueDateHint": "Das Fälligkeitsdatum wird gesetzt, sobald die Auslösebedingung erfüllt ist",
+ "giveTaskDueDate": "Dieser Aufgabe ein Fälligkeitsdatum geben",
+ "dueDateHelper": "Die Aufgabe muss bis zu einem bestimmten Zeitpunkt erledigt werden",
+ "startDateQuestion": "Wann beginnt diese Aufgabe?",
+ "nextDueQuestion": "Wann ist der nächste erste Zeitpunkt, zu dem diese Aufgabe fällig ist?",
+ "setSpecificTime": "Eine bestimmte Uhrzeit festlegen",
+ "specificTimeHelper": "Die Aufgabe ist zur angegebenen Uhrzeit fällig",
+ "endOfDayHelper": "Die Aufgabe ist am Ende des Tages fällig (23:59 Uhr)",
+ "taskWindowDescription": "Lege fest, wann diese Aufgabe erledigt werden kann und wann sie verfällt",
+ "setEarliestCompletionTime": "Frühesten Erledigungszeitpunkt festlegen",
+ "completionWindowHelper": "Die Aufgabe kann X Stunden vor dem Fälligkeitsdatum erledigt werden",
+ "afterDueDate": "nach dem Fälligkeitsdatum",
+ "schedulingPreferences": "Planungseinstellungen",
+ "schedulingPreferencesQuestion": "Wie soll das nächste Fälligkeitsdatum neu geplant werden?",
+ "rescheduleFromDueDate": "Vom Fälligkeitsdatum neu planen",
+ "rescheduleFromCompletionDate": "Vom Abschlussdatum neu planen",
+ "rescheduleFromDueDateHelper": "Die nächste Aufgabe wird vom ursprünglichen Fälligkeitsdatum aus geplant, auch wenn die vorherige Aufgabe verspätet erledigt wurde",
+ "rescheduleFromCompletionDateHelper": "Die nächste Aufgabe wird vom tatsächlichen Abschlussdatum der vorherigen Aufgabe aus geplant",
+ "notificationsBasicPlan": "Aufgabenbenachrichtigungen sind im Basic-Tarif nicht verfügbar. Upgrade auf Plus, um Erinnerungen zu erhalten, wenn Aufgaben fällig oder erledigt sind.",
+ "notifyForTask": "Für diese Aufgabe benachrichtigen",
+ "notifyForTaskHelper": "Wann sollen Benachrichtigungen für diese Aufgabe gesendet werden?",
+ "notificationSchedule": "Benachrichtigungsplan",
+ "whoToNotify": "Wen benachrichtigen",
+ "notifyAllAssignees": "Alle Zuständigen benachrichtigen",
+ "notifySpecificGroup": "Eine bestimmte Gruppe benachrichtigen",
+ "taskSettings": "Aufgabeneinstellungen",
+ "pointsHelper": "Vergib Punkte für diese Aufgabe und Nutzer erhalten Punkte, wenn sie sie erledigen",
+ "assignPoints": "Punkte für die Erledigung vergeben",
+ "approvalRequirement": "Freigabe erforderlich",
+ "requireAdminApproval": "Admin-Freigabe erforderlich",
+ "requireAdminApprovalHelper": "Diese Aufgabe benötigt eine Freigabe durch einen Admin, bevor sie als erledigt markiert wird",
+ "privacyQuestion": "Wer kann diese Aufgabe sehen?",
+ "privacyPublicHelper": "Alle in deinem Kreis",
+ "privacyLimitedHelper": "Du und andere Personen, die der Aufgabe zugewiesen sind",
+ "privacyLimitedDisabled": "Keine Zuständigen ausgewählt, die eingeschränkte Option ist deaktiviert",
+ "createdBy": "Erstellt von",
+ "updatedBy": "Aktualisiert von",
+ "archive": "Archivieren",
+ "unarchive": "Wiederherstellen",
+ "validation": {
+ "nameRequired": "Name ist erforderlich",
+ "assigneesRequired": "Mindestens eine zuständige Person ist erforderlich",
+ "assignedToRequired": "Zugewiesen an ist erforderlich",
+ "invalidFrequency": "Ungültige Häufigkeit, {{unit}} muss größer als 0 sein",
+ "selectDayOfWeek": "Bitte wähle mindestens einen Wochentag aus",
+ "selectDayOccurrence": "Bitte wähle mindestens ein Tagesvorkommen für den Monat aus",
+ "selectMonth": "Bitte wähle mindestens einen Monat aus",
+ "startDateRequired": "Startdatum ist erforderlich",
+ "dueDateRequired": "Fälligkeitsdatum ist erforderlich",
+ "thingTriggerInvalid": "Dinge-Auslöser ist ungültig",
+ "resolveErrors": "Bitte behebe die folgenden Fehler:"
+ },
+ "assignStrategies": {
+ "random": "Zufällig",
+ "least_assigned": "Am seltensten zugewiesen",
+ "least_completed": "Am seltensten abgeschlossen",
+ "keep_last_assigned": "Letzte Zuweisung beibehalten",
+ "random_except_last_assigned": "Zufällig außer letzte Zuweisung",
+ "round_robin": "Reihum",
+ "no_assignee": "Keine zuständige Person"
+ },
+ "saveSuccessTitle": "Aufgabe gespeichert",
+ "saveSuccessMessage": "Deine Aufgabe wurde erfolgreich gespeichert!",
+ "saveFailedTitle": "Speichern fehlgeschlagen",
+ "saveFailedMessage": "Die Aufgabe konnte nicht gespeichert werden. Bitte versuche es erneut.",
+ "deleteTitle": "Aufgabe löschen",
+ "deleteMessage": "Bist du sicher, dass du diese Aufgabe löschen möchtest?",
+ "deleteFailedTitle": "Löschen fehlgeschlagen"
+ },
+ "repeat": {
+ "repeat": "Wiederholen",
+ "repeatTask": "Diese Aufgabe wiederholen",
+ "repeatHelper": "Ist das etwas, das regelmäßig erledigt werden muss?",
+ "howOften": "Wie oft soll es wiederholt werden?",
+ "repeatOn": "Wiederholen am",
+ "timeOfDay": "Tageszeit",
+ "every": "Alle",
+ "unit": "Einheit",
+ "selectAll": "Alle auswählen",
+ "unselectAll": "Auswahl aufheben",
+ "everyWeek": "Jede Woche",
+ "weekOfMonth": "Woche des Monats",
+ "everyWeekHelper": "Die Aufgabe wiederholt sich jede Woche an den ausgewählten Tagen",
+ "weekOfMonthHelper": "Die Aufgabe wiederholt sich jeden Monat an bestimmten Tagesvorkommen (zum Beispiel 1. Montag oder 3. Freitag)",
+ "occurrencePrompt": "Wähle aus, welche Vorkommen der ausgewählten Tage verwendet werden sollen",
+ "occurrenceExample": "Beispiel: \"1. Montag\" bedeutet den ersten Montag jedes Monats",
+ "onThe": "am",
+ "ofSelectedMonths": "der ausgewählten Monate",
+ "ofMonth": "des Monats",
+ "at": "um",
+ "triggerTask": "Diese Aufgabe anhand eines Gerätezustands auslösen",
+ "triggerHelper": "Soll diese Aufgabe erledigt werden, wenn sich der Zustand eines Dings ändert?",
+ "plusFeature": "Plus-Funktion",
+ "triggerBasicPlan": "Dinge-basierte Auslöser sind im Basic-Tarif nicht verfügbar. Upgrade auf Plus, um Aufgaben automatisch auszulösen, wenn sich Gerätezustände ändern.",
+ "typeMessages": {
+ "adaptive": "Diese Aufgabe wird dynamisch anhand früherer Abschlussdaten geplant.",
+ "custom": "Diese Aufgabe wird anhand einer benutzerdefinierten Häufigkeit geplant."
+ },
+ "options": {
+ "custom": "Benutzerdefiniert",
+ "interval": "Intervall",
+ "days_of_the_week": "Wochentage",
+ "day_of_the_month": "Tag des Monats"
+ },
+ "days": {
+ "monday": "Montag",
+ "tuesday": "Dienstag",
+ "wednesday": "Mittwoch",
+ "thursday": "Donnerstag",
+ "friday": "Freitag",
+ "saturday": "Samstag",
+ "sunday": "Sonntag"
+ },
+ "dayAbbreviations": {
+ "monday": "Mo.",
+ "tuesday": "Di.",
+ "wednesday": "Mi.",
+ "thursday": "Do.",
+ "friday": "Fr.",
+ "saturday": "Sa.",
+ "sunday": "So."
+ },
+ "months": {
+ "january": "Januar",
+ "february": "Februar",
+ "march": "März",
+ "april": "April",
+ "may": "Mai",
+ "june": "Juni",
+ "july": "Juli",
+ "august": "August",
+ "september": "September",
+ "october": "Oktober",
+ "november": "November",
+ "december": "Dezember"
+ },
+ "occurrences": {
+ "1": "1. Vorkommen",
+ "2": "2. Vorkommen",
+ "3": "3. Vorkommen",
+ "4": "4. Vorkommen",
+ "-1": "Letztes Vorkommen"
+ }
+ },
+ "view": {
+ "previousNote": "Vorherige Notiz",
+ "taskActions": "Aufgabenaktionen",
+ "addNote": "Notiz hinzufügen",
+ "additionalNotes": "Zusätzliche Notizen",
+ "completionNotePlaceholder": "Füge eine Notiz zur Erledigung hinzu...",
+ "setCustomCompletionTime": "Benutzerdefinierte Erledigungszeit festlegen",
+ "markAsDone": "Als erledigt markieren",
+ "skipTask": "Aufgabe überspringen",
+ "skipTaskConfirm": "Bist du sicher, dass du diese Aufgabe überspringen möchtest?",
+ "availableToCompleteStarting": "Kann erledigt werden ab {{date}}",
+ "pendingApproval": "Wartet auf Freigabe",
+ "subtasks": "Unteraufgaben"
+ },
+ "addTask": {
+ "title": "Neue Aufgabe erstellen",
+ "taskInSentence": "Aufgabe in einem Satz",
+ "smartHelp": "Mit dieser Funktion kannst du eine Aufgabe einfach durch einen Satz erstellen. Der Satz wird analysiert, um Fälligkeitsdatum, Priorität und Häufigkeit zu erkennen.",
+ "examples": "Beispiele",
+ "priorityExample": "Für die höchste Priorität verwende P1, Urgent, Important oder ASAP. Für niedrigere Prioritäten verwende P2, P3 oder P4.",
+ "dueDateExample": "Gib Daten mit Formulierungen wie morgen, nächste Woche, Montag oder 1. August um 12 Uhr an.",
+ "frequencyExample": "Lege wiederkehrende Aufgaben mit täglich, wöchentlich, monatlich, jährlich oder Mustern wie jeden Dienstag und Donnerstag fest.",
+ "fullTextPlaceholder": "Gib hier den vollständigen Text ein...",
+ "description": "Beschreibung",
+ "dueDate": "Fälligkeitsdatum",
+ "editNotifications": "Benachrichtigungen bearbeiten",
+ "noPriority": "Keine Priorität",
+ "create": "Erstellen",
+ "anyone": "Jede Person",
+ "points": {
+ "one": "1 Punkt",
+ "other": "{{count}} Punkte"
+ }
+ },
+ "detail": {
+ "cards": {
+ "assignment": "Zuweisung",
+ "schedule": "Zeitplan",
+ "statistics": "Statistik",
+ "details": "Details",
+ "assigned": "Zugewiesen",
+ "last": "Zuletzt",
+ "due": "Fällig",
+ "deadline": "Deadline",
+ "completedTimes": "{{count}} Mal erledigt",
+ "createdBy": "Erstellt von",
+ "notAvailable": "k. A."
+ },
+ "notifications": {
+ "taskCompleted": "Aufgabe erledigt",
+ "taskCompletedMessage": "Deine Aufgabe wurde als erledigt markiert",
+ "taskSkipped": "Aufgabe übersprungen",
+ "undoSuccessful": "Rückgängig erfolgreich",
+ "taskCompletionUndone": "Das Abschließen der Aufgabe wurde rückgängig gemacht.",
+ "taskSkipUndone": "Das Überspringen der Aufgabe wurde rückgängig gemacht.",
+ "undoFailed": "Rückgängig fehlgeschlagen",
+ "undoFailedMessage": "Die Aktion konnte nicht rückgängig gemacht werden. Bitte versuche es erneut."
+ },
+ "timer": {
+ "resetTitle": "Timer zurücksetzen",
+ "resetMessage": "Möchtest du den Timer wirklich zurücksetzen? Dadurch werden alle Zeitaufzeichnungen gelöscht, seit du die Aufgabe gestartet hast.",
+ "resetConfirm": "Timer zurücksetzen",
+ "clearTitle": "Alle Zeitaufzeichnungen löschen",
+ "clearMessage": "Dadurch werden alle Timer für diese Aufgabe dauerhaft gelöscht und sie wird auf \"nicht gestartet\" zurückgesetzt.",
+ "clearConfirm": "Alle Zeiten löschen"
+ }
+ },
+ "main": {
+ "groupBy": "Gruppieren nach",
+ "quickFilters": "Schnellfilter",
+ "assignedTo": "Zugewiesen an:",
+ "createFilter": "Filter erstellen",
+ "createFilterDescription": "Erweiterte Filterregeln erstellen",
+ "createNewChoreShortcut": "Neue Aufgabe erstellen (Cmd+C)",
+ "archivedSearch": "Archivierte Aufgaben durchsuchen",
+ "archivedLoadFailed": "Archivierte Aufgaben konnten nicht geladen werden",
+ "tryAgainLater": "Bitte versuche es später erneut.",
+ "archivedRestoreMessage": "Die Aufgabe wurde wiederhergestellt und ist nun aktiv.",
+ "archivedDeleteMessage": "Die archivierte Aufgabe wurde dauerhaft gelöscht.",
+ "unexpectedError": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut.",
+ "shortcuts": {
+ "selectAllVisible": "Alle sichtbaren Aufgaben auswählen (Ctrl+A)",
+ "completeSelected": "Ausgewählte Aufgaben abschließen (Enter)",
+ "skipSelected": "Ausgewählte Aufgaben überspringen (/)",
+ "archiveSelected": "Ausgewählte Aufgaben archivieren (X)",
+ "restoreSelected": "Ausgewählte Aufgaben wiederherstellen (R)",
+ "deleteSelected": "Ausgewählte Aufgaben löschen (E)",
+ "clearMultiSelect": "Auswahl abwählen (Esc)",
+ "closeMultiSelect": "Mehrfachauswahl schließen (Esc)"
+ },
+ "selectedSingle": "{{count}} Aufgabe ausgewählt",
+ "selectedMultiple": "{{count}} Aufgaben ausgewählt",
+ "viewCompact": "Zur kompakten Ansicht wechseln",
+ "viewCalendar": "Zur Kalenderansicht wechseln",
+ "viewCard": "Zur Kartenansicht wechseln",
+ "exitMultiSelect": "Mehrfachauswahl beenden (Ctrl+S)",
+ "enableMultiSelect": "Mehrfachauswahl aktivieren (Ctrl+S)",
+ "cancelAllFilters": "Alle Filter aufheben",
+ "additionalFilter": "Zusätzlicher Filter: {{filter}}",
+ "nothingScheduled": "Nichts geplant",
+ "resetFilters": "Filter zurücksetzen",
+ "restoredTasksTitle": "Aufgaben wiederhergestellt",
+ "restoredTasks": "{{count}} Aufgabe erfolgreich wiederhergestellt.",
+ "restoredTasks_plural": "{{count}} Aufgaben erfolgreich wiederhergestellt.",
+ "restoredFailed": "{{count}} Aufgabe konnte nicht wiederhergestellt werden.",
+ "restoredFailed_plural": "{{count}} Aufgaben konnten nicht wiederhergestellt werden.",
+ "bulkRestoreFailed": "Sammelwiederherstellung fehlgeschlagen",
+ "deleteArchivedTitle": "Archivierte Aufgaben löschen",
+ "deleteArchivedConfirm": "{{count}} archivierte Aufgabe dauerhaft löschen?\n\nDiese Aktion kann nicht rückgängig gemacht werden.",
+ "deleteArchivedConfirm_plural": "{{count}} archivierte Aufgaben dauerhaft löschen?\n\nDiese Aktion kann nicht rückgängig gemacht werden.",
+ "archivedTitle": "Archivierte Aufgaben",
+ "archivedDescription": "Archivierte oder abgeschlossene Aufgaben ansehen und verwalten.",
+ "all": "Alle",
+ "clear": "Abwählen",
+ "restore": "Wiederherstellen",
+ "noArchivedFound": "Keine archivierten Aufgaben gefunden",
+ "noArchived": "Keine archivierten Aufgaben",
+ "adjustSearch": "Versuche, deine Suchbegriffe anzupassen",
+ "archivedWillAppear": "Archivierte Aufgaben erscheinen hier, wenn du sie in der Hauptliste archivierst",
+ "clearSearch": "Suche löschen",
+ "archivedCount": "{{count}} archivierte Aufgabe",
+ "archivedCount_plural": "{{count}} archivierte Aufgaben",
+ "matchingSearch": " passend zu \"{{term}}\"",
+ "filters": {
+ "anyone": "Jede Person",
+ "assignedToMe": "Mir zugewiesen",
+ "availableForMe": "Für mich verfügbar",
+ "assignedToOthers": "Anderen zugewiesen"
+ },
+ "otherFilters": {
+ "title": "Weitere",
+ "dueToday": "Heute fällig",
+ "dueInWeek": "Diese Woche fällig",
+ "dueLater": "Später fällig",
+ "createdByMe": "Von mir erstellt",
+ "assignedToMe": "Mir zugewiesen",
+ "noDueDate": "Kein Fälligkeitsdatum"
+ }
+ },
+ "nfc": {
+ "successTitle": "Erfolgreich!",
+ "successMessage": "Die URL wurde erfolgreich auf den NFC-Tag geschrieben.",
+ "instructions": "Drücke den Button unten, um auf NFC zu schreiben.",
+ "writeButton": "NFC schreiben",
+ "requestAccess": "Zugriff anfordern",
+ "autoCompleteWhenScanned": "Beim Scannen automatisch abschließen",
+ "urlCopied": "URL in die Zwischenablage kopiert!",
+ "unsupportedAlert": "NFC wird von diesem Browser nicht unterstützt.",
+ "unsupportedMessage": "NFC wird von diesem Browser nicht unterstützt. Du kannst die URL aber kopieren und mit einem kompatiblen Gerät auf einen NFC-Tag schreiben.",
+ "writeError": "Fehler beim Schreiben auf den NFC-Tag. Bitte versuche es erneut."
+ },
"choreView": {
"assignment": "Zuweisung",
"assigned": "Zugewiesen",
- "last": "Letzte",
+ "last": "Zuletzt",
"schedule": "Zeitplan",
"due": "Fällig",
- "statistics": "Statistiken",
+ "statistics": "Statistik",
"completed": "Abgeschlossen",
- "times": "mal",
+ "times": "Mal",
"details": "Details",
+ "deadline": "Frist",
"createdBy": "Erstellt von",
- "na": "Nicht verfügbar",
+ "na": "k. A.",
"taskCompleted": "Aufgabe abgeschlossen",
- "taskCompletedMessage": "Deine Aufgabe wurde als abgeschlossen markiert",
- "taskCompletionUndone": "Aufgabenabschluss wurde rückgängig gemacht.",
- "taskSkipUndone": "Aufgabe überspringen wurde rückgängig gemacht.",
- "undoSuccessful": "Rückgängig erfolgreich",
+ "taskCompletedMessage": "Deine Aufgabe wurde als erledigt markiert",
+ "taskCompletionUndone": "Das Abschließen der Aufgabe wurde rückgängig gemacht.",
+ "taskSkipUndone": "Das Überspringen der Aufgabe wurde rückgängig gemacht.",
+ "undoSuccessful": "Erfolgreich rückgängig gemacht",
"undoFailed": "Rückgängig fehlgeschlagen",
"undoFailedMessage": "Die Aktion konnte nicht rückgängig gemacht werden. Bitte versuche es erneut.",
"resetTimer": "Timer zurücksetzen",
- "resetTimerConfirmation": "Bist du sicher, dass du den Timer zurücksetzen möchtest? Dies löscht alle Zeitaufzeichnungen seit du die Aufgabe gestartet hast.",
+ "resetTimerConfirmation": "Möchtest du den Timer wirklich zurücksetzen? Dadurch werden alle Zeitaufzeichnungen gelöscht, seit du die Aufgabe gestartet hast.",
"clearAllTimeRecords": "Alle Zeitaufzeichnungen löschen",
- "clearAllTimeConfirmation": "Dies löscht dauerhaft alle Timer für diese Aufgabe und setzt sie zurück auf \"nicht gestartet\".",
+ "clearAllTimeConfirmation": "Dadurch werden alle Timer für diese Aufgabe dauerhaft gelöscht und sie wird auf \"nicht gestartet\" zurückgesetzt.",
"descriptionTitle": "Beschreibung",
- "description": "Beschreibung:",
+ "description": "Beschreibung",
"previousNote": "Vorherige Notiz",
- "previousNoteLabel": "Vorherige Notiz:",
- "subtasksLabel": "Unteraufgaben:",
+ "previousNoteLabel": "Vorherige Notiz",
+ "subtasksLabel": "Unteraufgaben",
"taskActions": "Aufgabenaktionen",
"addNote": "Notiz hinzufügen",
- "additionalNotes": "Zusätzliche Notizen:",
- "notePlaceholder": "Notiz zur Fertigstellung hinzufügen...",
- "setCustomCompletionTime": "Benutzerdefinierte Abschlusszeit festlegen",
- "skipTask": "Aufgabe überspringen",
- "skipTaskConfirmation": "Bist du sicher, dass du diese Aufgabe überspringen möchtest?",
- "markComplete": "Als abgeschlossen markieren",
+ "additionalNotes": "Zusätzliche Notizen",
+ "notePlaceholder": "Notiz eingeben",
+ "setCustomCompletionTime": "Benutzerdefinierte Abschlusszeit setzen",
+ "skipTask": "Aufgabe übersprungen",
+ "skipTaskConfirmation": "Möchtest du diese Aufgabe wirklich überspringen?",
+ "markComplete": "Abschließen",
"markAsDone": "Als erledigt markieren",
"edit": "Bearbeiten",
- "archive": "Archivieren",
- "unarchive": "Archivierung aufheben",
- "viewHistory": "Verlauf anzeigen",
+ "archive": "Archiviert",
+ "unarchive": "Wiederherstellen",
+ "viewHistory": "Verlauf ansehen",
"history": "Verlauf",
"startTimer": "Timer starten",
"start": "Starten",
"pauseTimer": "Timer pausieren",
"approve": "Genehmigen",
"reject": "Ablehnen",
- "pendingApproval": "Genehmigung ausstehend",
+ "pendingApproval": "Wartet auf Freigabe",
"undo": "Rückgängig",
"skip": "Überspringen",
"cancel": "Abbrechen",
"noPriority": "Keine Priorität",
"subtasks": "Unteraufgaben",
- "noDescription": "Keine Beschreibung verfügbar",
- "timer": {
- "active": "Timer aktiv",
- "paused": "Timer pausiert",
- "reset": "Timer zurücksetzen",
- "delete": "Sitzung löschen"
+ "noDescription": "Keine Beschreibung",
+ "timer": "Timer"
+ },
+ "actionFeedback": {
+ "undoable": {
+ "completed": "Aufgabe erledigt",
+ "approved": "Aufgabe genehmigt",
+ "rejected": "Aufgabe abgelehnt",
+ "skipped": "Aufgabe übersprungen"
+ },
+ "undoDone": {
+ "completed": "Das Abschließen der Aufgabe wurde rückgängig gemacht.",
+ "approved": "Die Genehmigung der Aufgabe wurde rückgängig gemacht.",
+ "rejected": "Die Ablehnung der Aufgabe wurde rückgängig gemacht.",
+ "skipped": "Das Überspringen der Aufgabe wurde rückgängig gemacht."
+ },
+ "undoSuccessTitle": "Erfolgreich rückgängig gemacht",
+ "undoFailedTitle": "Rückgängig fehlgeschlagen",
+ "undoFailedMessage": "Die Aktion konnte nicht rückgängig gemacht werden. Bitte versuche es erneut.",
+ "notifications": {
+ "rescheduledTitle": "Aufgabe neu geplant",
+ "rescheduledMessage": "Das Fälligkeitsdatum der Aufgabe wurde erfolgreich aktualisiert.",
+ "dueDateRemovedTitle": "Aufgabe ohne Plan",
+ "dueDateRemovedMessage": "Die Aufgabe ist nun ungeplant und hat kein Fälligkeitsdatum mehr.",
+ "restoredTitle": "Aufgabe wiederhergestellt",
+ "restoredMessage": "Die Aufgabe wurde wiederhergestellt und ist nun aktiv.",
+ "archivedTitle": "Aufgabe archiviert",
+ "archivedMessage": "Die Aufgabe wurde archiviert und aus der aktiven Liste entfernt.",
+ "startedTitle": "Aufgabe gestartet",
+ "startedMessage": "Die Aufgabe wurde als gestartet markiert.",
+ "pausedTitle": "Aufgabe pausiert",
+ "pausedMessage": "Die Aufgabe wurde pausiert.",
+ "deletedTitle": "Aufgabe gelöscht",
+ "deletedMessage": "Die Aufgabe wurde gelöscht."
+ },
+ "errors": {
+ "offlineRetry": "Die Anfrage wird erneut versucht, sobald du wieder online bist",
+ "failedToUpdate": "Aktualisierung fehlgeschlagen",
+ "failedToStart": "Start fehlgeschlagen",
+ "unableToStart": "Die Aufgabe konnte nicht gestartet werden",
+ "failedToPause": "Pausieren fehlgeschlagen",
+ "unableToPause": "Die Aufgabe konnte nicht pausiert werden",
+ "failedToApprove": "Genehmigung fehlgeschlagen",
+ "unableToApprove": "Die Aufgabe konnte nicht genehmigt werden",
+ "failedToReject": "Ablehnung fehlgeschlagen",
+ "unableToReject": "Die Aufgabe konnte nicht abgelehnt werden",
+ "deleteTitle": "Aufgabe löschen",
+ "deleteMessage": "Möchtest du diese Aufgabe wirklich löschen?",
+ "deleteFailed": "Löschen fehlgeschlagen",
+ "failedToArchive": "Archivieren fehlgeschlagen",
+ "unableToArchive": "Die Aufgabe konnte nicht archiviert werden",
+ "failedToSkip": "Überspringen fehlgeschlagen",
+ "failedRemoveDueDate": "Fälligkeitsdatum konnte nicht entfernt werden",
+ "failedReschedule": "Neuplanung fehlgeschlagen",
+ "unableUpdateDueDate": "Das Fälligkeitsdatum konnte nicht aktualisiert werden",
+ "nudgeFailedTitle": "Nudge senden fehlgeschlagen",
+ "nudgeFailedMessage": "Der Nudge kann derzeit nicht gesendet werden"
+ },
+ "nudgeSentTitle": "Nudge gesendet!",
+ "nudgeSentMessage": "Nudge erfolgreich gesendet",
+ "bulk": {
+ "completeTitle": "Aufgaben abschließen",
+ "completeConfirm": "{{count}} Aufgabe als erledigt markieren?",
+ "completeConfirm_plural": "{{count}} Aufgaben als erledigt markieren?",
+ "completeSuccessTitle": "Aufgaben abgeschlossen",
+ "completeSuccess": "{{count}} Aufgabe erfolgreich abgeschlossen.",
+ "completeSuccess_plural": "{{count}} Aufgaben erfolgreich abgeschlossen.",
+ "someFailedTitle": "Einige Aufgaben sind fehlgeschlagen",
+ "completeFailed": "{{count}} Aufgabe konnte nicht abgeschlossen werden.",
+ "completeFailed_plural": "{{count}} Aufgaben konnten nicht abgeschlossen werden.",
+ "completeUnexpectedTitle": "Sammelabschluss fehlgeschlagen",
+ "archiveTitle": "Aufgaben archivieren",
+ "archiveConfirm": "{{count}} Aufgabe archivieren?",
+ "archiveConfirm_plural": "{{count}} Aufgaben archivieren?",
+ "archiveSuccessTitle": "Aufgaben archiviert",
+ "archiveSuccess": "{{count}} Aufgabe erfolgreich archiviert.",
+ "archiveSuccess_plural": "{{count}} Aufgaben erfolgreich archiviert.",
+ "archiveFailed": "{{count}} Aufgabe konnte nicht archiviert werden.",
+ "archiveFailed_plural": "{{count}} Aufgaben konnten nicht archiviert werden.",
+ "archiveUnexpectedTitle": "Sammelarchivierung fehlgeschlagen",
+ "deleteTitle": "Aufgaben löschen",
+ "deleteConfirm": "{{count}} Aufgabe löschen?\n\nDiese Aktion kann nicht rückgängig gemacht werden.",
+ "deleteConfirm_plural": "{{count}} Aufgaben löschen?\n\nDiese Aktion kann nicht rückgängig gemacht werden.",
+ "deleteSuccessTitle": "Aufgaben gelöscht",
+ "deleteSuccess": "{{count}} Aufgabe erfolgreich gelöscht.",
+ "deleteSuccess_plural": "{{count}} Aufgaben erfolgreich gelöscht.",
+ "deleteFailed": "{{count}} Aufgabe konnte nicht gelöscht werden.",
+ "deleteFailed_plural": "{{count}} Aufgaben konnten nicht gelöscht werden.",
+ "deleteUnexpectedTitle": "Sammellöschung fehlgeschlagen"
}
+ },
+ "nudge": {
+ "description": "Sende eine Erinnerung, damit jemand diese Aufgabe im Blick behält.",
+ "headsUp": "Hinweis:",
+ "selfHostedWarning": "Erinnerungen funktionieren derzeit nur auf der offiziell gehosteten Donetick-Instanz.",
+ "customMessage": "Eigene Nachricht",
+ "customMessagePlaceholder": "Optionale Nachricht, die mit der Erinnerung gesendet wird",
+ "notifyAllDescription": "Benachrichtigt alle Zuständigen statt nur der aktuell zugewiesenen Person."
}
}
diff --git a/public/locales/de/common.json b/public/locales/de/common.json
index 50a53d09..d6858586 100644
--- a/public/locales/de/common.json
+++ b/public/locales/de/common.json
@@ -5,7 +5,7 @@
"edit": "Bearbeiten",
"close": "Schließen",
"confirm": "Bestätigen",
- "loading": "Laden...",
+ "loading": "Lädt...",
"error": "Fehler",
"success": "Erfolg",
"warning": "Warnung",
@@ -19,15 +19,174 @@
"backToCalendar": "Zurück zum Kalender",
"logout": "Abmelden",
"version": "Version",
+ "actions": {
+ "save": "Speichern",
+ "cancel": "Abbrechen",
+ "create": "Erstellen",
+ "close": "Schließen",
+ "remove": "Entfernen",
+ "restore": "Wiederherstellen",
+ "retry": "Erneut versuchen",
+ "approve": "Genehmigen",
+ "reject": "Ablehnen",
+ "skip": "Überspringen",
+ "test": "Testen",
+ "delete": "Löschen",
+ "edit": "Bearbeiten",
+ "clone": "Duplizieren",
+ "view": "Ansehen",
+ "archive": "Archivieren",
+ "unarchive": "Wiederherstellen",
+ "login": "Anmelden",
+ "logout": "Abmelden",
+ "signup": "Registrieren",
+ "backToLogin": "Zurück zur Anmeldung",
+ "continue": "Weiter",
+ "continueAs": "Weiter als {{name}}",
+ "verifyAndSignIn": "Prüfen und anmelden",
+ "changePhoto": "Foto ändern",
+ "rememberForFutureTasks": "Für zukünftige Aufgaben merken",
+ "registerDevice": "Gerät registrieren",
+ "navigateBack": "Zurück",
+ "or": "oder",
+ "complete": "Abschließen"
+ },
+ "modals": {
+ "selectUser": "Benutzer wählen"
+ },
+ "labels": {
+ "username": "Benutzername",
+ "email": "E-Mail",
+ "emailAddress": "E-Mail-Adresse",
+ "password": "Passwort",
+ "displayName": "Anzeigename",
+ "timezone": "Zeitzone",
+ "search": "Suchen",
+ "url": "URL",
+ "verificationCode": "Bestätigungscode",
+ "backupCode": "Backup-Code",
+ "points": "Punkte",
+ "tasks": "Aufgaben",
+ "task": "Aufgabe",
+ "overdue": "überfällig",
+ "name": "Name",
+ "title": "Titel",
+ "description": "Beschreibung",
+ "priority": "Priorität",
+ "project": "Projekt",
+ "defaultProject": "Standardprojekt",
+ "taskWindow": "Aufgabenfenster",
+ "notifications": "Benachrichtigungen",
+ "privacySettings": "Privatsphäre",
+ "assignmentStrategy": "Zuweisungsstrategie",
+ "labelsLabel": "Labels",
+ "subtasks": "Unteraufgaben",
+ "assignees": "Zuständige",
+ "dueDate": "Fälligkeitsdatum",
+ "startDate": "Startdatum",
+ "time": "Uhrzeit",
+ "hours": "Stunden",
+ "pointsLabel": "Punkte",
+ "public": "Öffentlich",
+ "limited": "Eingeschränkt",
+ "field": "Feld",
+ "condition": "Bedingung",
+ "value": "Wert",
+ "preview": "Vorschau",
+ "color": "Farbe",
+ "currentDevice": "Aktuelles Gerät",
+ "allAssignees": "Alle Zuständigen",
+ "specificGroup": "Bestimmte Gruppe",
+ "chatId": "Chat-ID",
+ "userKey": "Benutzerschlüssel",
+ "telegramGroupId": "Telegram-Gruppen-ID",
+ "stateIs": "Status ist",
+ "createAdvancedFilter": "Erweiterten Filter erstellen",
+ "editFilter": "Filter bearbeiten",
+ "plusFeature": "Plus-Funktion"
+ },
+ "errors": {
+ "nameCannotBeEmpty": "Name darf nicht leer sein",
+ "duplicateLabel": "Ein Label mit diesem Namen existiert bereits",
+ "selectColor": "Bitte wähle eine Farbe aus",
+ "unableToSaveLabel": "Label konnte nicht gespeichert werden. Bitte versuche es erneut."
+ },
+ "placeholders": {
+ "search": "Suchen",
+ "typeHere": "Hier eingeben...",
+ "enterDisplayName": "Anzeigename eingeben",
+ "selectTimezone": "Zeitzone auswählen",
+ "hours": "Stunden",
+ "telegramGroupId": "Telegram-Gruppen-ID"
+ },
+ "status": {
+ "loading": "Lädt...",
+ "offline": "Du bist offline",
+ "offlineDescription": "Das ist offline nicht verfügbar. Bitte prüfe deine Internetverbindung und versuche es erneut.",
+ "delayed": "Das dauert länger als üblich. Möglicherweise gibt es ein Problem.",
+ "noDataAvailable": "Keine Daten verfügbar",
+ "anyone": "Jede Person",
+ "unknown": "Unbekannt",
+ "unknownUser": "Unbekannter Benutzer",
+ "assignedToOther": "Jemand anderem zugewiesen",
+ "smartFilter": "Smarter Filter"
+ },
+ "legal": {
+ "privacyPolicy": "Datenschutzerklärung",
+ "termsOfUse": "Nutzungsbedingungen",
+ "termsOfService": "AGB"
+ },
+ "calendar": {
+ "today": "Heute",
+ "tomorrow": "Morgen",
+ "yesterday": "Gestern",
+ "weekend": "Wochenende",
+ "nextWeek": "Nächste Woche",
+ "removeDueDate": "Fälligkeitsdatum entfernen",
+ "tasksForDate": "Aufgaben für {{date}}"
+ },
+ "notifications": {
+ "titles": {
+ "error": "Fehler",
+ "success": "Erfolg",
+ "warning": "Warnung",
+ "info": "Information",
+ "undo": "Erfolgreich rückgängig gemacht",
+ "custom": "Benachrichtigung"
+ }
+ },
+ "editor": {
+ "placeholder": "Beschreibung eingeben...",
+ "plusFeature": "Plus-Funktion",
+ "plusFeatureMessage": "Bild-Uploads sind im Basic-Tarif nicht verfügbar. Upgrade auf Plus, um Bilder zu deinen Inhalten hinzuzufügen.",
+ "storageQuotaExceeded": "Speicherlimit überschritten",
+ "storageQuotaExceededMessage": "Du hast dein Kontingent für Datei-Uploads überschritten.",
+ "fileTooLarge": "Datei zu groß",
+ "fileTooLargeMessage": "Die Datei, die du hochladen möchtest, ist zu groß.",
+ "upgradeRequired": "Upgrade erforderlich",
+ "upgradeRequiredMessage": "Bild-Uploads sind nur für Plus-Konten verfügbar.",
+ "permissionDenied": "Zugriff verweigert",
+ "permissionDeniedMessage": "Du hast keine Berechtigung, Dateien hochzuladen.",
+ "uploadFailed": "Upload fehlgeschlagen",
+ "uploadFailedMessage": "Bild konnte nicht hochgeladen werden.",
+ "processingFailedMessage": "Beim Verarbeiten des Bildes ist ein Fehler aufgetreten."
+ },
+ "subtasks": {
+ "addSubtask": "Unteraufgabe hinzufügen",
+ "addNewSubtask": "Neue Unteraufgabe hinzufügen...",
+ "addNewTask": "Neue Aufgabe hinzufügen..."
+ },
"navigation": {
"allTasks": "Alle Aufgaben",
"archived": "Archiviert",
- "things": "Things",
- "labels": "Beschriftungen",
+ "things": "Dinge",
+ "labels": "Labels",
"projects": "Projekte",
"filters": "Filter",
"activities": "Aktivitäten",
"points": "Punkte",
- "settings": "Einstellungen"
+ "settings": "Einstellungen",
+ "back": "Zurück",
+ "backToCalendar": "Zurück zum Kalender"
}
}
diff --git a/public/locales/de/filters.json b/public/locales/de/filters.json
new file mode 100644
index 00000000..12c08644
--- /dev/null
+++ b/public/locales/de/filters.json
@@ -0,0 +1,64 @@
+{
+ "view": {
+ "title": "Filter",
+ "description": "Speichere deine bevorzugten Filterkombinationen für schnellen Zugriff. Erstelle eigene Ansichten, um Aufgaben schneller zu organisieren und zu finden.",
+ "noConditions": "Keine Bedingungen",
+ "oneCondition": "1 Bedingung",
+ "manyConditions": "{{count}} Bedingungen",
+ "tasksCount": "{{count}} Aufgaben",
+ "overdueCount": "{{count}} überfällig",
+ "usedCount": "{{count}}x verwendet",
+ "emptyTitle": "Noch keine gespeicherten Filter",
+ "emptyDescription": "Erstelle benutzerdefinierte Filter, um schnell auf deine meistgenutzten Aufgabenansichten zuzugreifen.",
+ "pin": "Anheften",
+ "unpin": "Lösen",
+ "deleteTitle": "Filter löschen",
+ "deleteMessage": "Möchtest du \"{{name}}\" wirklich löschen? Dies kann nicht rückgängig gemacht werden."
+ },
+ "advanced": {
+ "editTitle": "Filter bearbeiten",
+ "createTitle": "Erweiterten Filter erstellen",
+ "filterName": "Filtername",
+ "filterNamePlaceholder": "Zum Beispiel wichtige Aufgaben, die bald fällig sind, oder Aufgaben für John",
+ "descriptionOptional": "Beschreibung (optional)",
+ "descriptionPlaceholder": "Optionale Beschreibung für diesen Filter...",
+ "conditions": "Filterbedingungen (alle müssen zutreffen)",
+ "conditionNumber": "Bedingung {{index}}",
+ "addCondition": "Bedingung hinzufügen",
+ "preview": "Vorschau",
+ "noMatches": "Keine Aufgaben entsprechen diesen Filtern",
+ "andMore": "...und {{count}} weitere",
+ "selectAssignees": "Zuständige auswählen",
+ "selectCreators": "Ersteller auswählen",
+ "selectPriorities": "Prioritäten auswählen",
+ "selectLabels": "Labels auswählen",
+ "selectProjects": "Projekte auswählen",
+ "selectStatuses": "Status auswählen",
+ "defaultProject": "Standardprojekt",
+ "unknown": "Unbekannt",
+ "active": "Aktiv",
+ "started": "Gestartet",
+ "inProgress": "In Bearbeitung",
+ "pendingApproval": "Wartet auf Freigabe",
+ "isOverdue": "Ist überfällig",
+ "isDueToday": "Ist heute fällig",
+ "isDueTomorrow": "Ist morgen fällig",
+ "isDueThisWeek": "Ist diese Woche fällig",
+ "isDueThisMonth": "Ist diesen Monat fällig",
+ "hasNoDueDate": "Hat kein Fälligkeitsdatum",
+ "hasDueDate": "Hat ein Fälligkeitsdatum",
+ "equals": "Gleich",
+ "greaterThan": "Größer als",
+ "lessThan": "Kleiner als",
+ "greaterThanOrEqual": "Größer oder gleich",
+ "lessThanOrEqual": "Kleiner oder gleich",
+ "assignee": "Zuständige Person",
+ "createdBy": "Erstellt von",
+ "label": "Label",
+ "status": "Status",
+ "points": "Punkte",
+ "enterFilterName": "Bitte gib einen Filternamen ein",
+ "duplicateFilterName": "Ein Filter mit diesem Namen existiert bereits",
+ "addAtLeastOneCondition": "Bitte füge mindestens eine Filterbedingung hinzu"
+ }
+}
diff --git a/public/locales/de/history.json b/public/locales/de/history.json
new file mode 100644
index 00000000..15e0c21c
--- /dev/null
+++ b/public/locales/de/history.json
@@ -0,0 +1,64 @@
+{
+ "summaryTitle": "Aufgabenübersicht",
+ "activityTitle": "Aufgabenaktivität",
+ "common": {
+ "unknown": "Unbekannt",
+ "updatedAt": "Aktualisiert am {{date}}"
+ },
+ "assignedTo": "Zugewiesen an {{name}}",
+ "note": "Notiz",
+ "points_one": "{{count}} Punkt",
+ "points_other": "{{count}} Punkte",
+ "delete": {
+ "title": "Verlaufseintrag löschen",
+ "confirm": "Möchtest du diesen Verlaufseintrag wirklich löschen?"
+ },
+ "empty": {
+ "title": "Noch kein Verlauf",
+ "description": "Du hast noch keine Aufgaben erledigt. Sobald du Aufgaben abschließt, erscheinen sie hier.",
+ "backToChores": "Zurück zu den Aufgaben"
+ },
+ "info": {
+ "completedTitle": "Du hast abgeschlossen",
+ "completedValue": "12345 Aufgaben"
+ },
+ "edit": {
+ "title": "Verlauf bearbeiten",
+ "completedDate": "Abschlussdatum",
+ "note": "Notiz",
+ "additionalNotes": "Zusätzliche Notizen",
+ "deleteConfirm": "Möchtest du diesen Verlauf wirklich löschen?"
+ },
+ "stats": {
+ "allCompleted": "Alles abgeschlossen",
+ "averageTiming": "Durchschnittliches Timing",
+ "longestDelay": "Längste Verzögerung",
+ "completedMost": "Am häufigsten erledigt",
+ "membersInvolved": "Beteiligte Mitglieder",
+ "lastCompleted": "Zuletzt erledigt",
+ "onTime": "Pünktlich",
+ "neverLate": "Nie verspätet",
+ "times_one": "{{count}} Mal",
+ "times_other": "{{count}} Mal",
+ "membersCount_one": "{{count}} Mitglied",
+ "membersCount_other": "{{count}} Mitglieder"
+ },
+ "status": {
+ "inProgress": "In Arbeit",
+ "completed": "Abgeschlossen",
+ "skipped": "Übersprungen",
+ "pendingApproval": "Wartet auf Freigabe",
+ "rejected": "Abgelehnt",
+ "missed": "Verpasst",
+ "rescheduled": "Neu geplant",
+ "onTime": "Pünktlich",
+ "early": "Früh",
+ "late": "Spät"
+ },
+ "messages": {
+ "updatedTitle": "Verlauf aktualisiert",
+ "updatedMessage": "Der Verlaufseintrag wurde erfolgreich aktualisiert.",
+ "deletedTitle": "Verlauf gelöscht",
+ "deletedMessage": "Der Verlaufseintrag wurde erfolgreich gelöscht."
+ }
+}
diff --git a/public/locales/de/labelsView.json b/public/locales/de/labelsView.json
new file mode 100644
index 00000000..557464e4
--- /dev/null
+++ b/public/locales/de/labelsView.json
@@ -0,0 +1,9 @@
+{
+ "title": "Labels",
+ "description": "Verwalte deine Labels und organisiere deine Aufgaben effektiv. Labels werden automatisch mit deinem Kreis geteilt, wenn sie in einer geteilten Aufgabe verwendet werden.",
+ "shared": "Geteilt",
+ "deleteTitle": "Label löschen",
+ "deleteMessage": "Möchtest du dieses Label wirklich löschen? Es wird dann von allen Aufgaben entfernt.",
+ "loadFailed": "Labels konnten nicht geladen werden. Bitte versuche es erneut.",
+ "empty": "Keine Labels vorhanden. Füge ein neues Label hinzu, um zu starten."
+}
diff --git a/public/locales/de/navigation.json b/public/locales/de/navigation.json
new file mode 100644
index 00000000..aa1f1391
--- /dev/null
+++ b/public/locales/de/navigation.json
@@ -0,0 +1,13 @@
+{
+ "allTasks": "Alle Aufgaben",
+ "archived": "Archiviert",
+ "things": "Dinge",
+ "labels": "Labels",
+ "projects": "Projekte",
+ "filters": "Filter",
+ "activities": "Aktivitäten",
+ "points": "Punkte",
+ "settings": "Einstellungen",
+ "back": "Zurück",
+ "backToCalendar": "Zurück zum Kalender"
+}
diff --git a/public/locales/de/projects.json b/public/locales/de/projects.json
new file mode 100644
index 00000000..53ab9dbe
--- /dev/null
+++ b/public/locales/de/projects.json
@@ -0,0 +1,29 @@
+{
+ "view": {
+ "title": "Projekte",
+ "description": "Organisiere deine Aufgaben in Projekten. Erstelle eigene Arbeitsbereiche, damit deine Aufgaben strukturiert und leicht zugänglich bleiben.",
+ "default": "Standard",
+ "defaultProjectName": "Standardprojekt",
+ "defaultProjectDescription": "Alle Aufgaben ohne spezifisches Projekt",
+ "tasksCount": "{{count}} Aufgaben",
+ "shared": "Geteilt",
+ "loadFailed": "Projekte konnten nicht geladen werden. Bitte versuche es erneut.",
+ "deleteTitle": "Projekt löschen",
+ "deleteMessage": "Möchtest du \"{{name}}\" wirklich löschen? Das Projekt wird entfernt, aber alle Aufgaben bleiben erhalten und werden ins Standardprojekt verschoben."
+ },
+ "modal": {
+ "createTitle": "Neues Projekt erstellen",
+ "editTitle": "Projekt bearbeiten",
+ "name": "Projektname",
+ "namePlaceholder": "Projektnamen eingeben...",
+ "nameRequired": "Projektname ist erforderlich",
+ "descriptionPlaceholder": "Optionale Projektbeschreibung...",
+ "icon": "Projekticon",
+ "chooseIconTitle": "Projekticon auswählen",
+ "availableIcons": "Verfügbare Icons",
+ "selectIcon": "Icon auswählen",
+ "color": "Projektfarbe",
+ "createFailed": "Projekt konnte nicht erstellt werden",
+ "updateFailed": "Projekt konnte nicht aktualisiert werden"
+ }
+}
diff --git a/public/locales/de/settings.json b/public/locales/de/settings.json
index 372a1fce..6e0497a1 100644
--- a/public/locales/de/settings.json
+++ b/public/locales/de/settings.json
@@ -1,174 +1,682 @@
{
- "title": "Einstellungen",
- "circleSettings": {
- "title": "Kreis-Einstellungen",
- "description": "Dein Account wird automatisch mit einem Kreis verbunden, wenn du einen erstellst oder beitrittst. Lade einfach Freunde ein, indem du den einzigartigen Kreis-Code oder Link unten teilst. Du erhältst eine Benachrichtigung, wenn jemand deinem Kreis beitreten möchte. Wenn du gehen möchtest, klicke einfach auf 'Kreis verlassen'.",
- "circleCode": "Kreis-Code",
- "copyCode": "Code kopieren",
- "copyLink": "Link kopieren",
- "codeCopied": "Kreis-Code kopiert!",
- "linkCopied": "Kreis-Link kopiert!",
- "joinCircle": "Einem Kreis beitreten",
- "joinCirclePlaceholder": "Kreis-Code eingeben",
- "join": "Beitreten",
- "leave": "Kreis verlassen",
- "leaveConfirmTitle": "Kreis verlassen",
- "leaveConfirmMessage": "Bist du sicher, dass du diesen Kreis verlassen möchtest?",
- "circleMembers": "Kreis-Mitglieder",
- "circleMemberRequests": "Kreis-Beitrittsanfragen",
- "admin": "Administrator",
- "member": "Mitglied",
- "pending": "Ausstehend",
- "accept": "Akzeptieren",
- "reject": "Ablehnen",
- "makeAdmin": "Zum Administrator machen",
- "makeMember": "Zum Mitglied machen",
- "remove": "Entfernen",
- "webhookURL": "Webhook-URL",
- "webhookDescription": "Gib eine Webhook-URL ein, um Benachrichtigungen für Kreis-Ereignisse zu erhalten",
- "webhookPlaceholder": "https://deine-webhook-url.com"
- },
- "accountSettings": {
- "title": "Account-Einstellungen",
- "subscription": "Abonnement",
- "subscriptionStatus": "Aktueller Plan",
- "free": "Kostenlos",
- "plus": "Plus",
- "upgrade": "Upgraden",
- "cancel": "Kündigen",
- "changePassword": "Passwort ändern",
- "password": "Passwort",
- "dangerZone": "Gefahrenbereich",
- "dangerZoneDescription": "Sobald du dein Konto löschst, gibt es kein Zurück mehr. Bitte sei dir sicher.",
- "deleteAccount": "Account löschen"
- },
- "localization": {
- "title": "Sprach- und Formateinstellungen",
- "description": "Passe Sprache, Datumsformat und regionale Einstellungen für dein Konto an.",
- "language": "Sprache",
- "languageDescription": "Wähle deine bevorzugte Sprache",
- "dateFormat": "Datumsformat",
- "dateFormatDescription": "Wähle, wie Daten in der gesamten Anwendung angezeigt werden sollen",
- "timeFormat": "Zeitformat",
- "timeFormatDescription": "Wähle 12-Stunden- oder 24-Stunden-Zeitformat",
- "12hour": "12-Stunden (AM/PM)",
- "24hour": "24-Stunden",
- "firstDayOfWeek": "Erster Tag der Woche",
- "firstDayOfWeekDescription": "Wähle, welcher Tag deine Woche beginnt",
- "sunday": "Sonntag",
- "monday": "Montag",
- "saturday": "Samstag",
- "formats": {
- "mdy": "MM/TT/JJJJ (USA)",
- "dmy": "TT/MM/JJJJ (Europa)",
- "ymd": "JJJJ-MM-TT (ISO)",
- "long": "Langes Format (z.B. 1. Januar 2024)",
- "short": "Kurzes Format (z.B. 1. Jan 2024)"
- }
- },
- "sidepanel": {
- "title": "Seitenleisten-Anpassung",
- "description": "Passe das Layout und die Sichtbarkeit von Karten in der Seitenleiste an. Dieser Bereich ist nur auf großen Bildschirmgeräten wie Tablets und Desktops verfügbar."
- },
- "theme": {
- "title": "Theme-Einstellungen",
- "description": "Wähle, wie die Seite für dich aussieht. Wähle ein einzelnes Theme oder synchronisiere mit deinem System und wechsle automatisch zwischen Tag- und Nacht-Themes.",
- "themeMode": "Theme-Modus",
- "light": "Hell",
- "dark": "Dunkel",
- "system": "System"
- },
- "notifications": {
- "settingsSaved": "Einstellungen erfolgreich gespeichert",
- "settingsSaveFailed": "Speichern der Einstellungen fehlgeschlagen",
- "invalidWebhook": "Ungültige Webhook-URL"
- },
- "profile": {
- "title": "Profil-Einstellungen",
- "description": "Aktualisiere deinen Anzeigenamen und dein Profilbild.",
- "photoUpdated": "Foto aktualisiert",
- "photoUpdatedMessage": "Dein Profilbild wurde erfolgreich aktualisiert!",
- "uploadFailed": "Upload fehlgeschlagen",
- "uploadFailedMessage": "Das Hochladen deines Fotos ist fehlgeschlagen. Bitte versuche es erneut.",
- "profileUpdated": "Profil aktualisiert",
- "profileUpdatedMessage": "Deine Profilinformationen wurden erfolgreich gespeichert!",
- "updateFailed": "Update fehlgeschlagen",
- "updateFailedMessage": "Dein Profil konnte nicht aktualisiert werden. Bitte überprüfe deine Verbindung und versuche es erneut.",
- "changePhoto": "Foto ändern",
- "displayName": "Anzeigename",
- "displayNamePlaceholder": "Gib deinen Anzeigenamen ein",
- "timezone": "Zeitzone",
- "timezonePlaceholder": "Wähle deine Zeitzone",
- "save": "Speichern",
- "cancel": "Abbrechen"
- },
"overview": {
"title": "Einstellungen",
- "subtitle": "Passe deine Erfahrung an und verwalte deine Account-Einstellungen",
+ "subtitle": "Passe dein Erlebnis an und verwalte deine Kontoeinstellungen",
+ "cards": {
+ "profile": {
+ "title": "Profileinstellungen",
+ "description": "Aktualisiere dein Profil, Foto, deinen Anzeigenamen und die Zeitzone."
+ },
+ "circle": {
+ "title": "Kreis-Einstellungen",
+ "description": "Verwalte deinen Kreis, lade Mitglieder ein und bearbeite Anfragen."
+ },
+ "account": {
+ "title": "Kontoeinstellungen",
+ "description": "Verwalte dein Abo, ändere dein Passwort und lösche dein Konto."
+ },
+ "subaccounts": {
+ "title": "Verwaltete Konten",
+ "description": "Erstelle und verwalte Unterkonten, die sich anmelden und Aufgaben erledigen können."
+ },
+ "notifications": {
+ "title": "Benachrichtigungen",
+ "description": "Konfiguriere Push-Benachrichtigungen, E-Mail-Hinweise und Benachrichtigungsziele."
+ },
+ "mfa": {
+ "title": "Mehr-Faktor-Authentifizierung",
+ "description": "Füge mit MFA über Authenticator-Apps eine zusätzliche Sicherheitsebene hinzu."
+ },
+ "apitokens": {
+ "title": "API-Tokens",
+ "description": "Erstelle und verwalte Zugriffstokens für Integrationen und API-Zugriff."
+ },
+ "storage": {
+ "title": "Speichereinstellungen",
+ "description": "Sichere und stelle Daten wieder her und verwalte lokale Speicheroptionen."
+ },
+ "sidepanel": {
+ "title": "Seitenleiste anpassen",
+ "description": "Passe das Layout und die Sichtbarkeit von Karten in der Seitenleiste an."
+ },
+ "theme": {
+ "title": "Design",
+ "description": "Wähle dein bevorzugtes Design und konfiguriere Hell- und Dunkelmodus."
+ },
+ "language": {
+ "title": "Sprache",
+ "description": "Wähle die Sprache, die auf diesem Gerät in der App verwendet wird."
+ },
+ "advanced": {
+ "title": "Erweiterte Einstellungen",
+ "description": "Konfiguriere Webhooks, Echtzeit-Updates und weitere fortgeschrittene Funktionen."
+ },
+ "developer": {
+ "title": "Entwicklereinstellungen",
+ "description": "Zeige technische Informationen zu Tokens, SSE-Verbindungen und Debug-Daten an."
+ }
+ },
"upgrade": {
"title": "Auf Plus upgraden",
- "description": "Schalte mächtige Funktionen frei, um deine Produktivität zu steigern",
+ "description": "Schalte leistungsstarke Funktionen frei, um deine Produktivität zu steigern",
"button": "Jetzt upgraden",
"features": {
- "richText": "Rich-Text-Beschreibungen",
- "notifications": "Aufgaben-Benachrichtigungen",
+ "richText": "Formatierte Beschreibungen",
+ "notifications": "Aufgabenbenachrichtigungen",
"apiIntegrations": "API-Integrationen",
"advancedAutomation": "Erweiterte Automatisierung"
}
},
"sections": {
"profile": {
- "title": "Profil-Einstellungen",
- "description": "Aktualisiere deine Profilinformationen, Foto, Anzeigenamen und Zeitzonen-Einstellungen."
+ "title": "Profileinstellungen",
+ "description": "Aktualisiere dein Profil, Foto, deinen Anzeigenamen und die Zeitzone."
},
"circle": {
"title": "Kreis-Einstellungen",
- "description": "Verwalte deinen Kreis, lade Mitglieder ein und bearbeite Beitrittsanfragen."
+ "description": "Verwalte deinen Kreis, lade Mitglieder ein und bearbeite Anfragen."
},
"account": {
- "title": "Account-Einstellungen",
- "description": "Verwalte dein Abonnement, ändere dein Passwort und Account-Löschoptionen."
+ "title": "Kontoeinstellungen",
+ "description": "Verwalte dein Abo, ändere dein Passwort und lösche dein Konto."
},
"subaccounts": {
- "title": "Verwaltete Accounts",
- "description": "Erstelle und verwalte Unter-Accounts zum Anmelden und Erledigen zugewiesener Aufgaben."
+ "title": "Verwaltete Konten",
+ "description": "Erstelle und verwalte Unterkonten, die sich anmelden und Aufgaben erledigen können."
},
"notifications": {
"title": "Benachrichtigungen",
- "description": "Konfiguriere Push-Benachrichtigungen, E-Mail-Benachrichtigungen und Benachrichtigungsziele für Aufgaben."
+ "description": "Konfiguriere Push-Benachrichtigungen, E-Mail-Hinweise und Benachrichtigungsziele."
},
"mfa": {
- "title": "Multi-Faktor-Authentifizierung",
- "description": "Füge eine zusätzliche Sicherheitsebene mit MFA über Authenticator-Apps hinzu."
+ "title": "Mehr-Faktor-Authentifizierung",
+ "description": "Füge mit MFA über Authenticator-Apps eine zusätzliche Sicherheitsebene hinzu."
},
"apitokens": {
- "title": "API-Token",
- "description": "Generiere und verwalte Zugriffstoken für Drittanbieter-Integrationen und API-Zugang."
+ "title": "API-Tokens",
+ "description": "Erstelle und verwalte Zugriffstokens für Integrationen und API-Zugriff."
},
"storage": {
- "title": "Speicher-Einstellungen",
- "description": "Sichere und stelle deine Daten wieder her, verwalte lokalen Speicher und Synchronisierungseinstellungen."
+ "title": "Speichereinstellungen",
+ "description": "Sichere und stelle Daten wieder her und verwalte lokale Speicheroptionen."
},
"sidepanel": {
- "title": "Seitenleisten-Anpassung",
- "description": "Passe das Layout und die Sichtbarkeit von Karten in der Seitenleisten-Oberfläche an."
+ "title": "Seitenleiste anpassen",
+ "description": "Passe das Layout und die Sichtbarkeit von Karten in der Seitenleiste an."
},
"theme": {
- "title": "Theme-Einstellungen",
- "description": "Wähle dein bevorzugtes Theme und konfiguriere Dunkel-/Hell-Modus-Einstellungen."
+ "title": "Design",
+ "description": "Wähle dein bevorzugtes Design und konfiguriere Hell- und Dunkelmodus."
},
"localization": {
- "title": "Sprach- und Formateinstellungen",
+ "title": "Lokalisierung",
"description": "Passe Sprache, Datumsformat, Zeitformat und regionale Einstellungen an."
},
"advanced": {
"title": "Erweiterte Einstellungen",
- "description": "Konfiguriere Webhooks, Echtzeit-Updates und andere erweiterte Funktionen für erhöhte Produktivität."
+ "description": "Konfiguriere Webhooks, Echtzeit-Updates und weitere fortgeschrittene Funktionen."
},
"developer": {
- "title": "Entwickler-Einstellungen",
- "description": "Zeige technische Informationen über Authentifizierungs-Token, SSE-Verbindungen und Debug-Daten an."
+ "title": "Entwicklereinstellungen",
+ "description": "Zeige technische Informationen zu Tokens, SSE-Verbindungen und Debug-Daten an."
+ }
+ }
+ },
+ "pages": {
+ "profile": {
+ "title": "Profileinstellungen"
+ },
+ "circle": {
+ "title": "Kreis-Einstellungen"
+ },
+ "account": {
+ "title": "Kontoeinstellungen"
+ },
+ "managedAccounts": {
+ "title": "Verwaltete Konten"
+ },
+ "subAccountManagement": {
+ "title": "Unterkonten verwalten"
+ },
+ "notifications": {
+ "title": "Benachrichtigungseinstellungen"
+ },
+ "mfa": {
+ "title": "Mehr-Faktor-Authentifizierung"
+ },
+ "apiTokens": {
+ "title": "API-Tokens"
+ },
+ "storage": {
+ "title": "Speichereinstellungen"
+ },
+ "sidepanel": {
+ "title": "Seitenleiste anpassen"
+ },
+ "theme": {
+ "title": "Design"
+ },
+ "language": {
+ "title": "Sprache"
+ },
+ "advanced": {
+ "title": "Erweiterte Einstellungen"
+ },
+ "developer": {
+ "title": "Entwicklereinstellungen"
+ }
+ },
+ "sidepanel": {
+ "heading": "Karten der Seitenleiste anpassen",
+ "description": "Wähle aus, welche Karten in der Seitenleiste sichtbar sind, und ordne sie passend zu deinem Ablauf neu an.",
+ "reset": "Auf Standard zurücksetzen",
+ "resetHelp": "Stellt die empfohlene Kartenreihenfolge und Sichtbarkeit wieder her.",
+ "cards": {
+ "welcome": {
+ "title": "Benutzerwechsel",
+ "description": "Ermöglicht Admins und Managern, Aufgaben aus Sicht anderer Benutzer anzusehen."
+ },
+ "smartInsights": {
+ "title": "Smarte Hinweise",
+ "description": "Zeigt schnelle Aktionen basierend auf deinen aktuellen Aufgaben."
+ },
+ "assignees": {
+ "title": "Aufgaben nach Zuständigen",
+ "description": "Gruppiert Aufgaben nach der Person, der sie zugewiesen sind."
+ },
+ "calendar": {
+ "title": "Kalenderansicht",
+ "description": "Zeigt Aufgaben in einer Kalenderansicht."
+ },
+ "activities": {
+ "title": "Letzte Aktivitäten",
+ "description": "Zeigt kürzliche Abschlüsse und weitere Aktivitäten an."
+ },
+ "weeklyGoals": {
+ "title": "Wochenziele",
+ "description": "Zeigt den Wochenfortschritt und Abschlussstatistiken deines Kreises."
}
}
+ },
+ "theme": {
+ "description": "Lege fest, wie die App für dich aussieht. Wähle ein festes Design oder synchronisiere mit deinem System.",
+ "modeLabel": "Designmodus",
+ "light": "Hell",
+ "dark": "Dunkel",
+ "system": "System"
+ },
+ "profile": {
+ "description": "Aktualisiere deinen Anzeigenamen und dein Profilfoto.",
+ "title": "Profileinstellungen",
+ "photoUpdatedTitle": "Foto aktualisiert",
+ "photoUpdated": "Foto aktualisiert",
+ "photoUpdatedMessage": "Dein Profilfoto wurde erfolgreich aktualisiert.",
+ "uploadFailedTitle": "Upload fehlgeschlagen",
+ "uploadFailed": "Upload fehlgeschlagen",
+ "uploadFailedMessage": "Dein Foto konnte nicht hochgeladen werden. Bitte versuche es erneut.",
+ "profileUpdatedTitle": "Profil aktualisiert",
+ "profileUpdated": "Profil aktualisiert",
+ "profileUpdatedMessage": "Deine Profilinformationen wurden erfolgreich gespeichert.",
+ "updateFailedTitle": "Aktualisierung fehlgeschlagen",
+ "updateFailed": "Aktualisierung fehlgeschlagen",
+ "updateFailedMessage": "Dein Profil konnte nicht aktualisiert werden. Bitte prüfe deine Verbindung und versuche es erneut.",
+ "changePhoto": "Foto ändern",
+ "displayName": "Anzeigename",
+ "displayNamePlaceholder": "Anzeigename eingeben",
+ "timezone": "Zeitzone",
+ "timezonePlaceholder": "Zeitzone auswählen",
+ "save": "Speichern",
+ "cancel": "Abbrechen"
+ },
+ "language": {
+ "title": "Sprache",
+ "description": "Wähle die Sprache der App. Diese Einstellung wird nur auf diesem Gerät gespeichert.",
+ "fieldLabel": "App-Sprache",
+ "options": {
+ "en": "Englisch",
+ "de": "Deutsch",
+ "es": "Spanisch",
+ "pt": "Portugiesisch"
+ }
+ },
+ "childAccounts": {
+ "title": "Verwaltete Konten",
+ "accessDeniedTitle": "Unterkonten verwalten",
+ "parentOnly": "Nur Hauptkonten können Unterkonten verwalten.",
+ "description": "Verwalte Unterkonten. Unterkonten können sich anmelden und zugewiesene Aufgaben erledigen.",
+ "planWarning": "Unterkonten sind im Free-Tarif auf 1 begrenzt. Mit Plus sind bis zu 5 Unterkonten möglich.",
+ "sectionTitle": "Unterkonten ({{count}})",
+ "add": "Unterkonto hinzufügen",
+ "loading": "Unterkonten werden geladen...",
+ "emptyTitle": "Keine Unterkonten",
+ "emptyDescription": "Erstelle Unterkonten, damit Teammitglieder sich anmelden und ihre zugewiesenen Aufgaben erledigen können.",
+ "addFirst": "Erstes Unterkonto hinzufügen",
+ "username": "Benutzername: {{username}}",
+ "created": "Erstellt: {{date}}",
+ "changePasswordTitle": "Passwort ändern",
+ "deleteAccountTitle": "Konto löschen",
+ "howItWorksTitle": "So funktionieren verwaltete Konten",
+ "bulletOne": "Verwaltete Konten werden vom Hauptkonto erstellt und können von dort gelöscht oder im Passwort zurückgesetzt werden.",
+ "bulletTwo": "Unterkonten können sich mit eigenem Benutzernamen und Passwort anmelden.",
+ "bulletThree": "Verwaltete Konten können Aufgaben erledigen, haben aber eingeschränkte Verwaltungsrechte.",
+ "bulletFour": "Verwaltete Konten werden automatisch deinem Kreis hinzugefügt.",
+ "createSuccess": "Unterkonto \"{{name}}\" wurde erfolgreich erstellt!",
+ "createFailed": "Unterkonto konnte nicht erstellt werden: {{message}}",
+ "updatePasswordSuccess": "Passwort des Unterkontos erfolgreich aktualisiert",
+ "updatePasswordFailed": "Passwort konnte nicht aktualisiert werden: {{message}}",
+ "deleteConfirmTitle": "Unterkonto löschen",
+ "deleteConfirmMessage": "Möchtest du das Unterkonto \"{{name}}\" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
+ "deleteSuccess": "Unterkonto \"{{name}}\" wurde erfolgreich gelöscht",
+ "deleteFailed": "Unterkonto konnte nicht gelöscht werden: {{message}}",
+ "modal": {
+ "title": "Unterkonto erstellen",
+ "description": "Erstelle ein neues Unterkonto. Die Person kann sich mit dem kombinierten Benutzernamen anmelden und ihr zugewiesene Aufgaben erledigen.",
+ "nameLabel": "Name des Unterkontos",
+ "namePlaceholder": "Name des Unterkontos eingeben (z. B. sarah)",
+ "displayNamePlaceholder": "Anzeigename (optional, standardmäßig der Name des Unterkontos)",
+ "confirmPasswordLabel": "Passwort bestätigen",
+ "confirmPasswordPlaceholder": "Passwort bestätigen",
+ "createAction": "Konto erstellen"
+ },
+ "validation": {
+ "nameRequired": "Name des Unterkontos ist erforderlich",
+ "nameMin": "Name des Unterkontos muss mindestens 2 Zeichen lang sein",
+ "nameMax": "Name des Unterkontos muss kürzer als 20 Zeichen sein",
+ "namePattern": "Name des Unterkontos darf nur Kleinbuchstaben, Punkt und Bindestrich enthalten",
+ "displayNameMax": "Anzeigename muss kürzer als 50 Zeichen sein",
+ "passwordMismatch": "Passwörter stimmen nicht überein"
+ }
+ },
+ "advanced": {
+ "title": "Erweiterte Einstellungen",
+ "loading": "Lädt...",
+ "description": "Konfiguriere erweiterte Funktionen wie Webhooks und Echtzeit-Updates für mehr Produktivität.",
+ "webhookTitle": "Webhook",
+ "webhookDescription": "Webhooks senden Echtzeit-Benachrichtigungen an andere Dienste, wenn Ereignisse in deinem Kreis passieren. Konfiguriere eine Webhook-URL, um Updates zu erhalten.",
+ "webhookPlanWarning": "Webhook-Benachrichtigungen sind im Basic-Tarif nicht verfügbar. Upgrade auf Plus, um Echtzeit-Updates per Webhook zu erhalten.",
+ "enableWebhook": "Webhook aktivieren",
+ "enableWebhookHelper": "Webhook-Benachrichtigungen für Aufgaben- und Dinge-Updates aktivieren.",
+ "webhookUrl": "Webhook-URL",
+ "webhookSaved": "Webhook-URL erfolgreich aktualisiert",
+ "webhookSaveFailed": "Webhook-URL konnte nicht aktualisiert werden",
+ "realtimeTitle": "Echtzeit-Updates",
+ "realtimeSummary": "Erhalte sofortige Benachrichtigungen, wenn Aufgaben aktualisiert werden",
+ "enableRealtime": "Echtzeit-Updates aktivieren",
+ "realtimeUnavailable": "Echtzeit-Updates sind im Basic-Tarif nicht verfügbar. Upgrade auf Plus, um sofortige Benachrichtigungen bei Aufgabenänderungen zu erhalten.",
+ "realtimeDisabled": "Echtzeit-Updates sind deaktiviert. Aktiviere sie, um Live-Änderungen zu sehen, wenn du oder andere Mitglieder Aufgaben erledigen, überspringen oder ändern.",
+ "realtimeConnected": "Echtzeit-Updates funktionieren. Du siehst Live-Änderungen, wenn du oder andere Mitglieder Aufgaben erledigen, überspringen oder ändern.",
+ "realtimeConnecting": "Verbindung zu Echtzeit-Updates wird hergestellt...",
+ "realtimeError": "Echtzeit-Updates sind aktiviert, funktionieren aber nicht: {{error}}",
+ "realtimeEnabledNotConnected": "Echtzeit-Updates sind aktiviert, aber derzeit nicht verbunden.",
+ "sseTitle": "Echtzeit-Updates (SSE)",
+ "sseSummary": "Erhalte sofortige Benachrichtigungen per Server-Sent Events",
+ "enableSse": "Echtzeit-Updates (SSE) aktivieren",
+ "sseUnavailable": "Echtzeit-Updates (SSE) sind im Basic-Tarif nicht verfügbar. Upgrade auf Plus, um sofortige Benachrichtigungen bei Aufgabenänderungen zu erhalten.",
+ "sseDisabled": "Echtzeit-Updates (SSE) sind deaktiviert. Aktiviere sie, um Live-Änderungen zu sehen, wenn du oder andere Mitglieder Aufgaben erledigen, überspringen oder ändern.",
+ "sseConnected": "Echtzeit-Updates (SSE) funktionieren. Du siehst Live-Änderungen, wenn du oder andere Mitglieder Aufgaben erledigen, überspringen oder ändern.",
+ "sseConnecting": "Verbindung zu Echtzeit-Updates (SSE) wird hergestellt...",
+ "sseError": "Echtzeit-Updates (SSE) sind aktiviert, funktionieren aber nicht: {{error}}",
+ "sseEnabledNotConnected": "Echtzeit-Updates (SSE) sind aktiviert, aber derzeit nicht verbunden.",
+ "status": "Status:",
+ "connection": {
+ "connected": "Verbunden",
+ "connecting": "Verbinde...",
+ "disconnected": "Getrennt",
+ "tooltipBase": "Echtzeit-Updates (SSE): {{status}}",
+ "tooltipError": "Echtzeit-Updates (SSE): {{status}} - {{error}}",
+ "tooltipJoinCircle": "Echtzeit-Updates (SSE): {{status}} - Tritt einem Kreis bei, um Echtzeit-Updates zu aktivieren"
+ },
+ "on": "An",
+ "off": "Aus",
+ "webhookIntegration": "Webhook-Integration",
+ "realtimeSectionDescription": "Lege fest, wie du Live-Updates erhältst, wenn sich Aufgaben und Aktivitäten in deinem Kreis ändern."
+ },
+ "account": {
+ "description": "Ändere deine Kontoeinstellungen, deinen Abostatus oder dein Passwort.",
+ "accountType": "Kontotyp",
+ "loading": "Lädt...",
+ "plus": "Plus",
+ "free": "Kostenlos",
+ "activePlan": "Du bist derzeit im Plus-Tarif. Dein Abo verlängert sich am {{date}}.",
+ "cancelledPlan": "Du hast dein Abo gekündigt. Dein Konto wird am {{date}} auf den Free-Tarif umgestellt.",
+ "freePlan": "Du nutzt derzeit den Free-Tarif. Upgrade auf Plus, um weitere Funktionen freizuschalten.",
+ "upgrade": "Upgrade",
+ "password": "Passwort",
+ "changePassword": "Passwort ändern",
+ "dangerZone": "Gefahrenbereich",
+ "dangerDescription": "Sobald du dein Konto löschst, gibt es kein Zurück mehr. Bitte sei dir sicher.",
+ "deleteAccount": "Konto löschen",
+ "purchaseSuccess": "Kauf erfolgreich! Bitte starte die App neu, um Plus-Funktionen zu nutzen.",
+ "purchaseNetwork": "Verbindungsproblem mit dem Store. Bitte prüfe dein Netzwerk und versuche es erneut.",
+ "purchaseNotAllowed": "Käufe sind auf diesem Gerät nicht erlaubt. Bitte prüfe deine Geräteeinschränkungen.",
+ "purchaseUnavailable": "Dieses Abo ist nicht verfügbar. Bitte versuche es später erneut.",
+ "purchaseProcessed": "Dieser Kauf wurde bereits verarbeitet. Wenn du glaubst, dass das ein Fehler ist, kontaktiere bitte den Support.",
+ "purchaseReceiptMissing": "Kaufbeleg fehlt. Bitte versuche den Kauf erneut.",
+ "purchasePending": "Die Zahlung wartet auf Freigabe. Du erhältst Zugriff, sobald sie bestätigt wurde.",
+ "purchaseFailed": "Kauf fehlgeschlagen: {{message}}. Bitte versuche es erneut oder kontaktiere den Support.",
+ "passwordChanged": "Passwort erfolgreich geändert",
+ "passwordChangeFailed": "Passwortänderung fehlgeschlagen",
+ "subscriptionCancelled": "Abo gekündigt",
+ "subscriptionCancelFailed": "Abo konnte nicht gekündigt werden"
+ },
+ "backup": {
+ "title": "Backup & Wiederherstellung",
+ "createTab": "Backup erstellen",
+ "restoreTab": "Backup wiederherstellen",
+ "createDescription": "Erstelle ein verschlüsseltes Backup deiner Daten. Es enthält Aufgaben, Verlauf, Einstellungen und optional hochgeladene Dateien.",
+ "encryptionKeyLabel": "Verschlüsselungsschlüssel *",
+ "encryptionKeyPlaceholder": "Gib einen starken Verschlüsselungsschlüssel ein",
+ "encryptionKeyHint": "Bewahre diesen Schlüssel sicher auf. Du brauchst ihn, um dein Backup wiederherzustellen.",
+ "encryptionKeyRequired": "Verschlüsselungsschlüssel ist erforderlich",
+ "nameLabel": "Backup-Name (optional)",
+ "namePlaceholder": "z. B. wochen-backup",
+ "includeAssets": "Hochgeladene Dateien und Assets einschließen",
+ "createAction": "Backup erstellen",
+ "restoreAction": "Backup wiederherstellen",
+ "createFailed": "Backup konnte nicht erstellt werden",
+ "created": "Backup wurde erfolgreich erstellt und heruntergeladen",
+ "restoreFailed": "Backup konnte nicht wiederhergestellt werden",
+ "restored": "Backup wurde erfolgreich wiederhergestellt. Bitte aktualisiere die Seite.",
+ "readFailed": "Backup-Datei konnte nicht gelesen werden",
+ "fileLabel": "Backup-Datei *",
+ "selectFile": "Bitte wähle eine Backup-Datei aus",
+ "selectedFile": "Ausgewählt: {{name}}",
+ "restoreWarning": "Die Wiederherstellung ersetzt alle aktuellen Daten. Diese Aktion kann nicht rückgängig gemacht werden.",
+ "restoreKeyPlaceholder": "Gib den für dieses Backup verwendeten Verschlüsselungsschlüssel ein",
+ "creating": "Backup wird erstellt...",
+ "restoring": "Backup wird wiederhergestellt..."
+ },
+ "subscription": {
+ "title": "Auf Plus upgraden",
+ "included": "Was ist enthalten:",
+ "subscribe": "Abonnieren",
+ "footer": "Jederzeit kündbar. Keine versteckten Gebühren. Sichere Zahlung über Stripe.",
+ "unlockFeatures": "Premium-Funktionen freischalten",
+ "errorTitle": "Abo-Fehler",
+ "errorMessage": "Der Abo-Vorgang konnte nicht gestartet werden. Bitte versuche es erneut.",
+ "features": {
+ "notifications": "Aufgabenbenachrichtigungen und Erinnerungen",
+ "richText": "Rich-Text-Beschreibungen mit Bild-Uploads",
+ "thingTriggers": "Dinge-basierte Aufgabenauslöser",
+ "apiTokens": "API-Tokens für Integrationen",
+ "imageUploads": "Bild-Uploads in Beschreibungen",
+ "automation": "Erweiterte Aufgabenautomatisierung"
+ }
+ },
+ "menu": {
+ "impersonateUser": "Benutzer imitieren",
+ "switchUser": "Benutzer wechseln",
+ "actAsAnotherUser": "Als andere Person handeln",
+ "stopImpersonating": "Imitierung beenden",
+ "returnToYourAccount": "Zu deinem Konto zurückkehren",
+ "settings": "Einstellungen",
+ "accountAndPreferences": "Konto & Einstellungen",
+ "invitePeople": "Personen einladen",
+ "addMembersToYourCircle": "Mitglieder zu deinem Kreis hinzufügen",
+ "sidePanelSettings": "Seitenleisten-Einstellungen",
+ "customizeLayoutAndCards": "Layout und Karten anpassen",
+ "switchToLight": "Zu hell wechseln",
+ "switchToDark": "Zu dunkel wechseln",
+ "toggleThemeAppearance": "Design umschalten",
+ "impersonating": "Imitiert"
+ },
+ "nativeCancel": {
+ "title": "Abo kündigen",
+ "description": "Um dein Abo zu kündigen, folge bitte den Anweisungen für deine Plattform. Du solltest über dieselbe Plattform kündigen, über die du abonniert hast.",
+ "iosTitle": "Für iOS (iPhone/iPad):",
+ "iosSteps": {
+ "1": "1. Öffne die Einstellungen-App auf deinem Gerät",
+ "2": "2. Tippe oben auf dem Bildschirm auf deinen Namen",
+ "3": "3. Tippe auf Abonnements",
+ "4": "4. Suche Donetick und tippe darauf",
+ "5": "5. Tippe auf Abo kündigen"
+ },
+ "iosNote": "Wenn du über iOS abonniert hast und die Web- oder Desktop-Version nutzt, musst du wie oben beschrieben über die iOS-Einstellungen kündigen.",
+ "androidTitle": "Für Android:",
+ "androidSteps": {
+ "1": "1. Öffne die Google Play Store App",
+ "2": "2. Tippe oben rechts auf das Profilsymbol",
+ "3": "3. Tippe auf Zahlungen und Abos",
+ "4": "4. Tippe auf Abonnements",
+ "5": "5. Suche Donetick und tippe darauf",
+ "6": "6. Tippe auf Abo kündigen"
+ },
+ "androidNote": "Wenn du über Google Play abonniert hast und die Web- oder Desktop-Version nutzt, musst du wie oben beschrieben über Google Play kündigen.",
+ "webTitle": "Für Web-/Desktop-Abos:",
+ "webDescription": "Wenn du ursprünglich über unsere Website oder Desktop-App abonniert hast, kannst du dein Abo im Bereich Kontoeinstellungen auf unserer Website mit einem Webbrowser kündigen.",
+ "webImportant": "Du musst dein Abo über dieselbe Plattform kündigen, über die du es ursprünglich abgeschlossen hast. Wenn du über den iOS App Store oder Google Play abonniert hast, musst du über diese ursprüngliche Plattform kündigen.",
+ "billingPeriod": "Dein Abo bleibt bis zum Ende des aktuellen Abrechnungszeitraums aktiv.",
+ "cancelFromStore": "Ich kündige über meinen App Store",
+ "cancelDesktopNow": "Ich habe über Desktop abonniert - Jetzt kündigen"
+ },
+ "apiTokensPage": {
+ "heading": "Zugriffstoken",
+ "description": "Erstelle Tokens für die API, um Dinge, Aufgaben oder Chores zu aktualisieren.",
+ "plusFeature": "Plus-Funktion",
+ "plusDescription": "API-Tokens sind im Basic-Tarif nicht verfügbar. Upgrade auf Plus, um API-Tokens für Integrationen und Automatisierung zu erstellen.",
+ "hideToken": "Token ausblenden",
+ "showToken": "Token anzeigen",
+ "removeToken": "Token entfernen",
+ "removeTokenConfirm": "Möchtest du {{name}} wirklich entfernen?",
+ "removed": "Entfernt",
+ "removedMessage": "API-Token wurde entfernt",
+ "copied": "Token in die Zwischenablage kopiert",
+ "generateNew": "Neuen Token erzeugen",
+ "namePrompt": "Gib deinem neuen Token einen Namen, an den du dich erinnerst.",
+ "generateAction": "Token erzeugen"
+ },
+ "mfaPage": {
+ "description": "Füge deinem Konto mit Multi-Faktor-Authentifizierung (MFA) eine zusätzliche Sicherheitsebene hinzu. Wenn MFA aktiviert ist, benötigst du zusätzlich zum Passwort einen Bestätigungscode aus deiner Authenticator-App.",
+ "title": "Zwei-Faktor-Authentifizierung",
+ "enabled": "Dein Konto ist mit 2FA geschützt",
+ "disabled": "Sichere dein Konto mit einer Authenticator-App",
+ "enable": "Aktivieren",
+ "disable": "Deaktivieren",
+ "setupTitle": "Multi-Faktor-Authentifizierung einrichten",
+ "step1": "Schritt 1: Scanne den QR-Code unten mit deiner Authenticator-App (Google Authenticator, Authy usw.)",
+ "manualKey": "Manueller Schlüssel:",
+ "addedToApp": "Ich habe das Konto zu meiner App hinzugefügt",
+ "step2": "Schritt 2: Gib den 6-stelligen Bestätigungscode aus deiner Authenticator-App ein",
+ "verifyEnable": "Prüfen und aktivieren",
+ "enabledSuccess": "MFA erfolgreich aktiviert!",
+ "backupSaveTitle": "Speichere diese Backup-Codes an einem sicheren Ort",
+ "backupSaveDescription": "Du kannst diese Codes verwenden, um auf dein Konto zuzugreifen, falls du dein Authenticator-Gerät verlierst. Jeder Code kann nur einmal verwendet werden.",
+ "savedBackupCodes": "Ich habe meine Backup-Codes gespeichert",
+ "disableTitle": "Multi-Faktor-Authentifizierung deaktivieren",
+ "disableWarning": "Wenn du MFA deaktivierst, ist dein Konto weniger sicher. Möchtest du wirklich fortfahren?",
+ "disablePrompt": "Gib zur Bestätigung einen Bestätigungscode aus deiner Authenticator-App ein:",
+ "backupCodesTitle": "Neue Backup-Codes",
+ "backupCodesWarning": "Deine bisherigen Backup-Codes sind jetzt ungültig. Speichere diese neuen Codes an einem sicheren Ort. Jeder Code kann nur einmal verwendet werden.",
+ "failedQr": "QR-Code konnte nicht erzeugt werden",
+ "invalidResponse": "Ungültige Serverantwort. QR-Code oder Geheimnis fehlen.",
+ "endpointMissing": "Der MFA-Endpunkt wurde nicht gefunden. Diese Funktion ist möglicherweise noch nicht verfügbar.",
+ "unauthorized": "Nicht autorisiert. Bitte melde dich erneut an.",
+ "serverError": "Serverfehler. Bitte versuche es später erneut.",
+ "setupFailed": "MFA-Einrichtung fehlgeschlagen ({{status}}). Bitte versuche es erneut.",
+ "networkError": "Netzwerkfehler. Bitte prüfe deine Verbindung und versuche es erneut.",
+ "enabledToast": "MFA wurde erfolgreich aktiviert!",
+ "invalidCode": "Ungültiger Bestätigungscode. Bitte versuche es erneut.",
+ "confirmFailed": "MFA konnte nicht bestätigt werden. Bitte versuche es erneut.",
+ "disabledToast": "MFA wurde erfolgreich deaktiviert!",
+ "disableFailed": "MFA konnte nicht deaktiviert werden. Bitte versuche es erneut.",
+ "backupRegenerated": "Neue Backup-Codes wurden erzeugt!",
+ "regenerateFailed": "Backup-Codes konnten nicht neu erzeugt werden. Bitte versuche es erneut."
+ },
+ "developer": {
+ "title": "Entwicklereinstellungen",
+ "description": "Zeige technische Informationen zu Authentifizierungs-Token, Benachrichtigungen und Sitzungsstatus für Debugging-Zwecke an.",
+ "tokenRefreshed": "Token erfolgreich aktualisiert",
+ "tokenRefreshFailed": "Token-Aktualisierung fehlgeschlagen: {{message}}",
+ "tokenRefreshError": "Fehler bei der Token-Aktualisierung: {{message}}",
+ "refreshEndpointSuccess": "Refresh-Token-Endpunkt erfolgreich aufgerufen",
+ "refreshEndpointFailed": "Refresh-Token-Endpunkt fehlgeschlagen: {{status}} {{message}}",
+ "refreshEndpointError": "Fehler beim Refresh-Token-Endpunkt: {{message}}",
+ "notificationsLoaded": "{{count}} geplante Benachrichtigungen geladen",
+ "notificationsLoadError": "Fehler beim Laden der Benachrichtigungen: {{message}}",
+ "authTokens": "Authentifizierungs-Token",
+ "refreshTokenAction": "Token aktualisieren",
+ "callRefreshEndpoint": "Refresh-Endpunkt aufrufen",
+ "accessToken": "Access Token",
+ "refreshToken": "Refresh Token",
+ "timeLeft": "Verbleibende Zeit:",
+ "expires": "Läuft ab: {{date}}",
+ "refreshTokenCookie": "Refresh Tokens werden auf der Webplattform über HTTP-only-Cookies verwaltet.",
+ "platformInfo": "Plattforminformationen",
+ "platform": "Plattform:",
+ "native": "Nativ",
+ "web": "Web",
+ "scheduledNotifications": "Geplante lokale Benachrichtigungen",
+ "refresh": "Aktualisieren",
+ "noScheduledNotifications": "Keine geplanten Benachrichtigungen",
+ "totalScheduled": "Insgesamt geplant:",
+ "noTitle": "Kein Titel",
+ "noBody": "Kein Inhalt",
+ "pastDue": "Überfällig",
+ "choreId": "Chore-ID: {{id}}",
+ "sseTitle": "Server-Sent Events (SSE)",
+ "connectionStatus": "Verbindungsstatus",
+ "unknown": "Unbekannt",
+ "error": "Fehler: {{error}}",
+ "lastEventReceived": "Zuletzt empfangenes Ereignis",
+ "type": "Typ:",
+ "received": "Empfangen: {{date}}",
+ "notAvailable": "k. A.",
+ "timeExpired": "Abgelaufen"
+ },
+ "storage": {
+ "title": "Speichereinstellungen",
+ "serverUsageTitle": "Server-Speichernutzung",
+ "plusFeature": "Plus-Funktion",
+ "serverUsageDescription": "Das ist der Speicherplatz, den dein Konto auf unseren Servern verwendet, z. B. für Dateien, Bilder und hochgeladene Daten.",
+ "basicPlanUnavailable": "Server-Speicher ist im Basic-Tarif nicht verfügbar. Upgrade auf Plus, um deine Server-Speichernutzung zu sehen.",
+ "loading": "Lädt...",
+ "usedOfTotal": "{{used}} MB verwendet / {{total}} MB gesamt ({{percent}}%)",
+ "experimentalTitle": "Experimentelle Funktionen",
+ "comingSoon": "Bald verfügbar",
+ "offlineModeTitle": "Offline-Modus aktivieren",
+ "offlineModeDescription": "Ermöglicht der App, offline zu funktionieren, indem Daten lokal zwischengespeichert werden. Das ist experimentell und kann zu Verlangsamungen führen. Bei Leistungsproblemen solltest du es deaktivieren.",
+ "offlineModeWarning": "Der Offline-Modus ist aktiviert. Wenn die App langsam wird, deaktiviere diese Einstellung.",
+ "localStorageTitle": "{{platform}} lokaler Speicher und Cache",
+ "appPlatform": "App",
+ "browserPlatform": "Browser",
+ "localStorageDescription": "Diese Daten werden lokal für schnelleren Zugriff und Offline-Nutzung gespeichert. Das Löschen wirkt sich nicht auf deine Serverdaten aus, kann dich aber abmelden oder Offline-Aufgaben entfernen.",
+ "clearAllTitle": "Gesamten lokalen Speicher löschen",
+ "clearAllMessage": "Möchtest du wirklich deinen lokalen Speicher und Cache löschen? Dadurch werden alle Daten in diesem Browser entfernt und du musst dich erneut anmelden.",
+ "clearAllAction": "Alles löschen",
+ "clearAllButton": "Gesamten lokalen Speicher und Cache löschen",
+ "clearOfflineTitle": "Offline-Cache löschen",
+ "clearOfflineMessage": "Möchtest du wirklich nur den Offline-Cache und Offline-Aufgaben löschen?",
+ "clearOfflineAction": "Cache löschen",
+ "clearOfflineButton": "Offline-Cache und Offline-Aufgaben löschen",
+ "appPreferencesTitle": "App-Einstellungen",
+ "deviceOnly": "Nur auf diesem Gerät",
+ "appPreferencesDescription": "Diese Einstellungen werden lokal auf deinem Gerät gespeichert. Beim Löschen werden app-spezifische Einstellungen zurückgesetzt und du wirst möglicherweise abgemeldet, deine Serverdaten bleiben aber erhalten.",
+ "clearPreferencesTitle": "App-Einstellungen löschen",
+ "clearPreferencesMessage": "Möchtest du wirklich alle App-Einstellungen löschen? Dadurch werden deine App-Einstellungen zurückgesetzt und du musst dich möglicherweise erneut anmelden.",
+ "clearPreferencesAction": "Einstellungen löschen",
+ "clearPreferencesButton": "App-Einstellungen löschen"
+ },
+ "circlePage": {
+ "description": "Dein Konto wird automatisch mit einem Kreis verbunden, wenn du einen erstellst oder beitrittst. Lade Freunde mit dem Kreis-Code oder Join-Link unten ein. Beitrittsanfragen siehst du ebenfalls hier.",
+ "partOf": "Du bist Teil von {{name}}",
+ "codeIs": "Dein Kreis-Code ist:",
+ "copyCode": "Code kopieren",
+ "copyLink": "Link kopieren",
+ "leaveCircle": "Kreis verlassen",
+ "members": "Kreis-Mitglieder",
+ "requests": "Kreis-Anfragen",
+ "refresh": "Aktualisieren",
+ "refreshing": "Aktualisiere...",
+ "lastUpdated": "Zuletzt aktualisiert: {{date}}",
+ "wantsToJoin": "{{name}} möchte deinem Kreis beitreten.",
+ "accept": "Akzeptieren",
+ "joinPrompt": "Möchtest du dem Kreis einer anderen Person beitreten? Bitte sie um ihren Kreis-Code oder Join-Link. Gib den Code unten ein, um beizutreten.",
+ "enterCode": "Kreis-Code eingeben:",
+ "joinCircle": "Kreis beitreten",
+ "copyCodeSuccess": "Code in die Zwischenablage kopiert",
+ "copyLinkSuccess": "Link in die Zwischenablage kopiert",
+ "leaveConfirm": "Möchtest du deinen Kreis wirklich verlassen?",
+ "leaveTitle": "Kreis verlassen",
+ "leaveSuccess": "Kreis erfolgreich verlassen",
+ "leaveFailed": "Kreis konnte nicht verlassen werden",
+ "roleUpdateFailed": "Rolle konnte nicht aktualisiert werden",
+ "removeMemberTitle": "Mitglied entfernen",
+ "removeMemberConfirm": "Möchtest du {{name}} wirklich aus deinem Kreis entfernen?",
+ "removeMemberSuccess": "Mitglied erfolgreich entfernt",
+ "acceptTitle": "Mitgliedsanfrage akzeptieren",
+ "acceptConfirm": "Möchtest du {{name}} (Benutzername: {{username}}) wirklich in deinen Kreis aufnehmen?",
+ "acceptSuccess": "Anfrage erfolgreich akzeptiert",
+ "joinSuccess": "Kreis erfolgreich beigetreten. Warte darauf, dass der Kreis-Eigentümer deine Anfrage akzeptiert.",
+ "alreadyMember": "Du bist bereits Mitglied dieses Kreises",
+ "joinFailed": "Kreis-Beitritt fehlgeschlagen",
+ "pendingApproval": "Wartet auf Freigabe",
+ "joinedOn": "Beigetreten am {{date}}",
+ "requestedOn": "Beitrittsanfrage vom {{date}}",
+ "roleDescriptions": {
+ "member": "Normales Mitglied des Kreises",
+ "manager": "Kann Nutzer impersonieren und Aktionen in ihrem Namen ausführen",
+ "admin": "Vollzugriff auf den Kreis"
+ },
+ "newOwner": "Neue Eigentümerin / Neuer Eigentümer"
+ },
+ "modals": {
+ "passwordChange": {
+ "title": "Passwort ändern",
+ "intro": "Bitte gib dein neues Passwort ein.",
+ "newPassword": "Neues Passwort",
+ "confirmPassword": "Passwort bestätigen",
+ "mismatch": "Passwörter stimmen nicht überein",
+ "min": "Das Passwort muss mindestens 8 Zeichen lang sein",
+ "max": "Das Passwort darf höchstens 64 Zeichen lang sein"
+ },
+ "userDeletion": {
+ "title": "Konto löschen",
+ "warningTitle": "Konto löschen",
+ "warningIntro": "Diese Aktion kann nicht rückgängig gemacht werden. Beim Löschen deines Kontos werden dauerhaft entfernt:",
+ "items": {
+ "profile": "Dein Benutzerprofil und Authentifizierungsdaten",
+ "chores": "Alle deine Aufgaben, der Aufgabenverlauf und Zeiterfassungssitzungen",
+ "tokens": "API-Tokens, MFA-Sitzungen und Passwort-Reset-Tokens",
+ "storage": "Speicherdateien und Nutzungsdaten",
+ "points": "Punkteverlauf und Benachrichtigungen",
+ "circles": "Kreis-Mitgliedschaften und Beziehungen"
+ },
+ "passwordPrompt": "Gib dein Passwort ein, um fortzufahren",
+ "transferTitle": "Übertragung des Kreis-Eigentums erforderlich",
+ "transferIntro": "Du besitzt Kreise, die vor dem Löschen übertragen werden müssen. Bitte wähle neue Eigentümer aus:",
+ "circleLabel": "Kreis: {{name}}",
+ "finalTitle": "Letzte Bestätigung",
+ "finalPrompt": "Bitte gib dein Passwort ein und tippe DELETE, um die Kontolöschung zu bestätigen.",
+ "finalHint": "Nach erfolgreicher Löschung wirst du abgemeldet und zur Login-Seite weitergeleitet.",
+ "typeDelete": "Tippe \"DELETE\" zur Bestätigung",
+ "checkFailed": "Löschvoraussetzungen konnten nicht geprüft werden",
+ "confirmDelete": "Bitte gib dein Passwort ein und tippe DELETE zur Bestätigung",
+ "deleteFailed": "Konto konnte nicht gelöscht werden"
+ }
+ },
+ "localization": {
+ "rtlNotice": "Diese Sprache verwendet eine Schreibrichtung von rechts nach links (RTL)",
+ "title": "Lokalisierung",
+ "description": "Passe Sprache, Datumsformat und regionale Einstellungen für dein Konto an.",
+ "language": "Sprache",
+ "languageDescription": "Wähle deine bevorzugte Sprache aus",
+ "dateFormat": "Datumsformat",
+ "dateFormatDescription": "Lege fest, wie Datumsangaben in der gesamten Anwendung angezeigt werden",
+ "timeFormat": "Zeitformat",
+ "timeFormatDescription": "Wähle zwischen 12-Stunden- und 24-Stunden-Format",
+ "12hour": "12-Stunden (AM/PM)",
+ "24hour": "24-Stunden",
+ "firstDayOfWeek": "Erster Tag der Woche",
+ "firstDayOfWeekDescription": "Wähle, mit welchem Tag deine Woche beginnen soll",
+ "sunday": "Sonntag",
+ "monday": "Montag",
+ "saturday": "Samstag",
+ "formats": {
+ "mdy": "MM/DD/YYYY (USA)",
+ "dmy": "DD/MM/YYYY (Europa)",
+ "ymd": "YYYY-MM-DD (ISO)",
+ "long": "Langes Format (z. B. 1. Januar 2024)",
+ "short": "Kurzes Format (z. B. 1. Jan. 2024)"
+ }
}
-}
\ No newline at end of file
+}
diff --git a/public/locales/de/settingsExtras.json b/public/locales/de/settingsExtras.json
new file mode 100644
index 00000000..0c598811
--- /dev/null
+++ b/public/locales/de/settingsExtras.json
@@ -0,0 +1,58 @@
+{
+ "notifications": {
+ "deviceTitle": "Gerätebenachrichtigung",
+ "deviceDescription": "Verwalte deine Gerätebenachrichtigungen",
+ "deviceLabel": "Gerätebenachrichtigung",
+ "deviceNativeHelper": "Erhalte Benachrichtigungen auf deinem Gerät, wenn eine Aufgabe fällig ist",
+ "mobileOnlyHelper": "Diese Funktion ist nur auf Mobilgeräten verfügbar",
+ "testNotification": "Benachrichtigung testen",
+ "dueDateNotification": "Fälligkeitsbenachrichtigung",
+ "dueDateNotificationHelper": "Benachrichtigung, wenn die Aufgabe fällig ist",
+ "preDueNotification": "Vorab-Benachrichtigung",
+ "preDueNotificationHelper": "Benachrichtigung einige Stunden bevor die Aufgabe fällig ist",
+ "overdueNotification": "Überfälligkeitsbenachrichtigung",
+ "overdueNotificationHelper": "Benachrichtigung, wenn die Aufgabe überfällig ist",
+ "pushNotifications": "Push-Benachrichtigungen",
+ "pushNotificationsHelper": "Erhalte Nudges, Ankündigungen und Aufgaben-Zuweisungen per Push-Benachrichtigung",
+ "registeredDevices": "Registrierte Geräte ({{count}}/5)",
+ "registeredDevicesDescription": "Geräte, die registriert sind, um Push-Benachrichtigungen für dein Konto zu erhalten",
+ "currentDeviceUnregistered": "Dieses Gerät ist nicht für Push-Benachrichtigungen registriert",
+ "limitReached": "Limit erreicht",
+ "noRegisteredDevices": "Keine Geräte für Push-Benachrichtigungen registriert",
+ "customTitle": "Benachrichtigung über Drittanbieter",
+ "customDescription": "Benachrichtigungen über andere Plattformen wie Telegram oder Pushover",
+ "customLabel": "Benachrichtigung über Drittanbieter",
+ "customHelper": "Benachrichtigungen auf einer anderen Plattform erhalten",
+ "telegramSetup": "Du musst dem Bot zuerst eine Nachricht senden, damit Telegram-Benachrichtigungen funktionieren.",
+ "clickHere": "Hier klicken",
+ "startChat": "um einen Chat zu starten",
+ "chatIdHelp": "Wenn du deine Chat-ID nicht kennst, starte einen Chat mit userinfobot. Der Bot sendet dir dann deine Chat-ID.",
+ "chatIdPlaceholder": "Benutzer-ID / Chat-ID",
+ "userInfoBot": "um einen Chat mit userinfobot zu starten",
+ "removeDeviceFailed": "Gerät konnte nicht deregistriert werden",
+ "targetUpdated": "Benachrichtigungsziel aktualisiert",
+ "updateFailed": "Fehler beim Aktualisieren des Benachrichtigungsziels: {{status}}",
+ "registrationInitiated": "Registrierung gestartet",
+ "registrationInitiatedMessage": "Die Registrierung für Push-Benachrichtigungen wurde gestartet. Das Gerät wird automatisch registriert.",
+ "registrationFailed": "Registrierung fehlgeschlagen",
+ "registrationFailedMessage": "Das Gerät konnte nicht automatisch registriert werden. Bitte versuche es erneut.",
+ "deviceRegisteredMessage": "Gerät erfolgreich für Push-Benachrichtigungen registriert.",
+ "deviceLimitReached": "Gerätelimit erreicht",
+ "deviceLimitReachedMessage": "Du hast das Maximum von 5 registrierten Geräten erreicht. Bitte entferne zuerst ein Gerät.",
+ "permissionRequired": "Berechtigung erforderlich",
+ "permissionRequiredMessage": "Für die Registrierung dieses Geräts ist die Erlaubnis für Push-Benachrichtigungen erforderlich.",
+ "notificationPermissionDenied": "Benachrichtigungsberechtigung verweigert",
+ "notificationPermissionDeniedMessage": "Du hast Benachrichtigungsberechtigungen verweigert. Du kannst sie später in den Geräteeinstellungen aktivieren.",
+ "pushPermissionDenied": "Push-Benachrichtigungsberechtigung verweigert",
+ "pushPermissionDeniedMessage": "Push-Benachrichtigungen wurden deaktiviert. Du kannst sie bei Bedarf in den Geräteeinstellungen aktivieren.",
+ "testTitle": "Testbenachrichtigung",
+ "testBody": "Du hast bald eine fällige Aufgabe",
+ "createdAt": "Erstellt am",
+ "on": "An",
+ "off": "Aus",
+ "chatIdRequired": "Chat-ID ist erforderlich",
+ "invalidChatId": "Ungültige Chat-ID",
+ "userKeyRequired": "Benutzerschlüssel ist erforderlich",
+ "userKeyPlaceholder": "Benutzer-ID"
+ }
+}
diff --git a/public/locales/de/things.json b/public/locales/de/things.json
new file mode 100644
index 00000000..14bbc4fb
--- /dev/null
+++ b/public/locales/de/things.json
@@ -0,0 +1,94 @@
+{
+ "page": {
+ "title": "Dinge",
+ "description": "Dinge sind benutzerdefinierte Felder, die an Aufgaben angehängt werden können, um zusätzliche Informationen zu erfassen. Sie können Text-, Zahlen- oder Boolean-Werte enthalten.",
+ "emptyTitle": "Keine Dinge erstellt oder gefunden",
+ "savedTitle": "Gespeichert",
+ "savedMessage": "Ding wurde erfolgreich gespeichert",
+ "saveFailedTitle": "Ding konnte nicht gespeichert werden",
+ "saveQueued": "Du bist offline und die Anfrage wurde in die Warteschlange gestellt.",
+ "saveFailed": "Beim Speichern des Dings ist ein Fehler aufgetreten.",
+ "deleteTitle": "Ding löschen",
+ "deleteConfirm": "Möchtest du dieses Ding wirklich löschen?",
+ "deleteFailedAssociated": "Ein Ding mit zugehörigen Aufgaben kann nicht gelöscht werden.",
+ "deleteFailedTitle": "Ding konnte nicht gelöscht werden",
+ "deleteQueued": "Du bist offline und die Anfrage wurde in die Warteschlange gestellt.",
+ "deleteFailed": "Beim Löschen des Dings ist ein Fehler aufgetreten.",
+ "updatedTitle": "Aktualisiert",
+ "updatedMessage": "Ding-Status erfolgreich aktualisiert",
+ "updateFailedTitle": "Ding-Status konnte nicht aktualisiert werden",
+ "updateQueued": "Du bist offline und die Anfrage wurde in die Warteschlange gestellt.",
+ "updateFailed": "Beim Aktualisieren des Ding-Status ist ein Fehler aufgetreten.",
+ "swipeEdit": "Bearbeiten",
+ "swipeToggle": "Umschalten",
+ "swipeDelete": "Löschen",
+ "types": {
+ "text": "Text",
+ "number": "Zahl",
+ "boolean": "Boolean"
+ },
+ "states": {
+ "true": "Wahr",
+ "false": "Falsch"
+ }
+ },
+ "modal": {
+ "createTitle": "Ding erstellen",
+ "editTitle": "Ding bearbeiten",
+ "namePlaceholder": "Ding-Name",
+ "valuePlaceholder": "Ding-Wert",
+ "type": "Typ",
+ "nameRequired": "Name ist erforderlich",
+ "stateMustBeNumber": "Status muss eine Zahl sein",
+ "stateMustBeBoolean": "Status muss wahr oder falsch sein",
+ "stateRequired": "Status ist erforderlich",
+ "updateStateTitle": "Status aktualisieren"
+ },
+ "history": {
+ "overviewTitle": "Ding-Übersicht",
+ "empty": {
+ "title": "Kein Verlauf gefunden",
+ "description": "Für dieses Ding gibt es noch keinen Verlauf.",
+ "backToThings": "Zurück zu den Dingen"
+ },
+ "analytics": {
+ "updateFrequency": "Aktualisierungsfrequenz",
+ "lastUpdated": "Zuletzt aktualisiert",
+ "lastValue": "Letzter Wert",
+ "updateTrend": "Aktualisierungstrend",
+ "every": "Alle {{value}}",
+ "minutes_one": "{{count}} Minute",
+ "minutes_other": "{{count}} Minuten",
+ "hours_one": "{{count}} Stunde",
+ "hours_other": "{{count}} Stunden",
+ "days_one": "{{count}} Tag",
+ "days_other": "{{count}} Tage",
+ "intervalIncreasing": "Intervall wird größer",
+ "intervalDecreasing": "Intervall wird kleiner",
+ "intervalStable": "Intervall bleibt stabil"
+ }
+ },
+ "trigger": {
+ "validationMissing": "Bitte wähle ein Ding und einen Auslösezustand aus",
+ "booleanNoCondition": "Der boolesche Typ benötigt keine Bedingung",
+ "triggerStateNumber": "Der Auslösezustand muss eine Zahl sein",
+ "title": "Löse eine Aufgabe aus, wenn sich der Zustand eines Dings in einen gewünschten Zustand ändert",
+ "noThings": "Es sieht so aus, als hättest du noch keine Dinge. Erstelle eines, um eine Aufgabe auszulösen, wenn sich dessen Zustand ändert.",
+ "goToThings": "Zu den Dingen",
+ "createThingSuffix": "um ein Ding zu erstellen",
+ "selectThing": "Ein Ding auswählen",
+ "conditionHelp": "Erstelle eine Bedingung, um eine Aufgabe auszulösen, wenn sich der Zustand des Dings in den gewünschten Zustand ändert",
+ "dueWhenChanged": "Wenn sich der Zustand von {{name}} wie unten angegeben ändert, wird die Aufgabe fällig.",
+ "true": "Wahr",
+ "false": "Falsch",
+ "equal": "Gleich",
+ "notEqual": "Ungleich",
+ "greaterThan": "Größer als",
+ "greaterThanOrEqual": "Größer oder gleich",
+ "lessThan": "Kleiner als",
+ "lessThanOrEqual": "Kleiner oder gleich",
+ "textTriggerLabel": "Gib den Text ein, der die Aufgabe auslösen soll",
+ "type": "Typ",
+ "state": "Status"
+ }
+}
diff --git a/public/locales/de/timer.json b/public/locales/de/timer.json
new file mode 100644
index 00000000..c86b6d2e
--- /dev/null
+++ b/public/locales/de/timer.json
@@ -0,0 +1,63 @@
+{
+ "details": {
+ "title": "Timer-Details",
+ "updatedTitle": "Sitzung aktualisiert",
+ "updatedMessage": "Timer-Sitzung wurde erfolgreich aktualisiert.",
+ "updateFailedTitle": "Sitzung konnte nicht aktualisiert werden",
+ "errorUpdatingTitle": "Fehler beim Aktualisieren der Sitzung",
+ "startTitle": "Timer gestartet",
+ "startMessage": "Arbeitssitzung wurde erfolgreich gestartet.",
+ "startFailedTitle": "Timer konnte nicht gestartet werden",
+ "pauseTitle": "Timer pausiert",
+ "pauseMessage": "Arbeitssitzung wurde pausiert.",
+ "pauseFailedTitle": "Timer konnte nicht pausiert werden",
+ "tryAgain": "Bitte versuche es erneut.",
+ "deleteSessionTitle": "Sitzung löschen",
+ "deleteSessionMessage": "Das Löschen von Sitzung Nr. {{index}} würde hier implementiert werden",
+ "loading": "Timerdaten werden geladen...",
+ "notFound": "Für diese Aufgabe wurden keine Timerdaten gefunden.",
+ "activeWork": "Aktive Arbeit",
+ "breakTime": "Pausenzeit",
+ "sessions": "Sitzungen",
+ "totalTime": "Gesamtzeit",
+ "workVsBreak": "Verteilung Arbeit vs. Pause",
+ "activePercent": "{{percent}} % aktiv",
+ "noActiveTime": "Noch keine aktive Zeit",
+ "activityTimeline": "Aktivitätsverlauf",
+ "sessionTooltip": "Sitzung {{index}}: {{duration}} {{status}}",
+ "ongoing": "(laufend)",
+ "liveSession": "Live-Sitzung",
+ "started": "Gestartet: {{time}}",
+ "ended": "Beendet: {{time}}",
+ "now": "Jetzt: {{time}}",
+ "activeShort": "Aktiv: {{percent}}",
+ "noTimeline": "Kein Aktivitätsverlauf verfügbar. Starte die Arbeit, um dein Aktivitätsmuster zu sehen.",
+ "sessionBreakdown": "Sitzungsübersicht",
+ "saveChanges": "Änderungen speichern",
+ "workSessions": "Arbeitssitzungen ({{count}})",
+ "noSessions": "Für diesen Timer wurden keine Arbeitssitzungen gefunden.",
+ "live": "Live",
+ "unknownUser": "Unbekannt",
+ "sessionLabel": "Sitzung #{{index}} • {{date}}",
+ "ongoingArrow": "→ laufend",
+ "editSessions": "Sitzungen",
+ "addSession": "Sitzung hinzufügen",
+ "startTime": "Startzeit",
+ "endTime": "Endzeit",
+ "leaveEmptyOngoing": "Leer lassen, wenn die Sitzung noch läuft",
+ "durationAuto": "Dauer (automatisch berechnet)",
+ "pauseButton": "Timer pausieren",
+ "resumeButton": "Fortsetzen",
+ "startButton": "Timer starten"
+ },
+ "edit": {
+ "updatedTitle": "Sitzung aktualisiert",
+ "updatedMessage": "Timer-Sitzung wurde erfolgreich aktualisiert.",
+ "updateFailedTitle": "Sitzung konnte nicht aktualisiert werden",
+ "tryAgain": "Bitte versuche es erneut.",
+ "deletedTitle": "Sitzung gelöscht",
+ "deletedMessage": "Timer-Sitzung wurde erfolgreich gelöscht.",
+ "deleteTitle": "Timer-Sitzung löschen",
+ "deleteMessage": "Möchtest du diese Timer-Sitzung wirklich löschen?"
+ }
+}
diff --git a/public/locales/de/user.json b/public/locales/de/user.json
new file mode 100644
index 00000000..9df438b1
--- /dev/null
+++ b/public/locales/de/user.json
@@ -0,0 +1,73 @@
+{
+ "activitiesTimeline": "Aktivitätsverlauf",
+ "pointsWithCount": "{{count}} Punkte",
+ "activities": {
+ "title": "Benutzeraktivitäten",
+ "subtitle": "Übersicht über Benutzeraktivitäten und Aufgabenstatistiken",
+ "filterTitle": "Aktivitäten filtern",
+ "showFor": "Aktivitäten anzeigen für:",
+ "allUsers": "Alle Benutzer",
+ "timePeriod": "Zeitraum:",
+ "days7": "7 Tage",
+ "days30": "30 Tage",
+ "days90": "90 Tage",
+ "allTime": "Gesamte Zeit",
+ "showingFor": "Zeige Aktivitäten für {{user}} über {{period}}",
+ "noActivitiesTitle": "Keine Aktivitäten gefunden",
+ "noActivitiesDescription": "Für {{user}} wurden im Zeitraum {{period}} keine Aktivitäten gefunden.",
+ "noActivitiesHelp": "Wähle oben einen anderen Zeitraum oder Benutzerfilter aus.",
+ "backToChores": "Zurück zu den Aufgaben",
+ "unknownUser": "Unbekannter Benutzer",
+ "noLabels": "Keine Labels",
+ "unassigned": "Nicht zugewiesen",
+ "unknownTask": "Unbekannte Aufgabe",
+ "byAssignee": "nach Zuständigkeit",
+ "tasksGroupedByAssignee": "Aufgaben nach Zuständigen gruppiert",
+ "charts": {
+ "statusTitle": "Status",
+ "statusDescription": "Status abgeschlossener Aufgaben",
+ "dueDateTitle": "Fälligkeitsdatum",
+ "dueDateDescription": "Fälligkeitsdaten aktueller Aufgaben",
+ "priorityTitle": "Priorität",
+ "priorityDescription": "Aufgaben nach Priorität",
+ "labelsTitle": "Labels",
+ "labelsDescription": "Aufgaben nach Labels",
+ "labelsTimeTitle": "Labels (Zeit)",
+ "labelsTimeDescription": "Nach Labels aufgewendete Zeit (Stunden)",
+ "tasksTimeTitle": "Aufgaben (Zeit)",
+ "tasksTimeDescription": "Für einzelne Aufgaben aufgewendete Zeit (Stunden)"
+ }
+ },
+ "points": {
+ "pointsLeaderboard": "Punkte-Bestenliste",
+ "filterAndAnalysis": "Filter & Analyse",
+ "tasksLeaderboard": "Aufgaben-Bestenliste",
+ "rankingsPoints": "Rangliste basierend auf verdienten Punkten im ausgewählten Zeitraum",
+ "rankingsTasks": "Rangliste basierend auf erledigten Aufgaben im ausgewählten Zeitraum",
+ "modePoints": "Punkte",
+ "modeTasks": "Aufgaben",
+ "filterTitle": "Punkte filtern",
+ "showFor": "Punkte anzeigen für:",
+ "timePeriod": "Zeitraum:",
+ "days7": "7 Tage",
+ "months6": "6 Monate",
+ "allTime": "Gesamte Zeit",
+ "redeemPoints": "Punkte einlösen",
+ "you": "Du",
+ "tasksAvg": "{{tasks}} Aufgaben • {{avg}} im Schnitt pro Aufgabe",
+ "available": "{{count}} verfügbar",
+ "pointsLabel": "{{count}} Punkte",
+ "cardAvailable": "Verfügbar",
+ "cardAvailableSubtext": "Zum Einlösen bereit",
+ "cardRedeemed": "Eingelöst",
+ "cardRedeemedSubtext": "Bereits verwendet",
+ "cardTotal": "Gesamt",
+ "cardTotalSubtext": "Insgesamt verdient",
+ "cardPeriod": "Punkte im Zeitraum",
+ "lastDays": "Letzte {{count}} Tage",
+ "lastMonths6": "Letzte 6 Monate",
+ "pointsTrend": "Punkteverlauf",
+ "showingFor": "Zeige Punkte für {{user}} über {{period}}",
+ "unknownUser": "Unbekannter Benutzer"
+ }
+}
diff --git a/public/locales/en/auth.json b/public/locales/en/auth.json
new file mode 100644
index 00000000..ddbd018b
--- /dev/null
+++ b/public/locales/en/auth.json
@@ -0,0 +1,93 @@
+{
+ "subtitle": "Sign in to your account to continue",
+ "signupSubtitle": "Create an account to get started!",
+ "forgotPasswordSubtitle": "Enter your email, and we'll send you a link to get into your account.",
+ "forgotPasswordConfirmation": "If there is an account associated with the email you entered, you will receive an email with instructions on how to reset your password.",
+ "welcomeBack": "Welcome back, {{name}}",
+ "subAccountBadge": "(Sub Account)",
+ "tabs": {
+ "primary": "Primary Account",
+ "sub": "Sub Account"
+ },
+ "fields": {
+ "primaryAccountUsername": "Primary Account Username",
+ "subAccountUsername": "Sub Account Username"
+ },
+ "placeholders": {
+ "primaryAccountUsername": "Enter primary account username",
+ "subAccountUsername": "Enter sub account name",
+ "passwordRange": "Enter password (8-64 characters)",
+ "enterEmailAddress": "Enter your email address",
+ "verificationCode": "Enter 6-digit code",
+ "backupCode": "Enter backup code"
+ },
+ "actions": {
+ "signIn": "Sign In",
+ "signInSubAccount": "Sign In as Sub Account",
+ "forgotPassword": "Forgot password?",
+ "createAccount": "Create new account",
+ "continueWithGoogle": "Continue with Google",
+ "continueWithApple": "Continue with Apple",
+ "continueWithProvider": "Continue with {{provider}}",
+ "resetPassword": "Reset Password",
+ "goToLogin": "Go to Login",
+ "useBackupCode": "Can't access your authenticator? Use a backup code",
+ "useAuthenticatorApp": "Use authenticator app instead"
+ },
+ "messages": {
+ "agreement": "By signing up, you agree to our Terms of Service and Privacy Policy"
+ },
+ "errors": {
+ "validationTitle": "Validation Error",
+ "loginFailedTitle": "Login Failed",
+ "loginFailedMessage": "An error occurred, please try again",
+ "usernameRequired": "Username is required",
+ "passwordRequired": "Password is required",
+ "primaryUsernameRequired": "Primary username is required for sub account login",
+ "subAccountNameRequired": "Sub account name is required for sub account login",
+ "googleLoginFailedTitle": "Google Login Failed",
+ "googleLoginFailedMessage": "Couldn't log in with Google, please try again",
+ "appleLoginFailedTitle": "Apple Login Failed",
+ "appleLoginFailedMessage": "Couldn't log in with Apple, please try again",
+ "oauthErrorTitle": "OAuth Error",
+ "oauthBrowserError": "Failed to open authentication browser",
+ "twoFactorFailedTitle": "Two-Factor Authentication Failed",
+ "signupFailedTitle": "Signup Failed",
+ "signupDisabled": "Signup disabled, please contact admin",
+ "signupGeneric": "An error occurred during signup",
+ "usernameMin": "Username must be at least 4 characters",
+ "invalidEmail": "Invalid email address",
+ "passwordLength": "Password must be between 8 and 64 characters",
+ "displayNameRequired": "Display name is required",
+ "displayNamePattern": "Display name can only contain letters and numbers",
+ "usernamePattern": "Username can only contain lowercase letters, dot and dash",
+ "resetEmailSentTitle": "Reset Email Sent",
+ "resetEmailSentMessage": "Check your email for password reset instructions",
+ "resetFailedTitle": "Reset Failed",
+ "resetFailedMessage": "Failed to send reset email, please try again later",
+ "emailRequired": "Email is required",
+ "validEmail": "Please enter a valid email address"
+ },
+ "mfa": {
+ "title": "Two-Factor Authentication",
+ "instruction": "Enter the verification code from your authenticator app",
+ "invalidCode": "Invalid verification code. Please try again.",
+ "missingCode": "Please enter a verification code",
+ "verifyFailed": "Failed to verify code. Please try again.",
+ "helpText": "Having trouble? Make sure your authenticator app is synced and try again. Each backup code can only be used once."
+ },
+ "status": {
+ "authenticating": "Authenticating",
+ "pleaseWait": "Please wait",
+ "unknownProvider": "Unknown authentication provider",
+ "contactSupport": "Please contact support",
+ "failed": "Authentication failed",
+ "stateMismatch": "State does not match",
+ "tryAgain": "Please try again",
+ "passwordUpdatedRedirect": "Your password has been updated successfully. Redirecting to login...",
+ "passwordUpdateFailed": "Password Update Failed",
+ "passwordUpdateFailedMessage": "Failed to update password, please try again later",
+ "enterNewPassword": "Please enter your new password below",
+ "savePassword": "Save Password"
+ }
+}
diff --git a/public/locales/en/chores.json b/public/locales/en/chores.json
index ec820375..da61e9c3 100644
--- a/public/locales/en/chores.json
+++ b/public/locales/en/chores.json
@@ -1,4 +1,586 @@
{
+ "sidepanel": {
+ "summary": {
+ "title": "Summary",
+ "description": "This is a summary of your chores",
+ "dueToday": "Due Today",
+ "overdue": "Overdue"
+ },
+ "activities": {
+ "title": "Recent Activities",
+ "loading": "Loading activities...",
+ "empty": "No recent activities",
+ "unknownChore": "Unknown Chore",
+ "justNow": "Just now",
+ "hoursAgo_one": "{{count}}h ago",
+ "hoursAgo_other": "{{count}}h ago",
+ "daysAgo_one": "{{count}}d ago",
+ "daysAgo_other": "{{count}}d ago",
+ "by": "by",
+ "points_one": "{{count}} pt",
+ "points_other": "{{count}} pts",
+ "showMore": "Show more",
+ "noteTitle": "Note - {{name}}",
+ "status": {
+ "started": "Started",
+ "done": "Done",
+ "late": "Late",
+ "skipped": "Skipped",
+ "pendingApproval": "Pending Approval",
+ "rejected": "Rejected",
+ "completed": "Completed"
+ }
+ },
+ "assignees": {
+ "title": "Tasks by Assignee",
+ "loading": "Loading tasks by assignee...",
+ "empty": "No assigned tasks found",
+ "legend": {
+ "inProgress": "In Progress",
+ "overdue": "Overdue",
+ "scheduled": "Scheduled",
+ "pendingReview": "Pending Review"
+ }
+ },
+ "userSwitcher": {
+ "title": "View tasks as",
+ "switchTitle": "Switch to user view",
+ "switchDescription": "Tasks will be filtered to show only assignments for selected user",
+ "chooseUser": "Choose User",
+ "changeUser": "Change User"
+ },
+ "notifications": {
+ "title": "Need notifications?",
+ "description": "You need to enable permission to receive notifications. Do you want to enable it?",
+ "enable": "Yes",
+ "keepDisabled": "No, keep it disabled"
+ },
+ "insights": {
+ "title": "Smart Insights",
+ "active": "Active",
+ "clearHint": "Click active filter to clear",
+ "description": "Quick actions based on your tasks",
+ "items": {
+ "overdue": {
+ "title": "Overdue",
+ "description_one": "{{count}} task is overdue",
+ "description_other": "{{count}} tasks are overdue"
+ },
+ "dueToday": {
+ "title": "Due Today",
+ "description_one": "{{count}} task due by end of day",
+ "description_other": "{{count}} tasks due by end of day"
+ },
+ "pendingApproval": {
+ "title": "Pending Approval",
+ "description_one": "{{count}} task awaits approval",
+ "description_other": "{{count}} tasks await approval"
+ },
+ "dueThisWeek": {
+ "title": "Due This Week",
+ "description_one": "{{count}} task due in the next 7 days",
+ "description_other": "{{count}} tasks due in the next 7 days"
+ },
+ "highPriority": {
+ "title": "High Priority",
+ "description_one": "{{count}} task requires immediate attention",
+ "description_other": "{{count}} tasks require immediate attention"
+ },
+ "noDueDate": {
+ "title": "No Due Date",
+ "description_one": "{{count}} task needs a deadline",
+ "description_other": "{{count}} tasks need a deadline"
+ }
+ }
+ },
+ "multiSelect": {
+ "showShortcuts": "Show keyboard shortcuts",
+ "title": "Multi-select Mode",
+ "description": "Use these keyboard shortcuts to work more efficiently with multiple tasks:",
+ "sections": {
+ "selection": "Selection",
+ "actions": "Actions",
+ "interface": "Interface"
+ },
+ "shortcuts": {
+ "selectAllVisible": "Select all visible tasks",
+ "clearOrExit": "Clear selection or exit multi-select mode",
+ "markCompleted": "Mark selected tasks as completed",
+ "deleteSelected": "Delete selected tasks",
+ "quickAdd": "Quick add new task"
+ },
+ "gotIt": "Got it!"
+ }
+ },
+ "due": {
+ "noDueDate": "No Due Date",
+ "dueRelative": "Due {{when}}",
+ "overdueRelative": "Overdue {{when}}",
+ "overdueDay": "Overdue {{day}}"
+ },
+ "frequency": {
+ "once": "Once",
+ "trigger": "Trigger",
+ "daily": "Daily",
+ "adaptive": "Adaptive",
+ "weekly": "Weekly",
+ "monthly": "Monthly",
+ "yearly": "Yearly",
+ "dailyExcept": "Daily except {{days}}",
+ "monthlyExcept": "{{label}} except {{months}}",
+ "dayOfMonths": "{{day}} of {{months}}",
+ "everyUnit": "Every {{count}} {{unit}}",
+ "units": {
+ "hours": "Hours",
+ "day": "day",
+ "days": "days",
+ "week": "week",
+ "weeks": "weeks",
+ "month": "month",
+ "months": "months",
+ "year": "year",
+ "years": "years"
+ }
+ },
+ "actions": {
+ "completeWithNote": "Complete with note",
+ "completeInPast": "Complete in past",
+ "skipToNextDueDate": "Skip to next due date",
+ "delegate": "Delegate to someone else",
+ "selectPerformer": "Select a performer",
+ "addCompletionNote": "Add note to attach to this completion",
+ "sendNudge": "Send nudge",
+ "history": "History",
+ "changeDueDate": "Change due date",
+ "writeToNfc": "Write to NFC"
+ },
+ "groups": {
+ "started": "Started",
+ "pendingApproval": "Pending Approval",
+ "overdue": "Overdue",
+ "today": "Today",
+ "tomorrow": "Tomorrow",
+ "next7Days": "Next 7 Days",
+ "laterThisMonth": "Later This Month",
+ "future": "Future",
+ "anytime": "Anytime"
+ },
+ "labels": {
+ "calendarOverview": "Calendar Overview",
+ "highPriority": "High Priority",
+ "mediumPriority": "Medium Priority",
+ "lowPriority": "Low Priority",
+ "lowestPriority": "Lowest Priority",
+ "noPriority": "No Priority",
+ "noTasksForDate": "No tasks scheduled for this date"
+ },
+ "messages": {
+ "serverUnavailableTitle": "Unable to communicate with server",
+ "serverUnavailableDescription": "The server is currently unavailable. Please check your connection and try again."
+ },
+ "edit": {
+ "nameQuestion": "What is the name of this task?",
+ "descriptionQuestion": "What is this task about?",
+ "priorityQuestion": "How important is this task?",
+ "projectQuestion": "Which project does this task belong to?",
+ "labelsQuestion": "Things to remember about this task or to tag it",
+ "addNewLabel": "Add New Label",
+ "whoCanDoTask": "Who can do this task?",
+ "whoIsAssignedNext": "Who is assigned the next due?",
+ "noAssigneesAvailable": "No assignees yet can perform this task",
+ "selectAssignee": "Select an assignee for this task",
+ "assignmentStrategyQuestion": "How to pick the next assignee for the following task?",
+ "triggerDueDateHint": "Due date will be set when the trigger condition is met",
+ "giveTaskDueDate": "Give this task a due date",
+ "dueDateHelper": "Task needs to be completed by a specific time",
+ "startDateQuestion": "When does this task start?",
+ "nextDueQuestion": "When is the next first time this task is due?",
+ "setSpecificTime": "Set a specific time",
+ "specificTimeHelper": "Task will be due at the specified time",
+ "endOfDayHelper": "Task will be due at the end of the day (11:59 PM)",
+ "taskWindowDescription": "Define when this task can be completed and when it expires",
+ "setEarliestCompletionTime": "Set earliest completion time",
+ "completionWindowHelper": "Task becomes available to complete X hours before the due date",
+ "afterDueDate": "after due date",
+ "schedulingPreferences": "Scheduling Preferences",
+ "schedulingPreferencesQuestion": "How to reschedule the next due date?",
+ "rescheduleFromDueDate": "Reschedule from due date",
+ "rescheduleFromCompletionDate": "Reschedule from completion date",
+ "rescheduleFromDueDateHelper": "The next task will be scheduled from the original due date, even if the previous task was completed late",
+ "rescheduleFromCompletionDateHelper": "The next task will be scheduled from the actual completion date of the previous task",
+ "notificationsBasicPlan": "Task notifications are not available in the Basic plan. Upgrade to Plus to receive reminders when tasks are due or completed.",
+ "notifyForTask": "Notify for this task",
+ "notifyForTaskHelper": "When should notifications be sent for this task?",
+ "notificationSchedule": "Notification Schedule",
+ "whoToNotify": "Who to Notify",
+ "notifyAllAssignees": "Notify all assignees",
+ "notifySpecificGroup": "Notify a specific group",
+ "taskSettings": "Task Settings",
+ "pointsHelper": "Assign points to this task and users will earn points when they complete it",
+ "assignPoints": "Assign points for completion",
+ "approvalRequirement": "Approval Requirement",
+ "requireAdminApproval": "Require admin approval",
+ "requireAdminApprovalHelper": "This task will need approval from an admin before being marked as complete",
+ "privacyQuestion": "Who can see this task?",
+ "privacyPublicHelper": "Everyone in your circle",
+ "privacyLimitedHelper": "You and others who are assigned to the task",
+ "privacyLimitedDisabled": "No assignees selected, Limited option is disabled",
+ "createdBy": "Created by",
+ "updatedBy": "Updated by",
+ "archive": "Archive",
+ "unarchive": "Unarchive",
+ "validation": {
+ "nameRequired": "Name is required",
+ "assigneesRequired": "At least 1 assignee is required",
+ "assignedToRequired": "Assigned to is required",
+ "invalidFrequency": "Invalid frequency, the {{unit}} should be > 0",
+ "selectDayOfWeek": "Please select at least one day of the week",
+ "selectDayOccurrence": "Please select at least one day occurrence for the month",
+ "selectMonth": "Please select at least one month",
+ "startDateRequired": "Start date is required",
+ "dueDateRequired": "Due date is required",
+ "thingTriggerInvalid": "Thing trigger is invalid",
+ "resolveErrors": "Please resolve the following errors:"
+ },
+ "assignStrategies": {
+ "random": "Random",
+ "least_assigned": "Least Assigned",
+ "least_completed": "Least Completed",
+ "keep_last_assigned": "Keep Last Assigned",
+ "random_except_last_assigned": "Random Except Last Assigned",
+ "round_robin": "Round Robin",
+ "no_assignee": "No Assignee"
+ },
+ "saveSuccessTitle": "Chore Saved",
+ "saveSuccessMessage": "Your task has been saved successfully!",
+ "saveFailedTitle": "Save Failed",
+ "saveFailedMessage": "Failed to save chore, please try again.",
+ "deleteTitle": "Delete Chore",
+ "deleteMessage": "Are you sure you want to delete this chore?",
+ "deleteFailedTitle": "Delete Failed"
+ },
+ "repeat": {
+ "repeat": "Repeat",
+ "repeatTask": "Repeat this task",
+ "repeatHelper": "Is this something that needs to be done regularly?",
+ "howOften": "How often should it be repeated?",
+ "repeatOn": "Repeat on",
+ "timeOfDay": "Time of day",
+ "every": "Every",
+ "unit": "Unit",
+ "selectAll": "Select All",
+ "unselectAll": "Unselect All",
+ "everyWeek": "Every week",
+ "weekOfMonth": "Week of month",
+ "everyWeekHelper": "Task repeats every week on selected days",
+ "weekOfMonthHelper": "Task repeats on specific day occurrences each month (for example, 1st Monday or 3rd Friday)",
+ "occurrencePrompt": "Select which occurrences of the selected days",
+ "occurrenceExample": "Example: \"1st Monday\" means the first Monday of each month",
+ "onThe": "on the",
+ "ofSelectedMonths": "of the selected months",
+ "ofMonth": "of the month",
+ "at": "at",
+ "triggerTask": "Trigger this task based on a thing state",
+ "triggerHelper": "Is this something that should be done when a thing state changes?",
+ "plusFeature": "Plus Feature",
+ "triggerBasicPlan": "Thing-based triggers are not available in the Basic plan. Upgrade to Plus to automatically trigger tasks when device states change.",
+ "typeMessages": {
+ "adaptive": "This chore will be scheduled dynamically based on previous completion dates.",
+ "custom": "This chore will be scheduled based on a custom frequency."
+ },
+ "options": {
+ "custom": "Custom",
+ "interval": "Interval",
+ "days_of_the_week": "Days of the Week",
+ "day_of_the_month": "Day of the Month"
+ },
+ "days": {
+ "monday": "Monday",
+ "tuesday": "Tuesday",
+ "wednesday": "Wednesday",
+ "thursday": "Thursday",
+ "friday": "Friday",
+ "saturday": "Saturday",
+ "sunday": "Sunday"
+ },
+ "dayAbbreviations": {
+ "monday": "Mon",
+ "tuesday": "Tue",
+ "wednesday": "Wed",
+ "thursday": "Thu",
+ "friday": "Fri",
+ "saturday": "Sat",
+ "sunday": "Sun"
+ },
+ "months": {
+ "january": "January",
+ "february": "February",
+ "march": "March",
+ "april": "April",
+ "may": "May",
+ "june": "June",
+ "july": "July",
+ "august": "August",
+ "september": "September",
+ "october": "October",
+ "november": "November",
+ "december": "December"
+ },
+ "occurrences": {
+ "1": "1st occurrence",
+ "2": "2nd occurrence",
+ "3": "3rd occurrence",
+ "4": "4th occurrence",
+ "-1": "Last occurrence"
+ }
+ },
+ "view": {
+ "previousNote": "Previous Note",
+ "taskActions": "Task Actions",
+ "addNote": "Add a note",
+ "additionalNotes": "Additional Notes",
+ "completionNotePlaceholder": "Add a note about the completion...",
+ "setCustomCompletionTime": "Set custom completion time",
+ "markAsDone": "Mark as done",
+ "skipTask": "Skip Task",
+ "skipTaskConfirm": "Are you sure you want to skip this task?",
+ "availableToCompleteStarting": "Available to complete starting {{date}}",
+ "pendingApproval": "Pending Approval",
+ "subtasks": "Subtasks"
+ },
+ "addTask": {
+ "title": "Create new task",
+ "taskInSentence": "Task in a sentence",
+ "smartHelp": "This feature lets you create a task simply by typing a sentence. It attempts to parse the sentence to identify the task's due date, priority, and frequency.",
+ "examples": "Examples",
+ "priorityExample": "For highest priority use P1, Urgent, Important, or ASAP. For lower priorities, use P2, P3, or P4.",
+ "dueDateExample": "Specify dates with phrases like tomorrow, next week, Monday, or August 1st at 12pm.",
+ "frequencyExample": "Set recurring tasks with terms like daily, weekly, monthly, yearly, or patterns such as every Tuesday and Thursday.",
+ "fullTextPlaceholder": "Type your full text here...",
+ "description": "Description",
+ "dueDate": "Due Date",
+ "editNotifications": "Edit Notifications",
+ "noPriority": "No Priority",
+ "create": "Create",
+ "anyone": "Anyone",
+ "points": {
+ "one": "1 point",
+ "other": "{{count}} points"
+ }
+ },
+ "detail": {
+ "cards": {
+ "assignment": "Assignment",
+ "schedule": "Schedule",
+ "statistics": "Statistics",
+ "details": "Details",
+ "assigned": "Assigned",
+ "last": "Last",
+ "due": "Due",
+ "deadline": "Deadline",
+ "completedTimes": "Completed: {{count}} times",
+ "createdBy": "Created By",
+ "notAvailable": "N/A"
+ },
+ "notifications": {
+ "taskCompleted": "Task Completed",
+ "taskCompletedMessage": "Your task has been marked as complete",
+ "taskSkipped": "Task skipped",
+ "undoSuccessful": "Undo Successful",
+ "taskCompletionUndone": "Task completion has been undone.",
+ "taskSkipUndone": "Task skip has been undone.",
+ "undoFailed": "Undo Failed",
+ "undoFailedMessage": "Unable to undo the action. Please try again."
+ },
+ "timer": {
+ "resetTitle": "Reset Timer",
+ "resetMessage": "Are you sure you want to reset the timer? This will clear all time records since you started the task.",
+ "resetConfirm": "Reset Timer",
+ "clearTitle": "Clear All Time Records",
+ "clearMessage": "This will permanently delete all timers for this task and set it back to \"not started\".",
+ "clearConfirm": "Clear All Time"
+ }
+ },
+ "main": {
+ "groupBy": "Group by",
+ "quickFilters": "Quick Filters",
+ "assignedTo": "Assigned to:",
+ "createFilter": "Create Filter",
+ "createFilterDescription": "Build advanced filter rules",
+ "createNewChoreShortcut": "Create new chore (Cmd+C)",
+ "archivedSearch": "Search archived tasks",
+ "archivedLoadFailed": "Failed to load archived tasks",
+ "tryAgainLater": "Please try again later.",
+ "archivedRestoreMessage": "The task has been restored and is now active.",
+ "archivedDeleteMessage": "The archived task has been permanently deleted.",
+ "unexpectedError": "An unexpected error occurred. Please try again.",
+ "shortcuts": {
+ "selectAllVisible": "Select all visible tasks (Ctrl+A)",
+ "completeSelected": "Complete selected tasks (Enter)",
+ "skipSelected": "Skip selected tasks (/)",
+ "archiveSelected": "Archive selected tasks (X)",
+ "restoreSelected": "Restore selected tasks (R)",
+ "deleteSelected": "Delete selected tasks (E)",
+ "clearMultiSelect": "Clear multi-select (Esc)",
+ "closeMultiSelect": "Close multi-select (Esc)"
+ },
+ "selectedSingle": "{{count}} task selected",
+ "selectedMultiple": "{{count}} tasks selected",
+ "viewCompact": "Switch to Compact View",
+ "viewCalendar": "Switch to Calendar View",
+ "viewCard": "Switch to Card View",
+ "exitMultiSelect": "Exit Multi-select Mode (Ctrl+S)",
+ "enableMultiSelect": "Enable Multi-select Mode (Ctrl+S)",
+ "cancelAllFilters": "Cancel All Filters",
+ "additionalFilter": "Additional Filter: {{filter}}",
+ "nothingScheduled": "Nothing scheduled",
+ "resetFilters": "Reset filters",
+ "restoredTasksTitle": "Tasks Restored",
+ "restoredTasks": "Successfully restored {{count}} task.",
+ "restoredTasks_plural": "Successfully restored {{count}} tasks.",
+ "restoredFailed": "{{count}} task could not be restored.",
+ "restoredFailed_plural": "{{count}} tasks could not be restored.",
+ "bulkRestoreFailed": "Bulk Restore Failed",
+ "deleteArchivedTitle": "Delete Archived Tasks",
+ "deleteArchivedConfirm": "Permanently delete {{count}} archived task?\n\nThis action cannot be undone.",
+ "deleteArchivedConfirm_plural": "Permanently delete {{count}} archived tasks?\n\nThis action cannot be undone.",
+ "archivedTitle": "Archived Tasks",
+ "archivedDescription": "View and manage tasks that have been archived or completed.",
+ "all": "All",
+ "clear": "Clear",
+ "restore": "Restore",
+ "noArchivedFound": "No archived tasks found",
+ "noArchived": "No archived tasks",
+ "adjustSearch": "Try adjusting your search terms",
+ "archivedWillAppear": "Archived tasks will appear here when you archive them from the main task list",
+ "clearSearch": "Clear search",
+ "archivedCount": "{{count}} archived task",
+ "archivedCount_plural": "{{count}} archived tasks",
+ "matchingSearch": " matching \"{{term}}\"",
+ "filters": {
+ "anyone": "Anyone",
+ "assignedToMe": "Assigned to me",
+ "availableForMe": "Available for me",
+ "assignedToOthers": "Assigned to others"
+ },
+ "otherFilters": {
+ "title": "Other",
+ "dueToday": "Due today",
+ "dueInWeek": "Due in week",
+ "dueLater": "Due later",
+ "createdByMe": "Created by me",
+ "assignedToMe": "Assigned to me",
+ "noDueDate": "No due date"
+ }
+ },
+ "nfc": {
+ "successTitle": "Success!",
+ "successMessage": "URL written to NFC tag successfully!",
+ "instructions": "Press the button below to write to NFC.",
+ "writeButton": "Write NFC",
+ "requestAccess": "Request Access",
+ "autoCompleteWhenScanned": "Auto-complete when scanned",
+ "urlCopied": "URL copied to clipboard!",
+ "unsupportedAlert": "NFC is not supported by this browser.",
+ "unsupportedMessage": "NFC is not supported by this browser. You can still copy the URL and write it to an NFC tag using a compatible device.",
+ "writeError": "Error writing to NFC tag. Please try again."
+ },
+ "actionFeedback": {
+ "undoable": {
+ "completed": "Task completed",
+ "approved": "Task approved",
+ "rejected": "Task rejected",
+ "skipped": "Task skipped"
+ },
+ "undoDone": {
+ "completed": "Task completion has been undone.",
+ "approved": "Task approval has been undone.",
+ "rejected": "Task rejection has been undone.",
+ "skipped": "Task skip has been undone."
+ },
+ "undoSuccessTitle": "Undo Successful",
+ "undoFailedTitle": "Undo Failed",
+ "undoFailedMessage": "Unable to undo the action. Please try again.",
+ "notifications": {
+ "rescheduledTitle": "Task Rescheduled",
+ "rescheduledMessage": "The task due date has been updated successfully.",
+ "dueDateRemovedTitle": "Task Unplanned",
+ "dueDateRemovedMessage": "The task is now unplanned and has no due date.",
+ "restoredTitle": "Task Restored",
+ "restoredMessage": "The task has been restored and is now active.",
+ "archivedTitle": "Task Archived",
+ "archivedMessage": "The task has been archived and hidden from the active list.",
+ "startedTitle": "Task Started",
+ "startedMessage": "The task has been marked as started.",
+ "pausedTitle": "Task Paused",
+ "pausedMessage": "The task has been paused.",
+ "deletedTitle": "Task Deleted",
+ "deletedMessage": "The task has been deleted."
+ },
+ "errors": {
+ "offlineRetry": "Request will be retried when you are online",
+ "failedToUpdate": "Failed to update",
+ "failedToStart": "Failed to start",
+ "unableToStart": "Unable to start chore",
+ "failedToPause": "Failed to pause",
+ "unableToPause": "Unable to pause chore",
+ "failedToApprove": "Failed to approve",
+ "unableToApprove": "Unable to approve chore",
+ "failedToReject": "Failed to reject",
+ "unableToReject": "Unable to reject chore",
+ "deleteTitle": "Delete Chore",
+ "deleteMessage": "Are you sure you want to delete this chore?",
+ "deleteFailed": "Failed to delete",
+ "failedToArchive": "Failed to archive",
+ "unableToArchive": "Unable to archive chore",
+ "failedToSkip": "Failed to skip",
+ "failedRemoveDueDate": "Failed to remove due date",
+ "failedReschedule": "Failed to reschedule",
+ "unableUpdateDueDate": "Unable to update due date",
+ "nudgeFailedTitle": "Failed to Send Nudge",
+ "nudgeFailedMessage": "Unable to send nudge at this time"
+ },
+ "nudgeSentTitle": "Nudge Sent!",
+ "nudgeSentMessage": "Nudge sent successfully",
+ "bulk": {
+ "completeTitle": "Complete Tasks",
+ "completeConfirm": "Mark {{count}} task as completed?",
+ "completeConfirm_plural": "Mark {{count}} tasks as completed?",
+ "completeSuccessTitle": "Tasks Completed",
+ "completeSuccess": "Successfully completed {{count}} task.",
+ "completeSuccess_plural": "Successfully completed {{count}} tasks.",
+ "someFailedTitle": "Some Tasks Failed",
+ "completeFailed": "{{count}} task could not be completed.",
+ "completeFailed_plural": "{{count}} tasks could not be completed.",
+ "completeUnexpectedTitle": "Bulk Complete Failed",
+ "archiveTitle": "Archive Tasks",
+ "archiveConfirm": "Archive {{count}} task?",
+ "archiveConfirm_plural": "Archive {{count}} tasks?",
+ "archiveSuccessTitle": "Tasks Archived",
+ "archiveSuccess": "Successfully archived {{count}} task.",
+ "archiveSuccess_plural": "Successfully archived {{count}} tasks.",
+ "archiveFailed": "{{count}} task could not be archived.",
+ "archiveFailed_plural": "{{count}} tasks could not be archived.",
+ "archiveUnexpectedTitle": "Bulk Archive Failed",
+ "deleteTitle": "Delete Tasks",
+ "deleteConfirm": "Delete {{count}} task?\n\nThis action cannot be undone.",
+ "deleteConfirm_plural": "Delete {{count}} tasks?\n\nThis action cannot be undone.",
+ "deleteSuccessTitle": "Tasks Deleted",
+ "deleteSuccess": "Successfully deleted {{count}} task.",
+ "deleteSuccess_plural": "Successfully deleted {{count}} tasks.",
+ "deleteFailed": "{{count}} task could not be deleted.",
+ "deleteFailed_plural": "{{count}} tasks could not be deleted.",
+ "deleteUnexpectedTitle": "Bulk Delete Failed"
+ }
+ },
+ "nudge": {
+ "description": "Send a reminder to help someone stay on top of this task.",
+ "headsUp": "Heads up:",
+ "selfHostedWarning": "Nudges currently work only on the official hosted Donetick instance.",
+ "customMessage": "Custom message",
+ "customMessagePlaceholder": "Optional message to include with the nudge",
+ "notifyAllDescription": "Notify every assignee instead of only the currently assigned user."
+ },
"title": "Chores",
"myChores": "My Chores",
"allChores": "All Chores",
@@ -21,6 +603,7 @@
"completed": "Completed",
"times": "times",
"details": "Details",
+ "deadline": "Deadline",
"createdBy": "Created By",
"na": "N/A",
"taskCompleted": "Task Completed",
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index fa493cf2..2a56f18e 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -1,4 +1,174 @@
{
+ "actions": {
+ "save": "Save",
+ "cancel": "Cancel",
+ "create": "Create",
+ "close": "Close",
+ "remove": "Remove",
+ "restore": "Restore",
+ "retry": "Retry",
+ "approve": "Approve",
+ "reject": "Reject",
+ "skip": "Skip",
+ "test": "Test",
+ "delete": "Delete",
+ "edit": "Edit",
+ "clone": "Clone",
+ "view": "View",
+ "archive": "Archive",
+ "unarchive": "Unarchive",
+ "login": "Login",
+ "logout": "Logout",
+ "signup": "Sign Up",
+ "backToLogin": "Back to Login",
+ "continue": "Continue",
+ "continueAs": "Continue as {{name}}",
+ "verifyAndSignIn": "Verify & Sign In",
+ "changePhoto": "Change Photo",
+ "rememberForFutureTasks": "Remember for Future Tasks",
+ "registerDevice": "Register Device",
+ "navigateBack": "Navigate Back",
+ "or": "or",
+ "complete": "Complete"
+ },
+ "modals": {
+ "selectUser": "Select User"
+ },
+ "labels": {
+ "username": "Username",
+ "email": "Email",
+ "emailAddress": "Email Address",
+ "password": "Password",
+ "displayName": "Display Name",
+ "timezone": "Timezone",
+ "search": "Search",
+ "url": "URL",
+ "verificationCode": "Verification Code",
+ "backupCode": "Backup Code",
+ "points": "points",
+ "tasks": "Tasks",
+ "task": "Task",
+ "overdue": "overdue",
+ "name": "Name",
+ "title": "Title",
+ "description": "Description",
+ "priority": "Priority",
+ "project": "Project",
+ "defaultProject": "Default Project",
+ "taskWindow": "Task Window",
+ "notifications": "Notifications",
+ "privacySettings": "Privacy Settings",
+ "assignmentStrategy": "Assignment Strategy",
+ "labelsLabel": "Labels",
+ "subtasks": "Subtasks",
+ "assignees": "Assignees",
+ "dueDate": "Due Date",
+ "startDate": "Start Date",
+ "time": "Time",
+ "hours": "Hours",
+ "pointsLabel": "Points",
+ "public": "Public",
+ "limited": "Limited",
+ "field": "Field",
+ "condition": "Condition",
+ "value": "Value",
+ "preview": "Preview",
+ "color": "Color",
+ "currentDevice": "Current Device",
+ "allAssignees": "All Assignees",
+ "specificGroup": "Specific Group",
+ "chatId": "Chat ID",
+ "userKey": "User key",
+ "telegramGroupId": "Telegram Group ID",
+ "stateIs": "State is",
+ "createAdvancedFilter": "Create Advanced Filter",
+ "editFilter": "Edit Filter",
+ "plusFeature": "Plus Feature"
+ },
+ "errors": {
+ "nameCannotBeEmpty": "Name cannot be empty",
+ "duplicateLabel": "Label with this name already exists",
+ "selectColor": "Please select a color",
+ "unableToSaveLabel": "Unable to save label. Please try again."
+ },
+ "placeholders": {
+ "search": "Search",
+ "typeHere": "Type here...",
+ "enterDisplayName": "Enter your display name",
+ "selectTimezone": "Select your timezone",
+ "hours": "Hours",
+ "telegramGroupId": "Telegram Group ID"
+ },
+ "status": {
+ "loading": "Loading...",
+ "offline": "You are offline",
+ "offlineDescription": "This is not available while offline. Please check your internet connection and try again.",
+ "delayed": "This is taking longer than usual. There might be an issue.",
+ "noDataAvailable": "No data available",
+ "anyone": "Anyone",
+ "unknown": "Unknown",
+ "unknownUser": "Unknown User",
+ "assignedToOther": "Assigned to other",
+ "smartFilter": "Smart Filter"
+ },
+ "legal": {
+ "privacyPolicy": "Privacy Policy",
+ "termsOfUse": "Terms of Use",
+ "termsOfService": "Terms of Service"
+ },
+ "calendar": {
+ "today": "Today",
+ "tomorrow": "Tomorrow",
+ "yesterday": "Yesterday",
+ "weekend": "Weekend",
+ "nextWeek": "Next week",
+ "removeDueDate": "Remove due date",
+ "tasksForDate": "Tasks for {{date}}"
+ },
+ "notifications": {
+ "titles": {
+ "error": "Error",
+ "success": "Success",
+ "warning": "Warning",
+ "info": "Information",
+ "undo": "Undone Successfully",
+ "custom": "Notification"
+ }
+ },
+ "editor": {
+ "placeholder": "Enter description...",
+ "plusFeature": "Plus Feature",
+ "plusFeatureMessage": "Image uploads are not available in the Basic plan. Upgrade to Plus to add images to your content.",
+ "storageQuotaExceeded": "Storage Quota Exceeded",
+ "storageQuotaExceededMessage": "You have exceeded your quota for uploading files.",
+ "fileTooLarge": "File Too Large",
+ "fileTooLargeMessage": "The file you are trying to upload is too large.",
+ "upgradeRequired": "Upgrade Required",
+ "upgradeRequiredMessage": "Image uploads are only available for Plus accounts.",
+ "permissionDenied": "Permission Denied",
+ "permissionDeniedMessage": "You do not have permission to upload files.",
+ "uploadFailed": "Upload Failed",
+ "uploadFailedMessage": "Failed to upload image.",
+ "processingFailedMessage": "An error occurred while processing the image."
+ },
+ "subtasks": {
+ "addSubtask": "Add subtask",
+ "addNewSubtask": "Add new subtask...",
+ "addNewTask": "Add new task..."
+ },
+ "navigation": {
+ "allTasks": "All Tasks",
+ "archived": "Archived",
+ "things": "Things",
+ "labels": "Labels",
+ "projects": "Projects",
+ "filters": "Filters",
+ "activities": "Activities",
+ "points": "Points",
+ "settings": "Settings",
+ "back": "Back",
+ "backToCalendar": "Back to Calendar"
+ },
"save": "Save",
"cancel": "Cancel",
"delete": "Delete",
@@ -18,16 +188,5 @@
"back": "Back",
"backToCalendar": "Back to Calendar",
"logout": "Logout",
- "version": "Version",
- "navigation": {
- "allTasks": "All Tasks",
- "archived": "Archived",
- "things": "Things",
- "labels": "Labels",
- "projects": "Projects",
- "filters": "Filters",
- "activities": "Activities",
- "points": "Points",
- "settings": "Settings"
- }
+ "version": "Version"
}
diff --git a/public/locales/en/filters.json b/public/locales/en/filters.json
new file mode 100644
index 00000000..f757b7f4
--- /dev/null
+++ b/public/locales/en/filters.json
@@ -0,0 +1,64 @@
+{
+ "view": {
+ "title": "Filters",
+ "description": "Save your favorite filter combinations for quick access. Create custom views to organize and find tasks faster.",
+ "noConditions": "No conditions",
+ "oneCondition": "1 condition",
+ "manyConditions": "{{count}} conditions",
+ "tasksCount": "{{count}} tasks",
+ "overdueCount": "{{count}} overdue",
+ "usedCount": "Used {{count}}x",
+ "emptyTitle": "No saved filters yet",
+ "emptyDescription": "Create custom filters to quickly access your most used chores.",
+ "pin": "Pin",
+ "unpin": "Unpin",
+ "deleteTitle": "Delete Filter",
+ "deleteMessage": "Are you sure you want to delete \"{{name}}\"? This cannot be undone."
+ },
+ "advanced": {
+ "editTitle": "Edit Filter",
+ "createTitle": "Create Advanced Filter",
+ "filterName": "Filter Name",
+ "filterNamePlaceholder": "For example, important tasks due soon or tasks for John",
+ "descriptionOptional": "Description (Optional)",
+ "descriptionPlaceholder": "Optional description for this filter...",
+ "conditions": "Filter Conditions (All must match)",
+ "conditionNumber": "Condition {{index}}",
+ "addCondition": "Add Condition",
+ "preview": "Preview",
+ "noMatches": "No tasks match these filters",
+ "andMore": "...and {{count}} more",
+ "selectAssignees": "Select assignees",
+ "selectCreators": "Select creators",
+ "selectPriorities": "Select priorities",
+ "selectLabels": "Select labels",
+ "selectProjects": "Select projects",
+ "selectStatuses": "Select statuses",
+ "defaultProject": "Default Project",
+ "unknown": "Unknown",
+ "active": "Active",
+ "started": "Started",
+ "inProgress": "In Progress",
+ "pendingApproval": "Pending Approval",
+ "isOverdue": "Is Overdue",
+ "isDueToday": "Is Due Today",
+ "isDueTomorrow": "Is Due Tomorrow",
+ "isDueThisWeek": "Is Due This Week",
+ "isDueThisMonth": "Is Due This Month",
+ "hasNoDueDate": "Has No Due Date",
+ "hasDueDate": "Has Due Date",
+ "equals": "Equals",
+ "greaterThan": "Greater Than",
+ "lessThan": "Less Than",
+ "greaterThanOrEqual": "Greater Than or Equal",
+ "lessThanOrEqual": "Less Than or Equal",
+ "assignee": "Assignee",
+ "createdBy": "Created By",
+ "label": "Label",
+ "status": "Status",
+ "points": "Points",
+ "enterFilterName": "Please enter a filter name",
+ "duplicateFilterName": "A filter with this name already exists",
+ "addAtLeastOneCondition": "Please add at least one filter condition"
+ }
+}
diff --git a/public/locales/en/history.json b/public/locales/en/history.json
new file mode 100644
index 00000000..2091a7ed
--- /dev/null
+++ b/public/locales/en/history.json
@@ -0,0 +1,64 @@
+{
+ "summaryTitle": "Task Summary",
+ "activityTitle": "Task Activity",
+ "common": {
+ "unknown": "Unknown",
+ "updatedAt": "Updated at {{date}}"
+ },
+ "assignedTo": "Assigned to {{name}}",
+ "note": "Note",
+ "points_one": "{{count}} pt",
+ "points_other": "{{count}} pts",
+ "delete": {
+ "title": "Delete History Record",
+ "confirm": "Are you sure you want to delete this history record?"
+ },
+ "empty": {
+ "title": "No History Yet",
+ "description": "You haven't completed any tasks. Once you start finishing tasks, they'll show up here.",
+ "backToChores": "Go back to chores"
+ },
+ "info": {
+ "completedTitle": "You've completed",
+ "completedValue": "12345 Chores"
+ },
+ "edit": {
+ "title": "Edit History",
+ "completedDate": "Completed Date",
+ "note": "Note",
+ "additionalNotes": "Additional Notes",
+ "deleteConfirm": "Are you sure you want to delete this history?"
+ },
+ "stats": {
+ "allCompleted": "All Completed",
+ "averageTiming": "Average Timing",
+ "longestDelay": "Longest Delay",
+ "completedMost": "Completed Most",
+ "membersInvolved": "Members Involved",
+ "lastCompleted": "Last Completed",
+ "onTime": "On time",
+ "neverLate": "Never late",
+ "times_one": "{{count}} time",
+ "times_other": "{{count}} times",
+ "membersCount_one": "{{count}} member",
+ "membersCount_other": "{{count}} members"
+ },
+ "status": {
+ "inProgress": "In Progress",
+ "completed": "Completed",
+ "skipped": "Skipped",
+ "pendingApproval": "Pending Approval",
+ "rejected": "Rejected",
+ "missed": "Missed",
+ "rescheduled": "Rescheduled",
+ "onTime": "On Time",
+ "early": "Early",
+ "late": "Late"
+ },
+ "messages": {
+ "updatedTitle": "History Updated",
+ "updatedMessage": "The history record has been updated successfully.",
+ "deletedTitle": "History Deleted",
+ "deletedMessage": "The history record has been deleted successfully."
+ }
+}
diff --git a/public/locales/en/labelsView.json b/public/locales/en/labelsView.json
new file mode 100644
index 00000000..4c3a8877
--- /dev/null
+++ b/public/locales/en/labelsView.json
@@ -0,0 +1,9 @@
+{
+ "title": "Labels",
+ "description": "Manage your labels and organize your tasks effectively. Labels are automatically shared with your circle when they are used on a shared task.",
+ "shared": "Shared",
+ "deleteTitle": "Delete Label",
+ "deleteMessage": "Are you sure you want to delete this label? This will remove the label from all tasks.",
+ "loadFailed": "Failed to load labels. Please try again.",
+ "empty": "No labels available. Add a new label to get started."
+}
diff --git a/public/locales/en/navigation.json b/public/locales/en/navigation.json
new file mode 100644
index 00000000..8b58f773
--- /dev/null
+++ b/public/locales/en/navigation.json
@@ -0,0 +1,13 @@
+{
+ "allTasks": "All Tasks",
+ "archived": "Archived",
+ "things": "Things",
+ "labels": "Labels",
+ "projects": "Projects",
+ "filters": "Filters",
+ "activities": "Activities",
+ "points": "Points",
+ "settings": "Settings",
+ "back": "Back",
+ "backToCalendar": "Back to Calendar"
+}
diff --git a/public/locales/en/projects.json b/public/locales/en/projects.json
new file mode 100644
index 00000000..42bc7012
--- /dev/null
+++ b/public/locales/en/projects.json
@@ -0,0 +1,29 @@
+{
+ "view": {
+ "title": "Projects",
+ "description": "Organize your tasks into projects. Create custom workspaces to keep your tasks organized and easy to access.",
+ "default": "Default",
+ "defaultProjectName": "Default Project",
+ "defaultProjectDescription": "All tasks without a specific project",
+ "tasksCount": "{{count}} tasks",
+ "shared": "Shared",
+ "loadFailed": "Failed to load projects. Please try again.",
+ "deleteTitle": "Delete Project",
+ "deleteMessage": "Are you sure you want to delete \"{{name}}\"? This will remove the project but keep all tasks, which will move to the Default Project."
+ },
+ "modal": {
+ "createTitle": "Create New Project",
+ "editTitle": "Edit Project",
+ "name": "Project Name",
+ "namePlaceholder": "Enter project name...",
+ "nameRequired": "Project name is required",
+ "descriptionPlaceholder": "Optional project description...",
+ "icon": "Project Icon",
+ "chooseIconTitle": "Choose Project Icon",
+ "availableIcons": "Available Icons",
+ "selectIcon": "Select Icon",
+ "color": "Project Color",
+ "createFailed": "Failed to create project",
+ "updateFailed": "Failed to update project"
+ }
+}
diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json
index d03ef6d7..9bd27bbe 100644
--- a/public/locales/en/settings.json
+++ b/public/locales/en/settings.json
@@ -1,110 +1,61 @@
{
- "title": "Settings",
- "circleSettings": {
- "title": "Circle settings",
- "description": "Your account is automatically connected to a Circle when you create or join one. Easily invite friends by sharing the unique Circle code or link below. You'll receive a notification below when someone requests to join your Circle. If you'd like to leave, simply hit the 'Leave Circle' button.",
- "circleCode": "Circle Code",
- "copyCode": "Copy Code",
- "copyLink": "Copy Link",
- "codeCopied": "Circle code copied!",
- "linkCopied": "Circle link copied!",
- "joinCircle": "Join a Circle",
- "joinCirclePlaceholder": "Enter Circle Code",
- "join": "Join",
- "leave": "Leave Circle",
- "leaveConfirmTitle": "Leave Circle",
- "leaveConfirmMessage": "Are you sure you want to leave this circle?",
- "circleMembers": "Circle Members",
- "circleMemberRequests": "Circle Member Requests",
- "admin": "Admin",
- "member": "Member",
- "pending": "Pending",
- "accept": "Accept",
- "reject": "Reject",
- "makeAdmin": "Make Admin",
- "makeMember": "Make Member",
- "remove": "Remove",
- "webhookURL": "Webhook URL",
- "webhookDescription": "Enter a webhook URL to receive notifications for circle events",
- "webhookPlaceholder": "https://your-webhook-url.com"
- },
- "accountSettings": {
- "title": "Account Settings",
- "subscription": "Subscription",
- "subscriptionStatus": "Current Plan",
- "free": "Free",
- "plus": "Plus",
- "upgrade": "Upgrade",
- "cancel": "Cancel",
- "changePassword": "Change Password",
- "password": "Password",
- "dangerZone": "Danger Zone",
- "dangerZoneDescription": "Once you delete your account, there is no going back. Please be certain.",
- "deleteAccount": "Delete Account"
- },
- "localization": {
- "title": "Localization",
- "description": "Customize language, date format, and regional preferences for your account.",
- "language": "Language",
- "languageDescription": "Select your preferred language",
- "dateFormat": "Date Format",
- "dateFormatDescription": "Choose how dates should be displayed throughout the application",
- "timeFormat": "Time Format",
- "timeFormatDescription": "Select 12-hour or 24-hour time format",
- "12hour": "12-hour (AM/PM)",
- "24hour": "24-hour",
- "firstDayOfWeek": "First Day of Week",
- "firstDayOfWeekDescription": "Select which day starts your week",
- "sunday": "Sunday",
- "monday": "Monday",
- "saturday": "Saturday",
- "formats": {
- "mdy": "MM/DD/YYYY (US)",
- "dmy": "DD/MM/YYYY (Europe)",
- "ymd": "YYYY-MM-DD (ISO)",
- "long": "Long format (e.g., January 1, 2024)",
- "short": "Short format (e.g., Jan 1, 2024)"
- }
- },
- "sidepanel": {
- "title": "Sidepanel Customization",
- "description": "Customize the layout and visibility of cards in the sidepanel. This section is only available on large screen devices such as tablets and desktops."
- },
- "theme": {
- "title": "Theme preferences",
- "description": "Choose how the site looks to you. Select a single theme, or sync with your system and automatically switch between day and night themes.",
- "themeMode": "Theme mode",
- "light": "Light",
- "dark": "Dark",
- "system": "System"
- },
- "notifications": {
- "settingsSaved": "Settings saved successfully",
- "settingsSaveFailed": "Failed to save settings",
- "invalidWebhook": "Invalid webhook URL"
- },
- "profile": {
- "title": "Profile Settings",
- "description": "Update your display name and profile photo.",
- "photoUpdated": "Photo Updated",
- "photoUpdatedMessage": "Your profile photo has been updated successfully!",
- "uploadFailed": "Upload Failed",
- "uploadFailedMessage": "Failed to upload your photo. Please try again.",
- "profileUpdated": "Profile Updated",
- "profileUpdatedMessage": "Your profile information has been saved successfully!",
- "updateFailed": "Update Failed",
- "updateFailedMessage": "Unable to update your profile. Please check your connection and try again.",
- "changePhoto": "Change Photo",
- "displayName": "Display Name",
- "displayNamePlaceholder": "Enter your display name",
- "timezone": "Timezone",
- "timezonePlaceholder": "Select your timezone",
- "save": "Save",
- "cancel": "Cancel"
- },
"overview": {
"title": "Settings",
"subtitle": "Customize your experience and manage your account preferences",
+ "cards": {
+ "profile": {
+ "title": "Profile Settings",
+ "description": "Update your profile information, photo, display name, and timezone preferences."
+ },
+ "circle": {
+ "title": "Circle Settings",
+ "description": "Manage your circle, invite members, and handle join requests."
+ },
+ "account": {
+ "title": "Account Settings",
+ "description": "Manage your subscription, change password, and account deletion options."
+ },
+ "subaccounts": {
+ "title": "Managed Accounts",
+ "description": "Create and manage sub accounts to log in and complete assigned tasks."
+ },
+ "notifications": {
+ "title": "Notifications",
+ "description": "Configure push notifications, email alerts, and notification targets for tasks."
+ },
+ "mfa": {
+ "title": "Multi-Factor Authentication",
+ "description": "Add an extra layer of security with MFA using authenticator apps."
+ },
+ "apitokens": {
+ "title": "API Tokens",
+ "description": "Generate and manage access tokens for third-party integrations and API access."
+ },
+ "storage": {
+ "title": "Storage Settings",
+ "description": "Backup and restore your data, manage local storage and sync preferences."
+ },
+ "sidepanel": {
+ "title": "Sidepanel Customization",
+ "description": "Customize the layout and visibility of cards in the sidepanel interface."
+ },
+ "theme": {
+ "title": "Theme Preferences",
+ "description": "Choose your preferred theme and configure dark/light mode settings."
+ },
+ "language": {
+ "title": "Language",
+ "description": "Choose the language used across the app on this device."
+ },
+ "advanced": {
+ "title": "Advanced Settings",
+ "description": "Configure webhooks, real-time updates, and other advanced features for enhanced productivity."
+ },
+ "developer": {
+ "title": "Developer Settings",
+ "description": "View technical information about authentication tokens, SSE connections, and debug data."
+ }
+ },
"upgrade": {
"title": "Upgrade to Plus",
"description": "Unlock powerful features to enhance your productivity",
@@ -170,5 +121,613 @@
"description": "View technical information about authentication tokens, SSE connections, and debug data."
}
}
+ },
+ "pages": {
+ "profile": {
+ "title": "Profile Settings"
+ },
+ "circle": {
+ "title": "Circle Settings"
+ },
+ "account": {
+ "title": "Account Settings"
+ },
+ "managedAccounts": {
+ "title": "Managed Accounts"
+ },
+ "subAccountManagement": {
+ "title": "Sub Account Management"
+ },
+ "notifications": {
+ "title": "Notification Settings"
+ },
+ "mfa": {
+ "title": "Multi-Factor Authentication"
+ },
+ "apiTokens": {
+ "title": "API Tokens"
+ },
+ "storage": {
+ "title": "Storage Settings"
+ },
+ "sidepanel": {
+ "title": "Sidepanel Customization"
+ },
+ "theme": {
+ "title": "Theme Preferences"
+ },
+ "language": {
+ "title": "Language"
+ },
+ "advanced": {
+ "title": "Advanced Settings"
+ },
+ "developer": {
+ "title": "Developer Settings"
+ }
+ },
+ "sidepanel": {
+ "heading": "Customize sidepanel cards",
+ "description": "Customize the layout and visibility of cards in the sidepanel. This section is only available on large screen devices such as tablets and desktops.",
+ "reset": "Reset to defaults",
+ "resetHelp": "Restore the recommended card order and visibility.",
+ "cards": {
+ "welcome": {
+ "title": "User Switcher",
+ "description": "Allows admins and managers to view tasks as different users."
+ },
+ "smartInsights": {
+ "title": "Smart Insights",
+ "description": "Shows quick actions based on your current tasks."
+ },
+ "assignees": {
+ "title": "Tasks by Assignee",
+ "description": "Groups tasks by the person they are assigned to."
+ },
+ "calendar": {
+ "title": "Calendar View",
+ "description": "Shows tasks in a calendar layout."
+ },
+ "activities": {
+ "title": "Recent Activities",
+ "description": "Shows recent task completions and other activity."
+ },
+ "weeklyGoals": {
+ "title": "Weekly Goals",
+ "description": "Shows weekly progress and completion statistics for your circle."
+ }
+ },
+ "title": "Sidepanel Customization"
+ },
+ "theme": {
+ "description": "Choose how the site looks to you. Select a single theme, or sync with your system and automatically switch between day and night themes.",
+ "modeLabel": "Theme mode",
+ "light": "Light",
+ "dark": "Dark",
+ "system": "System",
+ "title": "Theme preferences",
+ "themeMode": "Theme mode"
+ },
+ "profile": {
+ "description": "Update your display name and profile photo.",
+ "photoUpdatedTitle": "Photo Updated",
+ "photoUpdatedMessage": "Your profile photo has been updated successfully!",
+ "uploadFailedTitle": "Upload Failed",
+ "uploadFailedMessage": "Failed to upload your photo. Please try again.",
+ "profileUpdatedTitle": "Profile Updated",
+ "profileUpdatedMessage": "Your profile information has been saved successfully!",
+ "updateFailedTitle": "Update Failed",
+ "updateFailedMessage": "Unable to update your profile. Please check your connection and try again.",
+ "title": "Profile Settings",
+ "photoUpdated": "Photo Updated",
+ "uploadFailed": "Upload Failed",
+ "profileUpdated": "Profile Updated",
+ "updateFailed": "Update Failed",
+ "changePhoto": "Change Photo",
+ "displayName": "Display Name",
+ "displayNamePlaceholder": "Enter your display name",
+ "timezone": "Timezone",
+ "timezonePlaceholder": "Select your timezone",
+ "save": "Save",
+ "cancel": "Cancel"
+ },
+ "language": {
+ "title": "Language",
+ "description": "Choose the language used throughout the app. This preference is saved on this device.",
+ "fieldLabel": "App language",
+ "options": {
+ "en": "English",
+ "de": "German",
+ "es": "Spanish",
+ "pt": "Portuguese"
+ }
+ },
+ "childAccounts": {
+ "title": "Managed Accounts",
+ "accessDeniedTitle": "Sub Account Management",
+ "parentOnly": "Only primary users can manage sub accounts.",
+ "description": "Manage sub accounts. Sub account users can log in and complete assigned tasks.",
+ "planWarning": "Sub accounts are limited to 1 on the Free plan. Upgrade to Plus to have up to 5 sub accounts.",
+ "sectionTitle": "Sub Accounts ({{count}})",
+ "add": "Add Sub Account",
+ "loading": "Loading sub accounts...",
+ "emptyTitle": "No Sub Accounts",
+ "emptyDescription": "Create sub accounts so team members can log in and complete their assigned tasks.",
+ "addFirst": "Add Your First Sub Account",
+ "username": "Username: {{username}}",
+ "created": "Created: {{date}}",
+ "changePasswordTitle": "Change Password",
+ "deleteAccountTitle": "Delete Account",
+ "howItWorksTitle": "How Managed Accounts Work",
+ "bulletOne": "Managed accounts are created by the primary user and can be deleted or have their password reset by that user.",
+ "bulletTwo": "Sub accounts can log in with their own username and password.",
+ "bulletThree": "Managed accounts can complete tasks but have limited administrative permissions.",
+ "bulletFour": "Managed accounts are automatically added to your circle.",
+ "createSuccess": "Child account \"{{name}}\" created successfully!",
+ "createFailed": "Failed to create child account: {{message}}",
+ "updatePasswordSuccess": "Child password updated successfully",
+ "updatePasswordFailed": "Failed to update password: {{message}}",
+ "deleteConfirmTitle": "Delete Sub Account",
+ "deleteConfirmMessage": "Are you sure you want to delete the child account \"{{name}}\"? This action cannot be undone.",
+ "deleteSuccess": "Sub account \"{{name}}\" deleted successfully",
+ "deleteFailed": "Failed to delete sub account: {{message}}",
+ "modal": {
+ "title": "Create Sub Account",
+ "description": "Create a new sub account. The user will be able to log in using their combined username and complete tasks assigned to them.",
+ "nameLabel": "Sub Account Name",
+ "namePlaceholder": "Enter sub account name (e.g., sarah)",
+ "displayNamePlaceholder": "Display name (optional, defaults to sub account name)",
+ "confirmPasswordLabel": "Confirm Password",
+ "confirmPasswordPlaceholder": "Confirm password",
+ "createAction": "Create Account"
+ },
+ "validation": {
+ "nameRequired": "Sub account name is required",
+ "nameMin": "Sub account name must be at least 2 characters",
+ "nameMax": "Sub account name must be less than 20 characters",
+ "namePattern": "Sub account name can only contain lowercase letters, dot and dash",
+ "displayNameMax": "Display name must be less than 50 characters",
+ "passwordMismatch": "Passwords do not match"
+ }
+ },
+ "advanced": {
+ "title": "Advanced Settings",
+ "loading": "Loading...",
+ "description": "Configure advanced features like webhooks and real-time updates for enhanced productivity.",
+ "webhookTitle": "Webhook",
+ "webhookDescription": "Webhooks let you send real-time notifications to other services when events happen in your Circle. Configure a webhook URL to receive updates.",
+ "webhookPlanWarning": "Webhook notifications are not available in the Basic plan. Upgrade to Plus to receive real-time updates via webhooks.",
+ "enableWebhook": "Enable Webhook",
+ "enableWebhookHelper": "Enable webhook notifications for tasks and things updates.",
+ "webhookUrl": "Webhook URL",
+ "webhookSaved": "Webhook URL updated successfully",
+ "webhookSaveFailed": "Failed to update webhook URL",
+ "realtimeTitle": "Real-time Updates",
+ "realtimeSummary": "Get instant notifications when tasks are updated",
+ "enableRealtime": "Enable Real-time Updates",
+ "realtimeUnavailable": "Real-time updates are not available in the Basic plan. Upgrade to Plus to receive instant notifications when tasks are updated.",
+ "realtimeDisabled": "Real-time updates are disabled. Enable them to see live changes when you or other circle members complete, skip, or modify tasks.",
+ "realtimeConnected": "Real-time updates are working. You'll see live changes when you or other circle members complete, skip, or modify tasks.",
+ "realtimeConnecting": "Connecting to real-time updates...",
+ "realtimeError": "Real-time updates are enabled but not working: {{error}}",
+ "realtimeEnabledNotConnected": "Real-time updates are enabled but not currently connected.",
+ "sseTitle": "Real-time Updates (SSE)",
+ "sseSummary": "Get instant notifications via Server-Sent Events",
+ "enableSse": "Enable Real-time Updates (SSE)",
+ "sseUnavailable": "Real-time updates (SSE) are not available in the Basic plan. Upgrade to Plus to receive instant notifications when chores are updated.",
+ "sseDisabled": "Real-time updates (SSE) are disabled. Enable to see live changes when you or other circle members complete, skip, or modify chores.",
+ "sseConnected": "Real-time updates (SSE) are working. You'll see live changes when you or other circle members complete, skip, or modify chores.",
+ "sseConnecting": "Connecting to real-time updates (SSE)...",
+ "sseError": "Real-time updates (SSE) are enabled but not working: {{error}}",
+ "sseEnabledNotConnected": "Real-time updates (SSE) are enabled but not currently connected.",
+ "status": "Status:",
+ "connection": {
+ "connected": "Connected",
+ "connecting": "Connecting...",
+ "disconnected": "Disconnected",
+ "tooltipBase": "Real-time updates (SSE): {{status}}",
+ "tooltipError": "Real-time updates (SSE): {{status}} - {{error}}",
+ "tooltipJoinCircle": "Real-time updates (SSE): {{status}} - Join a circle to enable real-time updates"
+ },
+ "on": "On",
+ "off": "Off",
+ "webhookIntegration": "Webhook Integration",
+ "realtimeSectionDescription": "Configure how you receive live updates when tasks and activities change in your circle."
+ },
+ "account": {
+ "description": "Change your account settings, subscription status, or password.",
+ "accountType": "Account Type",
+ "loading": "Loading...",
+ "plus": "Plus",
+ "free": "Free",
+ "activePlan": "You are currently subscribed to the Plus plan. Your subscription will renew on {{date}}.",
+ "cancelledPlan": "You have cancelled your subscription. Your account will be downgraded to the Free plan on {{date}}.",
+ "freePlan": "You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.",
+ "upgrade": "Upgrade",
+ "password": "Password",
+ "changePassword": "Change Password",
+ "dangerZone": "Danger Zone",
+ "dangerDescription": "Once you delete your account, there is no going back. Please be certain.",
+ "deleteAccount": "Delete Account",
+ "purchaseSuccess": "Purchase successful! Please restart the app to access Plus features.",
+ "purchaseNetwork": "Store connection issue. Please check your network and try again.",
+ "purchaseNotAllowed": "Purchases are not allowed on this device. Please check your device restrictions.",
+ "purchaseUnavailable": "This subscription is not available. Please try again later.",
+ "purchaseProcessed": "This purchase has already been processed. If you believe this is an error, please contact support.",
+ "purchaseReceiptMissing": "Purchase receipt missing. Please try purchasing again.",
+ "purchasePending": "Payment is pending approval. You will receive access once approved.",
+ "purchaseFailed": "Purchase failed: {{message}}. Please try again or contact support.",
+ "passwordChanged": "Password changed successfully",
+ "passwordChangeFailed": "Password change failed",
+ "subscriptionCancelled": "Subscription cancelled",
+ "subscriptionCancelFailed": "Failed to cancel subscription"
+ },
+ "backup": {
+ "title": "Backup & Restore",
+ "createTab": "Create Backup",
+ "restoreTab": "Restore Backup",
+ "createDescription": "Create an encrypted backup of your data. This includes your chores, history, settings, and optionally uploaded files.",
+ "encryptionKeyLabel": "Encryption Key *",
+ "encryptionKeyPlaceholder": "Enter a strong encryption key",
+ "encryptionKeyHint": "Keep this key safe. You'll need it to restore your backup.",
+ "encryptionKeyRequired": "Encryption key is required",
+ "nameLabel": "Backup Name (Optional)",
+ "namePlaceholder": "e.g., weekly-backup",
+ "includeAssets": "Include uploaded files and assets",
+ "createAction": "Create Backup",
+ "restoreAction": "Restore Backup",
+ "createFailed": "Failed to create backup",
+ "created": "Backup created and downloaded successfully",
+ "restoreFailed": "Failed to restore backup",
+ "restored": "Backup restored successfully. Please refresh the page.",
+ "readFailed": "Failed to read backup file",
+ "fileLabel": "Backup File *",
+ "selectFile": "Please select a backup file",
+ "selectedFile": "Selected: {{name}}",
+ "restoreWarning": "Restoring a backup will replace all your current data. This action cannot be undone.",
+ "restoreKeyPlaceholder": "Enter the encryption key used for this backup",
+ "creating": "Creating backup...",
+ "restoring": "Restoring backup..."
+ },
+ "subscription": {
+ "title": "Upgrade to Plus",
+ "included": "What's included:",
+ "subscribe": "Subscribe",
+ "footer": "Cancel anytime. No hidden fees. Secure payment powered by Stripe.",
+ "unlockFeatures": "Unlock premium features",
+ "errorTitle": "Subscription Error",
+ "errorMessage": "Failed to start subscription process. Please try again.",
+ "features": {
+ "notifications": "Task notifications and reminders",
+ "richText": "Rich text descriptions with image uploads",
+ "thingTriggers": "Thing-based task triggers",
+ "apiTokens": "API tokens for integrations",
+ "imageUploads": "Image uploads in descriptions",
+ "automation": "Advanced task automation"
+ }
+ },
+ "menu": {
+ "impersonateUser": "Impersonate User",
+ "switchUser": "Switch User",
+ "actAsAnotherUser": "Act as another user",
+ "stopImpersonating": "Stop Impersonating",
+ "returnToYourAccount": "Return to your account",
+ "settings": "Settings",
+ "accountAndPreferences": "Account & preferences",
+ "invitePeople": "Invite People",
+ "addMembersToYourCircle": "Add members to your circle",
+ "sidePanelSettings": "Side Panel Settings",
+ "customizeLayoutAndCards": "Customize layout & cards",
+ "switchToLight": "Switch to Light",
+ "switchToDark": "Switch to Dark",
+ "toggleThemeAppearance": "Toggle theme appearance",
+ "impersonating": "Impersonating"
+ },
+ "nativeCancel": {
+ "title": "Cancel Subscription",
+ "description": "To cancel your subscription, follow the instructions for your platform. You should cancel through the same platform you used to subscribe.",
+ "iosTitle": "For iOS (iPhone/iPad):",
+ "iosSteps": {
+ "1": "1. Open the Settings app on your device",
+ "2": "2. Tap your name at the top of the screen",
+ "3": "3. Tap Subscriptions",
+ "4": "4. Find and tap Donetick",
+ "5": "5. Tap Cancel Subscription"
+ },
+ "iosNote": "If you subscribed through iOS and are using the web or desktop version, you must cancel through iOS Settings as described above.",
+ "androidTitle": "For Android:",
+ "androidSteps": {
+ "1": "1. Open the Google Play Store app",
+ "2": "2. Tap the profile icon in the top right",
+ "3": "3. Tap Payments & subscriptions",
+ "4": "4. Tap Subscriptions",
+ "5": "5. Find and tap Donetick",
+ "6": "6. Tap Cancel subscription"
+ },
+ "androidNote": "If you subscribed through Google Play and are using the web or desktop version, you must cancel through Google Play as described above.",
+ "webTitle": "For Web/Desktop Subscriptions:",
+ "webDescription": "If you originally subscribed through our website or desktop app, you can cancel your subscription from the Account Settings section on our website using a web browser.",
+ "webImportant": "You must cancel your subscription through the same platform where you originally subscribed. If you subscribed through the iOS App Store or Google Play Store, you must cancel through that original platform.",
+ "billingPeriod": "Your subscription will remain active until the end of your current billing period.",
+ "cancelFromStore": "I'll cancel from my app store",
+ "cancelDesktopNow": "I subscribed via desktop - Cancel now"
+ },
+ "apiTokensPage": {
+ "heading": "Access Token",
+ "description": "Create tokens to use with the API for updating things, tasks, or chores.",
+ "plusFeature": "Plus Feature",
+ "plusDescription": "API tokens are not available in the Basic plan. Upgrade to Plus to generate API tokens for integrations and automation.",
+ "hideToken": "Hide Token",
+ "showToken": "Show Token",
+ "removeToken": "Remove Token",
+ "removeTokenConfirm": "Are you sure you want to remove {{name}}?",
+ "removed": "Removed",
+ "removedMessage": "API token has been removed",
+ "copied": "Token copied to clipboard",
+ "generateNew": "Generate New Token",
+ "namePrompt": "Give a name for your new token, something to remember it by.",
+ "generateAction": "Generate Token"
+ },
+ "mfaPage": {
+ "description": "Add an extra layer of security to your account with multi-factor authentication (MFA). When enabled, you'll need a verification code from your authenticator app in addition to your password when signing in.",
+ "title": "Two-Factor Authentication",
+ "enabled": "Your account is protected with 2FA",
+ "disabled": "Secure your account with an authenticator app",
+ "enable": "Enable",
+ "disable": "Disable",
+ "setupTitle": "Set up Multi-Factor Authentication",
+ "step1": "Step 1: Scan the QR code below with your authenticator app (Google Authenticator, Authy, etc.)",
+ "manualKey": "Manual entry key:",
+ "addedToApp": "I've added the account to my app",
+ "step2": "Step 2: Enter the 6-digit verification code from your authenticator app",
+ "verifyEnable": "Verify & Enable",
+ "enabledSuccess": "MFA Successfully Enabled!",
+ "backupSaveTitle": "Save these backup codes in a safe place",
+ "backupSaveDescription": "You can use these codes to access your account if you lose your authenticator device. Each code can only be used once.",
+ "savedBackupCodes": "I've saved my backup codes",
+ "disableTitle": "Disable Multi-Factor Authentication",
+ "disableWarning": "Disabling MFA will make your account less secure. Are you sure you want to continue?",
+ "disablePrompt": "Enter a verification code from your authenticator app to confirm:",
+ "backupCodesTitle": "New Backup Codes",
+ "backupCodesWarning": "Your previous backup codes are now invalid. Save these new codes in a safe place. Each code can only be used once.",
+ "failedQr": "Failed to generate QR code",
+ "invalidResponse": "Invalid response from server. Missing QR code or secret.",
+ "endpointMissing": "MFA setup endpoint not found. This feature may not be available yet.",
+ "unauthorized": "Unauthorized. Please login again.",
+ "serverError": "Server error. Please try again later.",
+ "setupFailed": "Failed to set up MFA ({{status}}). Please try again.",
+ "networkError": "Network error. Please check your connection and try again.",
+ "enabledToast": "MFA has been successfully enabled!",
+ "invalidCode": "Invalid verification code. Please try again.",
+ "confirmFailed": "Failed to confirm MFA. Please try again.",
+ "disabledToast": "MFA has been disabled successfully!",
+ "disableFailed": "Failed to disable MFA. Please try again.",
+ "backupRegenerated": "New backup codes have been generated!",
+ "regenerateFailed": "Failed to regenerate backup codes. Please try again."
+ },
+ "developer": {
+ "title": "Developer Settings",
+ "description": "View technical information about your authentication tokens, notifications, and session state for debugging.",
+ "tokenRefreshed": "Token refreshed successfully",
+ "tokenRefreshFailed": "Token refresh failed: {{message}}",
+ "tokenRefreshError": "Token refresh error: {{message}}",
+ "refreshEndpointSuccess": "Refresh token endpoint called successfully",
+ "refreshEndpointFailed": "Refresh token endpoint failed: {{status}} {{message}}",
+ "refreshEndpointError": "Refresh token endpoint error: {{message}}",
+ "notificationsLoaded": "Loaded {{count}} scheduled notifications",
+ "notificationsLoadError": "Error loading notifications: {{message}}",
+ "authTokens": "Authentication Tokens",
+ "refreshTokenAction": "Refresh Token",
+ "callRefreshEndpoint": "Call Refresh Endpoint",
+ "accessToken": "Access Token",
+ "refreshToken": "Refresh Token",
+ "timeLeft": "Time Left:",
+ "expires": "Expires: {{date}}",
+ "refreshTokenCookie": "Refresh tokens are managed via HTTP-only cookies on web platform.",
+ "platformInfo": "Platform Information",
+ "platform": "Platform:",
+ "native": "Native",
+ "web": "Web",
+ "scheduledNotifications": "Scheduled Local Notifications",
+ "refresh": "Refresh",
+ "noScheduledNotifications": "No scheduled notifications",
+ "totalScheduled": "Total scheduled:",
+ "noTitle": "No title",
+ "noBody": "No body",
+ "pastDue": "Past due",
+ "choreId": "Chore ID: {{id}}",
+ "sseTitle": "Server-Sent Events (SSE)",
+ "connectionStatus": "Connection Status",
+ "unknown": "Unknown",
+ "error": "Error: {{error}}",
+ "lastEventReceived": "Last Event Received",
+ "type": "Type:",
+ "received": "Received: {{date}}",
+ "notAvailable": "N/A",
+ "timeExpired": "Expired"
+ },
+ "storage": {
+ "title": "Storage Settings",
+ "serverUsageTitle": "Server Storage Usage",
+ "plusFeature": "Plus Feature",
+ "serverUsageDescription": "This is the storage used by your account on our servers, such as files, images, and uploaded data.",
+ "basicPlanUnavailable": "Server storage is not available in the Basic plan. Upgrade to Plus to track your server storage usage.",
+ "loading": "Loading...",
+ "usedOfTotal": "{{used}} MB used / {{total}} MB total ({{percent}}%)",
+ "experimentalTitle": "Experimental Features",
+ "comingSoon": "Coming Soon",
+ "offlineModeTitle": "Enable Offline Mode",
+ "offlineModeDescription": "Allows the app to work offline by caching data locally. This is experimental and may cause slowness. If you experience performance issues, turn this off.",
+ "offlineModeWarning": "Offline mode is enabled. If you experience slowness, disable this setting.",
+ "localStorageTitle": "{{platform}} Local Storage & Cache",
+ "appPlatform": "App",
+ "browserPlatform": "Browser",
+ "localStorageDescription": "This is data stored locally for faster access and offline use. Clearing it will not affect your server data, but may log you out or remove offline tasks.",
+ "clearAllTitle": "Clear All Local Storage",
+ "clearAllMessage": "Are you sure you want to clear your local storage and cache? This will remove all your data from this browser and require login.",
+ "clearAllAction": "Clear All",
+ "clearAllButton": "Clear All Local Storage and Cache",
+ "clearOfflineTitle": "Clear Offline Cache",
+ "clearOfflineMessage": "Are you sure you want to clear only the offline cache and tasks?",
+ "clearOfflineAction": "Clear Cache",
+ "clearOfflineButton": "Clear Offline Cache and Offline Tasks",
+ "appPreferencesTitle": "App Preferences",
+ "deviceOnly": "Device Only",
+ "appPreferencesDescription": "These preferences are stored locally on your device. Clearing them will reset app-specific settings and may log you out, but will not affect your server data.",
+ "clearPreferencesTitle": "Clear App Preferences",
+ "clearPreferencesMessage": "Are you sure you want to clear all app preferences? This will reset your app settings and may require you to log in again.",
+ "clearPreferencesAction": "Clear Preferences",
+ "clearPreferencesButton": "Clear App Preferences"
+ },
+ "circlePage": {
+ "description": "Your account is automatically connected to a Circle when you create or join one. Invite friends with the Circle code or join link below. You'll see join requests here as well.",
+ "partOf": "You are part of {{name}}",
+ "codeIs": "Your circle code is:",
+ "copyCode": "Copy Code",
+ "copyLink": "Copy Link",
+ "leaveCircle": "Leave Circle",
+ "members": "Circle Members",
+ "requests": "Circle Member Requests",
+ "refresh": "Refresh",
+ "refreshing": "Refreshing...",
+ "lastUpdated": "Last updated: {{date}}",
+ "wantsToJoin": "{{name}} wants to join your circle.",
+ "accept": "Accept",
+ "joinPrompt": "Want to join someone else's Circle? Ask them for their unique Circle code or join link. Enter the code below to join their Circle.",
+ "enterCode": "Enter Circle code:",
+ "joinCircle": "Join Circle",
+ "copyCodeSuccess": "Code copied to clipboard",
+ "copyLinkSuccess": "Link copied to clipboard",
+ "leaveConfirm": "Are you sure you want to leave your circle?",
+ "leaveTitle": "Leave Circle",
+ "leaveSuccess": "Left circle successfully",
+ "leaveFailed": "Failed to leave circle",
+ "roleUpdateFailed": "Failed to update role",
+ "removeMemberTitle": "Remove Member",
+ "removeMemberConfirm": "Are you sure you want to remove {{name}} from your circle?",
+ "removeMemberSuccess": "Removed member successfully",
+ "acceptTitle": "Accept Member Request",
+ "acceptConfirm": "Are you sure you want to accept {{name}} (username: {{username}}) into your circle?",
+ "acceptSuccess": "Accepted request successfully",
+ "joinSuccess": "Joined circle successfully, wait for the circle owner to accept your request.",
+ "alreadyMember": "You are already a member of this circle",
+ "joinFailed": "Failed to join circle",
+ "pendingApproval": "Pending Approval",
+ "joinedOn": "Joined on {{date}}",
+ "requestedOn": "Requested to join {{date}}",
+ "roleDescriptions": {
+ "member": "Just a regular member of the circle",
+ "manager": "Can impersonate users and perform actions on their behalf",
+ "admin": "Full access to the circle"
+ },
+ "newOwner": "New Owner"
+ },
+ "modals": {
+ "passwordChange": {
+ "title": "Change Password",
+ "intro": "Please enter your new password.",
+ "newPassword": "New Password",
+ "confirmPassword": "Confirm Password",
+ "mismatch": "Passwords do not match",
+ "min": "Password must be at least 8 characters",
+ "max": "Password must be less than 64 characters"
+ },
+ "userDeletion": {
+ "title": "Delete Account",
+ "warningTitle": "Delete Account",
+ "warningIntro": "This action cannot be undone. Deleting your account will permanently remove:",
+ "items": {
+ "profile": "Your user profile and authentication data",
+ "chores": "All your chores, chore history, and time tracking sessions",
+ "tokens": "API tokens, MFA sessions, and password reset tokens",
+ "storage": "Storage files and usage data",
+ "points": "Points history and notifications",
+ "circles": "Circle memberships and relationships"
+ },
+ "passwordPrompt": "Enter your password to continue",
+ "transferTitle": "Circle Ownership Transfer Required",
+ "transferIntro": "You own circles that require ownership transfer before deletion. Please select new owners:",
+ "circleLabel": "Circle: {{name}}",
+ "finalTitle": "Final Confirmation",
+ "finalPrompt": "Please enter your password and type DELETE to confirm account deletion.",
+ "finalHint": "On successful deletion, you will be logged out and redirected to the login page.",
+ "typeDelete": "Type \"DELETE\" to confirm",
+ "checkFailed": "Failed to check deletion requirements",
+ "confirmDelete": "Please enter your password and type DELETE to confirm",
+ "deleteFailed": "Failed to delete account"
+ }
+ },
+ "localization": {
+ "rtlNotice": "This language uses right-to-left (RTL) text direction",
+ "title": "Localization",
+ "description": "Customize language, date format, and regional preferences for your account.",
+ "language": "Language",
+ "languageDescription": "Select your preferred language",
+ "dateFormat": "Date Format",
+ "dateFormatDescription": "Choose how dates should be displayed throughout the application",
+ "timeFormat": "Time Format",
+ "timeFormatDescription": "Select 12-hour or 24-hour time format",
+ "12hour": "12-hour (AM/PM)",
+ "24hour": "24-hour",
+ "firstDayOfWeek": "First Day of Week",
+ "firstDayOfWeekDescription": "Select which day starts your week",
+ "sunday": "Sunday",
+ "monday": "Monday",
+ "saturday": "Saturday",
+ "formats": {
+ "mdy": "MM/DD/YYYY (US)",
+ "dmy": "DD/MM/YYYY (Europe)",
+ "ymd": "YYYY-MM-DD (ISO)",
+ "long": "Long format (e.g., January 1, 2024)",
+ "short": "Short format (e.g., Jan 1, 2024)"
+ }
+ },
+ "title": "Settings",
+ "circleSettings": {
+ "title": "Circle settings",
+ "description": "Your account is automatically connected to a Circle when you create or join one. Easily invite friends by sharing the unique Circle code or link below. You'll receive a notification below when someone requests to join your Circle. If you'd like to leave, simply hit the 'Leave Circle' button.",
+ "circleCode": "Circle Code",
+ "copyCode": "Copy Code",
+ "copyLink": "Copy Link",
+ "codeCopied": "Circle code copied!",
+ "linkCopied": "Circle link copied!",
+ "joinCircle": "Join a Circle",
+ "joinCirclePlaceholder": "Enter Circle Code",
+ "join": "Join",
+ "leave": "Leave Circle",
+ "leaveConfirmTitle": "Leave Circle",
+ "leaveConfirmMessage": "Are you sure you want to leave this circle?",
+ "circleMembers": "Circle Members",
+ "circleMemberRequests": "Circle Member Requests",
+ "admin": "Admin",
+ "member": "Member",
+ "pending": "Pending",
+ "accept": "Accept",
+ "reject": "Reject",
+ "makeAdmin": "Make Admin",
+ "makeMember": "Make Member",
+ "remove": "Remove",
+ "webhookURL": "Webhook URL",
+ "webhookDescription": "Enter a webhook URL to receive notifications for circle events",
+ "webhookPlaceholder": "https://your-webhook-url.com"
+ },
+ "accountSettings": {
+ "title": "Account Settings",
+ "subscription": "Subscription",
+ "subscriptionStatus": "Current Plan",
+ "free": "Free",
+ "plus": "Plus",
+ "upgrade": "Upgrade",
+ "cancel": "Cancel",
+ "changePassword": "Change Password",
+ "password": "Password",
+ "dangerZone": "Danger Zone",
+ "dangerZoneDescription": "Once you delete your account, there is no going back. Please be certain.",
+ "deleteAccount": "Delete Account"
+ },
+ "notifications": {
+ "settingsSaved": "Settings saved successfully",
+ "settingsSaveFailed": "Failed to save settings",
+ "invalidWebhook": "Invalid webhook URL"
}
}
diff --git a/public/locales/en/settingsExtras.json b/public/locales/en/settingsExtras.json
new file mode 100644
index 00000000..5d25b431
--- /dev/null
+++ b/public/locales/en/settingsExtras.json
@@ -0,0 +1,60 @@
+{
+ "notifications": {
+ "deviceTitle": "Device Notification",
+ "deviceDescription": "Manage your device notifications",
+ "deviceLabel": "Device Notification",
+ "deviceNativeHelper": "Receive notifications on your device when a task is due",
+ "mobileOnlyHelper": "This feature is only available on mobile devices",
+ "testNotification": "Test notification",
+ "dueDateNotification": "Due date notification",
+ "dueDateNotificationHelper": "Notification when the task is due",
+ "preDueNotification": "Pre-due notification",
+ "preDueNotificationHelper": "Notification a few hours before the task is due",
+ "overdueNotification": "Overdue notification",
+ "overdueNotificationHelper": "Notification when the task is overdue",
+ "pushNotifications": "Push notifications",
+ "pushNotificationsHelper": "Receive reminders, announcements, and task assignments via push notifications",
+ "registeredDevices": "Registered devices ({{count}}/5)",
+ "registeredDevicesDescription": "Devices registered to receive push notifications for your account",
+ "currentDeviceUnregistered": "This device is not registered for push notifications",
+ "limitReached": "Limit reached",
+ "noRegisteredDevices": "No devices registered for push notifications",
+ "customTitle": "Custom Notification",
+ "customDescription": "Notifications through other platforms such as Telegram or Pushover",
+ "customLabel": "Custom Notification",
+ "customHelper": "Receive notifications on another platform",
+ "telegramSetup": "You need to send a message to the bot before Telegram notifications will work.",
+ "clickHere": "Click here",
+ "startChat": "to start a chat",
+ "chatIdHelp": "If you do not know your chat ID, start a chat with userinfobot and it will send you your chat ID.",
+ "chatIdPlaceholder": "User ID / Chat ID",
+ "userInfoBot": "to start a chat with userinfobot",
+ "removeDeviceFailed": "Failed to unregister device",
+ "targetUpdated": "Notification target updated",
+ "updateFailed": "Failed to update notification target: {{status}}",
+ "registrationInitiated": "Registration initiated",
+ "registrationInitiatedMessage": "Push notification registration has been started. The device will register automatically.",
+ "registrationFailed": "Registration failed",
+ "registrationFailedMessage": "Failed to automatically register device. Please try again.",
+ "deviceRegisteredMessage": "Device registered successfully for push notifications.",
+ "deviceLimitReached": "Device limit reached",
+ "deviceLimitReachedMessage": "You have reached the maximum limit of 5 registered devices. Remove one before registering this device.",
+ "permissionRequired": "Permission required",
+ "permissionRequiredMessage": "Push notification permission is required to register this device.",
+ "notificationPermissionDenied": "Notification permission denied",
+ "notificationPermissionDeniedMessage": "You denied notification permissions. You can enable them later in your device settings.",
+ "pushPermissionDenied": "Push notification permission denied",
+ "pushPermissionDeniedMessage": "Push notifications were disabled. You can enable them in your device settings if needed.",
+ "testTitle": "Test notification",
+ "testBody": "You have a task due soon",
+ "createdAt": "Created at",
+ "on": "On",
+ "off": "Off",
+ "chatIdRequired": "Chat ID is required",
+ "invalidChatId": "Invalid chat ID",
+ "userKeyRequired": "User key is required",
+ "userKeyPlaceholder": "User ID",
+ "none": "None",
+ "webhooks": "Webhooks"
+ }
+}
diff --git a/public/locales/en/things.json b/public/locales/en/things.json
new file mode 100644
index 00000000..8ba553fa
--- /dev/null
+++ b/public/locales/en/things.json
@@ -0,0 +1,94 @@
+{
+ "page": {
+ "title": "Things",
+ "description": "Things are custom fields that can be attached to tasks to capture additional information. They can be text, number, or boolean values.",
+ "emptyTitle": "No things have been created or found",
+ "savedTitle": "Saved",
+ "savedMessage": "Thing saved successfully",
+ "saveFailedTitle": "Unable to save thing",
+ "saveQueued": "You are offline and the request has been queued.",
+ "saveFailed": "An error occurred while saving the thing.",
+ "deleteTitle": "Delete Thing",
+ "deleteConfirm": "Are you sure you want to delete this thing?",
+ "deleteFailedAssociated": "Unable to delete a thing with associated tasks.",
+ "deleteFailedTitle": "Unable to delete thing",
+ "deleteQueued": "You are offline and the request has been queued.",
+ "deleteFailed": "An error occurred while deleting the thing.",
+ "updatedTitle": "Updated",
+ "updatedMessage": "Thing state updated successfully",
+ "updateFailedTitle": "Unable to update thing state",
+ "updateQueued": "You are offline and the request has been queued.",
+ "updateFailed": "An error occurred while updating the thing state.",
+ "swipeEdit": "Edit",
+ "swipeToggle": "Toggle",
+ "swipeDelete": "Delete",
+ "types": {
+ "text": "Text",
+ "number": "Number",
+ "boolean": "Boolean"
+ },
+ "states": {
+ "true": "True",
+ "false": "False"
+ }
+ },
+ "modal": {
+ "createTitle": "Create Thing",
+ "editTitle": "Edit Thing",
+ "namePlaceholder": "Thing name",
+ "valuePlaceholder": "Thing value",
+ "type": "Type",
+ "nameRequired": "Name is required",
+ "stateMustBeNumber": "State must be a number",
+ "stateMustBeBoolean": "State must be true or false",
+ "stateRequired": "State is required",
+ "updateStateTitle": "Update state"
+ },
+ "history": {
+ "overviewTitle": "Things Overview",
+ "empty": {
+ "title": "No history found",
+ "description": "It looks like there is no history for this thing yet.",
+ "backToThings": "Go back to things"
+ },
+ "analytics": {
+ "updateFrequency": "Update Frequency",
+ "lastUpdated": "Last Updated",
+ "lastValue": "Last Value",
+ "updateTrend": "Update Trend",
+ "every": "Every {{value}}",
+ "minutes_one": "{{count}} minute",
+ "minutes_other": "{{count}} minutes",
+ "hours_one": "{{count}} hour",
+ "hours_other": "{{count}} hours",
+ "days_one": "{{count}} day",
+ "days_other": "{{count}} days",
+ "intervalIncreasing": "Interval increasing",
+ "intervalDecreasing": "Interval decreasing",
+ "intervalStable": "Interval stable"
+ }
+ },
+ "trigger": {
+ "validationMissing": "Please select a thing and trigger state",
+ "booleanNoCondition": "Boolean type does not require a condition",
+ "triggerStateNumber": "Trigger state must be a number",
+ "title": "Trigger a task when a thing state changes to a desired state",
+ "noThings": "It looks like you don't have any things yet. Create one to trigger a task when its state changes.",
+ "goToThings": "Go to Things",
+ "createThingSuffix": "to create a thing",
+ "selectThing": "Select a thing",
+ "conditionHelp": "Create a condition to trigger a task when the thing state changes to the desired state",
+ "dueWhenChanged": "When the state of {{name}} changes as specified below, the task will become due.",
+ "true": "True",
+ "false": "False",
+ "equal": "Equal",
+ "notEqual": "Not equal",
+ "greaterThan": "Greater than",
+ "greaterThanOrEqual": "Greater than or equal",
+ "lessThan": "Less than",
+ "lessThanOrEqual": "Less than or equal",
+ "textTriggerLabel": "Enter the text to trigger the task",
+ "type": "type",
+ "state": "state"
+ }
+}
diff --git a/public/locales/en/timer.json b/public/locales/en/timer.json
new file mode 100644
index 00000000..de2fa36f
--- /dev/null
+++ b/public/locales/en/timer.json
@@ -0,0 +1,63 @@
+{
+ "details": {
+ "title": "Timer Details",
+ "updatedTitle": "Session updated",
+ "updatedMessage": "Timer session has been updated successfully.",
+ "updateFailedTitle": "Failed to update session",
+ "errorUpdatingTitle": "Error updating session",
+ "startTitle": "Timer Started",
+ "startMessage": "Work session has been started successfully.",
+ "startFailedTitle": "Failed to start timer",
+ "pauseTitle": "Timer Paused",
+ "pauseMessage": "Work session has been paused.",
+ "pauseFailedTitle": "Failed to pause timer",
+ "tryAgain": "Please try again.",
+ "deleteSessionTitle": "Delete Session",
+ "deleteSessionMessage": "Session #{{index}} deletion would be implemented here",
+ "loading": "Loading timer data...",
+ "notFound": "No timer data found for this chore.",
+ "activeWork": "Active Work",
+ "breakTime": "Break Time",
+ "sessions": "Sessions",
+ "totalTime": "Total Time",
+ "workVsBreak": "Work vs Break Distribution",
+ "activePercent": "{{percent}}% active",
+ "noActiveTime": "No active time yet",
+ "activityTimeline": "Activity Timeline",
+ "sessionTooltip": "Session {{index}}: {{duration}} {{status}}",
+ "ongoing": "(ongoing)",
+ "liveSession": "Live Session",
+ "started": "Started: {{time}}",
+ "ended": "Ended: {{time}}",
+ "now": "Now: {{time}}",
+ "activeShort": "Active: {{percent}}",
+ "noTimeline": "No activity timeline available. Start working to see your activity pattern.",
+ "sessionBreakdown": "Session Breakdown",
+ "saveChanges": "Save Changes",
+ "workSessions": "Work Sessions ({{count}})",
+ "noSessions": "No work sessions found for this timer.",
+ "live": "Live",
+ "unknownUser": "Unknown",
+ "sessionLabel": "Session #{{index}} • {{date}}",
+ "ongoingArrow": "→ ongoing",
+ "editSessions": "Sessions",
+ "addSession": "Add Session",
+ "startTime": "Start Time",
+ "endTime": "End Time",
+ "leaveEmptyOngoing": "Leave empty if session is ongoing",
+ "durationAuto": "Duration (Auto-calculated)",
+ "pauseButton": "Pause Timer",
+ "resumeButton": "Resume",
+ "startButton": "Start Timer"
+ },
+ "edit": {
+ "updatedTitle": "Session updated",
+ "updatedMessage": "Timer session has been updated successfully.",
+ "updateFailedTitle": "Failed to update session",
+ "tryAgain": "Please try again.",
+ "deletedTitle": "Session deleted",
+ "deletedMessage": "Timer session has been deleted successfully.",
+ "deleteTitle": "Delete Timer Session",
+ "deleteMessage": "Are you sure you want to delete this timer session?"
+ }
+}
diff --git a/public/locales/en/user.json b/public/locales/en/user.json
new file mode 100644
index 00000000..dd525925
--- /dev/null
+++ b/public/locales/en/user.json
@@ -0,0 +1,73 @@
+{
+ "activitiesTimeline": "Activities Timeline",
+ "pointsWithCount": "{{count}} points",
+ "activities": {
+ "title": "User Activities",
+ "subtitle": "Overview of user activities and task statistics",
+ "filterTitle": "Filter Activities",
+ "showFor": "Show activities for:",
+ "allUsers": "All Users",
+ "timePeriod": "Time period:",
+ "days7": "7 Days",
+ "days30": "30 Days",
+ "days90": "90 Days",
+ "allTime": "All Time",
+ "showingFor": "Showing activities for {{user}} over the {{period}}",
+ "noActivitiesTitle": "No activities found",
+ "noActivitiesDescription": "No activities found for {{user}} in the {{period}}.",
+ "noActivitiesHelp": "Try selecting a different time period or user filter above.",
+ "backToChores": "Go back to chores",
+ "unknownUser": "Unknown User",
+ "noLabels": "No Labels",
+ "unassigned": "Unassigned",
+ "unknownTask": "Unknown Task",
+ "byAssignee": "by Assignee",
+ "tasksGroupedByAssignee": "Tasks grouped by assignee",
+ "charts": {
+ "statusTitle": "Status",
+ "statusDescription": "Completed tasks status",
+ "dueDateTitle": "Due Date",
+ "dueDateDescription": "Current tasks due date",
+ "priorityTitle": "Priority",
+ "priorityDescription": "Tasks by priority",
+ "labelsTitle": "Labels",
+ "labelsDescription": "Tasks by labels",
+ "labelsTimeTitle": "Labels (time)",
+ "labelsTimeDescription": "Time spent by labels (hours)",
+ "tasksTimeTitle": "Tasks (time)",
+ "tasksTimeDescription": "Time spent by individual tasks (hours)"
+ }
+ },
+ "points": {
+ "pointsLeaderboard": "Points Leaderboard",
+ "filterAndAnalysis": "Filter & Analysis",
+ "tasksLeaderboard": "Tasks Leaderboard",
+ "rankingsPoints": "Rankings based on points earned during the selected time period",
+ "rankingsTasks": "Rankings based on tasks completed during the selected time period",
+ "modePoints": "Points",
+ "modeTasks": "Tasks",
+ "filterTitle": "Filter Points",
+ "showFor": "Show points for:",
+ "timePeriod": "Time period:",
+ "days7": "7 Days",
+ "months6": "6 Months",
+ "allTime": "All Time",
+ "redeemPoints": "Redeem Points",
+ "you": "You",
+ "tasksAvg": "{{tasks}} tasks • {{avg}} avg per task",
+ "available": "{{count}} available",
+ "pointsLabel": "{{count}} points",
+ "cardAvailable": "Available",
+ "cardAvailableSubtext": "Ready to redeem",
+ "cardRedeemed": "Redeemed",
+ "cardRedeemedSubtext": "Previously used",
+ "cardTotal": "Total",
+ "cardTotalSubtext": "All time earned",
+ "cardPeriod": "Period Points",
+ "lastDays": "Last {{count}} Days",
+ "lastMonths6": "Last 6 Months",
+ "pointsTrend": "Points Trend",
+ "showingFor": "Showing points for {{user}} over the {{period}}",
+ "unknownUser": "Unknown User"
+ }
+}
diff --git a/src/components/RealTimeSettings.jsx b/src/components/RealTimeSettings.jsx
index e7b76229..aeed74b0 100644
--- a/src/components/RealTimeSettings.jsx
+++ b/src/components/RealTimeSettings.jsx
@@ -1,6 +1,7 @@
import { Sync, SyncDisabled } from '@mui/icons-material'
import { Box, Card, Chip, FormHelperText, Switch, Typography } from '@mui/joy'
import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useSSEContext } from '../hooks/useSSEContext'
import { useUserProfile } from '../queries/UserQueries'
import { isPlusAccount } from '../utils/Helpers'
@@ -12,6 +13,7 @@ const REALTIME_TYPES = {
}
const RealTimeSettings = () => {
+ const { t } = useTranslation(['settings', 'common'])
const { data: userProfile } = useUserProfile()
// SSE context
@@ -63,26 +65,26 @@ const RealTimeSettings = () => {
const getStatusDescription = () => {
if (!isPlusAccount(userProfile)) {
- return 'Real-time updates are not available in the Basic plan. Upgrade to Plus to receive instant notifications when tasks are updated.'
+ return t('settings:advanced.realtimeUnavailable')
}
if (realtimeType === REALTIME_TYPES.DISABLED) {
- return 'Real-time updates are disabled. Enable them to see live changes when you or other circle members complete, skip, or modify tasks.'
+ return t('settings:advanced.realtimeDisabled')
}
if (context.isConnected) {
- return "Real-time updates are working. You'll see live changes when you or other circle members complete, skip, or modify tasks."
+ return t('settings:advanced.realtimeConnected')
}
if (context.isConnecting) {
- return 'Connecting to real-time updates...'
+ return t('settings:advanced.realtimeConnecting')
}
if (context.error) {
- return `Real-time updates are enabled but not working: ${context.error}`
+ return t('settings:advanced.realtimeError', { error: context.error })
}
- return 'Real-time updates are enabled but not currently connected.'
+ return t('settings:advanced.realtimeEnabledNotConnected')
}
const getConnectionStatusComponent = () => {
@@ -109,7 +111,7 @@ const RealTimeSettings = () => {
realtimeType !== REALTIME_TYPES.DISABLED ? 'success' : 'neutral'
}
disabled={!isPlusAccount(userProfile)}
- inputProps={{ 'aria-label': 'Enable Real-time Updates' }}
+ inputProps={{ 'aria-label': t('settings:advanced.enableRealtime') }}
/>
{
}}
>
- Real-time Updates
+ {t('settings:advanced.realtimeTitle')}
{!isPlusAccount(userProfile) && (
- Plus Feature
+ {t('common:labels.plusFeature')}
)}
@@ -137,7 +139,7 @@ const RealTimeSettings = () => {
)}
- Get instant notifications when tasks are updated
+ {t('settings:advanced.realtimeSummary')}
@@ -167,7 +169,7 @@ const RealTimeSettings = () => {
isPlusAccount(userProfile) && (
- Status:
+ {t('settings:advanced.status')}
{getConnectionStatusComponent()}
{context.error && (
@@ -180,9 +182,7 @@ const RealTimeSettings = () => {
{!isPlusAccount(userProfile) && (
- Real-time updates are not available in the Basic plan. Upgrade to Plus
- to receive instant notifications when you or other circle members
- complete, skip, or modify tasks.
+ {t('settings:advanced.realtimeUnavailable')}
)}
diff --git a/src/components/SSEConnectionStatus.jsx b/src/components/SSEConnectionStatus.jsx
index 718d355d..ae39a506 100644
--- a/src/components/SSEConnectionStatus.jsx
+++ b/src/components/SSEConnectionStatus.jsx
@@ -1,5 +1,6 @@
import { Circle, SignalWifi4Bar, SignalWifiOff } from '@mui/icons-material'
import { Box, Chip, Tooltip, Typography } from '@mui/joy'
+import { useTranslation } from 'react-i18next'
import { useSSEContext } from '../hooks/useSSEContext'
const SSEConnectionStatus = ({
@@ -7,6 +8,7 @@ const SSEConnectionStatus = ({
showError = false,
sx = {},
}) => {
+ const { t } = useTranslation('settings')
const { isConnected, isConnecting, error, getConnectionStatus } =
useSSEContext()
@@ -23,18 +25,25 @@ const SSEConnectionStatus = ({
}
const getStatusText = () => {
- if (isConnected) return 'Connected'
- if (isConnecting) return 'Connecting...'
- return 'Disconnected'
+ if (isConnected) return t('advanced.connection.connected')
+ if (isConnecting) return t('advanced.connection.connecting')
+ return t('advanced.connection.disconnected')
}
const getTooltipText = () => {
const status = getConnectionStatus()
- if (error) return `Real-time updates (SSE): ${status} - ${error}`
+ if (error) {
+ return t('advanced.connection.tooltipError', {
+ status,
+ error,
+ })
+ }
if (!isConnected && !isConnecting) {
- return `Real-time updates (SSE): ${status} - Join a circle to enable real-time updates`
+ return t('advanced.connection.tooltipJoinCircle', {
+ status,
+ })
}
- return `Real-time updates (SSE): ${status}`
+ return t('advanced.connection.tooltipBase', { status })
}
if (variant === 'minimal') {
diff --git a/src/components/SSESettings.jsx b/src/components/SSESettings.jsx
index d9623ad5..534fe42f 100644
--- a/src/components/SSESettings.jsx
+++ b/src/components/SSESettings.jsx
@@ -9,12 +9,14 @@ import {
Switch,
Typography,
} from '@mui/joy'
+import { useTranslation } from 'react-i18next'
import { useSSEContext } from '../hooks/useSSEContext'
import { useUserProfile } from '../queries/UserQueries'
import { isPlusAccount } from '../utils/Helpers'
import SSEConnectionStatus from './SSEConnectionStatus'
const SSESettings = () => {
+ const { t } = useTranslation(['settings', 'common'])
const { data: userProfile } = useUserProfile()
const {
isConnected,
@@ -43,26 +45,26 @@ const SSESettings = () => {
const getStatusDescription = () => {
if (!isPlusAccount(userProfile)) {
- return 'Real-time updates (SSE) are not available in the Basic plan. Upgrade to Plus to receive instant notifications when chores are updated.'
+ return t('settings:advanced.sseUnavailable')
}
if (!isSSEEnabled()) {
- return 'Real-time updates (SSE) are disabled. Enable to see live changes when you or other circle members complete, skip, or modify chores.'
+ return t('settings:advanced.sseDisabled')
}
if (isConnected) {
- return "Real-time updates (SSE) are working. You'll see live changes when you or other circle members complete, skip, or modify chores."
+ return t('settings:advanced.sseConnected')
}
if (isConnecting) {
- return 'Connecting to real-time updates (SSE)...'
+ return t('settings:advanced.sseConnecting')
}
if (error) {
- return `Real-time updates (SSE) are enabled but not working: ${error}`
+ return t('settings:advanced.sseError', { error })
}
- return 'Real-time updates (SSE) are enabled but not currently connected.'
+ return t('settings:advanced.sseEnabledNotConnected')
}
return (
@@ -75,15 +77,15 @@ const SSESettings = () => {
)}
- Real-time Updates (SSE)
+ {t('settings:advanced.sseTitle')}
{!isPlusAccount(userProfile) && (
- Plus Feature
+ {t('common:labels.plusFeature')}
)}
- Get instant notifications via Server-Sent Events
+ {t('settings:advanced.sseSummary')}
{isSSEEnabled() && isPlusAccount(userProfile) && (
@@ -93,7 +95,7 @@ const SSESettings = () => {
- Enable Real-time Updates (SSE)
+ {t('settings:advanced.enableSse')}
{getStatusDescription()}
@@ -107,7 +109,9 @@ const SSESettings = () => {
}
variant='solid'
endDecorator={
- isSSEEnabled() && isPlusAccount(userProfile) ? 'On' : 'Off'
+ isSSEEnabled() && isPlusAccount(userProfile)
+ ? t('settings:advanced.on')
+ : t('settings:advanced.off')
}
slotProps={{ endDecorator: { sx: { minWidth: 24 } } }}
/>
@@ -116,7 +120,7 @@ const SSESettings = () => {
{isSSEEnabled() && isPlusAccount(userProfile) && (
- Status:
+ {t('settings:advanced.status')}
{
{!isPlusAccount(userProfile) && (
- Real-time updates (SSE) are not available in the Basic plan. Upgrade
- to Plus to receive instant notifications when you or other circle
- members complete, skip, or modify chores.
+ {t('settings:advanced.sseUnavailable')}
)}
diff --git a/src/components/SubscriptionModal.jsx b/src/components/SubscriptionModal.jsx
index 43bb2d3f..4ca3ea34 100644
--- a/src/components/SubscriptionModal.jsx
+++ b/src/components/SubscriptionModal.jsx
@@ -11,10 +11,12 @@ import {
Typography,
} from '@mui/joy'
import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNotification } from '../service/NotificationProvider'
import { GetSubscriptionSession } from '../utils/Fetcher'
const SubscriptionModal = ({ open, onClose }) => {
+ const { t } = useTranslation(['settings', 'common'])
const [selectedPlan, setSelectedPlan] = useState('yearly')
const [isLoading, setIsLoading] = useState(false)
const { showError } = useNotification()
@@ -36,12 +38,12 @@ const SubscriptionModal = ({ open, onClose }) => {
}
const features = [
- 'Task notifications and reminders',
- 'Rich text descriptions with images uploads',
- 'Thing-based task triggers',
- 'API tokens for integrations',
- 'Image uploads in descriptions',
- 'Advanced task automation',
+ t('settings:subscription.features.notifications'),
+ t('settings:subscription.features.richText'),
+ t('settings:subscription.features.thingTriggers'),
+ t('settings:subscription.features.apiTokens'),
+ t('settings:subscription.features.imageUploads'),
+ t('settings:subscription.features.automation'),
// 'Unlimited task history',
// 'Unlimited things history',
]
@@ -67,8 +69,8 @@ const SubscriptionModal = ({ open, onClose }) => {
} catch (error) {
console.error('Subscription error:', error)
showError({
- title: 'Subscription Error',
- message: 'Failed to start subscription process. Please try again.',
+ title: t('settings:subscription.errorTitle'),
+ message: t('settings:subscription.errorMessage'),
})
} finally {
setIsLoading(false)
@@ -91,14 +93,14 @@ const SubscriptionModal = ({ open, onClose }) => {
{/* Header */}
- Upgrade to Plus
+ {t('settings:subscription.title')}
{/* Features List */}
- What's included:
+ {t('settings:subscription.included')}
{features.map((feature, index) => (
@@ -230,7 +232,7 @@ const SubscriptionModal = ({ open, onClose }) => {
size='lg'
sx={{ mb: 1 }}
>
- Subscribe
+ {t('settings:subscription.subscribe')}
{
disabled={isLoading}
fullWidth
>
- Cancel
+ {t('common:actions.cancel')}
@@ -248,7 +250,7 @@ const SubscriptionModal = ({ open, onClose }) => {
color='neutral'
sx={{ textAlign: 'center', mt: 3 }}
>
- Cancel anytime. No hidden fees. Secure payment powered by Stripe.
+ {t('settings:subscription.footer')}
diff --git a/src/components/UserProfileAvatar.jsx b/src/components/UserProfileAvatar.jsx
index 0cfd1653..3b5dbd10 100644
--- a/src/components/UserProfileAvatar.jsx
+++ b/src/components/UserProfileAvatar.jsx
@@ -27,6 +27,7 @@ import {
import { useMediaQuery } from '@mui/material'
import moment from 'moment'
import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { useImpersonateUser } from '../contexts/ImpersonateUserContext'
import useStickyState from '../hooks/useStickyState'
@@ -38,6 +39,7 @@ import SubscriptionModal from './SubscriptionModal'
const UserProfileAvatar = () => {
const navigate = useNavigate()
+ const { t } = useTranslation(['common', 'settings'])
const { mode, setMode } = useColorScheme()
const { data: userProfile } = useUserProfile()
const {
@@ -60,20 +62,22 @@ const UserProfileAvatar = () => {
const isPlusUser = isPlusAccount(userProfile)
const getSubscriptionStatus = () => {
- if (!userProfile) return 'Free'
+ if (!userProfile) return t('settings:account.free')
if (userProfile.subscription === 'active') {
- return 'Plus'
+ return t('settings:account.plus')
}
if (
userProfile.subscription === 'cancelled' &&
moment().isBefore(userProfile.expiration)
) {
- return 'Plus (expires soon)'
+ return `${t('settings:account.plus')} (${moment(
+ userProfile.expiration,
+ ).format('MMM DD, YYYY')})`
}
- return 'Free'
+ return t('settings:account.free')
}
const handleLogout = () => {
@@ -265,7 +269,7 @@ const UserProfileAvatar = () => {
fontWeight: 500,
}}
>
- Impersonating
+ {t('settings:menu.impersonating')}
)}
@@ -291,13 +295,15 @@ const UserProfileAvatar = () => {
- {isImpersonating ? 'Switch User' : 'Impersonate User'}
+ {isImpersonating
+ ? t('settings:menu.switchUser')
+ : t('settings:menu.impersonateUser')}
- Act as another user
+ {t('settings:menu.actAsAnotherUser')}
@@ -319,13 +325,13 @@ const UserProfileAvatar = () => {
- Stop Impersonating
+ {t('settings:menu.stopImpersonating')}
- Return to your account
+ {t('settings:menu.returnToYourAccount')}
@@ -349,13 +355,13 @@ const UserProfileAvatar = () => {
- Settings
+ {t('settings:menu.settings')}
- Account & preferences
+ {t('settings:menu.accountAndPreferences')}
@@ -374,13 +380,13 @@ const UserProfileAvatar = () => {
- Invite People
+ {t('settings:menu.invitePeople')}
- Add members to your circle
+ {t('settings:menu.addMembersToYourCircle')}
@@ -401,13 +407,13 @@ const UserProfileAvatar = () => {
- Side Panel Settings
+ {t('settings:menu.sidePanelSettings')}
- Customize layout & cards
+ {t('settings:menu.customizeLayoutAndCards')}
@@ -427,13 +433,15 @@ const UserProfileAvatar = () => {
- {isDarkMode ? 'Switch to Light' : 'Switch to Dark'}
+ {isDarkMode
+ ? t('settings:menu.switchToLight')
+ : t('settings:menu.switchToDark')}
- Toggle theme appearance
+ {t('settings:menu.toggleThemeAppearance')}
@@ -460,9 +468,11 @@ const UserProfileAvatar = () => {
fontWeight: 500,
}}
>
- Upgrade to Plus
+ {t('settings:account.upgrade')}
+
+
+ {t('settings:subscription.unlockFeatures')}
- Unlock premium features
)}
@@ -511,7 +521,7 @@ const UserProfileAvatar = () => {
level='body-sm'
sx={{ fontWeight: 500, color: 'var(--joy-palette-danger-500)' }}
>
- Logout
+ {t('common:actions.logout')}
diff --git a/src/contexts/LocalizationContext.jsx b/src/contexts/LocalizationContext.jsx
index 5e4dfb85..3f224c30 100644
--- a/src/contexts/LocalizationContext.jsx
+++ b/src/contexts/LocalizationContext.jsx
@@ -23,6 +23,7 @@ export const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur']
export const AVAILABLE_LANGUAGES = [
{ code: 'de', name: 'German', nativeName: 'Deutsch' },
{ code: 'en', name: 'English', nativeName: 'English' },
+ { code: 'de', name: 'German', nativeName: 'Deutsch' },
{ code: 'es', name: 'Spanish', nativeName: 'Español' },
{ code: 'fr', name: 'French', nativeName: 'Français' },
{ code: 'nl', name: 'Dutch', nativeName: 'Nederlands' },
diff --git a/src/i18n/config.js b/src/i18n/config.js
index 1b3589f8..35e42f29 100644
--- a/src/i18n/config.js
+++ b/src/i18n/config.js
@@ -19,7 +19,20 @@ i18n
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
- ns: ['common', 'settings', 'chores'],
+ ns: [
+ 'common',
+ 'settings',
+ 'settingsExtras',
+ 'chores',
+ 'auth',
+ 'things',
+ 'projects',
+ 'filters',
+ 'history',
+ 'user',
+ 'labelsView',
+ 'timer',
+ ],
defaultNS: 'common',
detection: {
@@ -29,7 +42,7 @@ i18n
},
react: {
- useSuspense: true,
+ useSuspense: false,
},
})
diff --git a/src/utils/ChoreCardHelpers.jsx b/src/utils/ChoreCardHelpers.jsx
index 01008f6d..3d81b958 100644
--- a/src/utils/ChoreCardHelpers.jsx
+++ b/src/utils/ChoreCardHelpers.jsx
@@ -1,4 +1,5 @@
import moment from 'moment'
+import i18n from '../i18n/config'
const allMonths = [
'January',
'February',
@@ -20,7 +21,9 @@ const allMonths = [
* @returns {string} The formatted due date text
*/
export const getDueDateChipText = (nextDueDate, chore, timeFormat = 'h:mm A') => {
- if (chore?.nextDueDate === null || nextDueDate === null) return 'No Due Date'
+ if (chore?.nextDueDate === null || nextDueDate === null) {
+ return i18n.t('chores:due.noDueDate')
+ }
const dueDate = moment(nextDueDate)
const diff = moment(nextDueDate).diff(moment(), 'hours')
@@ -107,19 +110,19 @@ export const getRecurrentChipText = chore => {
}
}
if (chore.frequencyType === 'once') {
- return 'Once'
+ return i18n.t('chores:frequency.once')
} else if (chore.frequencyType === 'trigger') {
- return 'Trigger'
+ return i18n.t('chores:frequency.trigger')
} else if (chore.frequencyType === 'daily') {
- return 'Daily'
+ return i18n.t('chores:frequency.daily')
} else if (chore.frequencyType === 'adaptive') {
- return 'Adaptive'
+ return i18n.t('chores:frequency.adaptive')
} else if (chore.frequencyType === 'weekly') {
- return 'Weekly'
+ return i18n.t('chores:frequency.weekly')
} else if (chore.frequencyType === 'monthly') {
- return 'Monthly'
+ return i18n.t('chores:frequency.monthly')
} else if (chore.frequencyType === 'yearly') {
- return 'Yearly'
+ return i18n.t('chores:frequency.yearly')
} else if (chore.frequencyType === 'days_of_the_week') {
let days = metadata.days
if (days.length > 4) {
diff --git a/src/utils/Chores.jsx b/src/utils/Chores.jsx
index cde9fe74..d6138a27 100644
--- a/src/utils/Chores.jsx
+++ b/src/utils/Chores.jsx
@@ -1,4 +1,5 @@
import moment from 'moment'
+import i18n from '../i18n/config'
import { TASK_COLOR } from './Colors.jsx'
const priorityOrder = [1, 2, 3, 4, 0]
@@ -25,6 +26,7 @@ export const ChoreStatus = Object.freeze({
PAUSED: 2,
PENDING_APPROVAL: 3,
})
+
export const ChoresGrouper = (groupBy, chores, filter) => {
if (filter) {
chores = chores.filter(chore => filter(chore))
@@ -85,63 +87,63 @@ export const ChoresGrouper = (groupBy, chores, filter) => {
groups = []
if (groupRaw['Started'].length > 0) {
groups.push({
- name: 'Started',
+ name: i18n.t('chores:groups.started'),
content: groupRaw['Started'],
color: TASK_COLOR.STARTED,
})
}
if (groupRaw['PendingApproval'].length > 0) {
groups.push({
- name: 'Pending Approval',
+ name: i18n.t('chores:groups.pendingApproval'),
content: groupRaw['PendingApproval'],
color: TASK_COLOR.LATE,
})
}
if (groupRaw['Overdue'].length > 0) {
groups.push({
- name: 'Overdue',
+ name: i18n.t('chores:groups.overdue'),
content: groupRaw['Overdue'],
color: TASK_COLOR.OVERDUE,
})
}
if (groupRaw['Today'].length > 0) {
groups.push({
- name: 'Today',
+ name: i18n.t('chores:groups.today'),
content: groupRaw['Today'],
color: TASK_COLOR.TODAY,
})
}
if (groupRaw['Tomorrow'].length > 0) {
groups.push({
- name: 'Tomorrow',
+ name: i18n.t('chores:groups.tomorrow'),
content: groupRaw['Tomorrow'],
color: TASK_COLOR.TOMORROW,
})
}
if (groupRaw['Next 7 Days'].length > 0) {
groups.push({
- name: 'Next 7 Days',
+ name: i18n.t('chores:groups.next7Days'),
content: groupRaw['Next 7 Days'],
color: TASK_COLOR.NEXT_7_DAYS,
})
}
if (groupRaw['Later This Month'].length > 0) {
groups.push({
- name: 'Later This Month',
+ name: i18n.t('chores:groups.laterThisMonth'),
content: groupRaw['Later This Month'],
color: TASK_COLOR.LATER_THIS_MONTH,
})
}
if (groupRaw['Future'].length > 0) {
groups.push({
- name: 'Future',
+ name: i18n.t('chores:groups.future'),
content: groupRaw['Future'],
color: TASK_COLOR.FUTURE,
})
}
if (groupRaw['Anytime'].length > 0) {
groups.push({
- name: 'Anytime',
+ name: i18n.t('chores:groups.anytime'),
content: groupRaw['Anytime'],
color: TASK_COLOR.ANYTIME,
})
@@ -191,33 +193,37 @@ export const ChoresGrouper = (groupBy, chores, filter) => {
})
groups = [
{
- name: 'Overdue',
+ name: i18n.t('chores:groups.overdue'),
content: groupRaw['Overdue'],
color: TASK_COLOR.OVERDUE,
},
- { name: 'Today', content: groupRaw['Today'], color: TASK_COLOR.TODAY },
{
- name: 'Tomorrow',
+ name: i18n.t('chores:groups.today'),
+ content: groupRaw['Today'],
+ color: TASK_COLOR.TODAY,
+ },
+ {
+ name: i18n.t('chores:groups.tomorrow'),
content: groupRaw['Tomorrow'],
color: TASK_COLOR.TOMORROW,
},
{
- name: 'Next 7 Days',
+ name: i18n.t('chores:groups.next7Days'),
content: groupRaw['Next 7 Days'],
color: TASK_COLOR.NEXT_7_DAYS,
},
{
- name: 'Later This Month',
+ name: i18n.t('chores:groups.laterThisMonth'),
content: groupRaw['Later This Month'],
color: TASK_COLOR.LATER_THIS_MONTH,
},
{
- name: 'Future',
+ name: i18n.t('chores:groups.future'),
content: groupRaw['Future'],
color: TASK_COLOR.FUTURE,
},
{
- name: 'Anytime',
+ name: i18n.t('chores:groups.anytime'),
content: groupRaw['Anytime'],
color: TASK_COLOR.ANYTIME,
},
diff --git a/src/views/Authorization/Authenticating.jsx b/src/views/Authorization/Authenticating.jsx
index 693fd4bb..e02ccce5 100644
--- a/src/views/Authorization/Authenticating.jsx
+++ b/src/views/Authorization/Authenticating.jsx
@@ -5,17 +5,19 @@ import Logo from '../../Logo'
import { Capacitor } from '@capacitor/core'
import Cookies from 'js-cookie'
import { useRef } from 'react'
+import { useTranslation } from 'react-i18next'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { useUserProfile } from '../../queries/UserQueries'
import { apiClient } from '../../utils/ApiClient'
import { GetUserProfile } from '../../utils/Fetcher'
const AuthenticationLoading = () => {
+ const { t } = useTranslation(['auth', 'common'])
const { data: userProfile, refetch: refetchUserProfile } = useUserProfile()
const Navigate = useNavigate()
const hasCalledHandleOAuth2 = useRef(false)
- const [message, setMessage] = useState('Authenticating')
- const [subMessage, setSubMessage] = useState('Please wait')
+ const [message, setMessage] = useState(t('auth:status.authenticating'))
+ const [subMessage, setSubMessage] = useState(t('auth:status.pleaseWait'))
const [status, setStatus] = useState('pending')
const { provider } = useParams()
useEffect(() => {
@@ -23,10 +25,10 @@ const AuthenticationLoading = () => {
hasCalledHandleOAuth2.current = true
handleOAuth2()
} else if (provider !== 'oauth2') {
- setMessage('Unknown Authentication Provider')
- setSubMessage('Please contact support')
+ setMessage(t('auth:status.unknownProvider'))
+ setSubMessage(t('auth:status.contactSupport'))
}
- }, [provider])
+ }, [provider, t])
const getUserProfileAndNavigateToHome = () => {
GetUserProfile().then(data => {
data.json().then(data => {
@@ -52,8 +54,8 @@ const AuthenticationLoading = () => {
const storedState = localStorage.getItem('authState')
if (returnedState !== storedState) {
- setMessage('Authentication failed')
- setSubMessage('State does not match')
+ setMessage(t('auth:status.failed'))
+ setSubMessage(t('auth:status.stateMismatch'))
setStatus('error')
return
}
@@ -90,8 +92,8 @@ const AuthenticationLoading = () => {
})
} else {
console.error('Authentication failed')
- setMessage('Authentication failed')
- setSubMessage('Please try again')
+ setMessage(t('auth:status.failed'))
+ setSubMessage(t('auth:status.tryAgain'))
setStatus('error')
}
})
@@ -135,7 +137,7 @@ const AuthenticationLoading = () => {
mt: 4,
}}
>
- Go back Login
+ {t('common:actions.backToLogin')}
)}
diff --git a/src/views/Authorization/ForgotPasswordView.jsx b/src/views/Authorization/ForgotPasswordView.jsx
index e2902d00..1976b175 100644
--- a/src/views/Authorization/ForgotPasswordView.jsx
+++ b/src/views/Authorization/ForgotPasswordView.jsx
@@ -10,12 +10,14 @@ import {
Typography,
} from '@mui/joy'
import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import Logo from '../../Logo'
import { useNotification } from '../../service/NotificationProvider'
import { ResetPassword } from '../../utils/Fetcher'
const ForgotPasswordView = () => {
+ const { t } = useTranslation(['auth', 'common'])
const navigate = useNavigate()
const [resetStatusOk, setResetStatusOk] = useState(null)
const [email, setEmail] = useState('')
@@ -28,12 +30,12 @@ const ForgotPasswordView = () => {
const handleSubmit = async () => {
if (!email) {
- return setEmailError('Email is required')
+ return setEmailError(t('auth:errors.emailRequired'))
}
// validate email:
if (validateEmail(email)) {
- setEmailError('Please enter a valid email address')
+ setEmailError(t('auth:errors.validEmail'))
return
}
@@ -48,21 +50,21 @@ const ForgotPasswordView = () => {
setResetStatusOk(true)
showNotification({
type: 'success',
- title: 'Reset Email Sent',
- message: 'Check your email for password reset instructions',
+ title: t('auth:errors.resetEmailSentTitle'),
+ message: t('auth:errors.resetEmailSentMessage'),
})
} else {
setResetStatusOk(false)
showError({
- title: 'Reset Failed',
- message: 'Failed to send reset email, please try again later',
+ title: t('auth:errors.resetFailedTitle'),
+ message: t('auth:errors.resetFailedMessage'),
})
}
} catch (error) {
setResetStatusOk(false)
showError({
- title: 'Reset Failed',
- message: 'Failed to send reset email, please try again later',
+ title: t('auth:errors.resetFailedTitle'),
+ message: t('auth:errors.resetFailedMessage'),
})
}
}
@@ -70,7 +72,7 @@ const ForgotPasswordView = () => {
const handleEmailChange = e => {
setEmail(e.target.value)
if (validateEmail(e.target.value)) {
- setEmailError('Please enter a valid email address')
+ setEmailError(t('auth:errors.validEmail'))
} else {
setEmailError(null)
}
@@ -108,12 +110,11 @@ const ForgotPasswordView = () => {
{resetStatusOk === null && (
<>
- Enter your email, and we'll send you a link to get into your
- account.
+ {t('auth:forgotPasswordSubtitle')}
- Email Address
+ {t('common:labels.emailAddress')}
{
required
fullWidth
id='email'
- placeholder='Enter your email address'
+ placeholder={t('auth:placeholders.enterEmailAddress')}
type='email'
name='email'
autoComplete='email'
@@ -155,7 +156,7 @@ const ForgotPasswordView = () => {
}}
onClick={handleSubmit}
>
- Reset Password
+ {t('auth:actions.resetPassword')}
{
}}
color='neutral'
>
- Back to Login
+ {t('common:actions.backToLogin')}
>
)}
@@ -184,9 +185,7 @@ const ForgotPasswordView = () => {
level='body-md'
sx={{ textAlign: 'center', mt: 2, mb: 3 }}
>
- If there is an account associated with the email you entered,
- you will receive an email with instructions on how to reset your
- password.
+ {t('auth:forgotPasswordConfirmation')}
{
navigate('/login')
}}
>
- Go to Login
+ {t('auth:actions.goToLogin')}
>
)}
diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx
index 108ff14c..39800c7a 100644
--- a/src/views/Authorization/LoginView.jsx
+++ b/src/views/Authorization/LoginView.jsx
@@ -24,6 +24,7 @@ import {
import { useQueryClient } from '@tanstack/react-query'
import Cookies from 'js-cookie'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { LoginSocialGoogle } from 'reactjs-social-login'
import { GOOGLE_CLIENT_ID, REDIRECT_URL } from '../../Config'
@@ -38,6 +39,7 @@ import { buildChildUsername, getUserDisplayInfo } from '../../utils/UserHelpers'
import MFAVerificationModal from './MFAVerificationModal'
const LoginView = () => {
+ const { t } = useTranslation(['auth', 'common'])
// Use React Query client directly to invalidate the user profile query
const queryClient = useQueryClient()
// const [userProfile, setUserProfile] = useState(null)
@@ -105,23 +107,23 @@ const LoginView = () => {
if (loginType === 'sub') {
if (!parentUsername.trim()) {
showError({
- title: 'Validation Error',
- message: 'Primary username is required for sub account login',
+ title: t('auth:errors.validationTitle'),
+ message: t('auth:errors.primaryUsernameRequired'),
})
return
}
if (!childName.trim()) {
showError({
- title: 'Validation Error',
- message: 'Sub account name is required for sub account login',
+ title: t('auth:errors.validationTitle'),
+ message: t('auth:errors.subAccountNameRequired'),
})
return
}
} else {
if (!username.trim()) {
showError({
- title: 'Validation Error',
- message: 'Username is required',
+ title: t('auth:errors.validationTitle'),
+ message: t('auth:errors.usernameRequired'),
})
return
}
@@ -129,8 +131,8 @@ const LoginView = () => {
if (!password) {
showError({
- title: 'Validation Error',
- message: 'Password is required',
+ title: t('auth:errors.validationTitle'),
+ message: t('auth:errors.passwordRequired'),
})
return
}
@@ -162,8 +164,8 @@ const LoginView = () => {
}
} else {
showError({
- title: 'Login Failed',
- message: result.error || 'An error occurred, please try again',
+ title: t('auth:errors.loginFailedTitle'),
+ message: result.error || t('auth:errors.loginFailedMessage'),
})
}
}
@@ -233,15 +235,24 @@ const LoginView = () => {
} else {
const providerName = provider === 'apple' ? 'Apple' : 'Google'
showError({
- title: `${providerName} Login Failed`,
- message: `Couldn't log in with ${providerName}, please try again`,
+ title:
+ provider === 'apple'
+ ? t('auth:errors.appleLoginFailedTitle')
+ : t('auth:errors.googleLoginFailedTitle'),
+ message:
+ provider === 'apple'
+ ? t('auth:errors.appleLoginFailedMessage')
+ : t('auth:errors.googleLoginFailedMessage'),
})
}
} catch (error) {
const providerName = provider === 'apple' ? 'Apple' : 'Google'
showError({
- title: `${providerName} Login Error`,
- message: 'Network error occurred, please try again',
+ title:
+ provider === 'apple'
+ ? t('auth:errors.appleLoginFailedTitle')
+ : t('auth:errors.googleLoginFailedTitle'),
+ message: t('auth:errors.loginFailedMessage'),
})
}
}
@@ -285,7 +296,7 @@ const LoginView = () => {
const handleMFAError = errorMessage => {
showError({
- title: 'Two-Factor Authentication Failed',
+ title: t('auth:errors.twoFactorFailedTitle'),
message: errorMessage,
})
}
@@ -334,8 +345,8 @@ const LoginView = () => {
} catch (error) {
console.error('Failed to open OAuth browser:', error)
showError({
- title: 'OAuth Error',
- message: 'Failed to open authentication browser',
+ title: t('auth:errors.oauthErrorTitle'),
+ message: t('auth:errors.oauthBrowserError'),
})
}
} else {
@@ -409,8 +420,9 @@ const LoginView = () => {
sx={{ mt: 2, width: '96px', height: '96px', mb: 1 }}
/>
- Welcome back,{' '}
- {userProfile?.displayName || userProfile?.username}
+ {t('auth:welcomeBack', {
+ name: userProfile?.displayName || userProfile?.username,
+ })}
{getUserDisplayInfo(userProfile).userType === 'child' && (
{
color='neutral'
sx={{ ml: 1 }}
>
- (Sub Account)
+ {t('auth:subAccountBadge')}
)}
@@ -431,7 +443,9 @@ const LoginView = () => {
getUserProfileAndNavigateToHome()
}}
>
- Continue as {userProfile.displayName || userProfile.username}
+ {t('common:actions.continueAs', {
+ name: userProfile.displayName || userProfile.username,
+ })}
{
apiClient.handleLogout()
}}
>
- Logout
+ {t('common:actions.logout')}
>
)}
{!userProfile && (
<>
- Sign in to your account to continue
+ {t('auth:subtitle')}
{/* Login Type Tabs */}
@@ -485,7 +499,7 @@ const LoginView = () => {
fontWeight: 500,
}}
>
- Primary Account
+ {t('auth:tabs.primary')}
{
fontWeight: 500,
}}
>
- Sub Account
+ {t('auth:tabs.sub')}
- Username
+ {t('common:labels.username')}
{
- Primary Account Username
+ {t('auth:fields.primaryAccountUsername')}
{
fullWidth
id='parentUsername'
name='parentUsername'
- placeholder='Enter primary account username'
+ placeholder={t('auth:placeholders.primaryAccountUsername')}
autoFocus
value={parentUsername}
onChange={e => {
@@ -539,7 +553,7 @@ const LoginView = () => {
}}
/>
- Sub Account Username
+ {t('auth:fields.subAccountUsername')}
{
fullWidth
id='childName'
name='childName'
- placeholder='Enter sub account name'
+ placeholder={t('auth:placeholders.subAccountUsername')}
value={childName}
onChange={e => {
setChildName(e.target.value)
@@ -557,14 +571,14 @@ const LoginView = () => {
- Password:
+ {t('common:labels.password')}:
{
}}
onClick={handleSubmit}
>
- {loginType === 'sub' ? 'Sign In as Sub Account' : 'Sign In'}
+ {loginType === 'sub'
+ ? t('auth:actions.signInSubAccount')
+ : t('auth:actions.signIn')}
{
}}
onClick={handleForgotPassword}
>
- Forgot password?
+ {t('auth:actions.forgotPassword')}
>
)}
- or
+ {t('common:actions.or')}
{import.meta.env.VITE_IS_SELF_HOSTED !== 'true' && (
<>
{!Capacitor.isNativePlatform() && (
@@ -624,9 +640,8 @@ const LoginView = () => {
}}
onReject={() => {
showError({
- title: 'Google Login Failed',
- message:
- "Couldn't log in with Google, please try again",
+ title: t('auth:errors.googleLoginFailedTitle'),
+ message: t('auth:errors.googleLoginFailedMessage'),
})
}}
>
@@ -645,7 +660,7 @@ const LoginView = () => {
>
- Continue with Google
+ {t('auth:actions.continueWithGoogle')}
@@ -678,16 +693,15 @@ const LoginView = () => {
.catch(error => {
console.error('Apple login error:', error)
showError({
- title: 'Apple Login Failed',
- message:
- "Couldn't log in with Apple, please try again",
+ title: t('auth:errors.appleLoginFailedTitle'),
+ message: t('auth:errors.appleLoginFailedMessage'),
})
})
}}
>
- Continue with Apple
+ {t('auth:actions.continueWithApple')}
*/}
@@ -712,7 +726,7 @@ const LoginView = () => {
>
- Continue with Google
+ {t('auth:actions.continueWithGoogle')}
@@ -741,16 +755,15 @@ const LoginView = () => {
.catch(error => {
console.error('Apple login error:', error)
showError({
- title: 'Apple Login Failed',
- message:
- "Couldn't log in with Apple, please try again",
+ title: t('auth:errors.appleLoginFailedTitle'),
+ message: t('auth:errors.appleLoginFailedMessage'),
})
})
}}
>
- Continue with Apple
+ {t('auth:actions.continueWithApple')}
)}
@@ -767,7 +780,9 @@ const LoginView = () => {
sx={{ mt: 3, mb: 2 }}
onClick={handleAuthentikLogin}
>
- Continue with {resource?.identity_provider?.name}
+ {t('auth:actions.continueWithProvider', {
+ provider: resource?.identity_provider?.name,
+ })}
)}
@@ -781,7 +796,7 @@ const LoginView = () => {
size='lg'
// sx={{ mt: 3, mb: 2 }}
>
- Create new account
+ {t('auth:actions.createAccount')}
)}
@@ -795,7 +810,7 @@ const LoginView = () => {
window.open('https://donetick.com/privacy', '_blank')
}}
>
- Privacy Policy
+ {t('common:legal.privacyPolicy')}
{
window.open('https://donetick.com/terms', '_blank')
}}
>
- Terms of Use
+ {t('common:legal.termsOfUse')}
diff --git a/src/views/Authorization/Signup.jsx b/src/views/Authorization/Signup.jsx
index ceb7c3c5..09a6c923 100644
--- a/src/views/Authorization/Signup.jsx
+++ b/src/views/Authorization/Signup.jsx
@@ -11,12 +11,14 @@ import {
} from '@mui/joy'
import { useQueryClient } from '@tanstack/react-query'
import React from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import Logo from '../../Logo'
import { useNotification } from '../../service/NotificationProvider'
import { login, signUp } from '../../utils/Fetcher'
const SignupView = () => {
+ const { t } = useTranslation(['common', 'auth'])
const [username, setUsername] = React.useState('')
const [password, setPassword] = React.useState('')
const Navigate = useNavigate()
@@ -57,44 +59,42 @@ const SignupView = () => {
let isValid = true
if (!username.trim()) {
- setUsernameError('Username is required')
+ setUsernameError(t('auth:errors.usernameRequired'))
isValid = false
}
if (username.length < 4) {
- setUsernameError('Username must be at least 4 characters')
+ setUsernameError(t('auth:errors.usernameMin'))
isValid = false
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
- setEmailError('Invalid email address')
+ setEmailError(t('auth:errors.invalidEmail'))
isValid = false
}
if (password.length < 8) {
- setPasswordError('Password must be between 8 and 64 characters')
+ setPasswordError(t('auth:errors.passwordLength'))
isValid = false
}
if (password.length > 64) {
- setPasswordError('Password must be between 8 and 64 characters')
+ setPasswordError(t('auth:errors.passwordLength'))
isValid = false
}
if (!displayName.trim()) {
- setDisplayNameError('Display name is required')
+ setDisplayNameError(t('auth:errors.displayNameRequired'))
isValid = false
}
// display name should only contain letters and spaces and numbers:
if (!/^[a-zA-Z0-9 ]+$/.test(displayName)) {
- setDisplayNameError('Display name can only contain letters and numbers')
+ setDisplayNameError(t('auth:errors.displayNamePattern'))
isValid = false
}
// username should only contain lowercase letters, dot and dash:
if (!/^[a-z.-]+$/.test(username)) {
- setUsernameError(
- 'Username can only contain lowercase letters, dot and dash',
- )
+ setUsernameError(t('auth:errors.usernamePattern'))
isValid = false
}
@@ -110,15 +110,15 @@ const SignupView = () => {
handleLogin(username, password)
} else if (response.status === 403) {
showError({
- title: 'Signup Failed',
- message: 'Signup disabled, please contact admin',
+ title: t('auth:errors.signupFailedTitle'),
+ message: t('auth:errors.signupDisabled'),
})
} else {
console.log('Signup failed')
response.json().then(res => {
showError({
- title: 'Signup Failed',
- message: res.error || 'An error occurred during signup',
+ title: t('auth:errors.signupFailedTitle'),
+ message: res.error || t('auth:errors.signupGeneric'),
})
})
}
@@ -167,18 +167,18 @@ const SignupView = () => {
- Create an account to get started!
+ {t('auth:signupSubtitle')}
- Username
+ {t('common:labels.username')}
{
{/* Error message display */}
- Email
+ {t('common:labels.email')}
{
{emailError}
- Password:
+ {t('common:labels.password')}:
{
setPasswordError(null)
@@ -234,16 +234,16 @@ const SignupView = () => {
{passwordError}
- Display Name:
+ {t('common:labels.displayName')}:
{
setDisplayNameError(null)
@@ -257,7 +257,7 @@ const SignupView = () => {
level='body2'
sx={{ mt: 2, mb: 1, textAlign: 'center', color: 'text.secondary' }}
>
- By signing up, you agree to our Terms of Service and Privacy Policy
+ {t('auth:messages.agreement')}
{
sx={{ mt: 1, mb: 1 }}
onClick={handleSubmit}
>
- Sign Up
+ {t('common:actions.signup')}
- or
+ {t('common:actions.or')}
{
@@ -279,7 +279,7 @@ const SignupView = () => {
variant='soft'
// sx={{ mt: 3, mb: 2 }}
>
- Login
+ {t('common:actions.login')}
{
window.open('https://donetick.com/privacy-policy', '_blank')
}}
>
- Privacy Policy
+ {t('common:legal.privacyPolicy')}
{
window.open('https://donetick.com/terms', '_blank')
}}
>
- Terms of Use
+ {t('common:legal.termsOfUse')}
diff --git a/src/views/Authorization/UpdatePasswordView.jsx b/src/views/Authorization/UpdatePasswordView.jsx
index dbce3313..f75873a5 100644
--- a/src/views/Authorization/UpdatePasswordView.jsx
+++ b/src/views/Authorization/UpdatePasswordView.jsx
@@ -10,6 +10,7 @@ import {
Typography,
} from '@mui/joy'
import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate, useSearchParams } from 'react-router-dom'
import Logo from '../../Logo'
@@ -17,6 +18,7 @@ import { useNotification } from '../../service/NotificationProvider'
import { ChangePassword } from '../../utils/Fetcher'
const UpdatePasswordView = () => {
+ const { t } = useTranslation(['auth', 'common'])
const navigate = useNavigate()
const [password, setPassword] = useState('')
const [passwordConfirm, setPasswordConfirm] = useState('')
@@ -32,7 +34,7 @@ const UpdatePasswordView = () => {
const password = e.target.value
setPassword(password)
if (password.length < 8 || password.length > 64) {
- setPasswordError('Password must be between 8 and 64 characters')
+ setPasswordError(t('auth:errors.passwordLength'))
} else {
setPasswordError(null)
}
@@ -40,7 +42,7 @@ const UpdatePasswordView = () => {
const handlePasswordConfirmChange = e => {
setPasswordConfirm(e.target.value)
if (e.target.value !== password) {
- setPasswordConfirmationError('Passwords do not match')
+ setPasswordConfirmationError(t('settings:modals.passwordChange.mismatch'))
} else {
setPasswordConfirmationError(null)
}
@@ -56,9 +58,8 @@ const UpdatePasswordView = () => {
if (response.ok) {
showNotification({
type: 'success',
- title: 'Password Updated',
- message:
- 'Your password has been updated successfully. Redirecting to login...',
+ title: t('settings:account.passwordChanged'),
+ message: t('auth:status.passwordUpdatedRedirect'),
})
// wait 3 seconds and then redirect to login:
setTimeout(() => {
@@ -66,14 +67,14 @@ const UpdatePasswordView = () => {
}, 3000)
} else {
showError({
- title: 'Password Update Failed',
- message: 'Failed to update password, please try again later',
+ title: t('auth:status.passwordUpdateFailed'),
+ message: t('auth:status.passwordUpdateFailedMessage'),
})
}
} catch (error) {
showError({
- title: 'Password Update Failed',
- message: 'Failed to update password, please try again later',
+ title: t('auth:status.passwordUpdateFailed'),
+ message: t('auth:status.passwordUpdateFailedMessage'),
})
}
}
@@ -119,13 +120,13 @@ const UpdatePasswordView = () => {
- Please enter your new password below
+ {t('auth:status.enterNewPassword')}
{
{
}}
onClick={handleSubmit}
>
- Save Password
+ {t('auth:status.savePassword')}
{
navigate('/login')
}}
>
- Cancel
+ {t('common:actions.cancel')}
diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx
index 8dcf06b8..9fdb553a 100644
--- a/src/views/ChoreEdit/ChoreEdit.jsx
+++ b/src/views/ChoreEdit/ChoreEdit.jsx
@@ -28,6 +28,7 @@ import {
} from '@mui/joy'
import moment from 'moment'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import DurationInput from '../../components/common/DurationInput'
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
@@ -72,6 +73,7 @@ const REPEAT_ON_TYPE = ['interval', 'days_of_the_week', 'day_of_the_month']
const NO_DUE_DATE_REQUIRED_TYPE = ['no_repeat', 'once']
const NO_DUE_DATE_ALLOWED_TYPE = ['trigger']
const ChoreEdit = () => {
+ const { t } = useTranslation(['chores', 'common'])
const { data: userProfile, isLoading: isUserProfileLoading } =
useUserProfile()
@@ -162,24 +164,26 @@ const ChoreEdit = () => {
const errors = {}
if (name.trim() === '') {
- errors.name = 'Name is required'
+ errors.name = t('chores:edit.validation.nameRequired')
}
if (assignStrategy !== 'no_assignee') {
if (assignees.length === 0) {
- errors.assignees = 'At least 1 assignees is required'
+ errors.assignees = t('chores:edit.validation.assigneesRequired')
}
if (assignedTo === null || assignedTo < 0) {
- errors.assignedTo = 'Assigned to is required'
+ errors.assignedTo = t('chores:edit.validation.assignedToRequired')
}
}
if (frequencyType === 'interval' && !frequency > 0) {
- errors.frequency = `Invalid frequency, the ${frequencyMetadata.unit} should be > 0`
+ errors.frequency = t('chores:edit.validation.invalidFrequency', {
+ unit: frequencyMetadata.unit,
+ })
}
if (
frequencyType === 'days_of_the_week' &&
frequencyMetadata['days']?.length === 0
) {
- errors.frequency = 'Please select at least one day of the week'
+ errors.frequency = t('chores:edit.validation.selectDayOfWeek')
}
// Validate advanced scheduling patterns
@@ -189,14 +193,13 @@ const ChoreEdit = () => {
(!frequencyMetadata?.occurrences ||
frequencyMetadata.occurrences.length === 0)
) {
- errors.frequency =
- 'Please select at least one day occurrence for the month'
+ errors.frequency = t('chores:edit.validation.selectDayOccurrence')
}
if (
frequencyType === 'day_of_the_month' &&
frequencyMetadata['months']?.length === 0
) {
- errors.frequency = 'Please select at least one month'
+ errors.frequency = t('chores:edit.validation.selectMonth')
}
if (
dueDate === null &&
@@ -206,14 +209,14 @@ const ChoreEdit = () => {
if (REPEAT_ON_TYPE.includes(frequencyType)) {
console.log('VALIDATION:', dueDate, frequencyType)
- errors.dueDate = 'Start date is required'
+ errors.dueDate = t('chores:edit.validation.startDateRequired')
} else {
- errors.dueDate = 'Due date is required'
+ errors.dueDate = t('chores:edit.validation.dueDateRequired')
}
}
if (frequencyType === 'trigger') {
if (!isThingValid) {
- errors.thingTrigger = 'Thing trigger is invalid'
+ errors.thingTrigger = t('chores:edit.validation.thingTriggerInvalid')
}
}
@@ -226,7 +229,7 @@ const ChoreEdit = () => {
{errors[key]}
))
showError({
- title: 'Please resolve the following errors:',
+ title: t('chores:edit.validation.resolveErrors'),
message: {errorList}
,
})
return false
@@ -367,16 +370,16 @@ const ChoreEdit = () => {
SaveFunction(chore)
.then(() => {
showSuccess({
- title: 'Chore Saved',
- message: 'Your task has been saved successfully!',
+ title: t('chores:edit.saveSuccessTitle'),
+ message: t('chores:edit.saveSuccessMessage'),
})
Navigate('/chores')
})
.catch(error => {
console.error('Failed to save chore:', error)
showError({
- title: 'Save Failed',
- message: 'Failed to save chore, please try again.',
+ title: t('chores:edit.saveFailedTitle'),
+ message: t('chores:edit.saveFailedMessage'),
})
})
}
@@ -607,10 +610,10 @@ const ChoreEdit = () => {
const handleDelete = () => {
setConfirmModelConfig({
isOpen: true,
- title: 'Delete Chore',
- confirmText: 'Delete',
- cancelText: 'Cancel',
- message: 'Are you sure you want to delete this chore?',
+ title: t('chores:edit.deleteTitle'),
+ confirmText: t('common:actions.delete'),
+ cancelText: t('common:actions.cancel'),
+ message: t('chores:edit.deleteMessage'),
onClose: isConfirmed => {
if (isConfirmed === true) {
deleteChores.mutate([choreId], {
@@ -619,7 +622,7 @@ const ChoreEdit = () => {
},
onError: error => {
showError({
- title: 'Delete Failed',
+ title: t('chores:edit.deleteFailedTitle'),
message: `Failed to delete chore: ${error.message}`,
})
},
@@ -652,10 +655,8 @@ const ChoreEdit = () => {
- Name
-
- What is the name of this task?
-
+ {t('common:labels.name')}
+ {t('chores:edit.nameQuestion')}
setName(e.target.value)} />
{errors.name}
@@ -663,8 +664,8 @@ const ChoreEdit = () => {
- Description
- What is this task about?
+ {t('common:labels.description')}
+ {t('chores:edit.descriptionQuestion')}
{
- Priority
- How important is this task?
+ {t('common:labels.priority')}
+ {t('chores:edit.priorityQuestion')}
{/* Priority Chip Selection */}
{
minHeight: 34,
}}
>
- No Priority
+ {t('chores:labels.noPriority')}
@@ -731,10 +732,8 @@ const ChoreEdit = () => {
{/* Project Selection - Show only if there are multiple projects */}
{projects.length >= 1 && (
- Project
-
- Which project does this task belong to?
-
+ {t('common:labels.project')}
+ {t('chores:edit.projectQuestion')}
setProjectId(newValue)}
@@ -768,7 +767,7 @@ const ChoreEdit = () => {
)
})()}
- Default Project
+ {t('common:labels.defaultProject')}
{projects.map(project => (
@@ -809,10 +808,8 @@ const ChoreEdit = () => {
)}
- Labels
-
- Things to remember about this task or to tag it
-
+ {t('common:labels.labelsLabel')}
+ {t('chores:edit.labelsQuestion')}
{
@@ -872,13 +869,13 @@ const ChoreEdit = () => {
}}
>
- Add New Label
+ {t('chores:edit.addNewLabel')}
- Sub Tasks
+ {t('common:labels.subtasks')}
{/*
{
@@ -914,8 +911,8 @@ const ChoreEdit = () => {
{/* Section 2: Assignment & Responsibility */}
- Assignees
- Who can do this task?
+ {t('common:labels.assignees')}
+ {t('chores:edit.whoCanDoTask')}
{
overlay
disableIcon
variant='soft'
- label='Anyone'
+ label={t('common:status.anyone')}
/>
@@ -993,7 +990,7 @@ const ChoreEdit = () => {
setShowSaveAssigneeDefault(false)
}}
>
- Remember for Future Tasks
+ {t('common:actions.rememberForFutureTasks')}
)}
@@ -1002,15 +999,13 @@ const ChoreEdit = () => {
{assignees.length > 1 && (
<>
- Currently Assigned To
-
- Who is assigned the next due?
-
+ {t('chores:edit.whoIsAssignedNext')}
+ {t('chores:edit.whoIsAssignedNext')}
-1 ? assignedTo : null}
@@ -1032,10 +1027,8 @@ const ChoreEdit = () => {
- Assignment Strategy
-
- How to pick the next assignee for the following task?
-
+ {t('common:labels.assignmentStrategy')}
+ {t('chores:edit.assignmentStrategyQuestion')}
{
overlay
disableIcon
variant='soft'
- label={item
- .split('_')
- .map(x => x.charAt(0).toUpperCase() + x.slice(1))
- .join(' ')}
+ label={t(`chores:edit.assignStrategies.${item}`)}
/>
))}
@@ -1096,11 +1086,13 @@ const ChoreEdit = () => {
- {REPEAT_ON_TYPE.includes(frequencyType) ? 'Start Date' : 'Due Date'}
+ {REPEAT_ON_TYPE.includes(frequencyType)
+ ? t('common:labels.startDate')
+ : t('common:labels.dueDate')}
{frequencyType === 'trigger' && !dueDate && (
- Due Date will be set when the trigger of the thing is met
+ {t('chores:edit.triggerDueDateHint')}
)}
@@ -1126,10 +1118,10 @@ const ChoreEdit = () => {
defaultChecked={dueDate !== null}
checked={dueDate !== null}
overlay
- label='Give this task a due date'
+ label={t('chores:edit.giveTaskDueDate')}
/>
- Task needs to be completed by a specific time
+ {t('chores:edit.dueDateHelper')}
)}
@@ -1138,8 +1130,8 @@ const ChoreEdit = () => {
{REPEAT_ON_TYPE.includes(frequencyType)
- ? 'When does this task start?'
- : 'When is the next first time this task is due?'}
+ ? t('chores:edit.startDateQuestion')
+ : t('chores:edit.nextDueQuestion')}
{
checked={useCustomTime}
onChange={e => handleUseCustomTimeChange(e.target.checked)}
overlay
- label='Set a specific time'
+ label={t('chores:edit.setSpecificTime')}
/>
{useCustomTime
- ? 'Task will be due at the specified time'
- : 'Task will be due at the end of the day (11:59 PM)'}
+ ? t('chores:edit.specificTimeHelper')
+ : t('chores:edit.endOfDayHelper')}
{useCustomTime && (
- Time:
+ {t('common:labels.time')}:
{
{dueDate && (
- Task Window
-
- Define when this task can be completed and when it expires
-
+ {t('common:labels.taskWindow')}
+ {t('chores:edit.taskWindowDescription')}
{/* Available From (Completion Window) */}
@@ -1200,10 +1190,10 @@ const ChoreEdit = () => {
}
}}
overlay
- label='Set earliest completion time'
+ label={t('chores:edit.setEarliestCompletionTime')}
/>
- Task becomes available to complete X hours before the due date
+ {t('chores:edit.completionWindowHelper')}
@@ -1215,7 +1205,7 @@ const ChoreEdit = () => {
ml: 4,
}}
>
- Hours:
+ {t('common:labels.hours')}:
{
max: 24 * 7,
},
}}
- placeholder='Hours'
+ placeholder={t('common:placeholders.hours')}
onChange={e => {
setCompletionWindow(parseInt(e.target.value))
}}
@@ -1273,7 +1263,7 @@ const ChoreEdit = () => {
size='sm'
minValue={0}
/>
- after due date
+ {t('chores:edit.afterDueDate')}
)}
@@ -1281,21 +1271,18 @@ const ChoreEdit = () => {
{!['once', 'no_repeat'].includes(frequencyType) && (
- Scheduling Preferences
-
- How to reschedule the next due date?
-
+ {t('chores:edit.schedulingPreferences')}
+ {t('chores:edit.schedulingPreferencesQuestion')}
div': { p: 1 } }}>
setIsRolling(false)}
- label='Reschedule from due date'
+ label={t('chores:edit.rescheduleFromDueDate')}
/>
- the next task will be scheduled from the original due date,
- even if the previous task was completed late
+ {t('chores:edit.rescheduleFromDueDateHelper')}
@@ -1306,11 +1293,10 @@ const ChoreEdit = () => {
setIsRolling(true)
setDeadlineOffset(-1)
}}
- label='Reschedule from completion date'
+ label={t('chores:edit.rescheduleFromCompletionDate')}
/>
- the next task will be scheduled from the actual completion
- date of the previous task
+ {t('chores:edit.rescheduleFromCompletionDateHelper')}
@@ -1319,11 +1305,10 @@ const ChoreEdit = () => {
{/* Section 3.1: Notifications */}
- Notifications
+ {t('common:labels.notifications')}
{!isPlusAccount(userProfile) && (
- Task notifications are not available in the Basic plan. Upgrade to
- Plus to receive reminders when tasks are due or completed.
+ {t('chores:edit.notificationsBasicPlan')}
)}
@@ -1339,14 +1324,14 @@ const ChoreEdit = () => {
checked={isNotificable}
disabled={!isPlusAccount(userProfile)}
overlay
- label='Notify for this task'
+ label={t('chores:edit.notifyForTask')}
/>
- When should receive notifications for this task
+ {t('chores:edit.notifyForTaskHelper')}
@@ -1361,7 +1346,7 @@ const ChoreEdit = () => {
>
- Notification Schedule
+ {t('chores:edit.notificationSchedule')}
{
- Who to Notify
+ {t('chores:edit.whoToNotify')}
- Notify all assignees
+ {t('chores:edit.notifyAllAssignees')}
@@ -1409,9 +1394,9 @@ const ChoreEdit = () => {
? notificationMetadata?.circleGroup
: false
}
- label='Specific Group'
+ label={t('common:labels.specificGroup')}
/>
- Notify a specific group
+ {t('chores:edit.notifySpecificGroup')}
{notificationMetadata?.circleGroup && (
@@ -1421,11 +1406,11 @@ const ChoreEdit = () => {
ml: 4,
}}
>
- Telegram Group ID:
+ {t('common:labels.telegramGroupId')}:
{
setNotificationMetadata({
...notificationMetadata,
@@ -1450,11 +1435,11 @@ const ChoreEdit = () => {
pb: 1,
}}
>
- Task Settings:
+ {t('chores:edit.taskSettings')}
- Points System
+ {t('common:labels.pointsLabel')}
{
@@ -1466,11 +1451,10 @@ const ChoreEdit = () => {
}}
checked={points > -1}
overlay
- label='Assign points for completion'
+ label={t('chores:edit.assignPoints')}
/>
- Assign points to this task and user will earn points when they
- completed it
+ {t('chores:edit.pointsHelper')}
{points != -1 && (
@@ -1481,7 +1465,7 @@ const ChoreEdit = () => {
ml: 4,
}}
>
- Points:
+ {t('common:labels.pointsLabel')}:
{
max: 1000,
},
}}
- placeholder='Points'
+ placeholder={t('common:labels.pointsLabel')}
onChange={e => {
setPoints(parseInt(e.target.value))
}}
@@ -1503,7 +1487,7 @@ const ChoreEdit = () => {
- Approval Requirement
+ {t('chores:edit.approvalRequirement')}
{
@@ -1511,18 +1495,17 @@ const ChoreEdit = () => {
}}
checked={requireApproval}
overlay
- label='Require admin approval'
+ label={t('chores:edit.requireAdminApproval')}
/>
- This task will need approval from an admin before being marked as
- complete
+ {t('chores:edit.requireAdminApprovalHelper')}
- Privacy Settings
- Who can see this task?
+ {t('common:labels.privacySettings')}
+ {t('chores:edit.privacyQuestion')}
{
}}
>
-
- Everyone in your circle
+
+ {t('chores:edit.privacyPublicHelper')}
- You and others that are assigned to the task
+ {t('chores:edit.privacyLimitedHelper')}
{assignees.length === 0
- ? ' (No assignees selected, Limited option is disabled)'
+ ? ` (${t('chores:edit.privacyLimitedDisabled')})`
: ''}
@@ -1577,7 +1560,7 @@ const ChoreEdit = () => {
setShowSavePrivacyDefault(false)
}}
>
- Remember for Future Tasks
+ {t('common:actions.rememberForFutureTasks')}
)}
@@ -1594,7 +1577,7 @@ const ChoreEdit = () => {
}}
>
- Created by{' '}
+ {t('chores:edit.createdBy')}{' '}
{membersData.res.find(f => f.userId === createdBy)?.displayName}
{' '}
@@ -1605,7 +1588,7 @@ const ChoreEdit = () => {
- Updated by{' '}
+ {t('chores:edit.updatedBy')}{' '}
{
membersData.res.find(f => f.userId === updatedBy)
@@ -1655,7 +1638,9 @@ const ChoreEdit = () => {
: unarchiveChore.mutate(choreId)
}}
>
- {isActive ? 'Archive' : 'Unarchive'}
+ {isActive
+ ? t('common:actions.archive')
+ : t('common:actions.unarchive')}
{
- Delete
+ {t('common:actions.delete')}
@@ -1683,13 +1668,13 @@ const ChoreEdit = () => {
window.history.back()
}}
>
- Cancel
+ {t('common:actions.cancel')}
{showKeyboardShortcuts && (
)}
- {choreId > 0 ? 'Save' : 'Create'}
+ {choreId > 0 ? t('common:actions.save') : t('common:actions.create')}
{showKeyboardShortcuts && (
)}
diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx
index 9548bc8a..56cebe54 100644
--- a/src/views/ChoreEdit/ChoreView.jsx
+++ b/src/views/ChoreEdit/ChoreView.jsx
@@ -77,7 +77,7 @@ import TimePassedCard from './TimePassedCard.jsx'
import TimerSplitButton from './TimerSplitButton.jsx'
const ChoreView = () => {
- const { t } = useTranslation('chores')
+ const { t } = useTranslation(['chores', 'common'])
const { fmt } = useLocalization()
const [chore, setChore] = useState({})
const navigate = useNavigate()
@@ -156,7 +156,7 @@ const ChoreView = () => {
chore.lastCompletedDate
? performers.find(p => p.userId === chore.lastCompletedBy)
?.displayName
- : 'N/A'
+ : t('choreView.na')
}`,
},
{
@@ -174,7 +174,7 @@ const ChoreView = () => {
subtext2:
chore.deadlineOffset > 0 && chore.nextDueDate
- ? `Deadline: ${moment(chore.nextDueDate).add(chore.deadlineOffset, 'seconds').fromNow()}`
+ ? `${t('choreView.deadline')}: ${moment(chore.nextDueDate).add(chore.deadlineOffset, 'seconds').fromNow()}`
: null,
},
{
@@ -331,7 +331,7 @@ const ChoreView = () => {
title: t('choreView.resetTimer'),
message: t('choreView.resetTimerConfirmation'),
confirmText: t('choreView.resetTimer'),
- cancelText: t('common:cancel'),
+ cancelText: t('common:actions.cancel'),
onClose: confirmed => {
if (confirmed) {
resetChoreTimer.mutate(choreId, {
@@ -355,7 +355,7 @@ const ChoreView = () => {
title: t('choreView.clearAllTimeRecords'),
message: t('choreView.clearAllTimeConfirmation'),
confirmText: t('choreView.clearAllTimeRecords'),
- cancelText: t('common:cancel'),
+ cancelText: t('common:actions.cancel'),
onClose: async confirmed => {
if (confirmed) {
if (choreTimer?.res?.id) {
@@ -703,7 +703,7 @@ const ChoreView = () => {
}}
>
- Edit
+ {t('choreView.edit')}
diff --git a/src/views/ChoreEdit/RepeatSection.jsx b/src/views/ChoreEdit/RepeatSection.jsx
index a7d3c448..fb793b2c 100644
--- a/src/views/ChoreEdit/RepeatSection.jsx
+++ b/src/views/ChoreEdit/RepeatSection.jsx
@@ -18,6 +18,7 @@ import {
} from '@mui/joy'
import moment from 'moment'
import { useEffect } from 'react'
+import { useTranslation } from 'react-i18next'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useUserProfile } from '../../queries/UserQueries'
@@ -77,19 +78,19 @@ const DAY_OCCURRENCE_OPTIONS = [
{ value: -1, label: 'Last occurrence' },
]
// Helper function to generate schedule preview text
-const generateSchedulePreview = (metadata, formatTimeFn) => {
+const generateSchedulePreview = (metadata, formatTimeFn, t) => {
if (!metadata?.days?.length) return ''
const dayNames = metadata.days
- .map(day => day.charAt(0).toUpperCase() + day.slice(1, 3))
+ .map(day => t(`chores:repeat.dayAbbreviations.${day}`))
.join(', ')
const timeStr = metadata.time
? formatTimeFn(metadata.time)
- : '6:00 PM'
+ : formatTimeFn(moment().hour(18).minute(0).toISOString())
if (metadata.weekPattern === 'every_week' || !metadata.weekPattern) {
- return `Every ${dayNames} at ${timeStr}`
+ return `${t('chores:repeat.everyWeek')} ${dayNames} ${t('chores:repeat.at')} ${timeStr}`
}
if (
@@ -97,15 +98,12 @@ const generateSchedulePreview = (metadata, formatTimeFn) => {
metadata.occurrences?.length
) {
const occurrenceStr = metadata.occurrences
- .map(w => {
- if (w === -1) return 'last'
- return `${w}${w === 1 ? 'st' : w === 2 ? 'nd' : w === 3 ? 'rd' : 'th'}`
- })
+ .map(w => t(`chores:repeat.occurrences.${w}`))
.join(', ')
- return `Every ${occurrenceStr} ${dayNames} of the month at ${timeStr}`
+ return `${t('chores:repeat.every')} ${occurrenceStr} ${dayNames} ${t('chores:repeat.ofMonth')} ${t('chores:repeat.at')} ${timeStr}`
}
- return `Every ${dayNames} at ${timeStr}`
+ return `${t('chores:repeat.every')} ${dayNames} ${t('chores:repeat.at')} ${timeStr}`
}
const RepeatOnSections = ({
@@ -115,6 +113,7 @@ const RepeatOnSections = ({
frequencyMetadata,
onFrequencyMetadataUpdate,
}) => {
+ const { t } = useTranslation(['chores', 'common'])
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
@@ -144,7 +143,7 @@ const RepeatOnSections = ({
flexDirection: 'column',
}}
>
- Time of day:
+ {t('chores:repeat.timeOfDay')}:
- Every:
+ {t('chores:repeat.every')}:
@@ -197,7 +196,9 @@ const RepeatOnSections = ({
})
}}
>
- {item.charAt(0).toUpperCase() + item.slice(1)}
+ {item === 'hours'
+ ? t('common:labels.hours')
+ : t(`chores:frequency.units.${item}`)}
))}
@@ -241,7 +242,7 @@ const RepeatOnSections = ({
overlay
disableIcon
variant='soft'
- label={item.charAt(0).toUpperCase() + item.slice(1)}
+ label={t(`chores:repeat.days.${item}`)}
/>
))}
@@ -270,8 +271,8 @@ const RepeatOnSections = ({
disableIcon
>
{frequencyMetadata?.days?.length === 7
- ? 'Unselect All'
- : 'Select All'}
+ ? t('chores:repeat.unselectAll')
+ : t('chores:repeat.selectAll')}
@@ -295,17 +296,22 @@ const RepeatOnSections = ({
>
{Object.entries(WEEK_PATTERNS).map(([value, label]) => (
-
+
{value === 'every_week' && (
-
- Task repeats every week on selected days
-
+ {t('chores:repeat.everyWeekHelper')}
)}
{value === 'week_of_month' && (
-
- Task repeats on specific day occurrences each month
- (e.g., 1st Monday, 3rd Friday)
-
+ {t('chores:repeat.weekOfMonthHelper')}
)}
))}
@@ -314,10 +320,10 @@ const RepeatOnSections = ({
{frequencyMetadata?.weekPattern === 'week_of_month' && (
- Select which occurrences of the selected days:
+ {t('chores:repeat.occurrencePrompt')}:
- Example: "1st Monday" means the first Monday of each month
+ {t('chores:repeat.occurrenceExample')}
))}
@@ -389,8 +397,8 @@ const RepeatOnSections = ({
>
{frequencyMetadata?.occurrences?.length ===
DAY_OCCURRENCE_OPTIONS.length
- ? 'Unselect All'
- : 'Select All'}
+ ? t('chores:repeat.unselectAll')
+ : t('chores:repeat.selectAll')}
@@ -402,7 +410,7 @@ const RepeatOnSections = ({
{frequencyMetadata?.days?.length > 0 && (
- {generateSchedulePreview(frequencyMetadata, fmt.time)}
+ {generateSchedulePreview(frequencyMetadata, fmt.time, t)}
)}
@@ -460,7 +468,7 @@ const RepeatOnSections = ({
overlay
disableIcon
variant='soft'
- label={item.charAt(0).toUpperCase() + item.slice(1)}
+ label={t(`chores:repeat.months.${item}`)}
/>
))}
@@ -487,8 +495,8 @@ const RepeatOnSections = ({
disableIcon
>
{frequencyMetadata?.months?.length === 12
- ? 'Unselect All'
- : 'Select All'}
+ ? t('chores:repeat.unselectAll')
+ : t('chores:repeat.selectAll')}
@@ -499,7 +507,7 @@ const RepeatOnSections = ({
mb: 1.5,
}}
>
- on the
+ {t('chores:repeat.onThe')}
- of the above month/s
+ {t('chores:repeat.ofSelectedMonths')}
{timePickerComponent}
>
@@ -540,11 +548,12 @@ const RepeatSection = ({
isAttemptToSave,
selectedThing,
}) => {
+ const { t } = useTranslation(['chores', 'common'])
const { data: userProfile } = useUserProfile()
return (
- Repeat:
+ {t('chores:repeat.repeat')}:
{
@@ -557,16 +566,14 @@ const RepeatSection = ({
checked={!['once', 'trigger'].includes(frequencyType)}
value={!['once', 'trigger'].includes(frequencyType)}
overlay
- label='Repeat this task'
+ label={t('chores:repeat.repeatTask')}
/>
-
- Is this something needed to be done regularly?
-
+ {t('chores:repeat.repeatHelper')}
{!['once', 'trigger'].includes(frequencyType) && (
<>
- How often should it be repeated?
+ {t('chores:repeat.howOften')}
))}
- {FREQUENCY_TYPE_MESSAGE[frequencyType]}
+
+ {frequencyType === 'adaptive'
+ ? t('chores:repeat.typeMessages.adaptive')
+ : frequencyType === 'custom'
+ ? t('chores:repeat.typeMessages.custom')
+ : ''}
+
{frequencyType === 'custom' ||
(REPEAT_ON_TYPE.includes(frequencyType) && (
<>
- Repeat on:
+ {t('chores:repeat.repeatOn')}:
@@ -679,21 +693,7 @@ const RepeatSection = ({
}}
value={item}
disableIcon
- label={item
- .split('_')
- .map((i, idx) => {
- // first or last word
- if (
- idx === 0 ||
- idx === item.split('_').length - 1
- ) {
- return (
- i.charAt(0).toUpperCase() + i.slice(1)
- )
- }
- return i
- })
- .join(' ')}
+ label={t(`chores:repeat.options.${item}`)}
variant='plain'
sx={{
px: 2,
@@ -750,24 +750,23 @@ const RepeatSection = ({
value={frequencyType === 'trigger'}
disabled={!isPlusAccount(userProfile)}
overlay
- label='Trigger this task based on a thing state'
+ label={t('chores:repeat.triggerTask')}
/>
- Is this something that should be done when a thing state changes?{' '}
+ {t('chores:repeat.triggerHelper')}{' '}
{userProfile && !isPlusAccount(userProfile) && (
- Plus Feature
+ {t('chores:repeat.plusFeature')}
)}
{!isPlusAccount(userProfile) && (
- Thing-based triggers are not available in the Basic plan. Upgrade to
- Plus to automatically trigger tasks when device states change.
+ {t('chores:repeat.triggerBasicPlan')}
)}
diff --git a/src/views/ChoreEdit/ThingTriggerSection.jsx b/src/views/ChoreEdit/ThingTriggerSection.jsx
index e4c4d247..b9d6b4be 100644
--- a/src/views/ChoreEdit/ThingTriggerSection.jsx
+++ b/src/views/ChoreEdit/ThingTriggerSection.jsx
@@ -13,24 +13,26 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
+
const isValidTrigger = (thing, condition, triggerState) => {
const newErrors = {}
if (!thing || !triggerState) {
- newErrors.thing = 'Please select a thing and trigger state'
+ newErrors.thing = true
return false
}
if (thing.type === 'boolean') {
if (['true', 'false'].includes(triggerState)) {
return true
} else {
- newErrors.type = 'Boolean type does not require a condition'
+ newErrors.type = true
return false
}
}
if (thing.type === 'number') {
if (isNaN(triggerState)) {
- newErrors.triggerState = 'Trigger state must be a number'
+ newErrors.triggerState = true
return false
}
if (['eq', 'neq', 'gt', 'gte', 'lt', 'lte'].includes(condition)) {
@@ -42,7 +44,7 @@ const isValidTrigger = (thing, condition, triggerState) => {
return true
}
}
- newErrors.triggerState = 'Trigger state must be a number'
+ newErrors.triggerState = true
return false
}
@@ -54,6 +56,7 @@ const ThingTriggerSection = ({
selected,
isAttepmtingToSave,
}) => {
+ const { t } = useTranslation(['things', 'common'])
const [selectedThing, setSelectedThing] = useState(null)
const [condition, setCondition] = useState(null)
const [triggerState, setTriggerState] = useState(null)
@@ -82,15 +85,20 @@ const ThingTriggerSection = ({
}
}, [selectedThing, condition, triggerState])
+ const getTranslatedType = type =>
+ t(`things:page.types.${type}`, { defaultValue: type })
+
+ const getTranslatedState = state =>
+ state === 'true' || state === 'false'
+ ? t(`things:page.states.${state}`)
+ : state
+
return (
-
- Trigger a task when a thing state changes to a desired state
-
+ {t('things:trigger.title')}
{things?.length === 0 && (
- it's look like you don't have any things yet, create a thing to
- trigger a task when the state changes.
+ {t('things:trigger.noThings')}{' '}
}
size='sm'
@@ -98,9 +106,9 @@ const ThingTriggerSection = ({
navigate('/things')
}}
>
- Go to Things
+ {t('things:trigger.goToThings')}
{' '}
- to create a thing
+ {t('things:trigger.createThingSuffix')}
)}
@@ -127,27 +135,27 @@ const ThingTriggerSection = ({
- type: {option.type} {' '}
- state: {option.state}
+
+ {t('things:trigger.type')}: {getTranslatedType(option.type)}
+ {' '}
+
+ {t('things:trigger.state')}: {getTranslatedState(option.state)}
+
)}
renderInput={params => (
-
+
)}
/>
-
- Create a condition to trigger a task when the thing state changes to
- desired state
-
+ {t('things:trigger.conditionHelp')}
{selectedThing?.type == 'boolean' && (
- When the state of {selectedThing.name} changes as specified below,
- the task will become due.
+ {t('things:trigger.dueWhenChanged', { name: selectedThing.name })}
setTriggerState(state)}
>
- {state.charAt(0).toUpperCase() + state.slice(1)}
+ {t(`things:trigger.${state}`)}
))}
@@ -172,20 +180,19 @@ const ThingTriggerSection = ({
{selectedThing?.type == 'number' && (
- When the state of {selectedThing.name} changes as specified below,
- the task will become due.
+ {t('things:trigger.dueWhenChanged', { name: selectedThing.name })}
- State is
+ {t('common:labels.stateIs')}
{[
- { name: 'Equal', value: 'eq' },
- { name: 'Not equal', value: 'neq' },
- { name: 'Greater than', value: 'gt' },
- { name: 'Greater than or equal', value: 'gte' },
- { name: 'Less than', value: 'lt' },
- { name: 'Less than or equal', value: 'lte' },
+ { name: t('things:trigger.equal'), value: 'eq' },
+ { name: t('things:trigger.notEqual'), value: 'neq' },
+ { name: t('things:trigger.greaterThan'), value: 'gt' },
+ { name: t('things:trigger.greaterThanOrEqual'), value: 'gte' },
+ { name: t('things:trigger.lessThan'), value: 'lt' },
+ { name: t('things:trigger.lessThanOrEqual'), value: 'lte' },
].map(condition => (
- When the state of {selectedThing.name} changes as specified below,
- the task will become due.
+ {t('things:trigger.dueWhenChanged', { name: selectedThing.name })}
setTriggerState(e.target.value)}
- label='Enter the text to trigger the task'
+ label={t('things:trigger.textTriggerLabel')}
/>
)}
diff --git a/src/views/ChoreEdit/TimerSplitButton.jsx b/src/views/ChoreEdit/TimerSplitButton.jsx
index d494e0ee..24cf5468 100644
--- a/src/views/ChoreEdit/TimerSplitButton.jsx
+++ b/src/views/ChoreEdit/TimerSplitButton.jsx
@@ -7,6 +7,7 @@ import {
} from '@mui/icons-material'
import { Box, ButtonGroup, IconButton, Menu, MenuItem } from '@mui/joy'
import { useEffect, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
const TimerSplitButton = ({
chore,
@@ -17,6 +18,7 @@ const TimerSplitButton = ({
disabled = false,
fullWidth = false,
}) => {
+ const { t } = useTranslation(['timer', 'chores'])
const [anchorEl, setAnchorEl] = useState(null)
const isMenuOpen = Boolean(anchorEl)
const menuRef = useRef(null)
@@ -109,7 +111,9 @@ const TimerSplitButton = ({
}}
>
{chore.status === 1 ? : }
- {chore.status === 1 ? 'Pause' : 'Resume'}
+ {chore.status === 1
+ ? t('timer:details.pauseButton')
+ : t('timer:details.resumeButton')}
{/* Dropdown arrow button */}
@@ -143,7 +147,7 @@ const TimerSplitButton = ({
>
- Timer Details
+ {t('timer:details.title')}
{/*
@@ -151,7 +155,7 @@ const TimerSplitButton = ({
*/}
- Clear & Reset
+ {t('chores:choreView.clearAllTimeRecords')}
diff --git a/src/views/Chores/ActivitesCard.jsx b/src/views/Chores/ActivitesCard.jsx
index 54c31cb7..1eb78747 100644
--- a/src/views/Chores/ActivitesCard.jsx
+++ b/src/views/Chores/ActivitesCard.jsx
@@ -27,12 +27,14 @@ import {
} from '@mui/joy'
import moment from 'moment'
import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
import { useCircleMembers } from '../../queries/UserQueries'
import { resolvePhotoURL } from '../../utils/Helpers'
import NoteViewerModal from '../Modals/Inputs/NoteViewerModal'
const ActivityItem = ({ activity, members, onViewNote }) => {
+ const { t } = useTranslation(['chores', 'common'])
// Find the member who completed the activity
const completedByMember = members?.find(
member => member.userId === activity.completedBy,
@@ -58,11 +60,11 @@ const ActivityItem = ({ activity, members, onViewNote }) => {
const diffInDays = now.diff(completed, 'days')
if (diffInHours < 1) {
- return 'Just now'
+ return t('chores:sidepanel.activities.justNow')
} else if (diffInHours < 24) {
- return `${diffInHours}h ago`
+ return t('chores:sidepanel.activities.hoursAgo', { count: diffInHours })
} else if (diffInDays < 7) {
- return `${diffInDays}d ago`
+ return t('chores:sidepanel.activities.daysAgo', { count: diffInDays })
} else {
return completed.format('MMM DD')
}
@@ -72,7 +74,7 @@ const ActivityItem = ({ activity, members, onViewNote }) => {
if (activity.status === 0) {
return {
color: 'primary',
- text: 'Started',
+ text: t('chores:sidepanel.activities.status.started'),
icon: ,
}
} else if (activity.status === 1) {
@@ -83,32 +85,32 @@ const ActivityItem = ({ activity, members, onViewNote }) => {
if (wasOnTime) {
return {
color: 'success',
- text: 'Done',
+ text: t('chores:sidepanel.activities.status.done'),
icon: ,
}
} else {
return {
color: 'primary',
- text: 'Late',
+ text: t('chores:sidepanel.activities.status.late'),
icon: ,
}
}
} else if (activity.status === 2) {
return {
color: 'warning',
- text: 'Skipped',
+ text: t('chores:sidepanel.activities.status.skipped'),
icon: ,
}
} else if (activity.status === 3) {
return {
color: 'neutral',
- text: 'Pending Approval',
+ text: t('chores:sidepanel.activities.status.pendingApproval'),
icon: ,
}
} else if (activity.status === 4) {
return {
color: 'danger',
- text: 'Rejected',
+ text: t('chores:sidepanel.activities.status.rejected'),
icon: ,
}
}
@@ -116,7 +118,7 @@ const ActivityItem = ({ activity, members, onViewNote }) => {
// Fallback for completed status
return {
color: 'success',
- text: 'Completed',
+ text: t('chores:sidepanel.activities.status.completed'),
icon: ,
}
}
@@ -163,10 +165,10 @@ const ActivityItem = ({ activity, members, onViewNote }) => {
{getStatusInfo(activity).text}
- by{' '}
+ {t('chores:sidepanel.activities.by')}{' '}
{completedByMember?.displayName ||
completedByMember?.name ||
- 'Unknown'}
+ t('common:status.unknown')}
{/* Points chip */}
{activity.points && activity.points > 0 && (
@@ -176,7 +178,9 @@ const ActivityItem = ({ activity, members, onViewNote }) => {
color='success'
startDecorator={ }
>
- {activity.points} pts
+ {t('chores:sidepanel.activities.points', {
+ count: activity.points,
+ })}
)}
@@ -231,7 +235,7 @@ const ActivityItem = ({ activity, members, onViewNote }) => {
display: 'inline-block',
}}
>
- Show more
+ {t('chores:sidepanel.activities.showMore')}
)}
@@ -260,8 +264,10 @@ const groupActivitiesByDate = activities => {
return groups
}
-const ActivitiesCard = ({ title = 'Recent Activities' }) => {
+const ActivitiesCard = ({ title }) => {
+ const { t } = useTranslation(['chores', 'common'])
const [noteViewerConfig, setNoteViewerConfig] = useState({ isOpen: false })
+ const resolvedTitle = title || t('chores:sidepanel.activities.title')
// Use hooks to fetch data
const {
@@ -309,7 +315,7 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => {
}}
>
- {title}
+ {resolvedTitle}
{
}}
>
- Loading activities...
+ {t('chores:sidepanel.activities.loading')}
@@ -333,7 +339,8 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => {
const chore = chores?.find(c => c.id === history.choreId)
return {
...history,
- choreName: chore?.name || 'Unknown Chore',
+ choreName:
+ chore?.name || t('chores:sidepanel.activities.unknownChore'),
}
}) || []
@@ -366,7 +373,7 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => {
>
- {title}
+ {resolvedTitle}
{
}}
>
- No recent activities
+
+ {t('chores:sidepanel.activities.empty')}
+
)
@@ -411,7 +420,7 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => {
>
- {title}
+ {resolvedTitle}
@@ -441,9 +450,9 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => {
let dateLabel
if (isToday) {
- dateLabel = 'Today'
+ dateLabel = t('common:calendar.today')
} else if (isYesterday) {
- dateLabel = 'Yesterday'
+ dateLabel = t('common:calendar.yesterday')
} else {
dateLabel = moment(date).format('MMM DD')
}
@@ -479,7 +488,9 @@ const ActivitiesCard = ({ title = 'Recent Activities' }) => {
onViewNote={notes => {
setNoteViewerConfig({
isOpen: true,
- title: `Note - ${activity.choreName}`,
+ title: t('chores:sidepanel.activities.noteTitle', {
+ name: activity.choreName,
+ }),
content: notes,
onClose: () => setNoteViewerConfig({ isOpen: false }),
})
diff --git a/src/views/Chores/ArchivedTasks.jsx b/src/views/Chores/ArchivedTasks.jsx
index afc12cb5..37828f40 100644
--- a/src/views/Chores/ArchivedTasks.jsx
+++ b/src/views/Chores/ArchivedTasks.jsx
@@ -22,6 +22,7 @@ import {
} from '@mui/joy'
import Fuse from 'fuse.js'
import { useEffect, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
@@ -37,6 +38,7 @@ import CompactChoreCard from './CompactChoreCard'
import MultiSelectHelp from './MultiSelectHelp'
const ArchivedTasks = () => {
+ const { t } = useTranslation(['chores', 'common'])
const { data: userProfile, isLoading: isUserProfileLoading } =
useUserProfile()
const { showSuccess, showError } = useNotification()
@@ -78,8 +80,8 @@ const ArchivedTasks = () => {
setFilteredChores(sortedChores)
} catch (error) {
showError({
- title: 'Failed to load archived tasks',
- message: 'Please try again later.',
+ title: t('chores:main.archivedLoadFailed'),
+ message: t('chores:main.tryAgainLater'),
})
} finally {
setIsLoading(false)
@@ -238,7 +240,7 @@ const ArchivedTasks = () => {
showSuccess({
title: 'Task Restored',
- message: 'The task has been restored and is now active.',
+ message: t('chores:main.archivedRestoreMessage'),
})
}
}
@@ -255,7 +257,7 @@ const ArchivedTasks = () => {
showSuccess({
title: 'Task Deleted',
- message: 'The archived task has been permanently deleted.',
+ message: t('chores:main.archivedDeleteMessage'),
})
}
@@ -340,8 +342,10 @@ const ArchivedTasks = () => {
if (restoredTasks.length > 0) {
showSuccess({
- title: '📤 Tasks Restored',
- message: `Successfully restored ${restoredTasks.length} task${restoredTasks.length > 1 ? 's' : ''}.`,
+ title: t('chores:main.restoredTasksTitle'),
+ message: t('chores:main.restoredTasks', {
+ count: restoredTasks.length,
+ }),
})
// Remove restored tasks from archived list
@@ -358,16 +362,18 @@ const ArchivedTasks = () => {
if (failedTasks.length > 0) {
showError({
- title: 'Some Tasks Failed',
- message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be restored.`,
+ title: t('chores:actionFeedback.bulk.someFailedTitle'),
+ message: t('chores:main.restoredFailed', {
+ count: failedTasks.length,
+ }),
})
}
clearSelection()
} catch (error) {
showError({
- title: 'Bulk Restore Failed',
- message: 'An unexpected error occurred. Please try again.',
+ title: t('chores:main.bulkRestoreFailed'),
+ message: t('chores:main.unexpectedError'),
})
}
}
@@ -382,10 +388,12 @@ const ArchivedTasks = () => {
setConfirmModelConfig({
isOpen: true,
- title: 'Delete Archived Tasks',
- confirmText: 'Delete',
- cancelText: 'Cancel',
- message: `Permanently delete ${selectedData.length} archived task${selectedData.length > 1 ? 's' : ''}?\n\nThis action cannot be undone.`,
+ title: t('chores:main.deleteArchivedTitle'),
+ confirmText: t('common:actions.delete'),
+ cancelText: t('common:actions.cancel'),
+ message: t('chores:main.deleteArchivedConfirm', {
+ count: selectedData.length,
+ }),
onClose: async isConfirmed => {
if (isConfirmed === true) {
try {
@@ -403,8 +411,10 @@ const ArchivedTasks = () => {
if (deletedTasks.length > 0) {
showSuccess({
- title: '🗑️ Tasks Deleted',
- message: `Successfully deleted ${deletedTasks.length} task${deletedTasks.length > 1 ? 's' : ''}.`,
+ title: t('chores:actionFeedback.bulk.deleteSuccessTitle'),
+ message: t('chores:actionFeedback.bulk.deleteSuccess', {
+ count: deletedTasks.length,
+ }),
})
const deletedIds = new Set(deletedTasks.map(c => c.id))
@@ -420,16 +430,18 @@ const ArchivedTasks = () => {
if (failedTasks.length > 0) {
showError({
- title: 'Some Tasks Failed',
- message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be deleted.`,
+ title: t('chores:actionFeedback.bulk.someFailedTitle'),
+ message: t('chores:actionFeedback.bulk.deleteFailed', {
+ count: failedTasks.length,
+ }),
})
}
clearSelection()
} catch (error) {
showError({
- title: 'Bulk Delete Failed',
- message: 'An unexpected error occurred. Please try again.',
+ title: t('chores:actionFeedback.bulk.deleteUnexpectedTitle'),
+ message: t('chores:main.unexpectedError'),
})
}
}
@@ -471,10 +483,10 @@ const ArchivedTasks = () => {
level='h3'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Archived Tasks
+ {t('chores:main.archivedTitle')}
- View and manage tasks that have been archived or completed.
+ {t('chores:main.archivedDescription')}
@@ -519,7 +531,7 @@ const ArchivedTasks = () => {
>
{
onClick={toggleViewMode}
title={
viewMode === 'default'
- ? 'Switch to Compact View'
- : 'Switch to Card View'
+ ? t('chores:main.viewCompact')
+ : t('chores:main.viewCard')
}
>
{viewMode === 'default' ? : }
@@ -586,8 +598,8 @@ const ArchivedTasks = () => {
onClick={toggleMultiSelectMode}
title={
isMultiSelectMode
- ? 'Exit Multi-select Mode (Ctrl+S)'
- : 'Enable Multi-select Mode (Ctrl+S)'
+ ? t('chores:main.exitMultiSelect')
+ : t('chores:main.enableMultiSelect')
}
>
{isMultiSelectMode ? : }
@@ -683,9 +695,9 @@ const ArchivedTasks = () => {
'--Button-paddingInline': '0.75rem',
position: 'relative',
}}
- title='Select all visible tasks (Ctrl+A)'
+ title={t('chores:main.shortcuts.selectAllVisible')}
>
- All
+ {t('chores:main.all')}
{showKeyboardShortcuts && (
{
'--Button-paddingInline': '0.75rem',
position: 'relative',
}}
- title={`${selectedChores.size === 0 ? 'Close' : 'Clear'} multi-select (Esc)`}
+ title={
+ selectedChores.size === 0
+ ? t('chores:main.shortcuts.closeMultiSelect')
+ : t('chores:main.shortcuts.clearMultiSelect')
+ }
>
- {selectedChores.size === 0 ? 'Close' : 'Clear'}
+ {selectedChores.size === 0
+ ? t('common:actions.close')
+ : t('chores:main.clear')}
{showKeyboardShortcuts && (
{
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
position: 'relative',
}}
- title='Restore selected tasks (R)'
+ title={t('chores:main.shortcuts.restoreSelected')}
>
- Restore
+ {t('chores:main.restore')}
{showKeyboardShortcuts && selectedChores.size > 0 && (
{
'--Button-paddingInline': { xs: '0.75rem', sm: '1rem' },
position: 'relative',
}}
- title='Delete selected tasks (E)'
+ title={t('chores:main.shortcuts.deleteSelected')}
>
- Delete
+ {t('common:actions.delete')}
{showKeyboardShortcuts && selectedChores.size > 0 && (
{
}}
/>
- {searchTerm ? 'No archived tasks found' : 'No archived tasks'}
+ {searchTerm
+ ? t('chores:main.noArchivedFound')
+ : t('chores:main.noArchived')}
{searchTerm
- ? 'Try adjusting your search terms'
- : 'Archived tasks will appear here when you archive them from the main task list'}
+ ? t('chores:main.adjustSearch')
+ : t('chores:main.archivedWillAppear')}
{searchTerm && (
{
variant='outlined'
color='neutral'
>
- Clear search
+ {t('chores:main.clearSearch')}
)}
) : (
- {filteredChores.length} archived task
- {filteredChores.length !== 1 ? 's' : ''}
- {searchTerm && ` matching "${searchTerm}"`}
+ {t('chores:main.archivedCount', { count: filteredChores.length })}
+ {searchTerm && t('chores:main.matchingSearch', { term: searchTerm })}
diff --git a/src/views/Chores/ChoreCard.jsx b/src/views/Chores/ChoreCard.jsx
index 5b4ce407..8053db94 100644
--- a/src/views/Chores/ChoreCard.jsx
+++ b/src/views/Chores/ChoreCard.jsx
@@ -20,6 +20,7 @@ import {
IconButton,
Typography,
} from '@mui/joy'
+import { useTranslation } from 'react-i18next'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useUserProfile } from '../../queries/UserQueries.jsx'
@@ -47,6 +48,7 @@ const ChoreCard = ({
}) => {
const { data: userProfile } = useUserProfile()
const { timeFormat } = useLocalization()
+ const { t } = useTranslation(['common'])
const { impersonatedUser } = useImpersonateUser()
@@ -237,7 +239,7 @@ const ChoreCard = ({
{chore.assignedTo === null && (
}>
- Anyone
+ {t('common:status.anyone')}
)}
diff --git a/src/views/Chores/ChoreListView.jsx b/src/views/Chores/ChoreListView.jsx
index 5825a24b..65498171 100644
--- a/src/views/Chores/ChoreListView.jsx
+++ b/src/views/Chores/ChoreListView.jsx
@@ -17,6 +17,7 @@ import {
ThumbDown,
} from '@mui/icons-material'
import { Box, Typography } from '@mui/joy'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import ChoreCard from './ChoreCard'
import CompactChoreCard from './CompactChoreCard'
@@ -36,6 +37,7 @@ const ChoreListView = ({
toggleMultiSelectMode,
showActions = true,
}) => {
+ const { t } = useTranslation(['chores', 'common'])
const navigate = useNavigate()
const renderChoreCard = (chore, key) => {
const CardComponent = viewMode === 'compact' ? CompactChoreCard : ChoreCard
@@ -96,7 +98,7 @@ const ChoreListView = ({
>
- Reject
+ {t('common:actions.reject')}
@@ -117,7 +119,7 @@ const ChoreListView = ({
>
- Pending
+ {t('chores:groups.pendingApproval')}
@@ -150,7 +152,9 @@ const ChoreListView = ({
)}
- {chore.status !== 1 ? 'Start' : 'Complete'}
+ {chore.status !== 1
+ ? t('chores:choreView.start')
+ : t('common:actions.complete')}
@@ -173,7 +177,7 @@ const ChoreListView = ({
>
- Schedule
+ {t('common:labels.dueDate')}
@@ -193,7 +197,7 @@ const ChoreListView = ({
>
- Edit
+ {t('common:actions.edit')}
@@ -214,7 +218,7 @@ const ChoreListView = ({
>
- Nudge
+ {t('chores:actions.sendNudge')}
@@ -235,7 +239,7 @@ const ChoreListView = ({
>
- Delete
+ {t('common:actions.delete')}
diff --git a/src/views/Chores/CompactChoreCard.jsx b/src/views/Chores/CompactChoreCard.jsx
index 59c050b9..9835e8d4 100644
--- a/src/views/Chores/CompactChoreCard.jsx
+++ b/src/views/Chores/CompactChoreCard.jsx
@@ -9,6 +9,7 @@ import {
Webhook,
} from '@mui/icons-material'
import { Box, Checkbox, Chip, IconButton, Typography } from '@mui/joy'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
import { useLocalization } from '../../contexts/LocalizationContext'
@@ -40,6 +41,7 @@ const CompactChoreCard = ({
onlyClickable = false,
}) => {
const navigate = useNavigate()
+ const { t } = useTranslation(['common'])
const { data: userProfile } = useUserProfile()
const { timeFormat } = useLocalization()
@@ -91,7 +93,7 @@ const CompactChoreCard = ({
if (assignee) parts.push(assignee)
}
if (chore.assignedTo === null) {
- parts.push('Anyone')
+ parts.push(t('common:status.anyone'))
}
// Points
diff --git a/src/views/Chores/MultiSelectHelp.jsx b/src/views/Chores/MultiSelectHelp.jsx
index 1aabe7b4..da7764fb 100644
--- a/src/views/Chores/MultiSelectHelp.jsx
+++ b/src/views/Chores/MultiSelectHelp.jsx
@@ -1,9 +1,11 @@
import { Close, HelpOutline, Keyboard } from '@mui/icons-material'
import { Box, Button, Card, Divider, IconButton, Typography } from '@mui/joy'
import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useResponsiveModal } from '../../hooks/useResponsiveModal'
const MultiSelectHelp = ({ isVisible = true }) => {
+ const { t } = useTranslation('chores')
const { ResponsiveModal } = useResponsiveModal()
const [isHelpOpen, setIsHelpOpen] = useState(false)
@@ -28,7 +30,7 @@ const MultiSelectHelp = ({ isVisible = true }) => {
borderRadius: '50%',
boxShadow: 'lg',
}}
- title='Show keyboard shortcuts'
+ title={t('sidepanel.multiSelect.showShortcuts')}
>
@@ -45,7 +47,9 @@ const MultiSelectHelp = ({ isVisible = true }) => {
>
- Multi-select Mode
+
+ {t('sidepanel.multiSelect.title')}
+
{
- Use these keyboard shortcuts to work more efficiently with multiple
- tasks:
+ {t('sidepanel.multiSelect.description')}
{/* Selection shortcuts */}
- Selection
+ {t('sidepanel.multiSelect.sections.selection')}
@@ -80,16 +85,18 @@ const MultiSelectHelp = ({ isVisible = true }) => {
{/* Action shortcuts */}
- Actions
+ {t('sidepanel.multiSelect.sections.actions')}
@@ -97,12 +104,12 @@ const MultiSelectHelp = ({ isVisible = true }) => {
{/* Interface shortcuts */}
- Interface
+ {t('sidepanel.multiSelect.sections.interface')}
@@ -114,7 +121,7 @@ const MultiSelectHelp = ({ isVisible = true }) => {
onClick={() => setIsHelpOpen(false)}
sx={{ minWidth: 120 }}
>
- Got it!
+ {t('sidepanel.multiSelect.gotIt')}
diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx
index fe949816..25396544 100644
--- a/src/views/Chores/MyChores.jsx
+++ b/src/views/Chores/MyChores.jsx
@@ -31,6 +31,7 @@ import {
} from '@mui/joy'
import Fuse from 'fuse.js'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { useChores } from '../../queries/ChoreQueries'
import { useNotification } from '../../service/NotificationProvider'
@@ -81,6 +82,7 @@ import { INSIGHT_FILTER_DEFS } from './SmartInsightsCard'
import SortAndGrouping from './SortAndGrouping'
const MyChores = () => {
+ const { t } = useTranslation(['chores', 'common'])
const { data: userProfile, isLoading: isUserProfileLoading } =
useUserProfile()
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('md'))
@@ -112,6 +114,29 @@ const MyChores = () => {
const [taskInputFocus, setTaskInputFocus] = useState(0)
const searchInputRef = useRef(null)
const [searchInputFocus, setSearchInputFocus] = useState(0)
+
+ const getFilterDisplayName = filter => {
+ const filterMap = {
+ All: t('chores:main.all'),
+ Overdue: t('chores:groups.overdue'),
+ 'Due today': t('chores:main.otherFilters.dueToday'),
+ 'Due in week': t('chores:main.otherFilters.dueInWeek'),
+ 'Due Later': t('chores:main.otherFilters.dueLater'),
+ 'Created By Me': t('chores:main.otherFilters.createdByMe'),
+ 'Assigned To Me': t('chores:main.otherFilters.assignedToMe'),
+ 'No Due Date': t('chores:main.otherFilters.noDueDate'),
+ 'Pending Approval': t('chores:groups.pendingApproval'),
+ }
+
+ if (filter.startsWith('Priority: ')) {
+ return `${t('common:labels.priority')}: ${filter.replace('Priority: ', '')}`
+ }
+ if (filter.startsWith('Label: ')) {
+ return `${t('common:labels.labelsLabel')}: ${filter.replace('Label: ', '')}`
+ }
+
+ return filterMap[filter] || filter
+ }
const [selectedChoreSection, setSelectedChoreSection] = useState(
localStorage.getItem('selectedChoreSection') || 'due_date',
)
@@ -908,7 +933,7 @@ const MyChores = () => {
/>
}
selectedItem={selectedChoreSection}
@@ -960,10 +985,10 @@ const MyChores = () => {
onClick={toggleViewMode}
title={
viewMode === 'default'
- ? 'Switch to Compact View'
+ ? t('chores:main.viewCompact')
: viewMode === 'compact'
- ? 'Switch to Calendar View'
- : 'Switch to Card View'
+ ? t('chores:main.viewCalendar')
+ : t('chores:main.viewCard')
}
>
{viewMode === 'default' ? (
@@ -989,8 +1014,8 @@ const MyChores = () => {
onClick={toggleMultiSelectMode}
title={
isMultiSelectMode
- ? 'Exit Multi-select Mode (Ctrl+S)'
- : 'Enable Multi-select Mode (Ctrl+S)'
+ ? t('chores:main.exitMultiSelect')
+ : t('chores:main.enableMultiSelect')
}
>
{isMultiSelectMode ? : }
@@ -1022,7 +1047,7 @@ const MyChores = () => {
}
options={Priorities}
@@ -1036,7 +1061,7 @@ const MyChores = () => {
}
options={userLabels}
selectedItem={searchFilter}
@@ -1063,7 +1088,7 @@ const MyChores = () => {
borderRadius: 24,
}}
>
- {' Other'}
+ {t('chores:main.otherFilters.title')}
{
}
}}
>
- {filter}
+ {getFilterDisplayName(filter)}
@@ -1139,7 +1164,7 @@ const MyChores = () => {
updateFilterUrl(null, null)
}}
>
- Cancel All Filters
+ {t('chores:main.cancelAllFilters')}
))}
@@ -1248,7 +1273,9 @@ const MyChores = () => {
updateFilterUrl(null, null)
}}
>
- Additional Filter: {searchFilter}
+ {t('chores:main.additionalFilter', {
+ filter: getFilterDisplayName(searchFilter),
+ })}
)}
{/* Show "Nothing scheduled" when appropriate based on current view mode */}
@@ -1274,7 +1301,7 @@ const MyChores = () => {
}}
/>
- Nothing scheduled
+ {t('chores:main.nothingScheduled')}
{chores.length > 0 && (
<>
@@ -1290,7 +1317,7 @@ const MyChores = () => {
variant='outlined'
color='neutral'
>
- Reset filters
+ {t('chores:main.resetFilters')}
>
)}
@@ -1452,7 +1479,9 @@ const MyChores = () => {
{selectedCalendarDate && (
- Tasks for {selectedCalendarDate.toLocaleDateString()}
+ {t('common:calendar.tasksForDate', {
+ date: selectedCalendarDate.toLocaleDateString(),
+ })}
{
color: 'text.tertiary',
}}
>
- No tasks scheduled for this date
+ {t('chores:labels.noTasksForDate')}
) : (
{
onClick={() => {
Navigate(`/chores/create`)
}}
- title='Create new chore (Cmd+C)'
+ title={t('chores:main.createNewChoreShortcut')}
>
{
+ const { t } = useTranslation('chores')
const [open, setOpen] = useState(false)
// Define the function outside of useEffect
@@ -48,10 +50,11 @@ const NotificationAccessSnackbar = () => {
})}
>
- Need Notification?
+
+ {t('sidepanel.notifications.title')}
+
- You need to enable permission to receive notifications, do you want to
- enable it?
+ {t('sidepanel.notifications.description')}
{
setOpen(false)
}}
>
- Yes
+ {t('sidepanel.notifications.enable')}
{
setOpen(false)
}}
>
- No, Keep it Disabled
+ {t('sidepanel.notifications.keepDisabled')}
diff --git a/src/views/Chores/SmartInsightsCard.jsx b/src/views/Chores/SmartInsightsCard.jsx
index be316d04..0edea9de 100644
--- a/src/views/Chores/SmartInsightsCard.jsx
+++ b/src/views/Chores/SmartInsightsCard.jsx
@@ -8,6 +8,7 @@ import {
} from '@mui/icons-material'
import { Box, Button, Chip, Sheet, Typography } from '@mui/joy'
import { useMemo } from 'react'
+import { useTranslation } from 'react-i18next'
import { TASK_COLOR } from '../../utils/Colors'
// Static insight filter definitions – used for URL restoration
@@ -62,6 +63,7 @@ const SmartInsightsCard = ({
clearTempFilter,
tempFilter,
}) => {
+ const { t } = useTranslation('chores')
// Detect all possible insights from chores
const insights = useMemo(() => {
if (!chores || chores.length === 0) return []
@@ -84,8 +86,10 @@ const SmartInsightsCard = ({
id: 'overdue',
priority: 1,
count: overdueTasks.length,
- title: 'Overdue',
- description: `${overdueTasks.length} ${overdueTasks.length === 1 ? 'task is' : 'tasks are'} overdue`,
+ title: t('sidepanel.insights.items.overdue.title'),
+ description: t('sidepanel.insights.items.overdue.description', {
+ count: overdueTasks.length,
+ }),
color: 'danger',
bgColor: TASK_COLOR.OVERDUE,
icon: ,
@@ -113,8 +117,10 @@ const SmartInsightsCard = ({
id: 'due-today',
priority: 2,
count: dueTodayTasks.length,
- title: 'Due Today',
- description: `${dueTodayTasks.length} ${dueTodayTasks.length === 1 ? 'task' : 'tasks'} due by end of day`,
+ title: t('sidepanel.insights.items.dueToday.title'),
+ description: t('sidepanel.insights.items.dueToday.description', {
+ count: dueTodayTasks.length,
+ }),
color: 'warning',
bgColor: '#FFA500',
icon: ,
@@ -138,8 +144,13 @@ const SmartInsightsCard = ({
id: 'pending-approval',
priority: 3,
count: pendingApprovalTasks.length,
- title: 'Pending Approval',
- description: `${pendingApprovalTasks.length} ${pendingApprovalTasks.length === 1 ? 'task awaits' : 'tasks await'} approval`,
+ title: t('sidepanel.insights.items.pendingApproval.title'),
+ description: t(
+ 'sidepanel.insights.items.pendingApproval.description',
+ {
+ count: pendingApprovalTasks.length,
+ },
+ ),
color: 'neutral',
bgColor: TASK_COLOR.PENDING_REVIEW,
icon: ,
@@ -167,8 +178,10 @@ const SmartInsightsCard = ({
id: 'due-this-week',
priority: 4,
count: dueThisWeekTasks.length,
- title: 'Due This Week',
- description: `${dueThisWeekTasks.length} ${dueThisWeekTasks.length === 1 ? 'task' : 'tasks'} due in the next 7 days`,
+ title: t('sidepanel.insights.items.dueThisWeek.title'),
+ description: t('sidepanel.insights.items.dueThisWeek.description', {
+ count: dueThisWeekTasks.length,
+ }),
color: 'primary',
bgColor: TASK_COLOR.IN_PROGRESS,
icon: ,
@@ -194,8 +207,10 @@ const SmartInsightsCard = ({
id: 'high-priority',
priority: 5,
count: highPriorityTasks.length,
- title: 'High Priority',
- description: `${highPriorityTasks.length} ${highPriorityTasks.length === 1 ? 'task requires' : 'tasks require'} immediate attention`,
+ title: t('sidepanel.insights.items.highPriority.title'),
+ description: t('sidepanel.insights.items.highPriority.description', {
+ count: highPriorityTasks.length,
+ }),
color: 'warning',
bgColor: '#FF6B6B',
icon: ,
@@ -221,8 +236,10 @@ const SmartInsightsCard = ({
id: 'no-due-date',
priority: 6,
count: noDueDateTasks.length,
- title: 'No Due Date',
- description: `${noDueDateTasks.length} ${noDueDateTasks.length === 1 ? 'task needs' : 'tasks need'} a deadline`,
+ title: t('sidepanel.insights.items.noDueDate.title'),
+ description: t('sidepanel.insights.items.noDueDate.description', {
+ count: noDueDateTasks.length,
+ }),
color: 'neutral',
bgColor: '#9E9E9E',
icon: ,
@@ -241,7 +258,7 @@ const SmartInsightsCard = ({
// Sort by priority and return top 3
return detectedInsights.sort((a, b) => a.priority - b.priority).slice(0, 3)
- }, [chores])
+ }, [chores, t])
const handleInsightClick = insight => {
// Toggle: if already active, clear it; otherwise apply it
@@ -295,18 +312,20 @@ const SmartInsightsCard = ({
>
- Smart Insights
+
+ {t('sidepanel.insights.title')}
+
{tempFilter && (
- Active
+ {t('sidepanel.insights.active')}
)}
{tempFilter
- ? 'Click active filter to clear'
- : 'Quick actions based on your tasks'}
+ ? t('sidepanel.insights.clearHint')
+ : t('sidepanel.insights.description')}
diff --git a/src/views/Chores/SortAndGrouping.jsx b/src/views/Chores/SortAndGrouping.jsx
index 608fa056..efb2094b 100644
--- a/src/views/Chores/SortAndGrouping.jsx
+++ b/src/views/Chores/SortAndGrouping.jsx
@@ -12,6 +12,7 @@ import {
} from '@mui/joy'
import IconButton from '@mui/joy/IconButton'
import { useEffect, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import KeyboardShortcutHint from '../../components/common/KeyboardShortcutHint'
const SortAndGrouping = ({
@@ -28,6 +29,7 @@ const SortAndGrouping = ({
title,
onCreateNewFilter,
}) => {
+ const { t } = useTranslation(['chores', 'common'])
const [anchorEl, setAnchorEl] = useState(null)
const [selectedIndex, setSelectedIndex] = useState(0)
const [isKeyboardNavigating, setIsKeyboardNavigating] = useState(false)
@@ -79,10 +81,10 @@ const SortAndGrouping = ({
if (!anchorEl) return
const groupByItems = [
- { name: 'Smart', value: 'default' },
- { name: 'Due Date', value: 'due_date' },
- { name: 'Priority', value: 'priority' },
- { name: 'Labels', value: 'labels' },
+ { name: t('common:status.smartFilter'), value: 'default' },
+ { name: t('common:labels.dueDate'), value: 'due_date' },
+ { name: t('common:labels.priority'), value: 'priority' },
+ { name: t('common:labels.labelsLabel'), value: 'labels' },
]
const filterItems = [
@@ -143,6 +145,7 @@ const SortAndGrouping = ({
setSelectedItem,
setFilter,
onCreateNewFilter,
+ t,
])
// Reset selected index when menu opens
@@ -247,7 +250,7 @@ const SortAndGrouping = ({
height: 24,
borderRadius: 24,
}}
- title='Sort and Group (Ctrl+G)'
+ title={`${t('chores:main.groupBy')} (Ctrl+G)`}
>
{icon}
{label ? label : null}
@@ -277,7 +280,7 @@ const SortAndGrouping = ({
height: 24,
borderRadius: 24,
}}
- title='Sort and Group (Ctrl+G)'
+ title={`${t('chores:main.groupBy')} (Ctrl+G)`}
>
{label}
@@ -320,7 +323,7 @@ const SortAndGrouping = ({
>
- {title || 'Group By'}
+ {title || t('chores:main.groupBy')}
@@ -328,10 +331,10 @@ const SortAndGrouping = ({
{[
- { name: 'Smart', value: 'default' },
- { name: 'Due Date', value: 'due_date' },
- { name: 'Priority', value: 'priority' },
- { name: 'Labels', value: 'labels' },
+ { name: t('common:status.smartFilter'), value: 'default' },
+ { name: t('common:labels.dueDate'), value: 'due_date' },
+ { name: t('common:labels.priority'), value: 'priority' },
+ { name: t('common:labels.labelsLabel'), value: 'labels' },
].map((item, index) => (
- Quick Filters
+ {t('chores:main.quickFilters')}
@@ -418,7 +421,7 @@ const SortAndGrouping = ({
>
- Assigned to:
+ {t('chores:main.assignedTo')}
@@ -427,28 +430,28 @@ const SortAndGrouping = ({
key={`${k}-assignee-anyone`}
index={4}
filterKey='anyone'
- label='Anyone'
+ label={t('chores:main.filters.anyone')}
/>
@@ -481,13 +484,13 @@ const SortAndGrouping = ({
fontWeight: 500,
}}
>
- Create Filter
+ {t('chores:main.createFilter')}
- Build advanced filter rules
+ {t('chores:main.createFilterDescription')}
diff --git a/src/views/Chores/TasksByAssigneeCard.jsx b/src/views/Chores/TasksByAssigneeCard.jsx
index 787992b9..5de25328 100644
--- a/src/views/Chores/TasksByAssigneeCard.jsx
+++ b/src/views/Chores/TasksByAssigneeCard.jsx
@@ -1,11 +1,13 @@
import { BarChart, Person } from '@mui/icons-material'
import { Avatar, Box, Sheet, Typography } from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useCircleMembers } from '../../queries/UserQueries'
import { TASK_COLOR } from '../../utils/Colors'
import { resolvePhotoURL } from '../../utils/Helpers'
const TasksByAssigneeCard = ({ chores = [] }) => {
+ const { t } = useTranslation('chores')
const [assigneeData, setAssigneeData] = useState([])
const { data: circleMembersData, isLoading: isCircleMembersLoading } =
useCircleMembers()
@@ -104,7 +106,7 @@ const TasksByAssigneeCard = ({ chores = [] }) => {
}}
>
- Loading tasks by assignee...
+ {t('sidepanel.assignees.loading')}
)
@@ -129,7 +131,7 @@ const TasksByAssigneeCard = ({ chores = [] }) => {
>
- No assigned tasks found
+ {t('sidepanel.assignees.empty')}
)
@@ -160,7 +162,9 @@ const TasksByAssigneeCard = ({ chores = [] }) => {
}}
>
- Tasks by Assignee
+
+ {t('sidepanel.assignees.title')}
+
@@ -177,22 +181,22 @@ const TasksByAssigneeCard = ({ chores = [] }) => {
{[
{
key: 'inProgress',
- label: 'In Progress',
+ label: t('sidepanel.assignees.legend.inProgress'),
color: getStatusColor('inProgress'),
},
{
key: 'overdue',
- label: 'Overdue',
+ label: t('sidepanel.assignees.legend.overdue'),
color: getStatusColor('overdue'),
},
{
key: 'scheduled',
- label: 'Scheduled',
+ label: t('sidepanel.assignees.legend.scheduled'),
color: getStatusColor('scheduled'),
},
{
key: 'pendingReview',
- label: 'Pending Review',
+ label: t('sidepanel.assignees.legend.pendingReview'),
color: getStatusColor('pendingReview'),
},
].map(status => (
diff --git a/src/views/Chores/UserSwitcher.jsx b/src/views/Chores/UserSwitcher.jsx
index 64834359..ae087b58 100644
--- a/src/views/Chores/UserSwitcher.jsx
+++ b/src/views/Chores/UserSwitcher.jsx
@@ -2,16 +2,18 @@ import { SupervisorAccount } from '@mui/icons-material'
import { Avatar, Box, Button, Sheet, Typography } from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
import UserModal from '../Modals/Inputs/UserModal'
const UserSwitcher = () => {
- const {
- impersonatedUser,
+ const { t } = useTranslation(['chores', 'common'])
+ const {
+ impersonatedUser,
isImpersonating,
- startImpersonation,
+ startImpersonation,
stopImpersonation,
- canImpersonate
+ canImpersonate,
} = useImpersonateUser()
const { data: userProfile } = useUserProfile()
const [isModalOpen, setIsModalOpen] = useState(false)
@@ -50,17 +52,19 @@ const UserSwitcher = () => {
justifyContent: 'flex-start',
gap: 1,
}}
- >
+ >
- View tasks as
+
+ {t('chores:sidepanel.userSwitcher.title')}
+
- Switch to user view
+ {t('chores:sidepanel.userSwitcher.switchTitle')}
- Tasks will be filtered to show only assignments for selected user
+ {t('chores:sidepanel.userSwitcher.switchDescription')}
{
onClick={() => setIsModalOpen(true)}
size='sm'
>
- Choose User
+ {t('chores:sidepanel.userSwitcher.chooseUser')}
{
}}
>
- View tasks as
+
+ {t('chores:sidepanel.userSwitcher.title')}
+
@@ -153,7 +159,7 @@ const UserSwitcher = () => {
setIsModalOpen(true)
}}
>
- Change User
+ {t('chores:sidepanel.userSwitcher.changeUser')}
{
stopImpersonation()
}}
>
- Cancel
+ {t('common:actions.cancel')}
diff --git a/src/views/Chores/components/ChoreModals.jsx b/src/views/Chores/components/ChoreModals.jsx
index 2a2a6e8e..da6aa243 100644
--- a/src/views/Chores/components/ChoreModals.jsx
+++ b/src/views/Chores/components/ChoreModals.jsx
@@ -3,6 +3,7 @@ import NudgeModal from '../../Modals/Inputs/NudgeModal'
import SelectModal from '../../Modals/Inputs/SelectModal'
import TextModal from '../../Modals/Inputs/TextModal'
import WriteNFCModal from '../../Modals/Inputs/WriteNFCModal'
+import { useTranslation } from 'react-i18next'
const ChoreModals = ({
activeModal,
@@ -15,6 +16,7 @@ const ChoreModals = ({
onNudge,
onClose,
}) => {
+ const { t } = useTranslation(['chores', 'common'])
return (
<>
{activeModal === 'changeDueDate' && modalChore && (
@@ -22,7 +24,7 @@ const ChoreModals = ({
isOpen={true}
key={'changeDueDate' + modalChore.id}
current={modalChore.nextDueDate}
- title='Change due date'
+ title={t('chores:actions.changeDueDate')}
onClose={onClose}
onSave={onChangeDueDate}
/>
@@ -33,7 +35,7 @@ const ChoreModals = ({
isOpen={true}
key={'completedInPast' + modalChore.id}
current={modalChore.nextDueDate}
- title='Save Chore that you completed in the past'
+ title={t('chores:actions.completeInPast')}
onClose={onClose}
onSave={onCompleteWithPastDate}
/>
@@ -44,8 +46,8 @@ const ChoreModals = ({
isOpen={true}
options={membersData?.res || []}
displayKey='displayName'
- title='Delegate to someone else'
- placeholder='Select a performer'
+ title={t('chores:actions.delegate')}
+ placeholder={t('chores:actions.selectPerformer')}
onClose={onClose}
onSave={selected => onAssigneeChange(selected.id)}
/>
@@ -54,9 +56,9 @@ const ChoreModals = ({
{activeModal === 'completeWithNote' && modalChore && (
)}
diff --git a/src/views/Chores/components/MultiSelectToolbar.jsx b/src/views/Chores/components/MultiSelectToolbar.jsx
index 975c8280..43f80d4e 100644
--- a/src/views/Chores/components/MultiSelectToolbar.jsx
+++ b/src/views/Chores/components/MultiSelectToolbar.jsx
@@ -9,6 +9,7 @@ import {
SkipNext,
} from '@mui/icons-material'
import { Box, Button, Divider, Typography } from '@mui/joy'
+import { useTranslation } from 'react-i18next'
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
const MultiSelectToolbar = ({
@@ -23,6 +24,7 @@ const MultiSelectToolbar = ({
showKeyboardShortcuts,
selectAllDisabled,
}) => {
+ const { t } = useTranslation(['chores', 'common'])
return (
- {selectedCount} task{selectedCount !== 1 ? 's' : ''} selected
+ {selectedCount === 1
+ ? t('chores:main.selectedSingle', {
+ count: selectedCount,
+ })
+ : t('chores:main.selectedMultiple', {
+ count: selectedCount,
+ })}
@@ -103,9 +111,9 @@ const MultiSelectToolbar = ({
'--Button-paddingInline': '0.75rem',
position: 'relative',
}}
- title='Select all visible tasks (Ctrl+A)'
+ title={t('chores:main.shortcuts.selectAllVisible')}
>
- All
+ {t('chores:main.all')}
{showKeyboardShortcuts && (
- {selectedCount === 0 ? 'Close' : 'Clear'}
+ {selectedCount === 0
+ ? t('common:actions.close')
+ : t('chores:main.clear')}
{showKeyboardShortcuts && (
- Complete
+ {t('common:actions.complete')}
{showKeyboardShortcuts && selectedCount > 0 && (
- Skip
+ {t('common:actions.skip')}
{showKeyboardShortcuts && selectedCount > 0 && (
- Archive
+ {t('common:actions.archive')}
{showKeyboardShortcuts && selectedCount > 0 && (
- Delete
+ {t('common:actions.delete')}
{showKeyboardShortcuts && selectedCount > 0 && (
{
+ const { t } = useTranslation('common')
return (
{
+ const { t } = useTranslation(['chores', 'common'])
const queryClient = useQueryClient()
const archiveChore = useArchiveChore()
const startChore = useStartChore()
@@ -64,10 +66,10 @@ export const useChoreActions = ({
queryClient.invalidateQueries({ queryKey: ['chores'] })
const undoableActions = {
- completed: 'Task completed',
- approved: 'Task approved',
- rejected: 'Task rejected',
- skipped: 'Task skipped',
+ completed: t('chores:actionFeedback.undoable.completed'),
+ approved: t('chores:actionFeedback.undoable.approved'),
+ rejected: t('chores:actionFeedback.undoable.rejected'),
+ skipped: t('chores:actionFeedback.undoable.skipped'),
}
if (undoableActions[event]) {
@@ -79,13 +81,13 @@ export const useChoreActions = ({
if (undoResponse.ok) {
refetchChores()
const undoMessages = {
- completed: 'Task completion has been undone.',
- approved: 'Task approval has been undone.',
- rejected: 'Task rejection has been undone.',
- skipped: 'Task skip has been undone.',
+ completed: t('chores:actionFeedback.undoDone.completed'),
+ approved: t('chores:actionFeedback.undoDone.approved'),
+ rejected: t('chores:actionFeedback.undoDone.rejected'),
+ skipped: t('chores:actionFeedback.undoDone.skipped'),
}
showUndo({
- title: 'Undo Successful',
+ title: t('chores:actionFeedback.undoSuccessTitle'),
message: undoMessages[event],
})
} else {
@@ -93,8 +95,8 @@ export const useChoreActions = ({
}
} catch (error) {
showError({
- title: 'Undo Failed',
- message: 'Unable to undo the action. Please try again.',
+ title: t('chores:actionFeedback.undoFailedTitle'),
+ message: t('chores:actionFeedback.undoFailedMessage'),
})
}
},
@@ -105,38 +107,38 @@ export const useChoreActions = ({
const notifications = {
rescheduled: {
type: 'success',
- title: 'Task Rescheduled',
- message: 'The task due date has been updated successfully.',
+ title: t('chores:actionFeedback.notifications.rescheduledTitle'),
+ message: t('chores:actionFeedback.notifications.rescheduledMessage'),
},
'due-date-removed': {
type: 'success',
- title: 'Task Unplanned',
- message: 'The task is now unplanned and has no due date.',
+ title: t('chores:actionFeedback.notifications.dueDateRemovedTitle'),
+ message: t('chores:actionFeedback.notifications.dueDateRemovedMessage'),
},
unarchive: {
type: 'success',
- title: 'Task Restored',
- message: 'The task has been restored and is now active.',
+ title: t('chores:actionFeedback.notifications.restoredTitle'),
+ message: t('chores:actionFeedback.notifications.restoredMessage'),
},
archive: {
type: 'success',
- title: 'Task Archived',
- message: 'The task has been archived and hidden from the active list.',
+ title: t('chores:actionFeedback.notifications.archivedTitle'),
+ message: t('chores:actionFeedback.notifications.archivedMessage'),
},
started: {
type: 'success',
- title: 'Task Started',
- message: 'The task has been marked as started.',
+ title: t('chores:actionFeedback.notifications.startedTitle'),
+ message: t('chores:actionFeedback.notifications.startedMessage'),
},
paused: {
type: 'warning',
- title: 'Task Paused',
- message: 'The task has been paused.',
+ title: t('chores:actionFeedback.notifications.pausedTitle'),
+ message: t('chores:actionFeedback.notifications.pausedMessage'),
},
deleted: {
type: 'success',
- title: 'Task Deleted',
- message: 'The task has been deleted.',
+ title: t('chores:actionFeedback.notifications.deletedTitle'),
+ message: t('chores:actionFeedback.notifications.deletedMessage'),
},
}
@@ -206,8 +208,8 @@ export const useChoreActions = ({
refetchChores() // Network failed, revert to truth
if (error?.queued) {
showError({
- title: 'Update Failed',
- message: 'Request will be reattempt when you are online',
+ title: t('chores:actionFeedback.errors.failedToUpdate'),
+ message: t('chores:actionFeedback.errors.offlineRetry'),
})
} else {
showError({
@@ -227,8 +229,9 @@ export const useChoreActions = ({
},
onError: error => {
showError({
- title: 'Failed to start',
- message: error.message || 'Unable to start chore',
+ title: t('chores:actionFeedback.errors.failedToStart'),
+ message:
+ error.message || t('chores:actionFeedback.errors.unableToStart'),
})
},
})
@@ -243,8 +246,9 @@ export const useChoreActions = ({
},
onError: error => {
showError({
- title: 'Failed to pause',
- message: error.message || 'Unable to pause chore',
+ title: t('chores:actionFeedback.errors.failedToPause'),
+ message:
+ error.message || t('chores:actionFeedback.errors.unableToPause'),
})
},
})
@@ -259,8 +263,10 @@ export const useChoreActions = ({
}
} catch (error) {
showError({
- title: 'Failed to approve',
- message: error.message || 'Unable to approve chore',
+ title: t('chores:actionFeedback.errors.failedToApprove'),
+ message:
+ error.message ||
+ t('chores:actionFeedback.errors.unableToApprove'),
})
}
break
@@ -274,8 +280,10 @@ export const useChoreActions = ({
}
} catch (error) {
showError({
- title: 'Failed to reject',
- message: error.message || 'Unable to reject chore',
+ title: t('chores:actionFeedback.errors.failedToReject'),
+ message:
+ error.message ||
+ t('chores:actionFeedback.errors.unableToReject'),
})
}
break
@@ -283,10 +291,10 @@ export const useChoreActions = ({
case 'delete':
setConfirmModelConfig({
isOpen: true,
- title: 'Delete Chore',
- confirmText: 'Delete',
- cancelText: 'Cancel',
- message: 'Are you sure you want to delete this chore?',
+ title: t('chores:actionFeedback.errors.deleteTitle'),
+ confirmText: t('common:actions.delete'),
+ cancelText: t('common:actions.cancel'),
+ message: t('chores:actionFeedback.errors.deleteMessage'),
onClose: async isConfirmed => {
if (isConfirmed === true) {
try {
@@ -300,13 +308,13 @@ export const useChoreActions = ({
updateChoreInState(chore.id, 'deleted')
setFilteredChores(newFilteredChores)
showSuccess({
- title: 'Task Deleted',
- message: 'The task has been deleted successfully.',
+ title: t('chores:actionFeedback.notifications.deletedTitle'),
+ message: t('chores:actionFeedback.notifications.deletedMessage'),
})
}
} catch (error) {
showError({
- title: 'Failed to delete',
+ title: t('chores:actionFeedback.errors.deleteFailed'),
message: error,
})
}
@@ -326,8 +334,10 @@ export const useChoreActions = ({
},
onError: error => {
showError({
- title: 'Failed to archive',
- message: error.message || 'Unable to archive chore',
+ title: t('chores:actionFeedback.errors.failedToArchive'),
+ message:
+ error.message ||
+ t('chores:actionFeedback.errors.unableToArchive'),
})
reject(error)
},
@@ -346,7 +356,7 @@ export const useChoreActions = ({
}
} catch (error) {
showError({
- title: 'Failed to skip',
+ title: t('chores:actionFeedback.errors.failedToSkip'),
message: error,
})
}
@@ -366,9 +376,11 @@ export const useChoreActions = ({
showError({
title:
extraData.date === null
- ? 'Failed to remove due date'
- : 'Failed to reschedule',
- message: error.message || 'Unable to update due date',
+ ? t('chores:actionFeedback.errors.failedRemoveDueDate')
+ : t('chores:actionFeedback.errors.failedReschedule'),
+ message:
+ error.message ||
+ t('chores:actionFeedback.errors.unableUpdateDueDate'),
})
}
} else {
@@ -492,16 +504,17 @@ export const useChoreActions = ({
if (response.ok) {
const data = await response.json()
showSuccess({
- title: 'Nudge Sent!',
- message: data.message || 'Nudge sent successfully',
+ title: t('chores:actionFeedback.nudgeSentTitle'),
+ message: data.message || t('chores:actionFeedback.nudgeSentMessage'),
})
} else {
throw new Error('Failed to send nudge')
}
} catch (error) {
showError({
- title: 'Failed to Send Nudge',
- message: error.message || 'Unable to send nudge at this time',
+ title: t('chores:actionFeedback.errors.nudgeFailedTitle'),
+ message:
+ error.message || t('chores:actionFeedback.errors.nudgeFailedMessage'),
})
} finally {
closeModal()
@@ -516,10 +529,12 @@ export const useChoreActions = ({
setConfirmModelConfig({
isOpen: true,
- title: 'Complete Tasks',
- confirmText: 'Complete',
- cancelText: 'Cancel',
- message: `Mark ${selectedData.length} task${selectedData.length > 1 ? 's' : ''} as completed?`,
+ title: t('chores:actionFeedback.bulk.completeTitle'),
+ confirmText: t('common:actions.complete'),
+ cancelText: t('common:actions.cancel'),
+ message: t('chores:actionFeedback.bulk.completeConfirm', {
+ count: selectedData.length,
+ }),
onClose: async isConfirmed => {
if (isConfirmed === true) {
try {
@@ -544,15 +559,19 @@ export const useChoreActions = ({
if (completedTasks.length > 0) {
showSuccess({
- title: '✅ Tasks Completed',
- message: `Successfully completed ${completedTasks.length} task${completedTasks.length > 1 ? 's' : ''}.`,
+ title: t('chores:actionFeedback.bulk.completeSuccessTitle'),
+ message: t('chores:actionFeedback.bulk.completeSuccess', {
+ count: completedTasks.length,
+ }),
})
}
if (failedTasks.length > 0) {
showError({
- title: 'Some Tasks Failed',
- message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be completed.`,
+ title: t('chores:actionFeedback.bulk.someFailedTitle'),
+ message: t('chores:actionFeedback.bulk.completeFailed', {
+ count: failedTasks.length,
+ }),
})
}
@@ -560,8 +579,8 @@ export const useChoreActions = ({
clearSelection()
} catch (error) {
showError({
- title: 'Bulk Complete Failed',
- message: 'An unexpected error occurred. Please try again.',
+ title: t('chores:actionFeedback.bulk.completeUnexpectedTitle'),
+ message: t('chores:main.unexpectedError'),
})
}
}
@@ -576,10 +595,12 @@ export const useChoreActions = ({
setConfirmModelConfig({
isOpen: true,
- title: 'Archive Tasks',
- confirmText: 'Archive',
- cancelText: 'Cancel',
- message: `Archive ${selectedData.length} task${selectedData.length > 1 ? 's' : ''}?`,
+ title: t('chores:actionFeedback.bulk.archiveTitle'),
+ confirmText: t('common:actions.archive'),
+ cancelText: t('common:actions.cancel'),
+ message: t('chores:actionFeedback.bulk.archiveConfirm', {
+ count: selectedData.length,
+ }),
onClose: async isConfirmed => {
if (isConfirmed === true) {
try {
@@ -608,22 +629,26 @@ export const useChoreActions = ({
}
if (archivedTasks.length > 0) {
showSuccess({
- title: '📦 Tasks Archived',
- message: `Successfully archived ${archivedTasks.length} task${archivedTasks.length > 1 ? 's' : ''}.`,
+ title: t('chores:actionFeedback.bulk.archiveSuccessTitle'),
+ message: t('chores:actionFeedback.bulk.archiveSuccess', {
+ count: archivedTasks.length,
+ }),
})
}
if (failedTasks.length > 0) {
showError({
- title: 'Some Tasks Failed',
- message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be archived.`,
+ title: t('chores:actionFeedback.bulk.someFailedTitle'),
+ message: t('chores:actionFeedback.bulk.archiveFailed', {
+ count: failedTasks.length,
+ }),
})
}
refetchChores()
clearSelection()
} catch (error) {
showError({
- title: 'Bulk Archive Failed',
- message: 'An unexpected error occurred. Please try again.',
+ title: t('chores:actionFeedback.bulk.archiveUnexpectedTitle'),
+ message: t('chores:main.unexpectedError'),
})
}
}
@@ -638,10 +663,12 @@ export const useChoreActions = ({
setConfirmModelConfig({
isOpen: true,
- title: 'Delete Tasks',
- confirmText: 'Delete',
- cancelText: 'Cancel',
- message: `Delete ${selectedData.length} task${selectedData.length > 1 ? 's' : ''}?\n\nThis action cannot be undone.`,
+ title: t('chores:actionFeedback.bulk.deleteTitle'),
+ confirmText: t('common:actions.delete'),
+ cancelText: t('common:actions.cancel'),
+ message: t('chores:actionFeedback.bulk.deleteConfirm', {
+ count: selectedData.length,
+ }),
onClose: async isConfirmed => {
if (isConfirmed === true) {
try {
@@ -659,8 +686,10 @@ export const useChoreActions = ({
if (deletedTasks.length > 0) {
showSuccess({
- title: '🗑️ Tasks Deleted',
- message: `Successfully deleted ${deletedTasks.length} task${deletedTasks.length > 1 ? 's' : ''}.`,
+ title: t('chores:actionFeedback.bulk.deleteSuccessTitle'),
+ message: t('chores:actionFeedback.bulk.deleteSuccess', {
+ count: deletedTasks.length,
+ }),
})
const deletedIds = new Set(deletedTasks.map(c => c.id))
@@ -674,16 +703,18 @@ export const useChoreActions = ({
if (failedTasks.length > 0) {
showError({
- title: 'Some Tasks Failed',
- message: `${failedTasks.length} task${failedTasks.length > 1 ? 's' : ''} could not be deleted.`,
+ title: t('chores:actionFeedback.bulk.someFailedTitle'),
+ message: t('chores:actionFeedback.bulk.deleteFailed', {
+ count: failedTasks.length,
+ }),
})
}
refetchChores()
clearSelection()
} catch (error) {
showError({
- title: 'Bulk Delete Failed',
- message: 'An unexpected error occurred. Please try again.',
+ title: t('chores:actionFeedback.bulk.deleteUnexpectedTitle'),
+ message: t('chores:main.unexpectedError'),
})
}
}
@@ -698,10 +729,10 @@ export const useChoreActions = ({
setConfirmModelConfig({
isOpen: true,
- title: 'Skip Tasks',
- confirmText: 'Skip',
- cancelText: 'Cancel',
- message: `Skip ${selectedData.length} task${selectedData.length > 1 ? 's' : ''} to next due date?`,
+ title: t('common:actions.skip'),
+ confirmText: t('common:actions.skip'),
+ cancelText: t('common:actions.cancel'),
+ message: t('chores:actions.skipToNextDueDate') + ` (${selectedData.length})`,
onClose: async isConfirmed => {
if (isConfirmed === true) {
try {
@@ -719,8 +750,8 @@ export const useChoreActions = ({
if (skippedTasks.length > 0) {
showSuccess({
- title: '⏭️ Tasks Skipped',
- message: `Successfully skipped ${skippedTasks.length} task${skippedTasks.length > 1 ? 's' : ''}.`,
+ title: t('chores:actionFeedback.undoable.skipped'),
+ message: t('chores:actionFeedback.undoable.skipped'),
undoAction: async () => {
try {
for (const chore of skippedTasks) {
@@ -728,13 +759,13 @@ export const useChoreActions = ({
}
refetchChores()
showUndo({
- title: 'Undo Successful',
- message: `Undo skip for ${skippedTasks.length} task${skippedTasks.length > 1 ? 's' : ''}.`,
+ title: t('chores:actionFeedback.undoSuccessTitle'),
+ message: t('chores:actionFeedback.undoDone.skipped'),
})
} catch (error) {
showError({
- title: 'Undo Failed',
- message: 'Unable to undo the action. Please try again.',
+ title: t('chores:actionFeedback.undoFailedTitle'),
+ message: t('chores:actionFeedback.undoFailedMessage'),
})
}
},
@@ -743,8 +774,8 @@ export const useChoreActions = ({
if (failedTasks.length > 0) {
showError({
- title: 'Some Tasks Failed',
- message: `${failedTasks.length > 1 ? 's' : ''} could not be skipped.`,
+ title: t('chores:actionFeedback.bulk.someFailedTitle'),
+ message: t('chores:main.unexpectedError'),
})
}
@@ -752,8 +783,8 @@ export const useChoreActions = ({
clearSelection()
} catch (error) {
showError({
- title: 'Bulk Skip Failed',
- message: 'An unexpected error occurred. Please try again.',
+ title: t('common:actions.skip'),
+ message: t('chores:main.unexpectedError'),
})
}
}
diff --git a/src/views/Filters/FilterView.jsx b/src/views/Filters/FilterView.jsx
index c097057f..d97479c7 100644
--- a/src/views/Filters/FilterView.jsx
+++ b/src/views/Filters/FilterView.jsx
@@ -19,6 +19,7 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useMemo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import {
@@ -51,16 +52,17 @@ const FilterCardContent = ({
taskCount = 0,
overdueCount = 0,
onToggleActions,
+ t,
}) => {
// Get condition labels for display
const getConditionSummary = () => {
if (!filter.conditions || filter.conditions.length === 0) {
- return 'No conditions'
+ return t('filters:view.noConditions')
}
if (filter.conditions.length === 1) {
- return '1 condition'
+ return t('filters:view.oneCondition')
}
- return `${filter.conditions.length} conditions`
+ return t('filters:view.manyConditions', { count: filter.conditions.length })
}
return (
@@ -174,7 +176,7 @@ const FilterCardContent = ({
color: overdueCount > 0 ? 'danger.500' : 'primary.500',
}}
>
- {taskCount} tasks
+ {t('filters:view.tasksCount', { count: taskCount })}
{overdueCount > 0 && (
@@ -188,7 +190,7 @@ const FilterCardContent = ({
px: 0.75,
}}
>
- {overdueCount} overdue
+ {t('filters:view.overdueCount', { count: overdueCount })}
)}
@@ -219,7 +221,7 @@ const FilterCardContent = ({
color: 'success.600',
}}
>
- Used {filter.usageCount}x
+ {t('filters:view.usedCount', { count: filter.usageCount })}
)}
@@ -244,6 +246,7 @@ const FilterCardContent = ({
}
const FilterView = () => {
+ const { t } = useTranslation(['filters', 'common'])
const navigate = useNavigate()
const { data: userProfile } = useUserProfile()
const { data: chores = { res: [] } } = useChores(false)
@@ -329,11 +332,11 @@ const FilterView = () => {
const filter = savedFilters.find(f => f.id === id)
setConfirmationModel({
isOpen: true,
- title: 'Delete Filter',
- message: `Are you sure you want to delete "${filter?.name}"? This cannot be undone.`,
- confirmText: 'Delete',
+ title: t('filters:view.deleteTitle'),
+ message: t('filters:view.deleteMessage', { name: filter?.name }),
+ confirmText: t('common:actions.delete'),
color: 'danger',
- cancelText: 'Cancel',
+ cancelText: t('common:actions.cancel'),
onClose: confirmed => {
if (confirmed === true) {
handleDeleteFilter(id)
@@ -402,11 +405,10 @@ const FilterView = () => {
level='h3'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Filters
+ {t('filters:view.title')}
- Save your favorite filter combinations for quick access. Create
- custom views to organize and find tasks faster.
+ {t('filters:view.description')}
@@ -434,10 +436,10 @@ const FilterView = () => {
level='title-lg'
sx={{ mb: 1, color: 'text.secondary' }}
>
- No saved filters yet
+ {t('filters:view.emptyTitle')}
- Create custom filters to quickly access your most used chore
+ {t('filters:view.emptyDescription')}
) : (
@@ -483,6 +485,9 @@ const FilterView = () => {
)}
{filter.isPinned ? 'Unpin' : 'Pin'}
+ {filter.isPinned
+ ? t('filters:view.unpin')
+ : t('filters:view.pin')}
@@ -501,7 +506,7 @@ const FilterView = () => {
>
- Edit
+ {t('common:actions.edit')}
@@ -526,7 +531,7 @@ const FilterView = () => {
sx={{ mt: 0.5 }}
color='danger'
>
- Delete
+ {t('common:actions.delete')}
@@ -535,6 +540,7 @@ const FilterView = () => {
}
>
{
console.log(
'Toggling actions for filter:',
diff --git a/src/views/History/ChoreHistory.jsx b/src/views/History/ChoreHistory.jsx
index 17ed4107..f6a9223b 100644
--- a/src/views/History/ChoreHistory.jsx
+++ b/src/views/History/ChoreHistory.jsx
@@ -21,6 +21,7 @@ import EditIcon from '@mui/icons-material/Edit'
import { Box, Button, Card, Container, Grid, Sheet, Typography } from '@mui/joy'
import moment from 'moment'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { Link, useParams } from 'react-router-dom'
import { useLocalization } from '../../contexts/LocalizationContext'
import useConfirmationModal from '../../hooks/useConfirmationModal'
@@ -39,6 +40,7 @@ import NoteViewerModal from '../Modals/Inputs/NoteViewerModal'
import HistoryCard from './HistoryCard'
const ChoreHistory = () => {
+ const { t } = useTranslation(['history', 'common'])
const [userHistory, setUserHistory] = useState([])
const [historyInfo, setHistoryInfo] = useState([])
const { choreId } = useParams()
@@ -60,16 +62,16 @@ const ChoreHistory = () => {
const handleDelete = historyEntry => {
showConfirmation(
- `Are you sure you want to delete this history record?`,
- 'Delete History Record',
+ t('history:delete.confirm'),
+ t('history:delete.title'),
() => {
deleteChoreHistory.mutate({
choreId,
historyId: historyEntry.id,
})
},
- 'Delete',
- 'Cancel',
+ t('common:actions.delete'),
+ t('common:actions.cancel'),
'danger',
)
}
@@ -122,42 +124,50 @@ const ChoreHistory = () => {
const historyInfo = [
{
icon: ,
- text: 'All Completed',
- subtext: `${histories.filter(h => h.status === ChoreHistoryStatus.COMPLETED || h.status === ChoreHistoryStatus.SKIPPED).length} times`,
+ text: t('history:stats.allCompleted'),
+ subtext: t('history:stats.times', {
+ count: histories.filter(
+ h =>
+ h.status === ChoreHistoryStatus.COMPLETED ||
+ h.status === ChoreHistoryStatus.SKIPPED,
+ ).length,
+ }),
},
{
icon: ,
- text: 'Average Timing',
+ text: t('history:stats.averageTiming'),
subtext: moment.duration(averageDelayMoment).isValid()
? moment.duration(averageDelayMoment).humanize()
- : 'On time',
+ : t('history:stats.onTime'),
},
{
icon: ,
- text: 'Longest Delay',
+ text: t('history:stats.longestDelay'),
subtext: moment.duration(maxDelayMoment).isValid()
? moment.duration(maxDelayMoment).humanize()
- : 'Never late',
+ : t('history:stats.neverLate'),
},
{
icon: ,
- text: 'Completed Most',
+ text: t('history:stats.completedMost'),
subtext: `${
performers.find(p => p.userId === Number(userCompletedByMost))
- ?.displayName || 'Unknown'
+ ?.displayName || t('history:common.unknown')
}`,
},
{
icon: ,
- text: 'Members Involved',
- subtext: `${Object.keys(userHistories).length} members`,
+ text: t('history:stats.membersInvolved'),
+ subtext: t('history:stats.membersCount', {
+ count: Object.keys(userHistories).length,
+ }),
},
{
icon: ,
- text: 'Last Completed',
+ text: t('history:stats.lastCompleted'),
subtext: `${
performers.find(p => p.userId === Number(histories[0].completedBy))
- ?.displayName || 'Unknown'
+ ?.displayName || t('history:common.unknown')
}`,
},
]
@@ -191,14 +201,13 @@ const ChoreHistory = () => {
/>
- No History Yet
+ {t('history:empty.title')}
- You haven't completed any tasks. Once you start finishing tasks,
- they'll show up here.
+ {t('history:empty.description')}
- Go back to chores
+ {t('history:empty.backToChores')}
)
@@ -214,8 +223,8 @@ const ChoreHistory = () => {
- Task Summary
+ >
+ {t('history:summaryTitle')}
@@ -291,7 +300,7 @@ const ChoreHistory = () => {
level='title-md'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Task Activity
+ {t('history:activityTitle')}
{
>
- Edit
+ {t('common:actions.edit')}
@@ -353,7 +362,7 @@ const ChoreHistory = () => {
>
- Delete
+ {t('common:actions.delete')}
@@ -369,7 +378,9 @@ const ChoreHistory = () => {
onViewNote={notes => {
setNoteViewerConfig({
isOpen: true,
- title: `Updated at ${fmt.dateTime(historyEntry.updatedAt)}`,
+ title: t('history:common.updatedAt', {
+ date: fmt.dateTime(historyEntry.updatedAt),
+ }),
content: notes,
onClose: () => setNoteViewerConfig({ isOpen: false }),
})
@@ -411,8 +422,8 @@ const ChoreHistory = () => {
setIsEditModalOpen(false)
setEditHistory(null)
showSuccess({
- title: 'History Updated',
- message: `The history record has been updated successfully.`,
+ title: t('history:messages.updatedTitle'),
+ message: t('history:messages.updatedMessage'),
})
},
onError: error => {
@@ -433,8 +444,8 @@ const ChoreHistory = () => {
setIsEditModalOpen(false)
setEditHistory(null)
showSuccess({
- title: 'History Deleted',
- message: `The history record has been deleted successfully.`,
+ title: t('history:messages.deletedTitle'),
+ message: t('history:messages.deletedMessage'),
})
},
},
diff --git a/src/views/History/HistoryCard.jsx b/src/views/History/HistoryCard.jsx
index cd81f817..96150eaf 100644
--- a/src/views/History/HistoryCard.jsx
+++ b/src/views/History/HistoryCard.jsx
@@ -15,10 +15,11 @@ import {
} from '@mui/icons-material'
import { Avatar, Box, Chip, Grid, IconButton, Typography } from '@mui/joy'
import moment from 'moment'
+import { useTranslation } from 'react-i18next'
import { useLocalization } from '../../contexts/LocalizationContext'
import { TASK_COLOR } from '../../utils/Colors.jsx'
-const getCompletedChip = historyEntry => {
+const getCompletedChip = (historyEntry, t) => {
if (historyEntry.status === 0 || historyEntry.status === 5 || historyEntry.status === 6) {
return null
}
@@ -48,7 +49,7 @@ const getCompletedChip = historyEntry => {
sx={{ backgroundColor: TASK_COLOR.COMPLETED, color: 'white' }}
startDecorator={ }
>
- On Time
+ {t ? t('history:status.onTime') : 'On Time'}
)
} else if (performedAt.isBefore(dueDate)) {
@@ -59,7 +60,7 @@ const getCompletedChip = historyEntry => {
sx={{ backgroundColor: TASK_COLOR.SCHEDULED, color: 'white' }}
startDecorator={ }
>
- Early
+ {t ? t('history:status.early') : 'Early'}
)
} else {
@@ -70,7 +71,7 @@ const getCompletedChip = historyEntry => {
sx={{ backgroundColor: TASK_COLOR.LATE, color: 'white' }}
startDecorator={ }
>
- Late
+ {t ? t('history:status.late') : 'Late'}
)
}
@@ -97,6 +98,7 @@ const HistoryCard = ({
onToggleActions,
onViewNote,
}) => {
+ const { t } = useTranslation('history')
const { fmt } = useLocalization()
const performer = performers.find(p => p.userId === historyEntry.completedBy)
const assignedTo = performers.find(p => p.userId === historyEntry.assignedTo)
@@ -185,20 +187,20 @@ const HistoryCard = ({
}}
>
{historyEntry.status === 0
- ? 'In Progress'
+ ? t('status.inProgress')
: historyEntry.status === 1
- ? 'Completed'
+ ? t('status.completed')
: historyEntry.status === 2
- ? 'Skipped'
+ ? t('status.skipped')
: historyEntry.status === 3
- ? 'Pending Approval'
+ ? t('status.pendingApproval')
: historyEntry.status === 4
- ? 'Rejected'
+ ? t('status.rejected')
: historyEntry.status === 5
- ? 'Missed'
+ ? t('status.missed')
: historyEntry.status === 6
- ? 'Rescheduled'
- : 'Completed'}
+ ? t('status.rescheduled')
+ : t('status.completed')}
}>
@@ -208,7 +210,7 @@ const HistoryCard = ({
- {getCompletedChip(historyEntry)}
+ {getCompletedChip(historyEntry, t)}
@@ -254,7 +256,7 @@ const HistoryCard = ({
/>
}
>
- {performer?.displayName || 'Unknown'}
+ {performer?.displayName || t('common.unknown')}
)}
@@ -263,10 +265,10 @@ const HistoryCard = ({
}
- >
- Assigned to {assignedTo.displayName}
+ color='neutral'
+ startDecorator={ }
+ >
+ {t('assignedTo', { name: assignedTo.displayName })}
)}
@@ -286,7 +288,7 @@ const HistoryCard = ({
onViewNote?.(historyEntry.notes)
}}
>
- Note
+ {t('note')}
)}
{/* add a duration chip if we have duration */}
@@ -307,8 +309,7 @@ const HistoryCard = ({
color='success'
startDecorator={ }
>
- {historyEntry.points} pt
- {historyEntry.points > 1 ? 's' : ''}
+ {t('points', { count: historyEntry.points })}
)}
diff --git a/src/views/History/InfoCard.jsx b/src/views/History/InfoCard.jsx
index bf1ada88..4ab6a79a 100644
--- a/src/views/History/InfoCard.jsx
+++ b/src/views/History/InfoCard.jsx
@@ -4,8 +4,10 @@ import Card from '@mui/joy/Card'
import CardContent from '@mui/joy/CardContent'
import Typography from '@mui/joy/Typography'
import * as React from 'react'
+import { useTranslation } from 'react-i18next'
function InfoCard() {
+ const { t } = useTranslation('history')
return (
@@ -16,8 +18,8 @@ function InfoCard() {
}}
/>
- You've completed
- 12345 Chores
+ {t('info.completedTitle')}
+ {t('info.completedValue')}
)
diff --git a/src/views/Labels/LabelView.jsx b/src/views/Labels/LabelView.jsx
index 61912079..9720d609 100644
--- a/src/views/Labels/LabelView.jsx
+++ b/src/views/Labels/LabelView.jsx
@@ -11,6 +11,7 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import LabelModal from '../Modals/Inputs/LabelModal'
import {
@@ -30,7 +31,7 @@ import { getSafeBottomStyles } from '../../utils/SafeAreaUtils'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import { useLabels } from './LabelQueries'
-const LabelCardContent = ({ label, currentUserId, onToggleActions }) => {
+const LabelCardContent = ({ label, currentUserId, onToggleActions, t }) => {
// Check if current user owns this label
const isOwnedByCurrentUser = label.created_by === currentUserId
@@ -123,7 +124,7 @@ const LabelCardContent = ({ label, currentUserId, onToggleActions }) => {
fontWeight: 'md',
}}
>
- Shared
+ {t('labelsView:shared')}
)}
@@ -148,6 +149,7 @@ const LabelCardContent = ({ label, currentUserId, onToggleActions }) => {
}
const LabelView = () => {
+ const { t } = useTranslation(['labelsView', 'common'])
const { data: labels, isLabelsLoading, isError } = useLabels()
const { data: userProfile } = useUserProfile()
@@ -172,14 +174,11 @@ const LabelView = () => {
const handleDeleteClicked = id => {
setConfirmationModel({
isOpen: true,
- title: 'Delete Label',
-
- message:
- 'Are you sure you want to delete this label? This will remove the label from all tasks.',
-
- confirmText: 'Delete',
+ title: t('labelsView:deleteTitle'),
+ message: t('labelsView:deleteMessage'),
+ confirmText: t('common:actions.delete'),
color: 'danger',
- cancelText: 'Cancel',
+ cancelText: t('common:actions.cancel'),
onClose: confirmed => {
if (confirmed === true) {
handleDeleteLabel(id)
@@ -229,7 +228,7 @@ const LabelView = () => {
if (isError) {
return (
- Failed to load labels. Please try again.
+ {t('labelsView:loadFailed')}
)
}
@@ -243,12 +242,10 @@ const LabelView = () => {
level='h3'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Labels
+ {t('labelsView:title')}
- Manage your labels and organize your tasks effectively. Labels will
- be automatically shared with your circle if they are used on a
- shared task.
+ {t('labelsView:description')}
@@ -268,7 +265,7 @@ const LabelView = () => {
}}
>
- No labels available. Add a new label to get started.
+ {t('labelsView:empty')}
)}
@@ -301,7 +298,7 @@ const LabelView = () => {
>
- Edit
+ {t('common:actions.edit')}
@@ -320,7 +317,7 @@ const LabelView = () => {
>
- Delete
+ {t('common:actions.delete')}
@@ -331,6 +328,7 @@ const LabelView = () => {
{
if (showMoreInfoId === label.id) {
setShowMoreInfoId(null)
diff --git a/src/views/Modals/EditHistoryModal.jsx b/src/views/Modals/EditHistoryModal.jsx
index 959441c5..28acaa1f 100644
--- a/src/views/Modals/EditHistoryModal.jsx
+++ b/src/views/Modals/EditHistoryModal.jsx
@@ -1,11 +1,13 @@
import { Box, Button, FormLabel, Input } from '@mui/joy'
import moment from 'moment'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useResponsiveModal } from '../../hooks/useResponsiveModal'
import ConfirmationModal from './Inputs/ConfirmationModal'
function EditHistoryModal({ config, historyRecord }) {
+ const { t } = useTranslation(['history', 'common'])
const { ResponsiveModal } = useResponsiveModal()
const [completedDate, setCompletedDate] = useState('')
@@ -39,7 +41,7 @@ function EditHistoryModal({ config, historyRecord }) {
onClose={config?.onClose}
size='lg'
// fullWidth={true}
- title='Edit History'
+ title={t('history:edit.title')}
footer={
- Save
+ {t('common:actions.save')}
- Cancel
+ {t('common:actions.cancel')}
}
>
- Due Date
+ {t('common:labels.dueDate')}
- Completed Date
+ {t('history:edit.completedDate')}
- Note
+ {t('history:edit.note')}
{
if (e.target.value.trim() === '') {
@@ -115,10 +117,10 @@ function EditHistoryModal({ config, historyRecord }) {
}
setIsDeleteModalOpen(false)
},
- title: 'Delete History',
- message: 'Are you sure you want to delete this history?',
- confirmText: 'Delete',
- cancelText: 'Cancel',
+ title: t('history:delete.title'),
+ message: t('history:edit.deleteConfirm'),
+ confirmText: t('common:actions.delete'),
+ cancelText: t('common:actions.cancel'),
}}
/>
diff --git a/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx b/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx
index ee3f68d2..780437a2 100644
--- a/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx
+++ b/src/views/Modals/Inputs/AdvancedFilterBuilder.jsx
@@ -13,6 +13,7 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useMemo, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { FILTER_COLORS } from '../../../utils/Colors'
import { applyFilter } from '../../../utils/FilterEngine'
@@ -30,6 +31,7 @@ const AdvancedFilterBuilder = ({
userProfile = null,
editingFilter = null,
}) => {
+ const { t } = useTranslation(['filters', 'common'])
const { ResponsiveModal } = useResponsiveModal()
const listContainerRef = useRef(null)
const conditionRefs = useRef([])
@@ -151,13 +153,13 @@ const AdvancedFilterBuilder = ({
const handleSave = () => {
if (!filterName.trim()) {
- setError('Please enter a filter name')
+ setError(t('filters:advanced.enterFilterName'))
return
}
// Check for duplicate name, excluding current filter if editing
if (filterNameExists(filterName.trim(), editingFilter?.id)) {
- setError('A filter with this name already exists')
+ setError(t('filters:advanced.duplicateFilterName'))
return
}
@@ -167,7 +169,7 @@ const AdvancedFilterBuilder = ({
})
if (conditions.length === 0 || validConditions.length === 0) {
- setError('Please add at least one filter condition')
+ setError(t('filters:advanced.addAtLeastOneCondition'))
return
}
@@ -198,7 +200,7 @@ const AdvancedFilterBuilder = ({
onChange={(_, newValue) =>
updateCondition(index, 'value', newValue)
}
- placeholder='Select assignees'
+ placeholder={t('filters:advanced.selectAssignees')}
sx={{ width: '100%' }}
slotProps={{
listbox: {
@@ -215,7 +217,9 @@ const AdvancedFilterBuilder = ({
)
return (
- {member?.displayName || member?.username || 'Unknown'}
+ {member?.displayName ||
+ member?.username ||
+ t('filters:advanced.unknown')}
)
})}
@@ -241,7 +245,7 @@ const AdvancedFilterBuilder = ({
onChange={(_, newValue) =>
updateCondition(index, 'value', newValue)
}
- placeholder='Select creators'
+ placeholder={t('filters:advanced.selectCreators')}
sx={{ width: '100%' }}
slotProps={{
listbox: {
@@ -285,7 +289,7 @@ const AdvancedFilterBuilder = ({
onChange={(_, newValue) =>
updateCondition(index, 'value', newValue)
}
- placeholder='Select priorities'
+ placeholder={t('filters:advanced.selectPriorities')}
sx={{ width: '100%' }}
slotProps={{
listbox: {
@@ -326,7 +330,7 @@ const AdvancedFilterBuilder = ({
onChange={(_, newValue) =>
updateCondition(index, 'value', newValue)
}
- placeholder='Select labels'
+ placeholder={t('filters:advanced.selectLabels')}
sx={{ width: '100%' }}
slotProps={{
listbox: {
@@ -342,7 +346,7 @@ const AdvancedFilterBuilder = ({
const label = labels.find(l => String(l.id) === String(value))
return (
- {label?.name || 'Unknown'}
+ {label?.name || t('filters:advanced.unknown')}
)
})}
@@ -365,7 +369,7 @@ const AdvancedFilterBuilder = ({
onChange={(_, newValue) =>
updateCondition(index, 'value', newValue)
}
- placeholder='Select projects'
+ placeholder={t('filters:advanced.selectProjects')}
sx={{ width: '100%' }}
slotProps={{
listbox: {
@@ -380,7 +384,7 @@ const AdvancedFilterBuilder = ({
if (value === 'default')
return (
- Default
+ {t('filters:advanced.defaultProject')}
)
const project = projects.find(
@@ -388,14 +392,14 @@ const AdvancedFilterBuilder = ({
)
return (
- {project?.name || 'Unknown'}
+ {project?.name || t('filters:advanced.unknown')}
)
})}
)}
>
- Default Project
+ {t('filters:advanced.defaultProject')}
{projects
.filter(p => p.id !== 'default')
.map((project, idx) => (
@@ -417,7 +421,7 @@ const AdvancedFilterBuilder = ({
onChange={(_, newValue) =>
updateCondition(index, 'value', newValue)
}
- placeholder='Select statuses'
+ placeholder={t('filters:advanced.selectStatuses')}
sx={{ width: '100%' }}
slotProps={{
listbox: {
@@ -430,24 +434,24 @@ const AdvancedFilterBuilder = ({
{selected.map((selectedElement, idx) => {
const value = selectedElement.value
const statusLabels = {
- 0: 'Active',
- 1: 'Started',
- 2: 'In Progress',
- 3: 'Pending Approval',
+ 0: t('filters:advanced.active'),
+ 1: t('filters:advanced.started'),
+ 2: t('filters:advanced.inProgress'),
+ 3: t('filters:advanced.pendingApproval'),
}
return (
- {statusLabels[value] || 'Unknown'}
+ {statusLabels[value] || t('filters:advanced.unknown')}
)
})}
)}
>
- Active
- Started
- In Progress
- Pending Approval
+ {t('filters:advanced.active')}
+ {t('filters:advanced.started')}
+ {t('filters:advanced.inProgress')}
+ {t('filters:advanced.pendingApproval')}
)
@@ -466,13 +470,13 @@ const AdvancedFilterBuilder = ({
},
}}
>
- Is Overdue
- Is Due Today
- Is Due Tomorrow
- Is Due This Week
- Is Due This Month
- Has No Due Date
- Has Due Date
+ {t('filters:advanced.isOverdue')}
+ {t('filters:advanced.isDueToday')}
+ {t('filters:advanced.isDueTomorrow')}
+ {t('filters:advanced.isDueThisWeek')}
+ {t('filters:advanced.isDueThisMonth')}
+ {t('filters:advanced.hasNoDueDate')}
+ {t('filters:advanced.hasDueDate')}
)
@@ -492,11 +496,15 @@ const AdvancedFilterBuilder = ({
},
}}
>
- Equals
- Greater Than
- Less Than
- Greater Than or Equal
- Less Than or Equal
+ {t('filters:advanced.equals')}
+ {t('filters:advanced.greaterThan')}
+ {t('filters:advanced.lessThan')}
+
+ {t('filters:advanced.greaterThanOrEqual')}
+
+
+ {t('filters:advanced.lessThanOrEqual')}
+
- Cancel
+ {t('common:actions.cancel')}
- Save
+ {t('common:actions.save')}
}
@@ -547,10 +559,10 @@ const AdvancedFilterBuilder = ({
>
- Filter Name
+ {t('filters:advanced.filterName')}
{
setFilterName(e.target.value)
@@ -568,10 +580,10 @@ const AdvancedFilterBuilder = ({
- Description (Optional)
+ {t('filters:advanced.descriptionOptional')}
@@ -746,14 +760,14 @@ const AdvancedFilterBuilder = ({
mb: 1,
}}
>
- Preview
+ {t('filters:advanced.preview')}
- {previewCount} tasks
+ {previewCount} {t('common:labels.tasks').toLowerCase()}
{previewOverdueCount > 0 && (
- {previewOverdueCount} overdue
+ {previewOverdueCount} {t('common:labels.overdue')}
)}
@@ -776,7 +790,7 @@ const AdvancedFilterBuilder = ({
color='neutral'
sx={{ textAlign: 'center', py: 2 }}
>
- No tasks match these filters
+ {t('filters:advanced.noMatches')}
) : (
@@ -798,7 +812,9 @@ const AdvancedFilterBuilder = ({
color='neutral'
sx={{ textAlign: 'center', mt: 0.5 }}
>
- ...and {previewCount - 3} more
+ {t('filters:advanced.andMore', {
+ count: previewCount - 3,
+ })}
)}
diff --git a/src/views/Modals/Inputs/BackupRestoreModal.jsx b/src/views/Modals/Inputs/BackupRestoreModal.jsx
index a8f88be7..3cabe41d 100644
--- a/src/views/Modals/Inputs/BackupRestoreModal.jsx
+++ b/src/views/Modals/Inputs/BackupRestoreModal.jsx
@@ -13,10 +13,12 @@ import {
Typography,
} from '@mui/joy'
import { useCallback, useEffect, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { CreateBackup, RestoreBackup } from '../../../utils/Fetcher'
function BackupRestoreModal({ isOpen, onClose, showNotification }) {
+ const { t } = useTranslation(['settings', 'common'])
const { ResponsiveModal } = useResponsiveModal()
const [activeTab, setActiveTab] = useState(0)
@@ -66,7 +68,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
const handleCreateBackup = async () => {
if (!encryptionKey.trim()) {
- setError('Encryption key is required')
+ setError(t('settings:backup.encryptionKeyRequired'))
return
}
@@ -95,16 +97,16 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
showNotification({
type: 'success',
- message: 'Backup created and downloaded successfully',
+ message: t('settings:backup.created'),
})
handleClose()
} else {
const errorData = await response.json()
- setError(errorData.message || 'Failed to create backup')
+ setError(errorData.message || t('settings:backup.createFailed'))
}
} catch (err) {
- setError('Failed to create backup')
+ setError(t('settings:backup.createFailed'))
} finally {
setLoading(false)
}
@@ -120,12 +122,12 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
const handleRestore = async () => {
if (!restoreEncryptionKey.trim()) {
- setError('Encryption key is required')
+ setError(t('settings:backup.encryptionKeyRequired'))
return
}
if (!backupFile) {
- setError('Please select a backup file')
+ setError(t('settings:backup.selectFile'))
return
}
@@ -143,7 +145,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
const data = await response.json()
showNotification({
type: 'success',
- message: 'Backup restored successfully. Please refresh the page.',
+ message: t('settings:backup.restored'),
})
// Refresh the page after a short delay to allow user to see the message
@@ -154,23 +156,23 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
handleClose()
} else {
const errorData = await response.json()
- setError(errorData.message || 'Failed to restore backup')
+ setError(errorData.message || t('settings:backup.restoreFailed'))
}
} catch (err) {
- setError('Failed to restore backup')
+ setError(t('settings:backup.restoreFailed'))
} finally {
setLoading(false)
}
}
reader.onerror = () => {
- setError('Failed to read backup file')
+ setError(t('settings:backup.readFailed'))
setLoading(false)
}
reader.readAsText(backupFile)
} catch (err) {
- setError('Failed to restore backup')
+ setError(t('settings:backup.restoreFailed'))
setLoading(false)
}
}
@@ -199,29 +201,28 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
const renderBackupTab = () => (
- Create an encrypted backup of your data. This backup will include all
- your chores, history, settings, and optionally your uploaded files.
+ {t('settings:backup.createDescription')}
- Encryption Key *
+ {t('settings:backup.encryptionKeyLabel')}
setEncryptionKey(e.target.value)}
- placeholder='Enter a strong encryption key'
+ placeholder={t('settings:backup.encryptionKeyPlaceholder')}
/>
- Keep this key safe - you'll need it to restore your backup
+ {t('settings:backup.encryptionKeyHint')}
- Backup Name (Optional)
+ {t('settings:backup.nameLabel')}
setBackupName(e.target.value)}
- placeholder='e.g., weekly-backup'
+ placeholder={t('settings:backup.namePlaceholder')}
/>
@@ -229,7 +230,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
setIncludeAssets(e.target.checked)}
- label='Include uploaded files and assets'
+ label={t('settings:backup.includeAssets')}
/>
@@ -241,7 +242,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
- Cancel
+ {t('common:actions.cancel')}
- Create Backup
+ {t('settings:backup.createAction')}
@@ -260,12 +261,12 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
const renderRestoreTab = () => (
- Warning: Restoring a backup will replace all your
- current data. This action cannot be undone.
+ {t('common:notifications.titles.warning')}: {' '}
+ {t('settings:backup.restoreWarning')}
- Backup File *
+ {t('settings:backup.fileLabel')}
{backupFile && (
- Selected: {backupFile.name}
+ {t('settings:backup.selectedFile', { name: backupFile.name })}
)}
- Encryption Key *
+ {t('settings:backup.encryptionKeyLabel')}
setRestoreEncryptionKey(e.target.value)}
- placeholder='Enter the encryption key used for this backup'
+ placeholder={t('settings:backup.restoreKeyPlaceholder')}
/>
@@ -297,7 +298,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
- Cancel
+ {t('common:actions.cancel')}
- Restore Backup
+ {t('settings:backup.restoreAction')}
@@ -320,7 +321,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
size='lg'
fullWidth={true}
unmountDelay={250}
- title='🔄 Backup & Restore'
+ title={t('settings:backup.title')}
>
{loading ? (
- {activeTab === 0 ? 'Creating backup...' : 'Restoring backup...'}
+ {activeTab === 0
+ ? t('settings:backup.creating')
+ : t('settings:backup.restoring')}
) : (
@@ -340,8 +343,8 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
onChange={(event, newValue) => setActiveTab(newValue)}
>
- Create Backup
- Restore Backup
+ {t('settings:backup.createTab')}
+ {t('settings:backup.restoreTab')}
{renderBackupTab()}
diff --git a/src/views/Modals/Inputs/CreateChildUserModal.jsx b/src/views/Modals/Inputs/CreateChildUserModal.jsx
index 173d6185..96a77cfb 100644
--- a/src/views/Modals/Inputs/CreateChildUserModal.jsx
+++ b/src/views/Modals/Inputs/CreateChildUserModal.jsx
@@ -7,9 +7,11 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
+ const { t } = useTranslation(['settings', 'common', 'auth'])
const { ResponsiveModal } = useResponsiveModal()
const [childName, setChildName] = useState('')
@@ -25,35 +27,40 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
if (touched.childName) {
if (!childName.trim()) {
- newErrors.childName = 'Sub account name is required'
+ newErrors.childName = t('settings:childAccounts.validation.nameRequired')
} else if (childName.length < 2) {
- newErrors.childName = 'Sub account name must be at least 2 characters'
+ newErrors.childName = t('settings:childAccounts.validation.nameMin')
} else if (childName.length > 20) {
- newErrors.childName = 'Sub account name must be less than 20 characters'
+ newErrors.childName = t('settings:childAccounts.validation.nameMax')
} else if (!/^[a-z.-]+$/.test(childName)) {
- newErrors.childName =
- 'Sub account name can only contain lowercase letters, dot and dash'
+ newErrors.childName = t(
+ 'settings:childAccounts.validation.namePattern',
+ )
}
}
if (touched.password) {
if (!password) {
- newErrors.password = 'Password is required'
+ newErrors.password = t('auth:errors.passwordRequired')
} else if (password.length < 8) {
- newErrors.password = 'Password must be between 8 and 64 characters'
+ newErrors.password = t('auth:errors.passwordLength')
} else if (password.length > 64) {
- newErrors.password = 'Password must be between 8 and 64 characters'
+ newErrors.password = t('auth:errors.passwordLength')
}
}
if (touched.confirmPassword) {
if (password !== confirmPassword) {
- newErrors.confirmPassword = 'Passwords do not match'
+ newErrors.confirmPassword = t(
+ 'settings:childAccounts.validation.passwordMismatch',
+ )
}
}
if (touched.displayName && displayName.length > 50) {
- newErrors.displayName = 'Display name must be less than 50 characters'
+ newErrors.displayName = t(
+ 'settings:childAccounts.validation.displayNameMax',
+ )
}
setErrors(newErrors)
@@ -106,24 +113,23 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
return (
- Create Sub Account
+ {t('settings:childAccounts.modal.title')}
- Create a new sub account. The user will be able to log in using their
- combined username and complete tasks assigned to them.
+ {t('settings:childAccounts.modal.description')}
- Sub Account Name *
+ {t('settings:childAccounts.modal.nameLabel')} *
{
setChildName(e.target.value)
@@ -137,13 +143,13 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
- Display Name
+ {t('common:labels.displayName')}
{
setDisplayName(e.target.value)
@@ -157,7 +163,7 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
- Password *
+ {t('common:labels.password')} *
{
setPassword(e.target.value)
@@ -177,7 +183,7 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
- Confirm Password *
+ {t('settings:childAccounts.modal.confirmPasswordLabel')} *
{
setConfirmPassword(e.target.value)
@@ -205,7 +211,7 @@ function CreateChildUserModal({ isOpen, onClose, onSuccess }) {
disabled={isSubmitting}
sx={{ flex: 1 }}
>
- Cancel
+ {t('common:actions.cancel')}
- Create Account
+ {t('settings:childAccounts.modal.createAction')}
diff --git a/src/views/Modals/Inputs/CreateThingModal.jsx b/src/views/Modals/Inputs/CreateThingModal.jsx
index 164cc4b4..0242edad 100644
--- a/src/views/Modals/Inputs/CreateThingModal.jsx
+++ b/src/views/Modals/Inputs/CreateThingModal.jsx
@@ -10,9 +10,11 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
+ const { t } = useTranslation(['things', 'common'])
const { ResponsiveModal } = useResponsiveModal()
const [name, setName] = useState(currentThing?.name || '')
@@ -34,17 +36,17 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
const isValid = () => {
const newErrors = {}
if (!name || name.trim() === '') {
- newErrors.name = 'Name is required'
+ newErrors.name = t('things:modal.nameRequired')
}
if (type === 'number' && isNaN(state)) {
- newErrors.state = 'State must be a number'
+ newErrors.state = t('things:modal.stateMustBeNumber')
}
if (type === 'boolean' && !['true', 'false'].includes(state)) {
- newErrors.state = 'State must be true or false'
+ newErrors.state = t('things:modal.stateMustBeBoolean')
}
if ((type === 'text' && !state) || state.trim() === '') {
- newErrors.state = 'State is required'
+ newErrors.state = t('things:modal.stateRequired')
}
setErrors(newErrors)
@@ -65,12 +67,12 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
onClose={onClose}
size='lg'
fullWidth={true}
- title={`${currentThing?.id ? 'Edit' : 'Create'} Thing`}
+ title={currentThing?.id ? t('things:modal.editTitle') : t('things:modal.createTitle')}
>
- Name
+ {t('common:labels.name')}
- Type
+ {t('things:modal.type')}
{['text', 'number', 'boolean'].map(type => (
setType(type)}>
@@ -91,9 +93,9 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
{type === 'text' && (
- Value
+ {t('common:labels.value')}
setState(e.target.value)}
sx={{ minWidth: 300 }}
@@ -103,9 +105,9 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
)}
{type === 'number' && (
- Value
+ {t('common:labels.value')}
{
@@ -117,7 +119,7 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
)}
{type === 'boolean' && (
- Value
+ {t('common:labels.value')}
{['true', 'false'].map(value => (
setState(value)}>
@@ -130,10 +132,10 @@ function CreateThingModal({ isOpen, onClose, onSave, currentThing }) {
- {currentThing?.id ? 'Update' : 'Create'}
+ {currentThing?.id ? t('common:actions.save') : t('common:actions.create')}
- {currentThing?.id ? 'Cancel' : 'Close'}
+ {currentThing?.id ? t('common:actions.cancel') : t('common:actions.close')}
diff --git a/src/views/Modals/Inputs/DateModal.jsx b/src/views/Modals/Inputs/DateModal.jsx
index 52ed0d19..ff088ad2 100644
--- a/src/views/Modals/Inputs/DateModal.jsx
+++ b/src/views/Modals/Inputs/DateModal.jsx
@@ -1,8 +1,10 @@
import { Box, Button, Input } from '@mui/joy'
import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
function DateModal({ isOpen, onClose, onSave, current, title }) {
+ const { t } = useTranslation(['common'])
const { ResponsiveModal } = useResponsiveModal()
const [date, setDate] = useState(
@@ -79,10 +81,10 @@ function DateModal({ isOpen, onClose, onSave, current, title }) {
- Save
+ {t('common:actions.save')}
- Cancel
+ {t('common:actions.cancel')}
diff --git a/src/views/Modals/Inputs/EditThingState.jsx b/src/views/Modals/Inputs/EditThingState.jsx
index 2871b44e..bb87cace 100644
--- a/src/views/Modals/Inputs/EditThingState.jsx
+++ b/src/views/Modals/Inputs/EditThingState.jsx
@@ -7,9 +7,11 @@ import {
Typography,
} from '@mui/joy'
import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
+ const { t } = useTranslation(['things', 'common'])
const { ResponsiveModal } = useResponsiveModal()
const [state, setState] = useState(currentThing?.state || '')
@@ -19,7 +21,7 @@ function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
const newErrors = {}
if (state.trim() === '') {
- newErrors.state = 'State is required'
+ newErrors.state = t('things:modal.stateRequired')
}
setErrors(newErrors)
@@ -45,12 +47,12 @@ function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
onClose={onClose}
size='lg'
fullWidth={true}
- title='Update state'
+ title={t('things:modal.updateStateTitle')}
>
- Value
+ {t('common:labels.value')}
setState(e.target.value)}
sx={{ minWidth: 300 }}
@@ -60,10 +62,10 @@ function EditThingStateModal({ isOpen, onClose, onSave, currentThing }) {
- {currentThing?.id ? 'Update' : 'Create'}
+ {currentThing?.id ? t('common:actions.save') : t('common:actions.create')}
- {currentThing?.id ? 'Cancel' : 'Close'}
+ {currentThing?.id ? t('common:actions.cancel') : t('common:actions.close')}
diff --git a/src/views/Modals/Inputs/IconPickerModal.jsx b/src/views/Modals/Inputs/IconPickerModal.jsx
index 2a328441..b8b10851 100644
--- a/src/views/Modals/Inputs/IconPickerModal.jsx
+++ b/src/views/Modals/Inputs/IconPickerModal.jsx
@@ -7,6 +7,7 @@ import {
Grid,
Typography,
} from '@mui/joy'
+import { useTranslation } from 'react-i18next'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { getTextColorFromBackgroundColor } from '../../../utils/Colors'
import PROJECT_ICONS from '../../../utils/ProjectIcons'
@@ -18,6 +19,7 @@ const IconPickerModal = ({
currentIcon,
projectColor,
}) => {
+ const { t } = useTranslation(['projects', 'common'])
const { ResponsiveModal } = useResponsiveModal()
const handleIconClick = iconValue => {
@@ -32,11 +34,11 @@ const IconPickerModal = ({
size='lg'
fullWidth={true}
unmountDelay={250}
- title='Choose Project Icon'
+ title={t('projects:modal.chooseIconTitle')}
>
- Available Icons
+ {t('projects:modal.availableIcons')}
- Cancel
+ {t('common:actions.cancel')}
diff --git a/src/views/Modals/Inputs/LabelModal.jsx b/src/views/Modals/Inputs/LabelModal.jsx
index 80801902..ad154191 100644
--- a/src/views/Modals/Inputs/LabelModal.jsx
+++ b/src/views/Modals/Inputs/LabelModal.jsx
@@ -8,6 +8,7 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useQueryClient } from '@tanstack/react-query'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal.js'
@@ -17,6 +18,7 @@ import { CreateLabel, UpdateLabel } from '../../../utils/Fetcher'
import { useLabels } from '../../Labels/LabelQueries'
function LabelModal({ isOpen, onClose, label }) {
+ const { t } = useTranslation(['common', 'navigation'])
const { ResponsiveModal } = useResponsiveModal()
const [labelName, setLabelName] = useState('')
@@ -41,7 +43,7 @@ function LabelModal({ isOpen, onClose, label }) {
// Validation logic
const validateLabel = () => {
if (!labelName.trim()) {
- setError('Name cannot be empty')
+ setError(t('common:errors.nameCannotBeEmpty'))
return false
}
if (
@@ -49,11 +51,11 @@ function LabelModal({ isOpen, onClose, label }) {
userLabel => userLabel.name === labelName && userLabel.id !== label?.id,
)
) {
- setError('Label with this name already exists')
+ setError(t('common:errors.duplicateLabel'))
return false
}
if (!color) {
- setError('Please select a color')
+ setError(t('common:errors.selectColor'))
return false
}
return true
@@ -78,13 +80,13 @@ function LabelModal({ isOpen, onClose, label }) {
.catch(err => {
if (err.queued) {
showError({
- title: 'Failed to save label',
- message: 'Unable to save label. Please try again.',
+ title: t('common:notifications.titles.error'),
+ message: t('common:errors.unableToSaveLabel'),
})
} else {
showError({
- title: 'Failed to save label',
- message: 'Unable to save label. Please try again.',
+ title: t('common:notifications.titles.error'),
+ message: t('common:errors.unableToSaveLabel'),
})
}
})
@@ -96,14 +98,18 @@ function LabelModal({ isOpen, onClose, label }) {
onClose={onClose}
size='lg'
fullWidth={true}
- title={label ? 'Edit Label' : 'Add Label'}
+ title={
+ label
+ ? `${t('common:actions.edit')} ${t('navigation:labels')}`
+ : `${t('common:actions.create')} ${t('navigation:labels')}`
+ }
footer={
- {label ? 'Save Changes' : 'Add Label'}
+ {label ? t('common:actions.save') : t('common:actions.create')}
- Cancel
+ {t('common:actions.cancel')}
}
@@ -111,7 +117,7 @@ function LabelModal({ isOpen, onClose, label }) {
- Name
+ {t('common:labels.name')}
- Color
+ {t('common:labels.color')}
{
+ const { t } = useTranslation(['settings', 'common'])
const { ResponsiveModal } = useResponsiveModal()
return (
- Cancel Subscription
+ {t('settings:nativeCancel.title')}
- To cancel your subscription, please follow the instructions for your
- platform (you should cancel through the same platform you used to
- subscribe).
+ {t('settings:nativeCancel.description')}
- For iOS (iPhone/iPad):
-
-
- 1. Open the Settings app on your device
-
-
- 2. Tap your name at the top of the screen
-
-
- 3. Tap Subscriptions
-
-
- 4. Find and tap Donetick
-
-
- 5. Tap Cancel Subscription
+ {t('settings:nativeCancel.iosTitle')}
+ {[1, 2, 3, 4, 5].map(step => (
+
+ {t(`settings:nativeCancel.iosSteps.${step}`)}
+
+ ))}
- Note: If you subscribed through iOS and are using
- the web/desktop version, you must cancel through iOS Settings as
- described above.
+ {t('common:notifications.titles.info')}: {' '}
+ {t('settings:nativeCancel.iosNote')}
- For Android:
-
-
- 1. Open the Google Play Store app
-
-
- 2. Tap the profile icon in the top right
-
-
- 3. Tap Payments & subscriptions
-
-
- 4. Tap Subscriptions
-
-
- 5. Find and tap Donetick
-
-
- 6. Tap Cancel subscription
+ {t('settings:nativeCancel.androidTitle')}
+ {[1, 2, 3, 4, 5, 6].map(step => (
+
+ {t(`settings:nativeCancel.androidSteps.${step}`)}
+
+ ))}
- Note: If you subscribed through Google Play and are
- using the web/desktop version, you must cancel through Google Play
- as described above.
+ {t('common:notifications.titles.info')}: {' '}
+ {t('settings:nativeCancel.androidNote')}
- For Web/Desktop Subscriptions:
+ {t('settings:nativeCancel.webTitle')}
- If you originally subscribed through our website or desktop app, you
- can cancel your subscription by going to the Account Settings
- section on our website. using a web browser
+ {t('settings:nativeCancel.webDescription')}
- Important: You must cancel your subscription
- through the same platform where you originally subscribed. If you
- subscribed through the iOS App Store or Google Play Store (even if
- you're now using the web/desktop version), you must cancel through
- that original platform using the instructions above.
+ {t('common:notifications.titles.warning')}: {' '}
+ {t('settings:nativeCancel.webImportant')}
- Your subscription will remain active until the end of your current
- billing period.
+ {t('settings:nativeCancel.billingPeriod')}
- I'll cancel from my app store
+ {t('settings:nativeCancel.cancelFromStore')}
{
color='danger'
fullWidth
>
- I subscribed via desktop - Cancel now
+ {t('settings:nativeCancel.cancelDesktopNow')}
- Dismiss
+ {t('common:actions.close')}
diff --git a/src/views/Modals/Inputs/NudgeModal.jsx b/src/views/Modals/Inputs/NudgeModal.jsx
index 75babf00..54b3eae5 100644
--- a/src/views/Modals/Inputs/NudgeModal.jsx
+++ b/src/views/Modals/Inputs/NudgeModal.jsx
@@ -9,11 +9,13 @@ import {
Typography,
} from '@mui/joy'
import { useCallback, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import KeyboardShortcutHint from '../../../components/common/KeyboardShortcutHint'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { isOfficialDonetickInstanceSync } from '../../../utils/FeatureToggle'
function NudgeModal({ config }) {
+ const { t } = useTranslation(['chores', 'common'])
const { ResponsiveModal } = useResponsiveModal()
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
const [message, setMessage] = useState('')
@@ -107,31 +109,25 @@ function NudgeModal({ config }) {
size='lg'
fullWidth={true}
unmountDelay={250}
- title='Send Nudge'
+ title={t('chores:actions.sendNudge')}
>
- Send a gentle reminder to the assignee about this task. You can
- customize the message and choose who gets notified.
+ {t('chores:nudge.description')}
{!isOfficialInstance && (
- Heads up! This feature avaiable on Donetick Cloud!
- Since you're using a self-hosted instance, nudges will requires you
- to setup Google cloud account and Firebase Cloud Messaging (FCM).
- and build the Android or the iOS app by yourself.
-
- Will update if we come up with a solution to make this easier for to
- configure. for selfhosters
+ {t('chores:nudge.headsUp')} {' '}
+ {t('chores:nudge.selfHostedWarning')}
)}
- Custom Message (optional)
+ {t('chores:nudge.customMessage')}
diff --git a/src/views/Modals/Inputs/PasswordChangeModal.jsx b/src/views/Modals/Inputs/PasswordChangeModal.jsx
index 9d3cad72..c70c2bf2 100644
--- a/src/views/Modals/Inputs/PasswordChangeModal.jsx
+++ b/src/views/Modals/Inputs/PasswordChangeModal.jsx
@@ -7,9 +7,11 @@ import {
Typography,
} from '@mui/joy'
import React, { useEffect } from 'react'
+import { useTranslation } from 'react-i18next'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
function PassowrdChangeModal({ isOpen, onClose }) {
+ const { t } = useTranslation(['settings', 'auth', 'common'])
const { ResponsiveModal } = useResponsiveModal()
const [password, setPassword] = React.useState('')
@@ -22,15 +24,15 @@ function PassowrdChangeModal({ isOpen, onClose }) {
if (!passwordTouched || !confirmPasswordTouched) {
return
} else if (password !== confirmPassword) {
- setPasswordError('Passwords do not match')
+ setPasswordError(t('settings:modals.passwordChange.mismatch'))
} else if (password.length < 8) {
- setPasswordError('Password must be at least 8 characters')
+ setPasswordError(t('settings:modals.passwordChange.min'))
} else if (password.length > 64) {
- setPasswordError('Password must be less than 64 characters')
+ setPasswordError(t('settings:modals.passwordChange.max'))
} else {
setPasswordError(null)
}
- }, [password, confirmPassword, passwordTouched, confirmPasswordTouched])
+ }, [password, confirmPassword, passwordTouched, confirmPasswordTouched, t])
const handleAction = isConfirmed => {
if (!isConfirmed) {
@@ -46,24 +48,24 @@ function PassowrdChangeModal({ isOpen, onClose }) {
onClose={onClose}
size='lg'
fullWidth={true}
- title='Change Password'
+ title={t('settings:modals.passwordChange.title')}
>
- Please enter your new password.
+ {t('settings:modals.passwordChange.intro')}
- New Password
+ {t('settings:modals.passwordChange.newPassword')}
{
setPasswordTouched(true)
@@ -74,14 +76,14 @@ function PassowrdChangeModal({ isOpen, onClose }) {
- Confirm Password
+ {t('settings:modals.passwordChange.confirmPassword')}
- Change Password
+ {t('settings:modals.passwordChange.title')}
- Cancel
+ {t('common:actions.cancel')}
diff --git a/src/views/Modals/Inputs/ProjectModal.jsx b/src/views/Modals/Inputs/ProjectModal.jsx
index b3bc503b..c8e86c83 100644
--- a/src/views/Modals/Inputs/ProjectModal.jsx
+++ b/src/views/Modals/Inputs/ProjectModal.jsx
@@ -12,6 +12,7 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import PROJECT_COLORS, {
getTextColorFromBackgroundColor,
@@ -24,6 +25,7 @@ import {
import IconPickerModal from './IconPickerModal'
const ProjectModal = ({ isOpen, onClose, onSave, project }) => {
+ const { t } = useTranslation(['common', 'projects'])
const { ResponsiveModal } = useResponsiveModal()
const [projectName, setProjectName] = useState('')
const [projectDescription, setProjectDescription] = useState('')
@@ -59,7 +61,7 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => {
e.preventDefault()
if (!projectName.trim()) {
- setError('Project name is required')
+ setError(t('projects:modal.nameRequired'))
return
}
@@ -83,7 +85,7 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => {
},
onError: error => {
console.error('Error updating project:', error)
- setError('Failed to update project')
+ setError(t('projects:modal.updateFailed'))
},
},
)
@@ -96,7 +98,7 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => {
},
onError: error => {
console.error('Error creating project:', error)
- setError('Failed to create project')
+ setError(t('projects:modal.createFailed'))
},
})
}
@@ -125,7 +127,7 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => {
size='md'
unmountDelay={250}
fullWidth={true}
- title={project ? 'Edit Project' : 'Create New Project'}
+ title={project ? t('projects:modal.editTitle') : t('projects:modal.createTitle')}
footer={
{
fullWidth
size='lg'
>
- {project ? 'Update' : 'Create'}
+ {project ? t('common:actions.save') : t('common:actions.create')}
{
fullWidth
size='lg'
>
- Cancel
+ {t('common:actions.cancel')}
}
@@ -154,11 +156,11 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => {
{/* Project Name */}
- Project Name
+ {t('projects:modal.name')}
setProjectName(e.target.value)}
- placeholder='Enter project name...'
+ placeholder={t('projects:modal.namePlaceholder')}
autoFocus
disabled={isSubmitting}
/>
@@ -166,11 +168,11 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => {
{/* Project Description */}
- Description
+ {t('common:labels.description')}
@@ -373,10 +375,11 @@ const ProjectView = () => {
>
{/* Default project - not swipeable */}
{
onCardClick={() =>
handleCardClick({
id: 'default',
- name: 'Default Project',
+ name: t('projects:view.defaultProjectName'),
icon: 'FolderOpen',
color: '#1976d2',
})
@@ -426,7 +429,7 @@ const ProjectView = () => {
>
- Edit
+ {t('common:actions.edit')}
@@ -447,7 +450,7 @@ const ProjectView = () => {
>
- Delete
+ {t('common:actions.delete')}
@@ -456,6 +459,7 @@ const ProjectView = () => {
}
>
{
+ const { t } = useTranslation(['settings', 'common'])
const { data: userProfile } = useUserProfile()
const { showNotification } = useNotification()
const { fmt } = useLocalization()
@@ -38,8 +40,8 @@ const APITokenSettings = () => {
message,
title,
onConfirm,
- confirmText = 'Confirm',
- cancelText = 'Cancel',
+ confirmText = t('common:actions.continue'),
+ cancelText = t('common:actions.cancel'),
color = 'primary',
) => {
setConfirmModalConfig({
@@ -80,23 +82,20 @@ const APITokenSettings = () => {
}
return (
-
+
-
Access Token
+
{t('settings:apiTokensPage.heading')}
- Create token to use with the API to update things that trigger task or
- chores
+ {t('settings:apiTokensPage.description')}
{!isPlusAccount(userProfile) && (
<>
- Plus Feature
+ {t('common:labels.plusFeature')}
- API tokens are not available in the Basic plan. Upgrade to Plus to
- generate API tokens for integrating with external systems and
- automating your tasks.
+ {t('settings:apiTokensPage.plusDescription')}
>
)}
@@ -125,7 +124,9 @@ const APITokenSettings = () => {
setShowTokenId(token.id)
}}
>
- {showTokenId === token?.id ? 'Hide' : 'Show'} Token
+ {showTokenId === token?.id
+ ? t('settings:apiTokensPage.hideToken')
+ : t('settings:apiTokensPage.showToken')}
{
color='danger'
onClick={() => {
showConfirmation(
- `Are you sure you want to remove ${token.name}?`,
- 'Remove Token',
+ t('settings:apiTokensPage.removeTokenConfirm', {
+ name: token.name,
+ }),
+ t('settings:apiTokensPage.removeToken'),
() => {
DeleteLongLiveToken(token.id).then(resp => {
if (resp.ok) {
showNotification({
type: 'success',
- title: 'Removed',
- message: 'API token has been removed',
+ title: t('settings:apiTokensPage.removed'),
+ message: t(
+ 'settings:apiTokensPage.removedMessage',
+ ),
})
const newTokens = tokens.filter(
t => t.id !== token.id,
@@ -150,13 +155,13 @@ const APITokenSettings = () => {
}
})
},
- 'Remove',
- 'Cancel',
+ t('common:actions.remove'),
+ t('common:actions.cancel'),
'danger',
)
}}
>
- Remove
+ {t('settings:apiTokensPage.removeToken')}
@@ -174,7 +179,7 @@ const APITokenSettings = () => {
navigator.clipboard.writeText(token.token)
showNotification({
type: 'success',
- message: 'Token copied to clipboard',
+ message: t('settings:apiTokensPage.copied'),
})
setShowTokenId(null)
}}
@@ -200,15 +205,15 @@ const APITokenSettings = () => {
setIsGetTokenNameModalOpen(true)
}}
>
- Generate New Token
+ {t('settings:apiTokensPage.generateNew')}
{
setIsGetTokenNameModalOpen(false)
}}
- okText={'Generate Token'}
+ okText={t('settings:apiTokensPage.generateAction')}
onSave={handleSaveToken}
/>
diff --git a/src/views/Settings/AccountSettings.jsx b/src/views/Settings/AccountSettings.jsx
index a30dbc46..12fae0fb 100644
--- a/src/views/Settings/AccountSettings.jsx
+++ b/src/views/Settings/AccountSettings.jsx
@@ -4,6 +4,7 @@ import { Purchases } from '@revenuecat/purchases-capacitor'
import { useQueryClient } from '@tanstack/react-query'
import moment from 'moment'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import SubscriptionModal from '../../components/SubscriptionModal'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useUserProfile } from '../../queries/UserQueries'
@@ -15,6 +16,7 @@ import UserDeletionModal from '../Modals/Inputs/UserDeletionModal'
import SettingsLayout from './SettingsLayout'
const AccountSettings = () => {
+ const { t } = useTranslation(['settings', 'common'])
const { data: userProfile } = useUserProfile()
const queryClient = useQueryClient()
const { showNotification } = useNotification()
@@ -42,43 +44,45 @@ const AccountSettings = () => {
const getSubscriptionDetails = () => {
if (userProfile?.subscription === 'active') {
- return `You are currently subscribed to the Plus plan. Your subscription will renew on ${fmt.date(userProfile?.expiration)}.`
+ return t('settings:account.activePlan', {
+ date: 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 ${fmt.date(userProfile?.expiration)}.`
+ return t('settings:account.cancelledPlan', {
+ date: fmt.date(userProfile?.expiration),
+ })
} else {
- return `You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.`
+ return t('settings:account.freePlan')
}
}
const getSubscriptionStatus = () => {
if (userProfile?.subscription === 'active') {
- return `Plus`
+ return t('settings:account.plus')
} else if (userProfile?.subscription === 'cancelled') {
if (moment().isBefore(userProfile?.expiration)) {
- return `Plus(until ${fmt.date(userProfile?.expiration)})`
+ return `${t('settings:account.plus')} (${fmt.date(userProfile?.expiration)})`
}
- return `Free`
+ return t('settings:account.free')
} else {
- return `Free`
+ return t('settings:account.free')
}
}
if (!userProfile) {
return (
-
- Loading...
+
+ {t('common:status.loading')}
)
}
return (
-
+
-
- Change your account settings, type or update your password
-
+
{t('settings:account.description')}
- Account Type : {getSubscriptionStatus()}
+ {t('settings:account.accountType')} : {getSubscriptionStatus()}
{getSubscriptionDetails()}
@@ -110,8 +114,7 @@ const AccountSettings = () => {
queryClient.refetchQueries(['userProfile'])
showNotification({
type: 'success',
- message:
- 'Purchase successful! Please restart the app to access Plus features.',
+ message: t('settings:account.purchaseSuccess'),
})
}
} catch (error) {
@@ -122,38 +125,32 @@ const AccountSettings = () => {
} else if (error.code === '2') {
showNotification({
type: 'error',
- message:
- 'Store connection issue. Please check your network and try again.',
+ message: t('settings:account.purchaseNetwork'),
})
} else if (error.code === '3') {
showNotification({
type: 'error',
- message:
- 'Purchases are not allowed on this device. Please check your device restrictions.',
+ message: t('settings:account.purchaseNotAllowed'),
})
} else if (error.code === '4') {
showNotification({
type: 'error',
- message:
- 'This subscription is not available. Please try again later.',
+ message: t('settings:account.purchaseUnavailable'),
})
} else if (error.code === '5') {
showNotification({
type: 'error',
- message:
- 'This purchase has already been processed. If you believe this is an error, please contact support.',
+ message: t('settings:account.purchaseProcessed'),
})
} else if (error.code === '6') {
showNotification({
type: 'error',
- message:
- 'Purchase receipt missing. Please try purchasing again.',
+ message: t('settings:account.purchaseReceiptMissing'),
})
} else if (error.code === '7') {
showNotification({
type: 'error',
- message:
- 'Network error. Please check your connection and try again.',
+ message: t('settings:account.purchaseNetwork'),
})
} else if (error.code === '8') {
showNotification({
@@ -164,15 +161,16 @@ const AccountSettings = () => {
} else if (error.code === '9') {
showNotification({
type: 'warning',
- message:
- 'Payment is pending approval. You will receive access once approved.',
+ message: t('settings:account.purchasePending'),
})
} else {
console.error('Unexpected purchase error:', error)
console.error('Error occurred in purchase flow')
showNotification({
type: 'error',
- message: `Purchase failed: ${error.message || 'Unknown error'}. Please try again or contact support.`,
+ message: t('settings:account.purchaseFailed', {
+ message: error.message || t('common:status.unknown'),
+ }),
})
}
}
@@ -181,7 +179,7 @@ const AccountSettings = () => {
}
}}
>
- Upgrade
+ {t('settings:account.upgrade')}
{userProfile?.subscription === 'active' && (
@@ -197,14 +195,14 @@ const AccountSettings = () => {
setNativeCancelModal(true)
}}
>
- Cancel
+ {t('common:actions.cancel')}
)}
{import.meta.env.VITE_IS_SELF_HOSTED === 'true' && (
- Password :
+ {t('common:labels.password')} :
{
setChangePasswordModal(true)
}}
>
- Change Password
+ {t('settings:account.changePassword')}
{changePasswordModal ? (
{
if (resp.ok) {
showNotification({
type: 'success',
- message: 'Password changed successfully',
+ message: t('settings:account.passwordChanged'),
})
} else {
showNotification({
type: 'error',
- message: 'Password change failed',
+ message: t('settings:account.passwordChangeFailed'),
})
}
})
@@ -243,18 +241,17 @@ const AccountSettings = () => {
- Danger Zone
+ {t('settings:account.dangerZone')}
- Once you delete your account, there is no going back. Please be
- certain.
+ {t('settings:account.dangerDescription')}
setUserDeletionModal(true)}
>
- Delete Account
+ {t('settings:account.deleteAccount')}
@@ -271,7 +268,7 @@ const AccountSettings = () => {
if (success) {
showNotification({
type: 'success',
- message: 'Account deleted successfully',
+ message: t('settings:modals.userDeletion.title'),
})
}
}}
@@ -287,13 +284,13 @@ const AccountSettings = () => {
if (resp.ok) {
showNotification({
type: 'success',
- message: 'Subscription cancelled',
+ message: t('settings:account.subscriptionCancelled'),
})
window.location.reload()
} else {
showNotification({
type: 'error',
- message: 'Failed to cancel subscription',
+ message: t('settings:account.subscriptionCancelFailed'),
})
}
})
diff --git a/src/views/Settings/AdvancedSettings.jsx b/src/views/Settings/AdvancedSettings.jsx
index 72008444..e86d9b5a 100644
--- a/src/views/Settings/AdvancedSettings.jsx
+++ b/src/views/Settings/AdvancedSettings.jsx
@@ -10,6 +10,7 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import RealTimeSettings from '../../components/RealTimeSettings'
import { useUserProfile } from '../../queries/UserQueries'
import { useNotification } from '../../service/NotificationProvider'
@@ -18,6 +19,7 @@ import { isPlusAccount } from '../../utils/Helpers'
import SettingsLayout from './SettingsLayout'
const AdvancedSettings = () => {
+ const { t } = useTranslation(['settings', 'common'])
const { data: userProfile } = useUserProfile()
const { showNotification } = useNotification()
@@ -44,34 +46,31 @@ const AdvancedSettings = () => {
if (!userProfile) {
return (
-
- Loading...
+
+ {t('settings:advanced.loading')}
)
}
return (
-
+
- Configure advanced features like webhooks and real-time updates for enhanced productivity.
+ {t('settings:advanced.description')}
{/* Webhook Settings - Only show for admins */}
{isAdmin && (
<>
- Webhook Integration
+ {t('settings:advanced.webhookIntegration')}
- Webhooks allow you to send real-time notifications to other
- services when events happen in your Circle. Configure a webhook
- URL to receive real-time updates.
+ {t('settings:advanced.webhookDescription')}
{!isPlusAccount(userProfile) && (
- Webhook notifications are not available in the Basic plan.
- Upgrade to Plus to receive real-time updates via webhooks.
+ {t('settings:advanced.webhookPlanWarning')}
)}
@@ -85,7 +84,7 @@ const AdvancedSettings = () => {
}
}}
variant='soft'
- label='Enable Webhook'
+ label={t('settings:advanced.enableWebhook')}
disabled={!isPlusAccount(userProfile)}
overlay
/>
@@ -94,10 +93,10 @@ const AdvancedSettings = () => {
opacity: !isPlusAccount(userProfile) ? 0.5 : 1,
}}
>
- Enable webhook notifications for tasks and things updates.{' '}
+ {t('settings:advanced.enableWebhookHelper')}{' '}
{userProfile && !isPlusAccount(userProfile) && (
- Plus Feature
+ {t('common:labels.plusFeature')}
)}
@@ -105,7 +104,9 @@ const AdvancedSettings = () => {
{webhookURL !== null && (
- Webhook URL
+
+ {t('settings:advanced.webhookUrl')}
+
setWebhookURL(e.target.value)}
@@ -128,19 +129,19 @@ const AdvancedSettings = () => {
if (resp.ok) {
showNotification({
type: 'success',
- message: 'Webhook URL updated successfully',
+ message: t('settings:advanced.webhookSaved'),
})
} else {
showNotification({
type: 'error',
- message: 'Failed to update webhook URL',
+ message: t('settings:advanced.webhookSaveFailed'),
})
}
})
}}
disabled={!isPlusAccount(userProfile)}
>
- Save
+ {t('common:actions.save')}
)}
@@ -149,10 +150,10 @@ const AdvancedSettings = () => {
{/* Real-time Settings */}
- Real-time Updates
+ {t('settings:advanced.realtimeTitle')}
- Configure how you receive live updates when tasks and activities change in your circle.
+ {t('settings:advanced.realtimeSectionDescription')}
@@ -160,4 +161,4 @@ const AdvancedSettings = () => {
)
}
-export default AdvancedSettings
\ No newline at end of file
+export default AdvancedSettings
diff --git a/src/views/Settings/ChildUserSettings.jsx b/src/views/Settings/ChildUserSettings.jsx
index edda45de..513bca7f 100644
--- a/src/views/Settings/ChildUserSettings.jsx
+++ b/src/views/Settings/ChildUserSettings.jsx
@@ -13,6 +13,7 @@ import {
} from '@mui/joy'
import { useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
import useConfirmationModal from '../../hooks/useConfirmationModal'
import { useChildUsers, useUserProfile } from '../../queries/UserQueries'
import { useNotification } from '../../service/NotificationProvider'
@@ -28,6 +29,7 @@ import PasswordChangeModal from '../Modals/Inputs/PasswordChangeModal'
import SettingsLayout from './SettingsLayout'
const ChildUserSettings = () => {
+ const { t } = useTranslation(['settings', 'common'])
const { data: userProfile } = useUserProfile()
const { data: childUsers, isLoading, refetch } = useChildUsers()
const { showNotification } = useNotification()
@@ -54,7 +56,9 @@ const ChildUserSettings = () => {
const result = await response.json()
showNotification({
type: 'success',
- message: `Child account "${result.res.displayName}" created successfully!`,
+ message: t('settings:childAccounts.createSuccess', {
+ name: result.res.displayName,
+ }),
})
refetch()
queryClient.invalidateQueries(['childUsers'])
@@ -65,7 +69,9 @@ const ChildUserSettings = () => {
} catch (error) {
showNotification({
type: 'error',
- message: `Failed to create child account: ${error.message}`,
+ message: t('settings:childAccounts.createFailed', {
+ message: error.message,
+ }),
})
throw error
}
@@ -80,7 +86,7 @@ const ChildUserSettings = () => {
if (response.ok) {
showNotification({
type: 'success',
- message: 'Child password updated successfully',
+ message: t('settings:childAccounts.updatePasswordSuccess'),
})
} else {
const error = await response.json()
@@ -89,15 +95,19 @@ const ChildUserSettings = () => {
} catch (error) {
showNotification({
type: 'error',
- message: `Failed to update password: ${error.message}`,
+ message: t('settings:childAccounts.updatePasswordFailed', {
+ message: error.message,
+ }),
})
}
}
const handleDeleteChild = async (childId, childName) => {
showConfirmation(
- `Are you sure you want to delete the child account "${childName}"? This action cannot be undone.`,
- 'Delete Sub Account',
+ t('settings:childAccounts.deleteConfirmMessage', {
+ name: childName,
+ }),
+ t('settings:childAccounts.deleteConfirmTitle'),
async () => {
setDeletingChildId(childId)
try {
@@ -106,7 +116,9 @@ const ChildUserSettings = () => {
if (response.ok) {
showNotification({
type: 'success',
- message: `Sub account "${childName}" deleted successfully`,
+ message: t('settings:childAccounts.deleteSuccess', {
+ name: childName,
+ }),
})
refetch()
queryClient.invalidateQueries(['childUsers'])
@@ -117,39 +129,39 @@ const ChildUserSettings = () => {
} catch (error) {
showNotification({
type: 'error',
- message: `Failed to delete Sub account: ${error.message}`,
+ message: t('settings:childAccounts.deleteFailed', {
+ message: error.message,
+ }),
})
} finally {
setDeletingChildId(null)
}
},
- 'Delete',
- 'Cancel',
+ t('common:actions.delete'),
+ t('common:actions.cancel'),
'danger',
)
}
if (!isParentUser) {
return (
-
+
- Only primary users can manage sub accounts.
+ {t('settings:childAccounts.parentOnly')}
)
}
return (
-
+
- Manage sub accounts. Sub account users can log in and complete
- assigned tasks.
+ {t('settings:childAccounts.description')}
{!isPlusAccount(userProfile) && (
- Sub account limited to 1 on Free plan. Upgrade to Plus to have up to
- 5 sub accounts.
+ {t('settings:childAccounts.planWarning')}
)}
{
}}
>
- Sub Accounts ({childUsers?.length || 0})
+ {t('settings:childAccounts.sectionTitle', {
+ count: childUsers?.length || 0,
+ })}
}
onClick={() => setCreateModalOpen(true)}
>
- Add Sub Account
+ {t('settings:childAccounts.add')}
{isLoading ? (
- Loading sub accounts...
+ {t('settings:childAccounts.loading')}
) : childUsers?.length === 0 ? (
- No Sub Accounts
+ {t('settings:childAccounts.emptyTitle')}
- Create sub accounts so team members can log in and complete
- their assigned tasks.
+ {t('settings:childAccounts.emptyDescription')}
}
onClick={() => setCreateModalOpen(true)}
>
- Add Your First Sub Account
+ {t('settings:childAccounts.addFirst')}
@@ -206,11 +219,14 @@ const ChildUserSettings = () => {
{child.displayName || child.username}
- Username: {child.username}
+ {t('settings:childAccounts.username', {
+ username: child.username,
+ })}
- Created:{' '}
- {new Date(child.createdAt).toLocaleDateString()}
+ {t('settings:childAccounts.created', {
+ date: new Date(child.createdAt).toLocaleDateString(),
+ })}
@@ -222,7 +238,7 @@ const ChildUserSettings = () => {
setSelectedChildId(child.id)
setPasswordModalOpen(true)
}}
- title='Change Password'
+ title={t('settings:childAccounts.changePasswordTitle')}
>
@@ -237,7 +253,7 @@ const ChildUserSettings = () => {
)
}
loading={deletingChildId === child.id}
- title='Delete Account'
+ title={t('settings:childAccounts.deleteAccountTitle')}
>
@@ -253,21 +269,19 @@ const ChildUserSettings = () => {
- How Managed Accounts Work
+ {t('settings:childAccounts.howItWorksTitle')}
- • Managed accounts created by the primary user, these specific for
- user you want to have ability to delete and reset password.
+ • {t('settings:childAccounts.bulletOne')}
- • Sub accounts can log in with their own username and password.
+ • {t('settings:childAccounts.bulletTwo')}
- • Managed accounts can complete tasks but have limited
- administrative permissions
+ • {t('settings:childAccounts.bulletThree')}
- • Managed accounts automatically added to your circle
+ • {t('settings:childAccounts.bulletFour')}
diff --git a/src/views/Settings/CircleSettings.jsx b/src/views/Settings/CircleSettings.jsx
index b83885f6..a14fd0e5 100644
--- a/src/views/Settings/CircleSettings.jsx
+++ b/src/views/Settings/CircleSettings.jsx
@@ -14,6 +14,7 @@ import {
import { useQueryClient } from '@tanstack/react-query'
import moment from 'moment'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useUserProfile } from '../../queries/UserQueries'
@@ -33,6 +34,7 @@ import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import SettingsLayout from './SettingsLayout'
const CircleSettings = () => {
+ const { t } = useTranslation(['settings', 'common'])
const { data: userProfile } = useUserProfile()
const queryClient = useQueryClient()
const { showNotification } = useNotification()
@@ -52,8 +54,8 @@ const CircleSettings = () => {
message,
title,
onConfirm,
- confirmText = 'Confirm',
- cancelText = 'Cancel',
+ confirmText = t('common:actions.continue'),
+ cancelText = t('common:actions.cancel'),
color = 'primary',
) => {
setConfirmModalConfig({
@@ -82,7 +84,7 @@ const CircleSettings = () => {
} catch (error) {
showNotification({
type: 'error',
- message: 'Failed to refresh member requests',
+ message: t('settings:circlePage.roleUpdateFailed'),
})
} finally {
setIsRefreshing(false)
@@ -120,18 +122,15 @@ const CircleSettings = () => {
}
return (
-
+
- Your account is automatically connected to a Circle when you create or
- join one. Easily invite friends by sharing the unique Circle code or
- link below. You'll receive a notification below when someone requests
- to join your Circle.
+ {t('settings:circlePage.description')}
{userCircles[0]?.userRole === 'member'
- ? `You part of ${userCircles[0]?.name} `
- : `You circle code is:`}
+ ? t('settings:circlePage.partOf', { name: userCircles[0]?.name })
+ : t('settings:circlePage.codeIs')}
{
navigator.clipboard.writeText(userCircles[0]?.invite_code)
showNotification({
type: 'success',
- message: 'Code copied to clipboard',
+ message: t('settings:circlePage.copyCodeSuccess'),
})
}}
>
- Copy Code
+ {t('settings:circlePage.copyCode')}
{
)
showNotification({
type: 'success',
- message: 'Link copied to clipboard',
+ message: t('settings:circlePage.copyLinkSuccess'),
})
}}
>
- Copy Link
+ {t('settings:circlePage.copyLink')}
{userCircles.length > 0 && userCircles[0]?.userRole === 'member' && (
{
sx={{ ml: 1 }}
onClick={() => {
showConfirmation(
- 'Are you sure you want to leave your circle?',
- 'Leave Circle',
+ t('settings:circlePage.leaveConfirm'),
+ t('settings:circlePage.leaveTitle'),
() => {
LeaveCircle(userCircles[0]?.id).then(resp => {
if (resp.ok) {
showNotification({
type: 'success',
- message: 'Left circle successfully',
+ message: t('settings:circlePage.leaveSuccess'),
})
} else {
showNotification({
type: 'error',
- message: 'Failed to leave circle',
+ message: t('settings:circlePage.leaveFailed'),
})
}
})
},
- 'Leave',
- 'Cancel',
+ t('settings:circlePage.leaveCircle'),
+ t('common:actions.cancel'),
'danger',
)
}}
>
- Leave Circle
+ {t('settings:circlePage.leaveCircle')}
)}
-
Circle Members
+
{t('settings:circlePage.members')}
{circleMembers.map(member => (
@@ -215,20 +214,24 @@ const CircleSettings = () => {
{member.displayName.charAt(0).toUpperCase() +
member.displayName.slice(1)}
- {member.userId === userProfile.id ? '(You)' : ''}{' '}
+ {member.userId === userProfile.id ? ' (Du)' : ''}{' '}
- {' '}
- {member.isActive ? member.role : 'Pending Approval'}
+ {member.isActive
+ ? member.role
+ : t('settings:circlePage.pendingApproval')}
{member.isActive ? (
- Joined on {fmt.date(member.createdAt)}
+ {t('settings:circlePage.joinedOn', {
+ date: fmt.date(member.createdAt),
+ })}
) : (
- Request to join{' '}
- {fmt.date(member.updatedAt)}
+ {t('settings:circlePage.requestedOn', {
+ date: fmt.date(member.updatedAt),
+ })}
)}
@@ -258,7 +261,7 @@ const CircleSettings = () => {
} else {
showNotification({
type: 'error',
- message: 'Failed to update role',
+ message: t('settings:circlePage.roleUpdateFailed'),
})
}
})
@@ -267,16 +270,21 @@ const CircleSettings = () => {
{[
{
value: 'member',
- description: 'Just a regular member of the circle',
+ description: t(
+ 'settings:circlePage.roleDescriptions.member',
+ ),
},
{
value: 'manager',
- description:
- 'Can impersonate users and perform actions on their behalf',
+ description: t(
+ 'settings:circlePage.roleDescriptions.manager',
+ ),
},
{
value: 'admin',
- description: 'Full access to the circle',
+ description: t(
+ 'settings:circlePage.roleDescriptions.admin',
+ ),
},
].map((option, index) => (
@@ -317,8 +325,10 @@ const CircleSettings = () => {
size='sm'
onClick={() => {
showConfirmation(
- `Are you sure you want to remove ${member.displayName} from your circle?`,
- 'Remove Member',
+ t('settings:circlePage.removeMemberConfirm', {
+ name: member.displayName,
+ }),
+ t('settings:circlePage.removeMemberTitle'),
() => {
DeleteCircleMember(
member.circleId,
@@ -327,7 +337,9 @@ const CircleSettings = () => {
if (resp.ok) {
showNotification({
type: 'success',
- message: 'Removed member successfully',
+ message: t(
+ 'settings:circlePage.removeMemberSuccess',
+ ),
})
queryClient.invalidateQueries(['circleMembers'])
queryClient.invalidateQueries(['userCircle'])
@@ -341,8 +353,8 @@ const CircleSettings = () => {
}
})
},
- 'Remove',
- 'Cancel',
+ t('common:actions.remove'),
+ t('common:actions.cancel'),
'danger',
)
}}
@@ -363,11 +375,13 @@ const CircleSettings = () => {
mb: 1,
}}
>
- Circle Member Requests
+ {t('settings:circlePage.requests')}
{lastRefresh && (
- Last updated: {fmt.dateTime(lastRefresh)}
+ {t('settings:circlePage.lastUpdated', {
+ date: fmt.dateTime(lastRefresh),
+ })}
)}
{
isRefreshing ? :
}
>
- {isRefreshing ? 'Refreshing...' : 'Refresh'}
+ {isRefreshing
+ ? t('settings:circlePage.refreshing')
+ : t('settings:circlePage.refresh')}
@@ -387,21 +403,26 @@ const CircleSettings = () => {
{circleMemberRequests.map(request => (
- {request.displayName} wants to join your circle.
+ {t('settings:circlePage.wantsToJoin', {
+ name: request.displayName,
+ })}
{
showConfirmation(
- `Are you sure you want to accept ${request.displayName} (username: ${request.username}) to join your circle?`,
- 'Accept Member Request',
+ t('settings:circlePage.acceptConfirm', {
+ name: request.displayName,
+ username: request.username,
+ }),
+ t('settings:circlePage.acceptTitle'),
() => {
AcceptCircleMemberRequest(request.id).then(resp => {
if (resp.ok) {
showNotification({
type: 'success',
- message: 'Accepted request successfully',
+ message: t('settings:circlePage.acceptSuccess'),
})
queryClient.invalidateQueries(['circleMembers'])
queryClient.invalidateQueries(['circleMemberRequests'])
@@ -416,26 +437,25 @@ const CircleSettings = () => {
}
})
},
- 'Accept',
- 'Cancel',
+ t('settings:circlePage.accept'),
+ t('common:actions.cancel'),
)
}}
>
- Accept
+ {t('settings:circlePage.accept')}
))}
- or
+ {t('common:actions.or')}
- if want to join someone else's Circle? Ask them for their unique
- Circle code or join link. Enter the code below to join their Circle.
+ {t('settings:circlePage.joinPrompt')}
- Enter Circle code:
+ {t('settings:circlePage.enterCode')}
setCircleInviteCode(e.target.value)}
size='lg'
@@ -451,20 +471,19 @@ const CircleSettings = () => {
if (resp.ok) {
showNotification({
type: 'success',
- message:
- 'Joined circle successfully, wait for the circle owner to accept your request.',
+ message: t('settings:circlePage.joinSuccess'),
})
setTimeout(() => navigate('/'), 3000)
} else {
if (resp.status === 409) {
showNotification({
type: 'error',
- message: 'You are already a member of this circle',
+ message: t('settings:circlePage.alreadyMember'),
})
} else {
showNotification({
type: 'error',
- message: 'Failed to join circle',
+ message: t('settings:circlePage.joinFailed'),
})
}
setTimeout(() => navigate('/'), 3000)
@@ -472,7 +491,7 @@ const CircleSettings = () => {
})
}}
>
- Join Circle
+ {t('settings:circlePage.joinCircle')}
diff --git a/src/views/Settings/DeveloperSettings.jsx b/src/views/Settings/DeveloperSettings.jsx
index 35ef1835..b3d0d4ef 100644
--- a/src/views/Settings/DeveloperSettings.jsx
+++ b/src/views/Settings/DeveloperSettings.jsx
@@ -2,6 +2,7 @@ import { Refresh, Token } from '@mui/icons-material'
import { Box, Button, Card, Chip, Divider, Typography } from '@mui/joy'
import { useEffect, useState } from 'react'
import { LocalNotifications } from '@capacitor/local-notifications'
+import { useTranslation } from 'react-i18next'
import { useSSEContext } from '../../hooks/useSSEContext'
import { useNotification } from '../../service/NotificationProvider'
import { apiClient } from '../../utils/ApiClient'
@@ -9,6 +10,7 @@ import { RefreshToken } from '../../utils/Fetcher'
import { getRefreshTokenExpiry, isNative } from '../../utils/TokenStorage'
const DeveloperSettings = () => {
+ const { t } = useTranslation(['settings'])
const {
isConnected,
isConnecting,
@@ -112,8 +114,8 @@ const DeveloperSettings = () => {
}, [accessTokenExpiry, refreshTokenExpiry, getDebugInfo])
const formatTimeLeft = milliseconds => {
- if (milliseconds === null) return 'N/A'
- if (milliseconds === 0) return 'Expired'
+ if (milliseconds === null) return t('settings:developer.notAvailable')
+ if (milliseconds === 0) return t('settings:developer.timeExpired')
const totalSeconds = Math.floor(milliseconds / 1000)
const days = Math.floor(totalSeconds / 86400)
@@ -145,7 +147,7 @@ const DeveloperSettings = () => {
if (result.success) {
showNotification({
type: 'success',
- message: 'Token refreshed successfully',
+ message: t('settings:developer.tokenRefreshed'),
})
// Reload token expiry data
@@ -159,13 +161,17 @@ const DeveloperSettings = () => {
} else {
showNotification({
type: 'error',
- message: `Token refresh failed: ${result.error}`,
+ message: t('settings:developer.tokenRefreshFailed', {
+ message: result.error,
+ }),
})
}
} catch (error) {
showNotification({
type: 'error',
- message: `Token refresh error: ${error.message}`,
+ message: t('settings:developer.tokenRefreshError', {
+ message: error.message,
+ }),
})
} finally {
setIsRefreshing(false)
@@ -181,7 +187,7 @@ const DeveloperSettings = () => {
const data = await response.json()
showNotification({
type: 'success',
- message: 'Refresh token endpoint called successfully',
+ message: t('settings:developer.refreshEndpointSuccess'),
})
// Reload token expiry data
@@ -198,13 +204,18 @@ const DeveloperSettings = () => {
const error = await response.text()
showNotification({
type: 'error',
- message: `Refresh token endpoint failed: ${response.status} ${error}`,
+ message: t('settings:developer.refreshEndpointFailed', {
+ status: response.status,
+ message: error,
+ }),
})
}
} catch (error) {
showNotification({
type: 'error',
- message: `Refresh token endpoint error: ${error.message}`,
+ message: t('settings:developer.refreshEndpointError', {
+ message: error.message,
+ }),
})
} finally {
setIsRefreshingDirect(false)
@@ -226,13 +237,17 @@ const DeveloperSettings = () => {
setScheduledNotifications(sorted)
showNotification({
type: 'success',
- message: `Loaded ${sorted.length} scheduled notifications`,
+ message: t('settings:developer.notificationsLoaded', {
+ count: sorted.length,
+ }),
})
} catch (error) {
console.error('Error loading scheduled notifications:', error)
showNotification({
type: 'error',
- message: `Error loading notifications: ${error.message}`,
+ message: t('settings:developer.notificationsLoadError', {
+ message: error.message,
+ }),
})
} finally {
setIsLoadingNotifications(false)
@@ -254,12 +269,10 @@ const DeveloperSettings = () => {
return (
-
Developer Settings
+
{t('settings:developer.title')}
- View technical information about your authentication tokens and session
- state. This information is useful for debugging and development
- purposes.
+ {t('settings:developer.description')}
@@ -273,7 +286,9 @@ const DeveloperSettings = () => {
gap: 1,
}}
>
- Authentication Tokens
+
+ {t('settings:developer.authTokens')}
+
{
loading={isRefreshing}
disabled={isRefreshing || isRefreshingDirect}
>
- Refresh Token
+ {t('settings:developer.refreshTokenAction')}
{
loading={isRefreshingDirect}
disabled={isRefreshing || isRefreshingDirect}
>
- Call Refresh Endpoint
+ {t('settings:developer.callRefreshEndpoint')}
- Access Token
+ {t('settings:developer.accessToken')}
{
flexWrap: 'wrap',
}}
>
- Time Left:
+
+ {t('settings:developer.timeLeft')}
+
{formatTimeLeft(timeLeft.access)}
{accessTokenExpiry && (
- Expires: {new Date(accessTokenExpiry).toLocaleString()}
+ {t('settings:developer.expires', {
+ date: new Date(accessTokenExpiry).toLocaleString(),
+ })}
)}
@@ -327,7 +346,7 @@ const DeveloperSettings = () => {
- Refresh Token
+ {t('settings:developer.refreshToken')}
{isNativePlatform ? (
<>
@@ -339,7 +358,9 @@ const DeveloperSettings = () => {
flexWrap: 'wrap',
}}
>
- Time Left:
+
+ {t('settings:developer.timeLeft')}
+
{
{refreshTokenExpiry && (
- Expires: {new Date(refreshTokenExpiry).toLocaleString()}
+ {t('settings:developer.expires', {
+ date: new Date(refreshTokenExpiry).toLocaleString(),
+ })}
)}
>
) : (
- Refresh tokens are managed via HTTP-only cookies on web platform
+ {t('settings:developer.refreshTokenCookie')}
)}
@@ -364,13 +387,17 @@ const DeveloperSettings = () => {
- Platform Information
+
+ {t('settings:developer.platformInfo')}
+
- Platform:{' '}
+ {t('settings:developer.platform')}{' '}
- {isNativePlatform ? 'Native' : 'Web'}
+ {isNativePlatform
+ ? t('settings:developer.native')
+ : t('settings:developer.web')}
@@ -390,7 +417,7 @@ const DeveloperSettings = () => {
}}
>
- Scheduled Local Notifications
+ {t('settings:developer.scheduledNotifications')}
{
loading={isLoadingNotifications}
disabled={isLoadingNotifications}
>
- Refresh
+ {t('settings:developer.refresh')}
@@ -408,12 +435,12 @@ const DeveloperSettings = () => {
{scheduledNotifications.length === 0 ? (
- No scheduled notifications
+ {t('settings:developer.noScheduledNotifications')}
) : (
- Total scheduled:{' '}
+ {t('settings:developer.totalScheduled')}{' '}
{scheduledNotifications.length}
@@ -454,10 +481,12 @@ const DeveloperSettings = () => {
}}
>
- {notification.title || 'No title'}
+ {notification.title ||
+ t('settings:developer.noTitle')}
- {notification.body || 'No body'}
+ {notification.body ||
+ t('settings:developer.noBody')}
{
>
{timeUntil && timeUntil > 0
? formatTimeLeft(timeUntil)
- : 'Past due'}
+ : t('settings:developer.pastDue')}
{scheduledDate.toLocaleString()}
@@ -489,7 +518,9 @@ const DeveloperSettings = () => {
)}
{notification.extra?.choreId && (
- Chore ID: {notification.extra.choreId}
+ {t('settings:developer.choreId', {
+ id: notification.extra.choreId,
+ })}
)}
@@ -506,11 +537,13 @@ const DeveloperSettings = () => {
- Server-Sent Events (SSE)
+
+ {t('settings:developer.sseTitle')}
+
- Connection Status
+ {t('settings:developer.connectionStatus')}
{
>
{getConnectionStatus
? getConnectionStatus().toUpperCase()
- : 'Unknown'}
+ : t('settings:developer.unknown')}
{sseError && (
- Error: {sseError}
+ {t('settings:developer.error', { error: sseError })}
)}
@@ -542,21 +575,22 @@ const DeveloperSettings = () => {
- Last Event Received
+ {t('settings:developer.lastEventReceived')}
{lastEvent ? (
<>
- Type:{' '}
+ {t('settings:developer.type')}{' '}
{lastEvent.type}
- Received:{' '}
- {lastEvent.timestamp
- ? new Date(lastEvent.timestamp).toLocaleString()
- : 'N/A'}
+ {t('settings:developer.received', {
+ date: lastEvent.timestamp
+ ? new Date(lastEvent.timestamp).toLocaleString()
+ : t('settings:developer.notAvailable'),
+ })}
>
) : (
diff --git a/src/views/Settings/LocalizationSettings.jsx b/src/views/Settings/LocalizationSettings.jsx
index d4632282..29c1b829 100644
--- a/src/views/Settings/LocalizationSettings.jsx
+++ b/src/views/Settings/LocalizationSettings.jsx
@@ -19,7 +19,7 @@ import { useTranslation } from 'react-i18next'
import SettingsLayout from './SettingsLayout'
const LocalizationSettings = () => {
- const { t } = useTranslation('settings')
+ const { t } = useTranslation(['settings', 'common'])
const {
language,
setLanguage,
@@ -44,7 +44,7 @@ const LocalizationSettings = () => {
]
return (
-
+
{t('localization.description')}
@@ -110,7 +110,7 @@ const LocalizationSettings = () => {
))}
- Preview: {sampleDate.format(dateFormat)}
+ {t('common:labels.preview')}: {sampleDate.format(dateFormat)}
@@ -157,7 +157,7 @@ const LocalizationSettings = () => {
- Preview: {sampleDate.format(timeFormat)}
+ {t('common:labels.preview')}: {sampleDate.format(timeFormat)}
diff --git a/src/views/Settings/MFASettings.jsx b/src/views/Settings/MFASettings.jsx
index d913c2a0..17b788ec 100644
--- a/src/views/Settings/MFASettings.jsx
+++ b/src/views/Settings/MFASettings.jsx
@@ -13,6 +13,7 @@ import {
} from '@mui/joy'
import QRCode from 'qrcode'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import {
ConfirmMFA,
DisableMFA,
@@ -24,6 +25,7 @@ import LoadingComponent from '../components/Loading'
import SettingsLayout from './SettingsLayout'
const MFASettings = () => {
+ const { t } = useTranslation(['settings', 'common', 'auth'])
const [mfaEnabled, setMfaEnabled] = useState(false)
const [loading, setLoading] = useState(true)
const [setupModalOpen, setSetupModalOpen] = useState(false)
@@ -66,7 +68,7 @@ const MFASettings = () => {
setQrCodeDataUrl(qrCodeDataUrl)
} catch (error) {
console.error('Error generating QR code:', error)
- setError('Failed to generate QR code')
+ setError(t('settings:mfaPage.failedQr'))
}
}
@@ -89,7 +91,7 @@ const MFASettings = () => {
hasQrCodeUrl: !!data.qrCodeUrl,
hasSecret: !!data.secret,
})
- setError('Invalid response from server. Missing QR code or secret.')
+ setError(t('settings:mfaPage.invalidResponse'))
return
}
if (data.backupCodes) {
@@ -108,24 +110,22 @@ const MFASettings = () => {
} else {
// Handle different error status codes
if (response.status === 404) {
- setError(
- 'MFA setup endpoint not found. This feature may not be available yet.',
- )
+ setError(t('settings:mfaPage.endpointMissing'))
} else if (response.status === 401) {
- setError('Unauthorized. Please login again.')
+ setError(t('settings:mfaPage.unauthorized'))
} else if (response.status === 500) {
- setError('Server error. Please try again later.')
+ setError(t('settings:mfaPage.serverError'))
} else {
const errorData = await response.json().catch(() => ({}))
setError(
errorData.message ||
- `Failed to setup MFA (${response.status}). Please try again.`,
+ t('settings:mfaPage.setupFailed', { status: response.status }),
)
}
}
} catch (error) {
console.error('Error setting up MFA:', error)
- setError('Network error. Please check your connection and try again.')
+ setError(t('settings:mfaPage.networkError'))
}
}
@@ -140,12 +140,12 @@ const MFASettings = () => {
if (response.ok) {
setSetupStep(3)
setMfaEnabled(true)
- setSuccess('MFA has been successfully enabled!')
+ setSuccess(t('settings:mfaPage.enabledToast'))
} else {
- setError('Invalid verification code. Please try again.')
+ setError(t('settings:mfaPage.invalidCode'))
}
} catch (error) {
- setError('Failed to confirm MFA. Please try again.')
+ setError(t('settings:mfaPage.confirmFailed'))
console.error('Error confirming MFA:', error)
}
}
@@ -158,12 +158,12 @@ const MFASettings = () => {
setMfaEnabled(false)
setDisableModalOpen(false)
setDisableCode('')
- setSuccess('MFA has been disabled successfully!')
+ setSuccess(t('settings:mfaPage.disabledToast'))
} else {
- setError('Invalid verification code. Please try again.')
+ setError(t('settings:mfaPage.invalidCode'))
}
} catch (error) {
- setError('Failed to disable MFA. Please try again.')
+ setError(t('settings:mfaPage.disableFailed'))
console.error('Error disabling MFA:', error)
}
}
@@ -176,12 +176,12 @@ const MFASettings = () => {
const data = await response.json()
setBackupCodes(data.backupCodes)
setBackupCodesModalOpen(true)
- setSuccess('New backup codes have been generated!')
+ setSuccess(t('settings:mfaPage.backupRegenerated'))
} else {
- setError('Failed to regenerate backup codes. Please try again.')
+ setError(t('settings:mfaPage.regenerateFailed'))
}
} catch (error) {
- setError('Failed to regenerate backup codes. Please try again.')
+ setError(t('settings:mfaPage.regenerateFailed'))
console.error('Error regenerating backup codes:', error)
}
}
@@ -206,13 +206,10 @@ const MFASettings = () => {
}
return (
-
+
- Add an extra layer of security to your account with multi-factor
- authentication (MFA). When enabled, you'll need to provide a
- verification code from your authenticator app in addition to your
- password when signing in.
+ {t('settings:mfaPage.description')}
{success && (
@@ -233,12 +230,12 @@ const MFASettings = () => {
- Two-Factor Authentication
+ {t('settings:mfaPage.title')}
{mfaEnabled
- ? 'Your account is protected with 2FA'
- : 'Secure your account with an authenticator app'}
+ ? t('settings:mfaPage.enabled')
+ : t('settings:mfaPage.disabled')}
@@ -249,7 +246,7 @@ const MFASettings = () => {
variant='outlined'
onClick={() => setDisableModalOpen(true)}
>
- Disable
+ {t('settings:mfaPage.disable')}
) : (
{
variant='solid'
onClick={handleSetupMFA}
>
- Enable
+ {t('settings:mfaPage.enable')}
)}
@@ -294,14 +291,13 @@ const MFASettings = () => {
- Set up Multi-Factor Authentication
+ {t('settings:mfaPage.setupTitle')}
{setupStep === 1 && setupData && (
- Step 1: Scan the QR code below with your
- authenticator app (Google Authenticator, Authy, etc.)
+ {t('settings:mfaPage.step1')}
@@ -316,8 +312,7 @@ const MFASettings = () => {
/>
) : (
- QR code could not be generated. Please try again or use
- the manual entry key below.
+ {t('settings:mfaPage.failedQr')}
)}
@@ -332,7 +327,7 @@ const MFASettings = () => {
}}
>
- Manual entry key:
+ {t('settings:mfaPage.manualKey')}
{
onClick={() => setSetupStep(2)}
startDecorator={ }
>
- I've added the account to my app
+ {t('settings:mfaPage.addedToApp')}
)}
@@ -355,12 +350,11 @@ const MFASettings = () => {
{setupStep === 2 && (
- Step 2: Enter the 6-digit verification code
- from your authenticator app
+ {t('settings:mfaPage.step2')}
{
onClick={() => setSetupStep(1)}
sx={{ flex: 1 }}
>
- Back
+ {t('navigation:back', { defaultValue: 'Back' })}
{
disabled={verificationCode.length !== 6}
sx={{ flex: 1 }}
>
- Verify & Enable
+ {t('settings:mfaPage.verifyEnable')}
@@ -410,17 +404,16 @@ const MFASettings = () => {
- MFA Successfully Enabled!
+ {t('settings:mfaPage.enabledSuccess')}
- Save these backup codes in a safe place
+ {t('settings:mfaPage.backupSaveTitle')}
- You can use these codes to access your account if you lose
- your authenticator device. Each code can only be used once.
+ {t('settings:mfaPage.backupSaveDescription')}
@@ -439,7 +432,7 @@ const MFASettings = () => {
- I've saved my backup codes
+ {t('settings:mfaPage.savedBackupCodes')}
)}
@@ -451,24 +444,22 @@ const MFASettings = () => {
- Disable Multi-Factor Authentication
+ {t('settings:mfaPage.disableTitle')}
- Disabling MFA will make your account less secure. Are you sure
- you want to continue?
+ {t('settings:mfaPage.disableWarning')}
- Enter a verification code from your authenticator app to
- confirm:
+ {t('settings:mfaPage.disablePrompt')}
{
@@ -498,7 +489,7 @@ const MFASettings = () => {
onClick={closeDisableModal}
sx={{ flex: 1 }}
>
- Cancel
+ {t('common:actions.cancel')}
{
disabled={disableCode.length !== 6}
sx={{ flex: 1 }}
>
- Disable MFA
+ {t('settings:mfaPage.disableTitle')}
@@ -521,14 +512,13 @@ const MFASettings = () => {
- New Backup Codes
+ {t('settings:mfaPage.backupCodesTitle')}
- Your previous backup codes are now invalid. Save these new
- codes in a safe place. Each code can only be used once.
+ {t('settings:mfaPage.backupCodesWarning')}
@@ -550,7 +540,7 @@ const MFASettings = () => {
color='primary'
onClick={() => setBackupCodesModalOpen(false)}
>
- I've saved my backup codes
+ {t('settings:mfaPage.savedBackupCodes')}
diff --git a/src/views/Settings/NotificationSetting.jsx b/src/views/Settings/NotificationSetting.jsx
index ee22a23b..1d2a08b9 100644
--- a/src/views/Settings/NotificationSetting.jsx
+++ b/src/views/Settings/NotificationSetting.jsx
@@ -18,6 +18,7 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { PushNotifications } from '@capacitor/push-notifications'
import { registerPushNotifications } from '../../CapacitorListener'
@@ -31,6 +32,7 @@ import {
import SettingsLayout from './SettingsLayout'
const NotificationSetting = () => {
+ const { t } = useTranslation(['settingsExtras', 'common', 'settings'])
const { showWarning } = useNotification()
const { data: userProfile, refetch: refetchUserProfile } = useUserProfile()
const { data: deviceTokens, refetch: refetchDevices } = useDeviceTokens()
@@ -149,8 +151,8 @@ const NotificationSetting = () => {
const handleDeviceRegistered = () => {
refetchDevices()
showWarning({
- title: 'Success',
- message: 'Device registered successfully for push notifications.',
+ title: t('common:notifications.titles.success'),
+ message: t('settingsExtras:notifications.deviceRegisteredMessage'),
})
}
@@ -159,16 +161,15 @@ const NotificationSetting = () => {
if (status === 409) {
showWarning({
- title: 'Device Limit Reached',
- message:
- 'You have reached the maximum limit of 5 registered devices. Please remove a device before registering this one.',
+ title: t('settingsExtras:notifications.deviceLimitReached'),
+ message: t('settingsExtras:notifications.deviceLimitReachedMessage'),
})
} else {
showWarning({
- title: 'Registration Failed',
+ title: t('settingsExtras:notifications.registrationFailed'),
message:
error ||
- 'Failed to register device automatically. Please try again.',
+ t('settingsExtras:notifications.registrationFailedMessage'),
})
}
}
@@ -195,16 +196,16 @@ const NotificationSetting = () => {
switch (notificationTarget) {
case '1':
if (chatID === '') {
- setError('Chat ID is required')
+ setError(t('settingsExtras:notifications.chatIdRequired'))
return false
} else if (isNaN(chatID) || chatID === '0') {
- setError('Invalid Chat ID')
+ setError(t('settingsExtras:notifications.invalidChatId'))
return false
}
break
case '2':
if (chatID === '') {
- setError('User key is required')
+ setError(t('settingsExtras:notifications.userKeyRequired'))
return false
}
break
@@ -222,12 +223,16 @@ const NotificationSetting = () => {
type: Number(notificationTarget),
}).then(resp => {
if (resp.status != 200) {
- alert(`Error while updating notification target: ${resp.statusText}`)
+ alert(
+ t('settingsExtras:notifications.updateFailed', {
+ status: resp.statusText,
+ }),
+ )
return
}
refetchUserProfile()
- alert('Notification target updated')
+ alert(t('settingsExtras:notifications.targetUpdated'))
})
}
@@ -238,9 +243,8 @@ const NotificationSetting = () => {
const currentDeviceCount = deviceTokens ? deviceTokens.length : 0
if (currentDeviceCount >= 5) {
showWarning({
- title: 'Device Limit Reached',
- message:
- 'You have reached the maximum limit of 5 registered devices. Please remove a device before registering this one.',
+ title: t('settingsExtras:notifications.deviceLimitReached'),
+ message: t('settingsExtras:notifications.deviceLimitReachedMessage'),
})
return
}
@@ -251,9 +255,8 @@ const NotificationSetting = () => {
if (permStatus.receive !== 'granted') {
showWarning({
- title: 'Permission Required',
- message:
- 'Push notification permission is required to register this device.',
+ title: t('settingsExtras:notifications.permissionRequired'),
+ message: t('settingsExtras:notifications.permissionRequiredMessage'),
})
return
}
@@ -267,25 +270,26 @@ const NotificationSetting = () => {
setPushNotification(true)
showWarning({
- title: 'Registration Initiated',
- message:
- 'Push notification registration has been initiated. The device will be registered automatically.',
+ title: t('settingsExtras:notifications.registrationInitiated'),
+ message: t('settingsExtras:notifications.registrationInitiatedMessage'),
})
} catch (error) {
console.error('Error registering device:', error)
showWarning({
- title: 'Error',
- message: 'Failed to register device. Please try again.',
+ title: t('common:notifications.titles.error'),
+ message: t('settingsExtras:notifications.registrationFailedMessage'),
})
}
}
return (
-
+
-
Device Notification
+
{t('settingsExtras:notifications.deviceTitle')}
-
Manage your Device Notification
+
+ {t('settingsExtras:notifications.deviceDescription')}
+
{
setNotificationPreferences({ granted: true })
} else if (resp.display === 'denied') {
showWarning({
- title: 'Notification Permission Denied',
- message:
- 'You have denied notification permissions. You can enable them later in your device settings.',
+ title: t(
+ 'settingsExtras:notifications.notificationPermissionDenied',
+ ),
+ message: t(
+ 'settingsExtras:notifications.notificationPermissionDeniedMessage',
+ ),
})
setDeviceNotification(false)
setNotificationPreferences({ granted: false })
@@ -324,11 +331,11 @@ const NotificationSetting = () => {
sx={{ mr: 2 }}
/>
- Device Notification
+ {t('settingsExtras:notifications.deviceLabel')}
{Capacitor.isNativePlatform()
- ? 'Receive notification on your device when a task is due'
- : 'This feature is only available on mobile devices'}{' '}
+ ? t('settingsExtras:notifications.deviceNativeHelper')
+ : t('settingsExtras:notifications.mobileOnlyHelper')}{' '}
@@ -345,8 +352,8 @@ const NotificationSetting = () => {
LocalNotifications.schedule({
notifications: [
{
- title: 'Test Notification',
- body: 'You have a task due soon',
+ title: t('settingsExtras:notifications.testTitle'),
+ body: t('settingsExtras:notifications.testBody'),
id: 1,
schedule: { at: new Date(Date.now() + 2000) },
sound: null,
@@ -358,32 +365,32 @@ const NotificationSetting = () => {
})
}}
>
- Test Notification{' '}
+ {t('settingsExtras:notifications.testNotification')}{' '}
{deviceNotification && (
{[
{
- title: 'Due Date Notification',
+ title: t('settingsExtras:notifications.dueDateNotification'),
checked: dueNotification,
set: setDueNotification,
- label: 'Notification when the task is due',
+ label: t('settingsExtras:notifications.dueDateNotificationHelper'),
property: 'dueNotification',
disabled: false,
},
{
- title: 'Pre-Due Date Notification',
+ title: t('settingsExtras:notifications.preDueNotification'),
checked: preDueNotification,
set: setPreDueNotification,
- label: 'Notification a few hours before the task is due',
+ label: t('settingsExtras:notifications.preDueNotificationHelper'),
property: 'preDueNotification',
disabled: false,
},
{
- title: 'Overdue Notification',
+ title: t('settingsExtras:notifications.overdueNotification'),
checked: naggingNotification,
set: setNaggingNotification,
- label: 'Notification when the task is overdue',
+ label: t('settingsExtras:notifications.overdueNotificationHelper'),
property: 'naggingNotification',
disabled: false,
},
@@ -409,7 +416,11 @@ const NotificationSetting = () => {
}}
color={item.checked ? 'success' : ''}
variant='solid'
- endDecorator={item.checked ? 'On' : 'Off'}
+ endDecorator={
+ item.checked
+ ? t('settingsExtras:notifications.on')
+ : t('settingsExtras:notifications.off')
+ }
slotProps={{ endDecorator: { sx: { minWidth: 24 } } }}
/>
@@ -422,11 +433,11 @@ const NotificationSetting = () => {
sx={{ width: 400, justifyContent: 'space-between' }}
>
- Push Notifications
+ {t('settingsExtras:notifications.pushNotifications')}
{Capacitor.isNativePlatform()
- ? 'Receive Nudges, Announcements, and Chore Assignments via Push Notifications'
- : 'This feature is only available on mobile devices'}{' '}
+ ? t('settingsExtras:notifications.pushNotificationsHelper')
+ : t('settingsExtras:notifications.mobileOnlyHelper')}{' '}
{
}
if (resp.receive !== 'granted') {
showWarning({
- title: 'Push Notification Permission Denied',
- message:
- 'Push notifications have been disabled. You can enable them in your device settings if needed.',
+ title: t(
+ 'settingsExtras:notifications.pushPermissionDenied',
+ ),
+ message: t(
+ 'settingsExtras:notifications.pushPermissionDeniedMessage',
+ ),
})
setPushNotification(false)
setPushNotificationPreferences({ granted: false })
@@ -463,7 +477,11 @@ const NotificationSetting = () => {
}}
color={pushNotification ? 'success' : 'neutral'}
variant={pushNotification ? 'solid' : 'outlined'}
- endDecorator={pushNotification ? 'On' : 'Off'}
+ endDecorator={
+ pushNotification
+ ? t('settingsExtras:notifications.on')
+ : t('settingsExtras:notifications.off')
+ }
slotProps={{
endDecorator: {
sx: {
@@ -478,11 +496,13 @@ const NotificationSetting = () => {
{isOfficialInstance && (
<>
- Registered Devices ({deviceTokens ? deviceTokens.length : 0}/5)
+ {t('settingsExtras:notifications.registeredDevices', {
+ count: deviceTokens ? deviceTokens.length : 0,
+ })}
- Devices registered to receive push notifications for your account
+ {t('settingsExtras:notifications.registeredDevicesDescription')}
{/* Show register current device option if not registered */}
@@ -508,12 +528,12 @@ const NotificationSetting = () => {
)}
- Current Device:{' '}
+ {t('common:labels.currentDevice')}:{' '}
{currentDevice.platform === 'ios' ? 'iOS' : 'Android'}{' '}
{currentDevice.model}
- This device is not registered for push notifications
+ {t('settingsExtras:notifications.currentDeviceUnregistered')}
@@ -525,8 +545,8 @@ const NotificationSetting = () => {
onClick={handleRegisterCurrentDevice}
>
{deviceTokens && deviceTokens.length >= 5
- ? 'Limit Reached'
- : 'Register Device'}
+ ? t('settingsExtras:notifications.limitReached')
+ : t('common:actions.registerDevice')}
@@ -557,12 +577,12 @@ const NotificationSetting = () => {
sx={{ fontWeight: 'bold' }}
>
{device.platform === 'ios' ? 'iOS' : 'Android'}{' '}
- {device.deviceModel || 'Unknown Device'}
+ {device.deviceModel || t('common:status.unknown')}
{device.createdAt && (
- Created At:{' '}
+ {t('settingsExtras:notifications.createdAt')}:{' '}
{new Date(device.createdAt).toLocaleDateString()}
)}
@@ -582,19 +602,23 @@ const NotificationSetting = () => {
refetchDevices()
} else {
showWarning({
- title: 'Error',
- message: 'Failed to unregister device',
+ title: t('common:notifications.titles.error'),
+ message: t(
+ 'settingsExtras:notifications.removeDeviceFailed',
+ ),
})
}
} catch (error) {
showWarning({
- title: 'Error',
- message: 'Failed to unregister device',
+ title: t('common:notifications.titles.error'),
+ message: t(
+ 'settingsExtras:notifications.removeDeviceFailed',
+ ),
})
}
}}
>
- Remove
+ {t('common:actions.remove')}
@@ -602,16 +626,16 @@ const NotificationSetting = () => {
) : (
- No devices registered for push notifications
+ {t('settingsExtras:notifications.noRegisteredDevices')}
)}
>
)}
-
Custom Notification
+
{t('settingsExtras:notifications.customTitle')}
- Notification through other platform like Telegram or Pushover
+ {t('settingsExtras:notifications.customDescription')}
@@ -649,9 +673,9 @@ const NotificationSetting = () => {
sx={{ mr: 2 }}
/>
- Custom Notification
+ {t('settingsExtras:notifications.customLabel')}
- Receive notification on other platform
+ {t('settingsExtras:notifications.customHelper')}
@@ -668,16 +692,17 @@ const NotificationSetting = () => {
sx={{ maxWidth: '200px' }}
onChange={(e, selected) => setNotificationTarget(selected)}
>
-
None
+
{t('settingsExtras:notifications.none')}
Telegram
Pushover
-
Webhooks
+
+ {t('settingsExtras:notifications.webhooks')}
+
{notificationTarget === '1' && (
<>
- You need to initiate a message to the bot in order for the
- Telegram notification to work{' '}
+ {t('settingsExtras:notifications.telegramSetup')}{' '}
{
}}
href='https://t.me/DonetickBot'
>
- Click here
+ {t('settingsExtras:notifications.clickHere')}
{' '}
- to start a chat
+ {t('settingsExtras:notifications.startChat')}
-
Chat ID
+
{t('common:labels.chatId')}
setChatID(e.target.value)}
- placeholder='User ID / Chat ID'
+ placeholder={t(
+ 'settingsExtras:notifications.chatIdPlaceholder',
+ )}
sx={{
width: '200px',
}}
/>
- If you don't know your Chat ID, start chat with userinfobot
- and it will send you your Chat ID.{' '}
+ {t('settingsExtras:notifications.chatIdHelp')}{' '}
{
}}
href='https://t.me/userinfobot'
>
- Click here
+ {t('settingsExtras:notifications.clickHere')}
{' '}
- to start chat with userinfobot{' '}
+ {t('settingsExtras:notifications.userInfoBot')}{' '}
>
)}
{notificationTarget === '2' && (
<>
-
User key
+
{t('common:labels.userKey')}
setChatID(e.target.value)}
- placeholder='User ID'
+ placeholder={t(
+ 'settingsExtras:notifications.userKeyPlaceholder',
+ )}
sx={{
width: '200px',
}}
@@ -742,7 +770,7 @@ const NotificationSetting = () => {
}}
onClick={handleSave}
>
- Save
+ {t('common:actions.save')}
)}
diff --git a/src/views/Settings/Settings.jsx b/src/views/Settings/Settings.jsx
index 5ff6d9be..b431b264 100644
--- a/src/views/Settings/Settings.jsx
+++ b/src/views/Settings/Settings.jsx
@@ -20,6 +20,7 @@ import { Purchases } from '@revenuecat/purchases-capacitor'
import { useQueryClient } from '@tanstack/react-query'
import moment from 'moment'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import RealTimeSettings from '../../components/RealTimeSettings'
import SubscriptionModal from '../../components/SubscriptionModal'
@@ -56,6 +57,7 @@ import StorageSettings from './StorageSettings'
import ThemeToggle from './ThemeToggle'
const Settings = () => {
+ const { t } = useTranslation(['settings', 'common'])
const { data: userProfile } = useUserProfile()
const queryClient = useQueryClient()
const { showNotification } = useNotification()
@@ -82,8 +84,8 @@ const Settings = () => {
message,
title,
onConfirm,
- confirmText = 'Confirm',
- cancelText = 'Cancel',
+ confirmText = t('common:actions.continue'),
+ cancelText = t('common:actions.cancel'),
color = 'primary',
) => {
setConfirmModalConfig({
@@ -240,7 +242,7 @@ const Settings = () => {
-
Circle settings
+
{t('settings:pages.circle.title')}
Your account is automatically connected to a Circle when you create or
@@ -327,7 +329,9 @@ const Settings = () => {
)}
-
Circle Members
+
+ {t('settings:circlePage.members')}
+
{circleMembers.map(member => (
@@ -485,7 +489,9 @@ const Settings = () => {
mb: 1,
}}
>
- Circle Member Requests
+
+ {t('settings:circlePage.requests')}
+
{lastRefresh && (
@@ -501,7 +507,9 @@ const Settings = () => {
isRefreshing ? :
}
>
- {isRefreshing ? 'Refreshing...' : 'Refresh'}
+ {isRefreshing
+ ? t('settings:circlePage.refreshing')
+ : t('settings:circlePage.refresh')}
@@ -509,21 +517,26 @@ const Settings = () => {
{circleMemberRequests.map(request => (
- {request.displayName} wants to join your circle.
+ {t('settings:circlePage.wantsToJoin', {
+ name: request.displayName,
+ })}
{
showConfirmation(
- `Are you sure you want to accept ${request.displayName} (username: ${request.username}) to join your circle?`,
- 'Accept Member Request',
+ t('settings:circlePage.acceptConfirm', {
+ name: request.displayName,
+ username: request.username,
+ }),
+ t('settings:circlePage.acceptTitle'),
() => {
AcceptCircleMemberRequest(request.id).then(resp => {
if (resp.ok) {
showNotification({
type: 'success',
- message: 'Accepted request successfully',
+ message: t('settings:circlePage.acceptSuccess'),
})
// Invalidate and refetch circle-related queries
queryClient.invalidateQueries(['circleMembers'])
@@ -540,26 +553,25 @@ const Settings = () => {
}
})
},
- 'Accept',
- 'Cancel',
+ t('settings:circlePage.accept'),
+ t('common:actions.cancel'),
)
}}
>
- Accept
+ {t('settings:circlePage.accept')}
))}
- or
+ {t('common:actions.or')}
- if want to join someone else's Circle? Ask them for their unique
- Circle code or join link. Enter the code below to join their Circle.
+ {t('settings:circlePage.joinPrompt')}
- Enter Circle code:
+ {t('settings:circlePage.enterCode')}
setCircleInviteCode(e.target.value)}
size='lg'
@@ -575,20 +587,19 @@ const Settings = () => {
if (resp.ok) {
showNotification({
type: 'success',
- message:
- 'Joined circle successfully, wait for the circle owner to accept your request.',
+ message: t('settings:circlePage.joinSuccess'),
})
setTimeout(() => navigate('/'), 3000)
} else {
if (resp.status === 409) {
showNotification({
type: 'error',
- message: 'You are already a member of this circle',
+ message: t('settings:circlePage.alreadyMember'),
})
} else {
showNotification({
type: 'error',
- message: 'Failed to join circle',
+ message: t('settings:circlePage.joinFailed'),
})
}
setTimeout(() => navigate('/'), 3000)
@@ -596,24 +607,21 @@ const Settings = () => {
})
}}
>
- Join Circle
+ {t('settings:circlePage.joinCircle')}
{circleMembers.find(m => userProfile.id == m.userId)?.role ===
'admin' && (
<>
- Webhook
+ {t('settings:advanced.webhookTitle')}
- Webhooks allow you to send real-time notifications to other
- services when events happen in your Circle. Configure a webhook
- URL to receive real-time updates.
+ {t('settings:advanced.webhookDescription')}
{!isPlusAccount(userProfile) && (
- Webhook notifications are not available in the Basic plan.
- Upgrade to Plus to receive real-time updates via webhooks.
+ {t('settings:advanced.webhookPlanWarning')}
)}
@@ -627,7 +635,7 @@ const Settings = () => {
}
}}
variant='soft'
- label='Enable Webhook'
+ label={t('settings:advanced.enableWebhook')}
disabled={!isPlusAccount(userProfile)}
overlay
/>
@@ -636,10 +644,10 @@ const Settings = () => {
opacity: !isPlusAccount(userProfile) ? 0.5 : 1,
}}
>
- Enable webhook notifications for tasks and things updates.{' '}
+ {t('settings:advanced.enableWebhookHelper')}{' '}
{userProfile && !isPlusAccount(userProfile) && (
- Plus Feature
+ {t('common:labels.plusFeature')}
)}
@@ -647,7 +655,9 @@ const Settings = () => {
{webhookURL !== null && (
- Webhook URL
+
+ {t('settings:advanced.webhookUrl')}
+
setWebhookURL(e.target.value)}
@@ -670,19 +680,19 @@ const Settings = () => {
if (resp.ok) {
showNotification({
type: 'success',
- message: 'Webhook URL updated successfully',
+ message: t('settings:advanced.webhookSaved'),
})
} else {
showNotification({
type: 'error',
- message: 'Failed to update webhook URL',
+ message: t('settings:advanced.webhookSaveFailed'),
})
}
})
}}
disabled={!isPlusAccount(userProfile)}
>
- Save
+ {t('common:actions.save')}
)}
@@ -695,13 +705,13 @@ const Settings = () => {
-
Account Settings
+
{t('settings:pages.account.title')}
- Change your account settings, type or update your password
+ {t('settings:account.description')}
- Account Type : {getSubscriptionStatus()}
+ {t('settings:account.accountType')} : {getSubscriptionStatus()}
{getSubscriptionDetails()}
@@ -900,32 +910,26 @@ const Settings = () => {
-
Sidepanel Customization
+
{t('settings:pages.sidepanel.title')}
- Customize the layout and visibility of cards in the sidepanel. the
- section only available on large screen devices such as tablets and
- desktops..
+ {t('settings:overview.cards.sidepanel.description')}
-
Theme preferences
+
{t('settings:pages.theme.title')}
-
- Choose how the site looks to you. Select a single theme, or sync with
- your system and automatically switch between day and night themes.
-
+
{t('settings:theme.description')}
-
Localization
+
{t('settings:localization.title')}
- Customize language, date format, and regional preferences for your
- account. These settings will apply throughout the application.
+ {t('settings:localization.description')}
diff --git a/src/views/Settings/SidepanelSettings.jsx b/src/views/Settings/SidepanelSettings.jsx
index 64678587..daf03bfc 100644
--- a/src/views/Settings/SidepanelSettings.jsx
+++ b/src/views/Settings/SidepanelSettings.jsx
@@ -26,6 +26,7 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import {
DEFAULT_SIDEPANEL_CONFIG,
getSidepanelConfig,
@@ -34,8 +35,18 @@ import {
import SettingsLayout from './SettingsLayout'
const SidepanelSettings = () => {
+ const { t } = useTranslation(['settings'])
const [config, setConfig] = useState(getSidepanelConfig())
+ const getItemText = item => ({
+ name: t(`settings:sidepanel.cards.${item.id}.title`, {
+ defaultValue: item.name,
+ }),
+ description: t(`settings:sidepanel.cards.${item.id}.description`, {
+ defaultValue: item.description,
+ }),
+ })
+
const getIcon = iconName => {
switch (iconName) {
case 'SupervisorAccount':
@@ -93,15 +104,14 @@ const SidepanelSettings = () => {
}
return (
-
+
- Sidepanel Settings
+ {t('settings:sidepanel.heading')}
- Customize which cards appear in the sidepanel and their order. Drag
- and drop to reorder, or toggle visibility for each card.
+ {t('settings:sidepanel.description')}
@@ -118,7 +128,9 @@ const SidepanelSettings = () => {
draggableId={item.id}
index={index}
>
- {(provided, snapshot) => (
+ {(provided, snapshot) => {
+ const itemText = getItemText(item)
+ return (
{
level='title-sm'
sx={{ fontWeight: 600 }}
>
- {item.name}
+ {itemText.name}
{
color: 'var(--joy-palette-text-tertiary)',
}}
>
- - {item.description}
+ - {itemText.description}
@@ -204,7 +216,8 @@ const SidepanelSettings = () => {
- )}
+ )
+ }}
))}
{provided.placeholder}
@@ -226,10 +239,10 @@ const SidepanelSettings = () => {
onClick={resetToDefaults}
size='sm'
>
- Reset to Defaults
+ {t('settings:sidepanel.reset')}
- This will restore all cards to their default visibility and order.
+ {t('settings:sidepanel.resetHelp')}
diff --git a/src/views/Settings/StorageSettings.jsx b/src/views/Settings/StorageSettings.jsx
index 66259c8e..16b6a6bf 100644
--- a/src/views/Settings/StorageSettings.jsx
+++ b/src/views/Settings/StorageSettings.jsx
@@ -8,6 +8,7 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { useUserProfile } from '../../queries/UserQueries'
import {
@@ -21,6 +22,7 @@ import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import SettingsLayout from './SettingsLayout'
const StorageSettings = () => {
+ const { t } = useTranslation(['settings', 'common'])
const Navigate = useNavigate()
const { data: userProfile } = useUserProfile()
const [usage, setUsage] = useState({ used: 0, total: 0 })
@@ -34,8 +36,8 @@ const StorageSettings = () => {
message,
title,
onConfirm,
- confirmText = 'Confirm',
- cancelText = 'Cancel',
+ confirmText = t('common:actions.continue'),
+ cancelText = t('common:actions.cancel'),
color = 'primary',
) => {
setConfirmModalConfig({
@@ -76,20 +78,19 @@ const StorageSettings = () => {
const totalMB = (usage.total / (1024 * 1024)).toFixed(2)
return (
-
+
- Server Storage Usage
+ {t('settings:storage.serverUsageTitle')}
{!isPlusAccount(userProfile) && (
- Plus Feature
+ {t('settings:storage.plusFeature')}
)}
- This is the storage used by your account on our servers (e.g. files,
- images, and data you have uploaded).
+ {t('settings:storage.serverUsageDescription')}
{!isPlusAccount(userProfile) ? (
<>
@@ -108,20 +109,25 @@ const StorageSettings = () => {
-- MB used / -- MB total (--)
- Server storage is not available in the Basic plan. Upgrade to
- Plus to track your server storage usage.
+ {t('settings:storage.basicPlanUnavailable')}
>
) : loading ? (
<>
- Loading...
+
+ {t('settings:storage.loading')}
+
>
) : (
<>
- {usedMB} MB used / {totalMB} MB total ({percent}%)
+ {t('settings:storage.usedOfTotal', {
+ used: usedMB,
+ total: totalMB,
+ percent,
+ })}
>
)}
@@ -129,20 +135,18 @@ const StorageSettings = () => {
- Experimental Features
+ {t('settings:storage.experimentalTitle')}
- Coming Soon
+ {t('settings:storage.comingSoon')}
- Enable Offline Mode
+ {t('settings:storage.offlineModeTitle')}
- Allows the app to work offline by caching data locally. This is
- experimental and may cause some slowness. If you experience
- performance issues, we recommend turning this off.
+ {t('settings:storage.offlineModeDescription')}
{
{offlineModeEnabled && (
- ⚠️ Offline mode is enabled. If you experience slowness, disable
- this setting.
+ {t('settings:storage.offlineModeWarning')}
)}
- {Capacitor.isNativePlatform() ? 'App' : 'Browser'} Local Storage &
- Cache
+ {t('settings:storage.localStorageTitle', {
+ platform: Capacitor.isNativePlatform()
+ ? t('settings:storage.appPlatform')
+ : t('settings:storage.browserPlatform'),
+ })}
- This is data stored locally in your browser for faster access and
- offline use. Clearing this will not affect your server data, but may
- log you out or remove offline tasks.
+ {t('settings:storage.localStorageDescription')}
{
showConfirmation(
- 'Are you sure you want to clear your local storage and cache? This will remove all your data from this browser and require login.',
- 'Clear All Local Storage',
+ t('settings:storage.clearAllMessage'),
+ t('settings:storage.clearAllTitle'),
() => {
localStorage.clear()
Navigate('/login')
},
- 'Clear All',
- 'Cancel',
+ t('settings:storage.clearAllAction'),
+ t('common:actions.cancel'),
'danger',
)
}}
>
- Clear All Local Storage and Cache
+ {t('settings:storage.clearAllButton')}
{
showConfirmation(
- 'Are you sure you want to clear only the offline cache and tasks?',
- 'Clear Offline Cache',
+ t('settings:storage.clearOfflineMessage'),
+ t('settings:storage.clearOfflineTitle'),
() => {
localStorage.removeItem('offline_cache')
localStorage.removeItem('offline_request_queue')
localStorage.removeItem('offlineTasks')
},
- 'Clear Cache',
- 'Cancel',
+ t('settings:storage.clearOfflineAction'),
+ t('common:actions.cancel'),
'danger',
)
}}
sx={{ mt: 1 }}
>
- Clear Offline Cache and Offline Tasks
+ {t('settings:storage.clearOfflineButton')}
{Capacitor.isNativePlatform() && (
- App Preferences
+ {t('settings:storage.appPreferencesTitle')}
- Device Only
+ {t('settings:storage.deviceOnly')}
- These are preferences and settings stored locally on your device
- by the app. Clearing them will reset app-specific settings and may
- log you out, but will not affect your server data.
+ {t('settings:storage.appPreferencesDescription')}
{
showConfirmation(
- 'Are you sure you want to clear all app preferences? This will reset your app settings and may require you to log in again.',
- 'Clear App Preferences',
+ t('settings:storage.clearPreferencesMessage'),
+ t('settings:storage.clearPreferencesTitle'),
async () => {
try {
const { Preferences } = await import(
@@ -243,13 +245,13 @@ const StorageSettings = () => {
// Optionally show error feedback
}
},
- 'Clear Preferences',
- 'Cancel',
+ t('settings:storage.clearPreferencesAction'),
+ t('common:actions.cancel'),
'danger',
)
}}
>
- Clear App Preferences
+ {t('settings:storage.clearPreferencesButton')}
)}
diff --git a/src/views/Settings/ThemeSettings.jsx b/src/views/Settings/ThemeSettings.jsx
index 06f1d7b8..1c281b66 100644
--- a/src/views/Settings/ThemeSettings.jsx
+++ b/src/views/Settings/ThemeSettings.jsx
@@ -1,19 +1,19 @@
import { Typography } from '@mui/joy'
+import { useTranslation } from 'react-i18next'
import SettingsLayout from './SettingsLayout'
import ThemeToggle from './ThemeToggle'
const ThemeSettings = () => {
+ const { t } = useTranslation('settings')
+
return (
-
+
-
- Choose how the site looks to you. Select a single theme, or sync with
- your system and automatically switch between day and night themes.
-
+ {t('theme.description')}
)
}
-export default ThemeSettings
\ No newline at end of file
+export default ThemeSettings
diff --git a/src/views/SummaryCard.jsx b/src/views/SummaryCard.jsx
index ac1d23ab..7e8609a7 100644
--- a/src/views/SummaryCard.jsx
+++ b/src/views/SummaryCard.jsx
@@ -1,13 +1,17 @@
+import { MoreVert } from '@mui/icons-material'
import { Card, IconButton, Typography } from '@mui/joy'
+import { useTranslation } from 'react-i18next'
const SummaryCard = () => {
+ const { t } = useTranslation('chores')
+
return (
- Summary
+ {t('sidepanel.summary.title')}
- This is a summary of your chores
+ {t('sidepanel.summary.description')}
@@ -16,11 +20,11 @@ const SummaryCard = () => {
- Due Today
+ {t('sidepanel.summary.dueToday')}
3
- Overdue
+ {t('sidepanel.summary.overdue')}
1
diff --git a/src/views/Things/ThingsHistory.jsx b/src/views/Things/ThingsHistory.jsx
index 854355f5..7df7ba5c 100644
--- a/src/views/Things/ThingsHistory.jsx
+++ b/src/views/Things/ThingsHistory.jsx
@@ -25,6 +25,7 @@ import {
} from '@mui/joy'
import { useTheme } from '@mui/joy/styles'
import moment from 'moment'
+import { useTranslation } from 'react-i18next'
import { Link, useParams } from 'react-router-dom'
import { useLocalization } from '../../contexts/LocalizationContext'
import {
@@ -39,6 +40,7 @@ import { useThingHistory } from '../../queries/ThingQueries'
import LoadingComponent from '../components/Loading'
const ThingsHistory = () => {
+ const { t } = useTranslation('things')
const { id } = useParams()
const theme = useTheme()
const { fmt } = useLocalization()
@@ -69,10 +71,14 @@ const ThingsHistory = () => {
const frequency = totalDuration / (thingsHistory.length - 1)
avgUpdateFrequency =
frequency < 1
- ? `${Math.round(frequency * 60)} minutes`
+ ? t('history.analytics.minutes', {
+ count: Math.round(frequency * 60),
+ })
: frequency < 24
- ? `${Math.round(frequency)} hours`
- : `${Math.round(frequency / 24)} days`
+ ? t('history.analytics.hours', { count: Math.round(frequency) })
+ : t('history.analytics.days', {
+ count: Math.round(frequency / 24),
+ })
}
const lastUpdated = thingsHistory[0]
@@ -91,30 +97,31 @@ const ThingsHistory = () => {
.filter(d => d !== null)
const last = diffs[0]
const prev = diffs[1]
- if (last > prev) updateTrend = 'Interval increasing'
- else if (last < prev) updateTrend = 'Interval decreasing'
- else updateTrend = 'Interval stable'
+ if (last > prev) updateTrend = t('history.analytics.intervalIncreasing')
+ else if (last < prev)
+ updateTrend = t('history.analytics.intervalDecreasing')
+ else updateTrend = t('history.analytics.intervalStable')
}
return [
{
icon: ,
- text: 'Update Frequency',
- subtext: `Every ${avgUpdateFrequency}`,
+ text: t('history.analytics.updateFrequency'),
+ subtext: t('history.analytics.every', { value: avgUpdateFrequency }),
},
{
icon: ,
- text: 'Last Updated',
+ text: t('history.analytics.lastUpdated'),
subtext: lastUpdated,
},
{
icon: ,
- text: 'Last Value',
+ text: t('history.analytics.lastValue'),
subtext: thingsHistory[0]?.state ?? '--',
},
{
icon: ,
- text: 'Update Trend',
+ text: t('history.analytics.updateTrend'),
subtext: updateTrend,
},
]
@@ -173,13 +180,13 @@ const ThingsHistory = () => {
/>
- No history found
+ {t('history.empty.title')}
- It looks like there is no history for this thing yet.
+ {t('history.empty.description')}
- Go back to things
+ {t('history.empty.backToThings')}
)
@@ -195,7 +202,7 @@ const ThingsHistory = () => {
level='title-md'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Things Overview
+ {t('history.overviewTitle')}
diff --git a/src/views/Things/ThingsView.jsx b/src/views/Things/ThingsView.jsx
index 4964f6f9..cb9eea38 100644
--- a/src/views/Things/ThingsView.jsx
+++ b/src/views/Things/ThingsView.jsx
@@ -27,6 +27,7 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { useNotification } from '../../service/NotificationProvider'
import {
@@ -41,23 +42,7 @@ import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import CreateThingModal from '../Modals/Inputs/CreateThingModal'
import EditThingStateModal from '../Modals/Inputs/EditThingState'
-const ThingCardContent = ({ thing, onCardClick, onToggleActions }) => {
- const getThingIcon = type => {
- if (type === 'text') {
- return
- } else if (type === 'number') {
- return
- } else if (type === 'boolean') {
- if (thing.state === 'true') {
- return
- } else {
- return
- }
- } else {
- return
- }
- }
-
+const ThingCardContent = ({ thing, onCardClick, onToggleActions, t }) => {
const getThingAvatar = () => {
const typeConfig = {
text: { color: 'primary', icon: },
@@ -85,6 +70,14 @@ const ThingCardContent = ({ thing, onCardClick, onToggleActions }) => {
)
}
+ const getTranslatedType = type =>
+ t(`things:page.types.${type}`, { defaultValue: type })
+
+ const getTranslatedState = state =>
+ state === 'true' || state === 'false'
+ ? t(`things:page.states.${state}`)
+ : state
+
return (
{
ml: 1,
}}
>
- {thing?.state}
+ {getTranslatedState(thing?.state)}
@@ -180,7 +173,7 @@ const ThingCardContent = ({ thing, onCardClick, onToggleActions }) => {
px: 0.75,
}}
>
- {thing?.type}
+ {getTranslatedType(thing?.type)}
@@ -204,6 +197,7 @@ const ThingCardContent = ({ thing, onCardClick, onToggleActions }) => {
}
const ThingsView = () => {
+ const { t } = useTranslation(['things', 'common'])
const navigate = useNavigate()
const [things, setThings] = useState([])
const [isShowCreateThingModal, setIsShowCreateThingModal] = useState(false)
@@ -244,21 +238,21 @@ const ThingsView = () => {
}
showNotification({
type: 'success',
- title: 'Saved',
- message: 'Thing saved successfully',
+ title: t('things:page.savedTitle'),
+ message: t('things:page.savedMessage'),
})
})
})
.catch(error => {
if (error?.queued) {
showError({
- title: 'Unable to save thing',
- message: 'You are offline and the request has been queued',
+ title: t('things:page.saveFailedTitle'),
+ message: t('things:page.saveQueued'),
})
} else {
showError({
- title: 'Unable to save thing',
- message: 'An error occurred while saving the thing',
+ title: t('things:page.saveFailedTitle'),
+ message: t('things:page.saveFailed'),
})
}
})
@@ -270,10 +264,10 @@ const ThingsView = () => {
const handleDeleteClick = thing => {
setConfirmModelConfig({
isOpen: true,
- title: 'Delete Things',
- confirmText: 'Delete',
- cancelText: 'Cancel',
- message: 'Are you sure you want to delete this Thing?',
+ title: t('things:page.deleteTitle'),
+ confirmText: t('common:actions.delete'),
+ cancelText: t('common:actions.cancel'),
+ message: t('things:page.deleteConfirm'),
onClose: isConfirmed => {
if (isConfirmed === true) {
DeleteThing(thing.id)
@@ -287,8 +281,8 @@ const ThingsView = () => {
setThings(currentThings)
} else if (response.status === 405) {
showError({
- title: 'Unable to Delete Thing',
- message: 'Unable to delete thing with associated tasks',
+ title: t('things:page.deleteFailedTitle'),
+ message: t('things:page.deleteFailedAssociated'),
})
}
// if method not allwo show snackbar:
@@ -296,13 +290,13 @@ const ThingsView = () => {
.catch(error => {
if (error?.queued) {
showError({
- title: 'Unable to delete thing',
- message: 'You are offline and the request has been queued',
+ title: t('things:page.deleteFailedTitle'),
+ message: t('things:page.deleteQueued'),
})
} else {
showError({
- title: 'Unable to delete thing',
- message: 'An error occurred while deleting the thing',
+ title: t('things:page.deleteFailedTitle'),
+ message: t('things:page.deleteFailed'),
})
}
})
@@ -335,21 +329,21 @@ const ThingsView = () => {
setThings(currentThings)
showNotification({
type: 'success',
- title: 'Updated',
- message: 'Thing state updated successfully',
+ title: t('things:page.updatedTitle'),
+ message: t('things:page.updatedMessage'),
})
})
})
.catch(error => {
if (error?.queued) {
showError({
- title: 'Unable to update thing state',
- message: 'You are offline and the request has been queued',
+ title: t('things:page.updateFailedTitle'),
+ message: t('things:page.updateQueued'),
})
} else {
showError({
- title: 'Unable to update thing state',
- message: 'An error occurred while updating the thing state',
+ title: t('things:page.updateFailedTitle'),
+ message: t('things:page.updateFailed'),
})
}
})
@@ -364,13 +358,10 @@ const ThingsView = () => {
level='h3'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Things
+ {t('things:page.title')}
- Things are custom fields that can be attached to tasks to capture
- additional information. They can be of type text, number, or
- boolean. You can associate things with tasks and have the task due
- once condition is met
+ {t('things:page.description')}
@@ -396,7 +387,7 @@ const ThingsView = () => {
}}
/>
- No things has been created/found
+ {t('things:page.emptyTitle')}
)}
@@ -446,7 +437,9 @@ const ThingsView = () => {
)}
- {thing?.type === 'text' ? 'Edit' : 'Toggle'}
+ {thing?.type === 'text'
+ ? t('things:page.swipeEdit')
+ : t('things:page.swipeToggle')}
@@ -465,7 +458,7 @@ const ThingsView = () => {
>
- Edit
+ {t('things:page.swipeEdit')}
@@ -484,7 +477,7 @@ const ThingsView = () => {
>
- Delete
+ {t('things:page.swipeDelete')}
@@ -494,6 +487,7 @@ const ThingsView = () => {
>
{
if (showMoreInfoId === thing.id) {
setShowMoreInfoId(null)
diff --git a/src/views/Timer/TimerDetails.jsx b/src/views/Timer/TimerDetails.jsx
index 349e15b8..df4be975 100644
--- a/src/views/Timer/TimerDetails.jsx
+++ b/src/views/Timer/TimerDetails.jsx
@@ -37,6 +37,7 @@ import {
} from '@mui/joy'
import moment from 'moment'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import { useLocalization } from '../../contexts/LocalizationContext'
import {
@@ -52,6 +53,7 @@ import { getSafeBottom } from '../../utils/SafeAreaUtils'
import LoadingComponent from '../components/Loading'
const TimerDetails = () => {
+ const { t } = useTranslation(['timer', 'common'])
const { choreId } = useParams()
const { fmt } = useLocalization()
const [timerData, setTimerData] = useState(null)
@@ -221,23 +223,23 @@ const TimerDetails = () => {
{
onSuccess: () => {
showSuccess({
- title: 'Session updated',
- message: 'Timer session has been updated successfully.',
+ title: t('timer:details.updatedTitle'),
+ message: t('timer:details.updatedMessage'),
})
refetchTimer()
cancelEditingSession(sessionId)
},
onError: () => {
showError({
- title: 'Failed to update session',
- message: 'Please try again.',
+ title: t('timer:details.updateFailedTitle'),
+ message: t('timer:details.tryAgain'),
})
},
},
)
} catch (error) {
showError({
- title: 'Error updating session',
+ title: t('timer:details.errorUpdatingTitle'),
message: error.message,
})
} finally {
@@ -251,15 +253,15 @@ const TimerDetails = () => {
startChore.mutate(choreId, {
onSuccess: () => {
showSuccess({
- title: 'Timer Started',
- message: 'Work session has been started successfully.',
+ title: t('timer:details.startTitle'),
+ message: t('timer:details.startMessage'),
})
refetchTimer()
},
onError: () => {
showError({
- title: 'Failed to start timer',
- message: 'Please try again.',
+ title: t('timer:details.startFailedTitle'),
+ message: t('timer:details.tryAgain'),
})
},
onSettled: () => {
@@ -273,15 +275,15 @@ const TimerDetails = () => {
pauseChore.mutate(choreId, {
onSuccess: () => {
showSuccess({
- title: 'Timer Paused',
- message: 'Work session has been paused.',
+ title: t('timer:details.pauseTitle'),
+ message: t('timer:details.pauseMessage'),
})
refetchTimer()
},
onError: () => {
showError({
- title: 'Failed to pause timer',
- message: 'Please try again.',
+ title: t('timer:details.pauseFailedTitle'),
+ message: t('timer:details.tryAgain'),
})
},
onSettled: () => {
@@ -345,8 +347,10 @@ const TimerDetails = () => {
const handleDeleteSession = sessionIndex => {
// For now, just show an alert since we'd need to implement session deletion API
showError({
- title: 'Delete Session',
- message: `Session #${sessionIndex + 1} deletion would be implemented here`,
+ title: t('timer:details.deleteSessionTitle'),
+ message: t('timer:details.deleteSessionMessage', {
+ index: sessionIndex + 1,
+ }),
})
}
@@ -360,13 +364,13 @@ const TimerDetails = () => {
{loading && (
- Loading timer data...
+ {t('timer:details.loading')}
)}
{!loading && !timerData && (
- No timer data found for this chore.
+ {t('timer:details.notFound')}
)}
@@ -417,7 +421,7 @@ const TimerDetails = () => {
color: 'text.primary',
}}
>
- Active Work
+ {t('timer:details.activeWork')}
@@ -472,7 +476,7 @@ const TimerDetails = () => {
color: 'text.primary',
}}
>
- Break Time
+ {t('timer:details.breakTime')}
@@ -527,7 +531,7 @@ const TimerDetails = () => {
color: 'text.primary',
}}
>
- Sessions
+ {t('timer:details.sessions')}
@@ -582,7 +586,7 @@ const TimerDetails = () => {
color: 'text.primary',
}}
>
- Total Time
+ {t('timer:details.totalTime')}
@@ -616,12 +620,18 @@ const TimerDetails = () => {
level='body-sm'
sx={{ color: 'text.secondary', fontWeight: 'medium' }}
>
- Work vs Break Distribution
+ {t('timer:details.workVsBreak')}
{calculateCurrentActiveDuration() > 0
- ? `${Math.round((calculateCurrentActiveDuration() / calculateTotalDuration()) * 100)}% active`
- : 'No active time yet'}
+ ? t('timer:details.activePercent', {
+ percent: Math.round(
+ (calculateCurrentActiveDuration() /
+ calculateTotalDuration()) *
+ 100,
+ ),
+ })
+ : t('timer:details.noActiveTime')}
{
level='body-sm'
sx={{ color: 'text.secondary', fontWeight: 'medium', mb: 2 }}
>
- Activity Timeline
+ {t('timer:details.activityTimeline')}
{timerData &&
@@ -718,7 +728,13 @@ const TimerDetails = () => {
zIndex: 1,
},
}}
- title={`Session ${index + 1}: ${formatDuration(sessionDuration)} ${isOngoing ? '(ongoing)' : ''}`}
+ title={t('timer:details.sessionTooltip', {
+ index: index + 1,
+ duration: formatDuration(sessionDuration),
+ status: isOngoing
+ ? t('timer:details.ongoing')
+ : '',
+ })}
/>
)
})
@@ -758,7 +774,7 @@ const TimerDetails = () => {
level='body-xs'
sx={{ color: 'text.tertiary' }}
>
- Active Work
+ {t('timer:details.activeWork')}
{
level='body-xs'
sx={{ color: 'text.tertiary' }}
>
- Break Time
+ {t('timer:details.breakTime')}
{isTimerRunning() && (
@@ -807,7 +823,7 @@ const TimerDetails = () => {
level='body-xs'
sx={{ color: 'text.tertiary' }}
>
- Live Session
+ {t('timer:details.liveSession')}
)}
@@ -843,10 +859,12 @@ const TimerDetails = () => {
level='body-xs'
sx={{ color: 'text.tertiary' }}
>
- Active:{' '}
- {calculateCurrentActiveDuration() > 0
- ? `${Math.round((calculateCurrentActiveDuration() / calculateTotalDuration()) * 100)}%`
- : '0%'}
+ {t('timer:details.activeShort', {
+ percent:
+ calculateCurrentActiveDuration() > 0
+ ? `${Math.round((calculateCurrentActiveDuration() / calculateTotalDuration()) * 100)}%`
+ : '0%',
+ })}
@@ -854,8 +872,7 @@ const TimerDetails = () => {
) : (
- No activity timeline available. Start working to see your
- activity pattern.
+ {t('timer:details.noTimeline')}
)}
@@ -873,7 +890,9 @@ const TimerDetails = () => {
mb: 2,
}}
>
- Session Breakdown
+
+ {t('timer:details.sessionBreakdown')}
+
{!editingSessions[timerData.id] && (
{
onClick={() => startEditingSession()}
size='sm'
>
- Edit
+ {t('common:actions.edit')}
)}
{editingSessions[timerData.id] && (
@@ -892,7 +911,7 @@ const TimerDetails = () => {
onClick={() => cancelEditingSession(timerData.id)}
size='sm'
>
- Cancel
+ {t('common:actions.cancel')}
{
loading={loading}
size='sm'
>
- Save Changes
+ {t('timer:details.saveChanges')}
)}
@@ -916,7 +935,9 @@ const TimerDetails = () => {
level='body-md'
sx={{ fontWeight: 'bold', mb: 2 }}
>
- Work Sessions ({timerData.pauseLog.length})
+ {t('timer:details.workSessions', {
+ count: timerData.pauseLog.length,
+ })}
@@ -979,7 +1000,7 @@ const TimerDetails = () => {
level='body-xs'
sx={{ mt: 0.5 }}
>
- Edit
+ {t('common:actions.edit')}
@@ -1005,7 +1026,7 @@ const TimerDetails = () => {
level='body-xs'
sx={{ mt: 0.5 }}
>
- Delete
+ {t('common:actions.delete')}
@@ -1069,7 +1090,7 @@ const TimerDetails = () => {
variant='soft'
sx={{ fontSize: '0.7rem' }}
>
- Live
+ {t('timer:details.live')}
)}
{/* User chip showing who started the session */}
@@ -1104,7 +1125,7 @@ const TimerDetails = () => {
>
{sessionUser?.displayName ||
sessionUser?.name ||
- 'Unknown'}
+ t('timer:details.unknownUser')}
) : null
})()}
@@ -1127,7 +1148,10 @@ const TimerDetails = () => {
mb: 0.2,
}}
>
- Session #{pauseIndex + 1} • {sessionDate}
+ {t('timer:details.sessionLabel', {
+ index: pauseIndex + 1,
+ date: sessionDate,
+ })}
{
}}
>
{startTime}{' '}
- {endTime ? `→ ${endTime}` : '→ ongoing'}
+ {endTime
+ ? `→ ${endTime}`
+ : t('timer:details.ongoingArrow')}
@@ -1166,7 +1192,7 @@ const TimerDetails = () => {
{(!timerData.pauseLog || timerData.pauseLog.length === 0) && (
- No work sessions found for this timer.
+ {t('timer:details.noSessions')}
)}
@@ -1191,7 +1217,7 @@ const TimerDetails = () => {
}}
>
- Sessions
+ {t('timer:details.editSessions')}
{
startDecorator={ }
onClick={() => addPauseLogEntry(timerData.id)}
>
- Add Session
+ {t('timer:details.addSession')}
@@ -1222,7 +1248,10 @@ const TimerDetails = () => {
level='body-md'
sx={{ fontWeight: 'bold' }}
>
- Session #{pauseIndex + 1}
+ {t('timer:details.sessionLabel', {
+ index: pauseIndex + 1,
+ date: '',
+ }).replace(' • ', '')}
{
level='body-sm'
sx={{ fontWeight: 'bold', mb: 1 }}
>
- Start Time
+ {t('timer:details.startTime')}
{
level='body-sm'
sx={{ fontWeight: 'bold', mb: 1 }}
>
- End Time
+ {t('timer:details.endTime')}
{
}
/>
- Leave empty if session is ongoing
+ {t('timer:details.leaveEmptyOngoing')}
@@ -1304,7 +1333,7 @@ const TimerDetails = () => {
level='body-sm'
sx={{ fontWeight: 'bold', mb: 1 }}
>
- Duration (Auto-calculated)
+ {t('timer:details.durationAuto')}
{
},
},
}}
- title={isTimerRunning() ? 'Pause Timer' : 'Start Timer'}
+ title={
+ isTimerRunning()
+ ? t('timer:details.pauseButton')
+ : t('timer:details.startButton')
+ }
>
{isTimerRunning() ? (
diff --git a/src/views/User/UserActivities.jsx b/src/views/User/UserActivities.jsx
index bfc0c355..0d6f288b 100644
--- a/src/views/User/UserActivities.jsx
+++ b/src/views/User/UserActivities.jsx
@@ -33,6 +33,7 @@ import {
Typography,
} from '@mui/joy'
import React, { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
@@ -58,7 +59,16 @@ const groupByDate = history => {
return aggregated
}
-const ChoreHistoryItem = ({ time, name, points, status, performer, notes, onViewNote }) => {
+const ChoreHistoryItem = ({
+ time,
+ name,
+ points,
+ status,
+ performer,
+ notes,
+ onViewNote,
+}) => {
+ const { t } = useTranslation(['user', 'history'])
const getStatusIcon = status => {
switch (status) {
case 0:
@@ -120,7 +130,7 @@ const ChoreHistoryItem = ({ time, name, points, status, performer, notes, onView
{points && (
}>
- {`${points} points`}
+ {t('user:pointsWithCount', { count: points })}
)}
{notes && (
@@ -135,7 +145,7 @@ const ChoreHistoryItem = ({ time, name, points, status, performer, notes, onView
onViewNote?.(notes)
}}
>
- Note
+ {t('history:note')}
)}
@@ -145,6 +155,7 @@ const ChoreHistoryItem = ({ time, name, points, status, performer, notes, onView
const ChoreHistoryTimeline = ({ history, onViewNote }) => {
+ const { t } = useTranslation(['user', 'common', 'history'])
const { fmt } = useLocalization()
const groupedHistory = groupByDate(history)
@@ -158,7 +169,7 @@ const ChoreHistoryTimeline = ({ history, onViewNote }) => {
- Activities Timeline
+ {t('user:activitiesTimeline')}
@@ -192,7 +203,7 @@ const ChoreHistoryTimeline = ({ history, onViewNote }) => {
)
}
-const renderPieChart = (data, size, isPrimary, chartType = null) => {
+const renderPieChart = (data, size, isPrimary, chartType = null, t) => {
// Filter out items with zero or negative values
const validData = data.filter(item => item.value > 0)
@@ -211,7 +222,7 @@ const renderPieChart = (data, size, isPrimary, chartType = null) => {
}}
>
- No data available
+ {t('common:status.noDataAvailable')}
)
@@ -395,6 +406,7 @@ const USER_FILTER = (history, userId) => {
}
const UserActivites = () => {
+ const { t } = useTranslation(['user', 'common'])
const { data: userProfile } = useUserProfile()
const [tabValue, setTabValue] = React.useState(7)
@@ -538,7 +550,7 @@ const UserActivites = () => {
// Add unlabeled tasks if there are any
if (unlabeledCount > 0) {
result.push({
- label: 'No Labels',
+ label: t('user:activities.noLabels'),
value: unlabeledCount,
color: TASK_COLOR.ANYTIME,
id: 'unlabeled',
@@ -561,7 +573,9 @@ const UserActivites = () => {
const assignee = circleUsers.find(
user => user.userId === chore.assignedTo,
)
- const assigneeName = assignee ? assignee.displayName : 'Unassigned'
+ const assigneeName = assignee
+ ? assignee.displayName
+ : t('user:activities.unassigned')
const assigneeId = chore.assignedTo || 'unassigned'
if (assigneeCounts[assigneeId]) {
@@ -654,7 +668,7 @@ const UserActivites = () => {
// Add unlabeled tasks duration if there is any
if (unlabeledDuration > 0) {
result.push({
- label: 'No Labels',
+ label: t('user:activities.noLabels'),
value: Math.round((unlabeledDuration / 3600) * 10) / 10, // Convert to hours and round to 1 decimal
color: TASK_COLOR.ANYTIME,
id: 'unlabeled',
@@ -675,7 +689,7 @@ const UserActivites = () => {
// Iterate through ChoreHistory to get actual time spent per task
history.forEach(historyItem => {
const duration = historyItem.duration || 0 // duration in seconds from ChoreHistory
- const taskName = historyItem.choreName || 'Unknown Task'
+ const taskName = historyItem.choreName || t('user:activities.unknownTask')
if (taskDurations[taskName]) {
taskDurations[taskName].duration += duration
@@ -738,7 +752,7 @@ const UserActivites = () => {
if (totalCompleted > 0) {
result.push({
- label: `On time`,
+ label: t('history:status.onTime'),
value: totalCompleted,
color: TASK_COLOR.COMPLETED,
id: 1,
@@ -747,7 +761,7 @@ const UserActivites = () => {
if (totalLate > 0) {
result.push({
- label: `Late`,
+ label: t('history:status.late'),
value: totalLate,
color: TASK_COLOR.LATE,
id: 2,
@@ -756,7 +770,7 @@ const UserActivites = () => {
if (totalNoDueDate > 0) {
result.push({
- label: `Completed`,
+ label: t('history:status.completed'),
value: totalNoDueDate,
color: TASK_COLOR.ANYTIME,
id: 3,
@@ -771,13 +785,13 @@ const UserActivites = () => {
const chartData = {
history: {
data: historyPieChartData || [],
- title: 'Status',
- description: 'Completed tasks status',
+ title: t('user:activities.charts.statusTitle'),
+ description: t('user:activities.charts.statusDescription'),
},
due: {
data: choreDuePieChartData || [],
- title: 'Due Date',
- description: 'Current tasks due date',
+ title: t('user:activities.charts.dueDateTitle'),
+ description: t('user:activities.charts.dueDateDescription'),
},
// assigned: {
// data: choresAssignedChartData,
@@ -786,28 +800,28 @@ const UserActivites = () => {
// },
priority: {
data: choresPriorityChartData || [],
- title: 'Priority',
- description: 'Tasks by priority',
+ title: t('user:activities.charts.priorityTitle'),
+ description: t('user:activities.charts.priorityDescription'),
},
labels: {
data: choresLabelsChartData || [],
- title: 'Labels',
- description: 'Tasks by labels',
+ title: t('user:activities.charts.labelsTitle'),
+ description: t('user:activities.charts.labelsDescription'),
},
labelsDuration: {
data: choresLabelsDurationChartData || [],
- title: 'Labels (time)',
- description: 'Time spent by labels (hours)',
+ title: t('user:activities.charts.labelsTimeTitle'),
+ description: t('user:activities.charts.labelsTimeDescription'),
},
tasksTime: {
data: tasksTimeChartData || [],
- title: 'Tasks (time)',
- description: 'Time spent by individual tasks (hours)',
+ title: t('user:activities.charts.tasksTimeTitle'),
+ description: t('user:activities.charts.tasksTimeDescription'),
},
assigneeBreakdown: {
data: choresAssigneeBreakdownChartData || [],
- title: 'by Assignee',
- description: 'Tasks grouped by assignee',
+ title: t('user:activities.byAssignee'),
+ description: t('user:activities.tasksGroupedByAssignee'),
},
}
if (!userProfile) {
@@ -829,10 +843,10 @@ const UserActivites = () => {
level='h3'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- User Activities
+ {t('user:activities.title')}
- Overview of user activities and task statistics
+ {t('user:activities.subtitle')}
@@ -852,7 +866,7 @@ const UserActivites = () => {
>
- Filter Activities
+ {t('user:activities.filterTitle')}
{
{/* User Filter */}
- Show activities for:
+ {t('user:activities.showFor')}
{
}
>
- All Users
+ {t('user:activities.allUsers')}
)
}
@@ -925,7 +939,7 @@ const UserActivites = () => {
}
>
- All Users
+ {t('user:activities.allUsers')}
{circleUsers.map(user => (
@@ -954,7 +968,7 @@ const UserActivites = () => {
{/* Time Period Filter */}
- Time period:
+ {t('user:activities.timePeriod')}
{
@@ -979,10 +993,10 @@ const UserActivites = () => {
}}
>
{[
- { label: '7 Days', value: 7 },
- { label: '30 Days', value: 30 },
- { label: '90 Days', value: 90 },
- { label: 'All Time', value: 365 },
+ { label: t('user:activities.days7'), value: 7 },
+ { label: t('user:activities.days30'), value: 30 },
+ { label: t('user:activities.days90'), value: 90 },
+ { label: t('user:activities.allTime'), value: 365 },
].map((tab, index) => (
{
{/* Current Filter Summary */}
- Showing activities for{' '}
-
- {selectedUser === undefined || selectedUser === 'all'
- ? 'All Users'
- : circleUsers.find(user => user.userId === selectedUser)
- ?.displayName || 'Unknown User'}
- {' '}
- over the{' '}
-
- {tabValue === 365 ? 'All Time' : `Last ${tabValue} Days`}
-
+ {t('user:activities.showingFor', {
+ user:
+ selectedUser === undefined || selectedUser === 'all'
+ ? t('user:activities.allUsers')
+ : circleUsers.find(user => user.userId === selectedUser)
+ ?.displayName || t('user:activities.unknownUser'),
+ period:
+ tabValue === 365
+ ? t('user:activities.allTime')
+ : t(`user:activities.days${tabValue}`),
+ })}
@@ -1060,33 +1068,26 @@ const UserActivites = () => {
/>
- No activities found
+ {t('user:activities.noActivitiesTitle')}
- No activities found for{' '}
-
- {selectedUser === undefined || selectedUser === 'all'
- ? 'All Users'
- : circleUsers.find(user => user.userId === selectedUser)
- ?.displayName || 'Unknown User'}
- {' '}
- in the{' '}
-
- {tabValue === 365 ? 'All Time' : `Last ${tabValue} Days`}
-
- .
+ {t('user:activities.noActivitiesDescription', {
+ user:
+ selectedUser === undefined || selectedUser === 'all'
+ ? t('user:activities.allUsers')
+ : circleUsers.find(user => user.userId === selectedUser)
+ ?.displayName || t('user:activities.unknownUser'),
+ period:
+ tabValue === 365
+ ? t('user:activities.allTime')
+ : t(`user:activities.days${tabValue}`),
+ })}
- Try selecting a different time period or user filter above.
+ {t('user:activities.noActivitiesHelp')}
- Go back to chores
+ {t('user:activities.backToChores')}
) : (
@@ -1190,6 +1191,7 @@ const UserActivites = () => {
300, // Increased size for better chart container
true,
selectedChart,
+ t,
)}
@@ -1249,7 +1251,7 @@ const UserActivites = () => {
alignItems: 'center',
}}
>
- {renderPieChart(data, 70, false)}
+ {renderPieChart(data, 70, false, null, t)}
diff --git a/src/views/User/UserPoints.jsx b/src/views/User/UserPoints.jsx
index 7ab2dee0..b4a22749 100644
--- a/src/views/User/UserPoints.jsx
+++ b/src/views/User/UserPoints.jsx
@@ -40,14 +40,18 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import LoadingComponent from '../components/Loading.jsx'
+import { useLocalization } from '../../contexts/LocalizationContext'
import { useChoresHistory } from '../../queries/ChoreQueries.jsx'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
import { RedeemPoints } from '../../utils/Fetcher.jsx'
import { resolvePhotoURL } from '../../utils/Helpers.jsx'
import RedeemPointsModal from '../Modals/RedeemPointsModal'
const UserPoints = () => {
+ const { t, i18n } = useTranslation('user')
+ const { language } = useLocalization()
const [tabValue, setTabValue] = useState(7)
const [isRedeemModalOpen, setIsRedeemModalOpen] = useState(false)
const [leaderboardMode, setLeaderboardMode] = useState('points') // 'points' or 'tasks'
@@ -98,21 +102,32 @@ const UserPoints = () => {
setSelectedUser(userProfile?.id)
}, [userProfile])
+ const formatPeriodLabel = value => {
+ if (value === 24 * 30) return t('points.allTime')
+ if (value === 6 * 30) return t('points.lastMonths6')
+ return t('points.lastDays', { count: value })
+ }
+
const generateWeeklySummary = (history, userId) => {
const daysAggregated = []
for (let i = 6; i > -1; i--) {
const currentDate = new Date()
currentDate.setDate(currentDate.getDate() - i)
daysAggregated.push({
- label: currentDate.toLocaleString('en-US', { weekday: 'short' }),
+ label: currentDate.toLocaleString(language || i18n.language, {
+ weekday: 'short',
+ }),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
- const dayName = new Date(chore.performedAt).toLocaleString('en-US', {
- weekday: 'short',
- })
+ const dayName = new Date(chore.performedAt).toLocaleString(
+ language || i18n.language,
+ {
+ weekday: 'short',
+ },
+ )
const dayIndex = daysAggregated.findIndex(dayData => {
if (userId)
@@ -133,15 +148,20 @@ const UserPoints = () => {
const currentDate = new Date()
currentDate.setDate(currentDate.getDate() - i)
daysAggregated.push({
- label: currentDate.toLocaleString('en-US', { day: 'numeric' }),
+ label: currentDate.toLocaleString(language || i18n.language, {
+ day: 'numeric',
+ }),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
- const dayName = new Date(chore.performedAt).toLocaleString('en-US', {
- day: 'numeric',
- })
+ const dayName = new Date(chore.performedAt).toLocaleString(
+ language || i18n.language,
+ {
+ day: 'numeric',
+ },
+ )
const dayIndex = daysAggregated.findIndex(dayData => {
if (userId)
@@ -164,15 +184,20 @@ const UserPoints = () => {
const currentMonth = new Date()
currentMonth.setMonth(currentMonth.getMonth() - i)
monthlyAggregated.push({
- label: currentMonth.toLocaleString('en-US', { month: 'short' }),
+ label: currentMonth.toLocaleString(language || i18n.language, {
+ month: 'short',
+ }),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
- const monthName = new Date(chore.performedAt).toLocaleString('en-US', {
- month: 'short',
- })
+ const monthName = new Date(chore.performedAt).toLocaleString(
+ language || i18n.language,
+ {
+ month: 'short',
+ },
+ )
const monthIndex = monthlyAggregated.findIndex(monthData => {
if (userId)
@@ -195,15 +220,20 @@ const UserPoints = () => {
const currentYear = new Date()
currentYear.setFullYear(currentYear.getFullYear() - i)
yearlyAggregated.push({
- label: currentYear.toLocaleString('en-US', { year: 'numeric' }),
+ label: currentYear.toLocaleString(language || i18n.language, {
+ year: 'numeric',
+ }),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
- const yearName = new Date(chore.performedAt).toLocaleString('en-US', {
- year: 'numeric',
- })
+ const yearName = new Date(chore.performedAt).toLocaleString(
+ language || i18n.language,
+ {
+ year: 'numeric',
+ },
+ )
const yearIndex = yearlyAggregated.findIndex(yearData => {
if (userId)
@@ -305,14 +335,14 @@ const UserPoints = () => {
level='h3'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- {leaderboardMode === 'points' ? 'Points' : 'Tasks'} Leaderboard
+ {leaderboardMode === 'points'
+ ? t('points.pointsLeaderboard')
+ : t('points.tasksLeaderboard')}
- Rankings based on{' '}
{leaderboardMode === 'points'
- ? 'points earned'
- : 'tasks completed'}{' '}
- during the selected time period
+ ? t('points.rankingsPoints')
+ : t('points.rankingsTasks')}
@@ -358,9 +388,9 @@ const UserPoints = () => {
}}
>
{[
- { label: '7D', value: 7 },
- { label: '6M', value: 6 * 30 },
- { label: 'All', value: 24 * 30 },
+ { label: t('points.days7'), value: 7 },
+ { label: t('points.months6'), value: 6 * 30 },
+ { label: t('points.allTime'), value: 24 * 30 },
].map((tab, index) => (
{
sx={{ cursor: 'pointer' }}
onClick={() => setLeaderboardMode('points')}
>
- Points
+ {t('points.modePoints')}
{
sx={{ cursor: 'pointer' }}
onClick={() => setLeaderboardMode('tasks')}
>
- Tasks
+ {t('points.modeTasks')}
@@ -512,7 +542,7 @@ const UserPoints = () => {
color='primary'
sx={{ ml: 1 }}
>
- You
+ {t('points.you')}
)}
@@ -520,8 +550,10 @@ const UserPoints = () => {
level='body-xs'
sx={{ color: 'text.secondary' }}
>
- {user.periodTasks} tasks • {user.avgPointsPerTask} avg
- per task
+ {t('points.tasksAvg', {
+ tasks: user.periodTasks,
+ avg: user.avgPointsPerTask,
+ })}
@@ -552,8 +584,10 @@ const UserPoints = () => {
{leaderboardMode === 'points'
- ? `${user.availablePoints} available`
- : `${user.periodPoints} points`}
+ ? t('points.available', { count: user.availablePoints })
+ : t('points.pointsLabel', {
+ count: user.periodPoints,
+ })}
@@ -591,7 +625,7 @@ const UserPoints = () => {
- Filter & Analysis
+ {t('points.filterAndAnalysis')}
@@ -610,7 +644,7 @@ const UserPoints = () => {
>
- Filter Points
+ {t('points.filterTitle')}
{
{/* User Filter */}
- Show points for:
+ {t('points.showFor')}
{
{/* Time Period Filter */}
- Time period:
+ {t('points.timePeriod')}
{
@@ -713,9 +747,9 @@ const UserPoints = () => {
}}
>
{[
- { label: '7 Days', value: 7 },
- { label: '6 Months', value: 6 * 30 },
- { label: 'All Time', value: 24 * 30 },
+ { label: t('points.days7'), value: 7 },
+ { label: t('points.months6'), value: 6 * 30 },
+ { label: t('points.allTime'), value: 24 * 30 },
].map((tab, index) => (
{
}}
sx={{ mt: 'auto' }}
>
- Redeem Points
+ {t('points.redeemPoints')}
)}
@@ -788,27 +822,27 @@ const UserPoints = () => {
const pointsCards = [
{
icon: ,
- title: 'Available',
- text: `${availablePoints} points`,
- subtext: 'Ready to redeem',
+ title: t('points.cardAvailable'),
+ text: t('points.pointsLabel', { count: availablePoints }),
+ subtext: t('points.cardAvailableSubtext'),
},
{
icon: ,
- title: 'Redeemed',
- text: `${redeemedPoints} points`,
- subtext: 'Previously used',
+ title: t('points.cardRedeemed'),
+ text: t('points.pointsLabel', { count: redeemedPoints }),
+ subtext: t('points.cardRedeemedSubtext'),
},
{
icon: ,
- title: 'Total',
- text: `${totalPoints} points`,
- subtext: 'All time earned',
+ title: t('points.cardTotal'),
+ text: t('points.pointsLabel', { count: totalPoints }),
+ subtext: t('points.cardTotalSubtext'),
},
{
icon: ,
- title: 'Period Points',
- text: `${periodPoints} points`,
- subtext: `${tabValue === 24 * 30 ? 'All time' : tabValue === 6 * 30 ? 'Last 6 months' : `Last ${tabValue} days`}`,
+ title: t('points.cardPeriod'),
+ text: t('points.pointsLabel', { count: periodPoints }),
+ subtext: formatPeriodLabel(tabValue),
},
]
@@ -871,25 +905,12 @@ const UserPoints = () => {
{/* Current Filter Summary */}
- Showing points for{' '}
-
- {circleUsers.find(user => user.userId === selectedUser)
- ?.displayName || 'Unknown User'}
- {' '}
- over the{' '}
-
- {tabValue === 24 * 30
- ? 'All Time'
- : tabValue === 6 * 30
- ? 'Last 6 Months'
- : `Last ${tabValue} Days`}
-
+ {t('points.showingFor', {
+ user:
+ circleUsers.find(user => user.userId === selectedUser)
+ ?.displayName || t('points.unknownUser'),
+ period: formatPeriodLabel(tabValue),
+ })}
@@ -908,7 +929,7 @@ const UserPoints = () => {
level='h4'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Points Trend
+ {t('points.pointsTrend')}
diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx
index 2254cf76..a5f11515 100644
--- a/src/views/components/AddTaskModal.jsx
+++ b/src/views/components/AddTaskModal.jsx
@@ -13,6 +13,7 @@ import { FormControl } from '@mui/material'
import * as chrono from 'chrono-node'
import moment from 'moment'
import { useCallback, useEffect, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useResponsiveModal } from '../../hooks/useResponsiveModal'
import { useCreateChore } from '../../queries/ChoreQueries'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
@@ -53,6 +54,7 @@ const getDefaultNotification = () => {
}
const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
+ const { t } = useTranslation(['chores', 'common'])
const { ResponsiveModal } = useResponsiveModal()
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
const { data: circleMembers, isLoading: isCircleMembersLoading } =
@@ -668,7 +670,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
onClose={handleCloseModal}
size='lg'
fullWidth={true}
- title='Create new task'
+ title={t('chores:addTask.title')}
footer={
{
color='neutral'
onClick={handleCloseModal}
>
- Cancel
+ {t('common:actions.cancel')}
{showKeyboardShortcuts && (
{
color='primary'
onClick={createChore}
>
- Create
+ {t('chores:addTask.create')}
{showKeyboardShortcuts && (
)}
@@ -716,18 +718,16 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
alignItems: 'center',
}}
>
- Task in a sentence:
+ {t('chores:addTask.taskInSentence')}:
- This feature lets you create a task simply by typing a
- sentence. It attempt parses the sentence to identify the
- task's due date, priority, and frequency.
+ {t('chores:addTask.smartHelp')}
- Examples:
+ {t('chores:addTask.examples')}:
{
sx={{ pl: 2, mt: 1, listStyle: 'disc' }}
>
- Priority: For highest priority any of the
- following keyword P1 , Urgent ,{' '}
- Important , or ASAP . For lower priorities,
- use P2 , P3 , or P4 .
+ {t('common:labels.priority')}: {' '}
+ {t('chores:addTask.priorityExample')}
- Due date: Specify dates with phrases like{' '}
- tomorrow , next week , Monday , or{' '}
- August 1st at 12pm .
+ {t('common:labels.dueDate')}: {' '}
+ {t('chores:addTask.dueDateExample')}
- Frequency: Set recurring tasks with terms
- like daily , weekly , monthly ,{' '}
- yearly , or patterns such as{' '}
- every Tuesday and Thursday .
+ {t('chores:repeat.repeat')}: {' '}
+ {t('chores:addTask.frequencyExample')}
>
@@ -761,7 +756,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
{
setTaskText(text)
}}
@@ -786,8 +781,8 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
'@': {
value: 'userId',
display: 'displayName',
- options: [
- { userId: 'anyone', displayName: 'Anyone' },
+ options: [
+ { userId: 'anyone', displayName: t('chores:addTask.anyone') },
...(circleMembers?.res || []),
],
},
@@ -795,12 +790,15 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
value: 'id',
display: 'name',
options: [
- { id: '1', name: '1 point' },
- { id: '5', name: '5 points' },
- { id: '10', name: '10 points' },
- { id: '25', name: '25 points' },
- { id: '50', name: '50 points' },
- { id: '100', name: '100 points' },
+ { id: '1', name: t('chores:addTask.points', { count: 1 }) },
+ { id: '5', name: t('chores:addTask.points', { count: 5 }) },
+ { id: '10', name: t('chores:addTask.points', { count: 10 }) },
+ { id: '25', name: t('chores:addTask.points', { count: 25 }) },
+ { id: '50', name: t('chores:addTask.points', { count: 50 }) },
+ {
+ id: '100',
+ name: t('chores:addTask.points', { count: 100 }),
+ },
],
},
}}
@@ -829,7 +827,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
showKeyboardShortcuts &&
}
>
- Description
+ {t('chores:addTask.description')}
)}
@@ -845,7 +843,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
showKeyboardShortcuts &&
}
>
- Subtasks
+ {t('common:labels.subtasks')}
)}
{!dueDate && (
@@ -864,7 +862,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
showKeyboardShortcuts &&
}
>
- Due Date
+ {t('chores:addTask.dueDate')}
)}
{!hasNotifications && dueDate && (
@@ -874,11 +872,11 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
size='sm'
onClick={() => {
setHasNotifications(true)
- setFrequencyHumanReadable('Once')
+ setFrequencyHumanReadable(t('chores:frequency.once'))
setFrequency(null)
}}
>
- Edit Notifications
+ {t('chores:addTask.editNotifications')}
)}
{/* {!hasDeadline && dueDate && (
@@ -898,7 +896,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
{hasDescription && (
- Description:
+ {t('chores:addTask.description')}:
{
)}
{hasSubTasks && (
- Subtasks:
+ {t('common:labels.subtasks')}:
{
>
{priority > 0 && (
- Priority
+ {t('common:labels.priority')}
setPriority(value)}
>
- No Priority
+ {t('chores:addTask.noPriority')}
P1
P2
P3
@@ -946,7 +944,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
)}
{dueDate && (
- Due Date
+ {t('common:labels.dueDate')}
{
size='sm'
checked={useCustomTime}
onChange={e => handleUseCustomTimeChange(e.target.checked)}
- label='Set a specific time'
+ label={t('chores:edit.setSpecificTime')}
sx={{ mt: 1 }}
/>
{useCustomTime
- ? 'Task will be due at the specified time'
- : 'Task will be due at the end of the day (11:59 PM)'}
+ ? t('chores:edit.specificTimeHelper')
+ : t('chores:edit.endOfDayHelper')}
{useCustomTime && (
{
alignItems: 'center',
}}
>
- Notification Schedule
+ {t('chores:edit.notificationSchedule')}
{
diff --git a/src/views/components/AutocompleteInput.jsx b/src/views/components/AutocompleteInput.jsx
index cc889f56..3586446a 100644
--- a/src/views/components/AutocompleteInput.jsx
+++ b/src/views/components/AutocompleteInput.jsx
@@ -1,7 +1,9 @@
import { Chip, List, ListItem, ListItemButton, Textarea } from '@mui/joy'
import React, { useEffect, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
const AutocompleteInput = ({ options, ref, value, onChange, ...props }) => {
+ const { t } = useTranslation(['common'])
const [filteredOptions, setFilteredOptions] = useState([])
const [menuVisible, setMenuVisible] = useState(false)
const [highlightedIndex, setHighlightedIndex] = useState(-1)
@@ -82,7 +84,7 @@ const AutocompleteInput = ({ options, ref, value, onChange, ...props }) => {
value={value}
onChange={onChange}
onKeyDown={handleKeyDown}
- placeholder='Type here...'
+ placeholder={t('common:placeholders.typeHere')}
/>
{menuVisible && (
diff --git a/src/views/components/CalendarCard.jsx b/src/views/components/CalendarCard.jsx
index 39ed4217..cb55e169 100644
--- a/src/views/components/CalendarCard.jsx
+++ b/src/views/components/CalendarCard.jsx
@@ -2,6 +2,7 @@ import { CalendarMonth } from '@mui/icons-material'
import { Avatar, Box, Chip, Grid, Typography } from '@mui/joy'
import moment from 'moment'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
@@ -15,6 +16,7 @@ const getAssigneeColor = (assignee, userProfile) => {
: TASK_COLOR.ASSIGNED_TO_OTHER
}
const CalendarCard = ({ chores }) => {
+ const { t } = useTranslation(['chores', 'common'])
const { data: userProfile } = useUserProfile()
const { fmt } = useLocalization()
@@ -58,7 +60,9 @@ const CalendarCard = ({ chores }) => {
return userProfile.displayName
}
const assignee = circleMembers.find(member => member.userId === assignedTo)
- return assignee ? `${assignee.displayName}` : 'Assigned to other'
+ return assignee
+ ? `${assignee.displayName}`
+ : t('common:status.assignedToOther')
}
return (
@@ -83,7 +87,9 @@ const CalendarCard = ({ chores }) => {
}}
>
- Calendar Overview
+
+ {t('labels.calendarOverview')}
+
@@ -119,25 +125,25 @@ const CalendarCard = ({ chores }) => {
// Add priority levels that exist in the chores
if (priorityLevels.has(1)) {
legendItems.push({
- name: 'High Priority',
+ name: t('labels.highPriority'),
color: TASK_COLOR.PRIORITY_1,
})
}
if (priorityLevels.has(2)) {
legendItems.push({
- name: 'Medium Priority',
+ name: t('labels.mediumPriority'),
color: TASK_COLOR.PRIORITY_2,
})
}
if (priorityLevels.has(3)) {
legendItems.push({
- name: 'Low Priority',
+ name: t('labels.lowPriority'),
color: TASK_COLOR.PRIORITY_3,
})
}
if (priorityLevels.has(4)) {
legendItems.push({
- name: 'Lowest Priority',
+ name: t('labels.lowestPriority'),
color: TASK_COLOR.PRIORITY_4,
})
}
@@ -148,7 +154,7 @@ const CalendarCard = ({ chores }) => {
)
) {
legendItems.push({
- name: 'No Priority',
+ name: t('labels.noPriority'),
color: TASK_COLOR.NO_PRIORITY,
})
}
@@ -210,7 +216,7 @@ const CalendarCard = ({ chores }) => {
const selectedLocalDate = selectedDate.toLocaleDateString()
return choreDate === selectedLocalDate
}).length
- return `${count} Tasks`
+ return `${count} ${t('common:labels.tasks')}`
})()}
diff --git a/src/views/components/CalendarDual.jsx b/src/views/components/CalendarDual.jsx
index bc5cd6f9..ff22088c 100644
--- a/src/views/components/CalendarDual.jsx
+++ b/src/views/components/CalendarDual.jsx
@@ -1,5 +1,6 @@
import { Box, Typography } from '@mui/joy'
import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import Calendar from 'react-calendar'
import { useNavigate } from 'react-router-dom'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
@@ -13,6 +14,7 @@ const getAssigneeColor = (assignee, userProfile) => {
: TASK_COLOR.ASSIGNED_TO_OTHER
}
const CalendarDual = ({ chores, onDateChange }) => {
+ const { t } = useTranslation('common')
const { data: userProfile } = useUserProfile()
const { firstDayOfWeek, fmt } = useLocalization()
const calendarType =
@@ -44,7 +46,9 @@ const CalendarDual = ({ chores, onDateChange }) => {
return userProfile.displayName
}
const assignee = circleMembers.find(member => member.userId === assignedTo)
- return assignee ? `${assignee.displayName}` : 'Assigned to other'
+ return assignee
+ ? `${assignee.displayName}`
+ : t('status.assignedToOther')
}
const tileContent = ({ date, view }) => {
diff --git a/src/views/components/ChoreActionMenu.jsx b/src/views/components/ChoreActionMenu.jsx
index 57c58e8a..b1c7dfd3 100644
--- a/src/views/components/ChoreActionMenu.jsx
+++ b/src/views/components/ChoreActionMenu.jsx
@@ -22,6 +22,7 @@ import {
} from '@mui/icons-material'
import { Divider, IconButton, Menu, MenuItem, Tooltip } from '@mui/joy'
import React, { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { isOfficialDonetickInstanceSync } from '../../utils/FeatureToggle'
@@ -41,6 +42,7 @@ const ChoreActionMenu = ({
sx = {},
variant = 'soft',
}) => {
+ const { t } = useTranslation(['chores', 'common'])
const [anchorEl, setAnchorEl] = React.useState(null)
const [isOfficialInstance, setIsOfficialInstance] = useState(false)
const menuRef = React.useRef(null)
@@ -235,7 +237,7 @@ const ChoreActionMenu = ({
}}
>
- Complete with note
+ {t('chores:actions.completeWithNote')}
{
@@ -245,7 +247,7 @@ const ChoreActionMenu = ({
}}
>
- Complete in past
+ {t('chores:actions.completeInPast')}
{
@@ -254,7 +256,7 @@ const ChoreActionMenu = ({
}}
>
- Skip to next due date
+ {t('chores:actions.skipToNextDueDate')}
{
@@ -264,7 +266,7 @@ const ChoreActionMenu = ({
}}
>
- Delegate to someone else
+ {t('chores:actions.delegate')}
{isOfficialInstance && (
-
- Send nudge
-
- )}
+ >
+
+ {t('chores:actions.sendNudge')}
+
+ )}
{
@@ -286,7 +288,7 @@ const ChoreActionMenu = ({
}}
>
- History
+ {t('chores:actions.history')}
e.stopPropagation()}
>
-
+
{
@@ -313,7 +315,7 @@ const ChoreActionMenu = ({
-
+
{
@@ -335,7 +337,7 @@ const ChoreActionMenu = ({
*/}
-
+
{
@@ -346,7 +348,7 @@ const ChoreActionMenu = ({
-
+
{
@@ -357,7 +359,7 @@ const ChoreActionMenu = ({
-
+
- Change due date
+ {t('chores:actions.changeDueDate')}
{
@@ -389,7 +391,7 @@ const ChoreActionMenu = ({
}}
>
- Write to NFC
+ {t('chores:actions.writeToNfc')}
{
@@ -398,7 +400,7 @@ const ChoreActionMenu = ({
}}
>
- Edit
+ {t('common:actions.edit')}
{
@@ -407,7 +409,7 @@ const ChoreActionMenu = ({
}}
>
- Clone
+ {t('common:actions.clone')}
{
@@ -416,7 +418,7 @@ const ChoreActionMenu = ({
}}
>
- View
+ {t('common:actions.view')}
{
@@ -426,7 +428,9 @@ const ChoreActionMenu = ({
color='neutral'
>
{chore.isActive ? : }
- {chore.isActive ? 'Archive' : 'Unarchive'}
+ {chore.isActive
+ ? t('common:actions.archive')
+ : t('common:actions.unarchive')}
- Delete
+ {t('common:actions.delete')}
>
diff --git a/src/views/components/NavBar.jsx b/src/views/components/NavBar.jsx
index 5a74b295..4dc81bdb 100644
--- a/src/views/components/NavBar.jsx
+++ b/src/views/components/NavBar.jsx
@@ -282,7 +282,7 @@ const NavBar = () => {
-
{t('logout')}
+
{t('actions.logout')}
- Search
+ {t('labels.search')}
{
+ const { t } = useTranslation(['common'])
const { showError } = useNotification()
const { data: userProfile } = useUserProfile()
const quillRef = useRef(null)
@@ -56,9 +58,8 @@ const RichTextEditor = forwardRef(
// Check if user has plus account
if (!isPlusAccount(userProfile)) {
showError({
- title: 'Plus Feature',
- message:
- 'Image uploads are not available in the Basic plan. Upgrade to Plus to add images to your content.',
+ title: t('common:editor.plusFeature'),
+ message: t('common:editor.plusFeatureMessage'),
})
return
}
@@ -110,33 +111,32 @@ const RichTextEditor = forwardRef(
if (response.status === 507) {
showError({
- title: 'Storage Quota Exceeded',
- message: 'You have exceeded your quota for uploading files.',
+ title: t('common:editor.storageQuotaExceeded'),
+ message: t('common:editor.storageQuotaExceededMessage'),
})
return
} else if (response.status === 413) {
showError({
- title: 'File Too Large',
- message: 'The file you are trying to upload is too large.',
+ title: t('common:editor.fileTooLarge'),
+ message: t('common:editor.fileTooLargeMessage'),
})
return
} else if (response.status === 403 && !isPlusAccount()) {
showError({
- title: 'Upgrade Required',
- message:
- 'Image uploads are only available for Plus accounts. Please ',
+ title: t('common:editor.upgradeRequired'),
+ message: t('common:editor.upgradeRequiredMessage'),
})
return
} else if (response.status === 403) {
showError({
- title: 'Permission Denied',
- message: 'You do not have permission to upload files.',
+ title: t('common:editor.permissionDenied'),
+ message: t('common:editor.permissionDeniedMessage'),
})
return
} else if (!response.ok) {
showError({
- title: 'Upload Failed',
- message: 'Failed to upload image.',
+ title: t('common:editor.uploadFailed'),
+ message: t('common:editor.uploadFailedMessage'),
})
return
}
@@ -149,12 +149,12 @@ const RichTextEditor = forwardRef(
} catch (error) {
console.error('Error during image processing or upload:', error)
showError({
- title: 'Upload Failed',
- message: 'An error occurred while processing the image.',
+ title: t('common:editor.uploadFailed'),
+ message: t('common:editor.processingFailedMessage'),
})
}
}
- }, [entityId, entityType, showError, userProfile]) // Dependencies for useCallback
+ }, [entityId, entityType, showError, t, userProfile]) // Dependencies for useCallback
useEffect(() => {
if (!quillRef.current) return
@@ -176,7 +176,7 @@ const RichTextEditor = forwardRef(
},
},
},
- placeholder: placeholder,
+ placeholder: placeholder || t('common:editor.placeholder'),
})
new QuillMarkdown(editorRef.current, {})
editorRef.current.root.innerHTML = value
diff --git a/src/views/components/SubTask.jsx b/src/views/components/SubTask.jsx
index 01a505f3..4063cfe4 100644
--- a/src/views/components/SubTask.jsx
+++ b/src/views/components/SubTask.jsx
@@ -32,6 +32,7 @@ import {
Typography,
} from '@mui/joy'
import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext'
import { useUserProfile } from '../../queries/UserQueries'
@@ -49,6 +50,7 @@ function SortableItem({
editMode,
performers = [],
}) {
+ const { t } = useTranslation(['common'])
const { fmt } = useLocalization()
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({
@@ -234,7 +236,7 @@ function SortableItem({
color='primary'
size='sm'
onClick={handleAddSubtaskClick}
- title='Add subtask'
+ title={t('common:subtasks.addSubtask')}
>
@@ -265,7 +267,7 @@ function SortableItem({
>
setNewSubtask(e.target.value)}
onKeyPress={handleKeyPress}
@@ -313,6 +315,7 @@ const SubTasks = ({
performers,
shouldFocus = false,
}) => {
+ const { t } = useTranslation(['common'])
const [newTask, setNewTask] = useState('')
const { data: userProfile } = useUserProfile()
const { impersonatedUser } = useImpersonateUser()
@@ -507,7 +510,7 @@ const SubTasks = ({
setNewTask(e.target.value)}
onKeyPress={handleKeyPress}