From 7d36090e462306a79a80cc28f73a3220e250a78a Mon Sep 17 00:00:00 2001 From: everysingletear Date: Sat, 15 Aug 2026 09:03:28 +0800 Subject: [PATCH] i18n: extract the chore list and its modals (`chores` namespace) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of #145. Twenty-three files across the task list zone: the list and card views, sorting and grouping, multi-select and its toolbar and help sheet, archived tasks, the assignee card, the chore action menu, the nudge/NFC/photo modals, the rich text editor, the scan panel, the notification templates and the keyboard-shortcut toasts. Extends the existing `chores` namespace, so `src/i18n/config.js` is untouched. English only β€” no translations, no behaviour change. Every t() value is checked against this branch's base: the string must appear character-for-character in the code it replaces (247 call sites). Two values are matched loosely and worth naming: archived.closeMultiSelect in both the archived view and the toolbar. The base builds that tooltip as `${size === 0 ? 'Close' : 'Clear'} multi-select (Esc)`, so only one branch of the ternary exists contiguously in the source. Both keys hold exactly what each branch renders; the tooltip is kept whole so a translator can reorder it. Rebased on current `develop` again after #215 and #216 landed β€” the dictionary conflict was theirs, not the code's. No code file in this PR was touched upstream in the meantime. --- public/locales/en/chores.json | 215 +++++++++++++++++- public/locales/en/common.json | 13 +- src/components/NotificationTemplate.jsx | 44 ++-- src/views/Chores/ArchivedTasks.jsx | 125 +++++----- src/views/Chores/ChoreListView.jsx | 4 +- src/views/Chores/CompactChoreCard.jsx | 6 +- src/views/Chores/IconButtonWithMenu.jsx | 4 +- src/views/Chores/MultiSelectHelp.jsx | 24 +- src/views/Chores/MyChores.jsx | 36 +-- .../Chores/NotificationAccessSnackbar.jsx | 11 +- src/views/Chores/SortAndGrouping.jsx | 48 ++-- src/views/Chores/TasksByAssigneeCard.jsx | 14 +- src/views/Chores/UserSwitcher.jsx | 18 +- src/views/Chores/components/ChoreModals.jsx | 14 +- .../Chores/components/CustomFilterChips.jsx | 10 +- .../Chores/components/MultiSelectToolbar.jsx | 32 +-- .../Chores/hooks/useKeyboardShortcuts.js | 14 +- src/views/ChoresOverview.jsx | 12 +- .../Modals/Inputs/AttachmentBrowserModal.jsx | 10 +- src/views/Modals/Inputs/NudgeModal.jsx | 17 +- src/views/Modals/Inputs/WriteNFCModal.jsx | 38 ++-- src/views/components/ChoreActionMenu.jsx | 40 ++-- src/views/components/PhotoTaskModal.jsx | 22 +- src/views/components/RichTextEditor.jsx | 34 +-- src/views/components/ScanToTask/ScanPanel.jsx | 10 +- 25 files changed, 542 insertions(+), 273 deletions(-) diff --git a/public/locales/en/chores.json b/public/locales/en/chores.json index 31db9aa..cbf48a0 100644 --- a/public/locales/en/chores.json +++ b/public/locales/en/chores.json @@ -100,11 +100,24 @@ "saveFilter": "Save Filter", "saveAsNew": "Save as New Filter", "groupBy": "Group by", - "showTasksFor": "Show tasks for" + "showTasksFor": "Show tasks for", + "type": { + "assignee": "Assignee" + } }, "sort": { "assignedToMe": "Assigned to me", - "smart": "Smart" + "smart": "Smart", + "createdDate": "Created Date", + "updatedDate": "Updated Date", + "sortAndGroup": "Sort and Group (Ctrl+G)", + "groupBy": "Group By", + "quickFilters": "Quick Filters", + "assignedTo": "Assigned to:", + "availableForMe": "Available for me", + "assignedToOthers": "Assigned to others", + "createFilter": "Create Filter", + "createFilterHint": "Build advanced filter rules" }, "thing": { "triggerHint": "Trigger a task when a thing state changes to a desired state", @@ -124,7 +137,9 @@ "dueToday": "Due Today", "pendingApproval": "Pending Approval", "dueThisWeek": "Due This Week", - "noDueDate": "No Due Date" + "noDueDate": "No Due Date", + "dueDate": "Due Date", + "dueLater": "Due Later" }, "insights": { "highPriority": "High Priority" @@ -134,7 +149,16 @@ }, "actionMenu": { "view": "View", - "archive": "Archive" + "archive": "Archive", + "writeNFC": "Write to NFC", + "completeWithNote": "Complete with note", + "completeInPast": "Complete in past", + "skipToNext": "Skip to next due date", + "sendNudge": "Send nudge", + "history": "History", + "clone": "Clone", + "moveToProject": "Move to project", + "removeDueDate": "Remove due date" }, "actions": { "rescheduledTitle": "Task Rescheduled", @@ -187,18 +211,195 @@ "someFailedTitle": "Some Tasks Failed", "unexpectedError": "An unexpected error occurred. Please try again.", "deletedBulkTitle": "πŸ—‘οΈ Tasks Deleted", - "bulkDeleteFailTitle": "Bulk Delete Failed" + "bulkDeleteFailTitle": "Bulk Delete Failed", + "archivedDate": "Archived Date", + "loadFailTitle": "Failed to load archived tasks", + "loadFailMsg": "Please try again later.", + "deletedMsg": "The archived task has been permanently deleted.", + "restoreTasksTitle": "Restore Tasks", + "restore": "Restore", + "restoreConfirm_one": "Restore {{count}} task to active list?", + "restoreConfirm_other": "Restore {{count}} tasks to active list?", + "restoredBulkTitle": "πŸ“€ Tasks Restored", + "restoredCount_one": "Restored {{count}} task{{offline}}.", + "restoredCount_other": "Restored {{count}} tasks{{offline}}.", + "restoreFailCount_one": "{{count}} task could not be restored.", + "restoreFailCount_other": "{{count}} tasks could not be restored.", + "bulkRestoreFailTitle": "Bulk Restore Failed", + "deleteTasksTitle": "Delete Archived Tasks", + "deleteConfirm_one": "Permanently delete {{count}} archived task?\n\nThis action cannot be undone.", + "deleteConfirm_other": "Permanently delete {{count}} archived tasks?\n\nThis action cannot be undone.", + "deletedCount_one": "Successfully deleted {{count}} task.", + "deletedCount_other": "Successfully deleted {{count}} tasks.", + "deleteFailCount_one": "{{count}} task could not be deleted.", + "deleteFailCount_other": "{{count}} tasks could not be deleted.", + "title": "Archived Tasks", + "subtitle": "View and manage tasks that have been archived or completed.", + "heading": "Archived Tasks", + "searchPlaceholder": "Search archived tasks", + "switchToCompact": "Switch to Compact View", + "switchToCard": "Switch to Card View", + "exitMultiSelect": "Exit Multi-select Mode (Ctrl+S)", + "enableMultiSelect": "Enable Multi-select Mode (Ctrl+S)", + "selected_one": "{{count}} task selected", + "selected_other": "{{count}} tasks selected", + "selectAllTitle": "Select all visible tasks (Ctrl+A)", + "all": "All", + "closeMultiSelect": "Close multi-select (Esc)", + "clearMultiSelect": "Clear multi-select (Esc)", + "close": "Close", + "clear": "Clear", + "restoreSelectedTitle": "Restore selected tasks (R)", + "deleteSelectedTitle": "Delete selected tasks (E)", + "clearSearch": "Clear search", + "clearFilters": "Clear filters", + "count_one": "{{count}} archived task", + "count_other": "{{count}} archived tasks", + "matching": " matching \"{{term}}\"" }, "edit": { "deleteConfirm": "Are you sure you want to delete this chore?" }, "list": { - "complete": "Complete" + "complete": "Complete", + "filterSaved": "Filter Saved", + "filterSavedMsg": "\"{{name}}\" has been saved", + "nothingScheduled": "Nothing scheduled", + "createChore": "Create new chore (Cmd+C)", + "filterUpdated": "Filter Updated", + "filterUpdatedMsg": "\"{{name}}\" has been updated successfully", + "advancedFilterCreated": "Advanced Filter Created", + "advancedFilterCreatedMsg": "\"{{name}}\" has been created successfully" }, "multiToolbar": { - "skip": "Skip" + "skip": "Skip", + "completeTitle": "Complete selected tasks (Enter)", + "complete": "Complete", + "skipTitle": "Skip selected tasks (/)", + "archiveTitle": "Archive selected tasks (X)", + "archive": "Archive", + "deleteTitle": "Delete selected tasks (Shift+X)" }, "remind": { "title": "Reminders" + }, + "assignee": { + "anyone": "Anyone" + }, + "filterChip": { + "cancelAll": "Cancel All Filters", + "unpin": "Unpin filter", + "pin": "Pin filter", + "edit": "Edit filter", + "delete": "Delete filter" + }, + "multiSelect": { + "showShortcuts": "Show keyboard shortcuts", + "title": "Multi-select Mode", + "selection": "Selection", + "selectAll": "Select all visible tasks", + "clearOrExit": "Clear selection or exit multi-select mode", + "actions": "Actions", + "markCompleted": "Mark selected tasks as completed", + "deleteSelected": "Delete selected tasks", + "interface": "Interface", + "quickAdd": "Quick add new task" + }, + "notifications": { + "needTitle": "Need Notification?", + "needBody": "You need to enable permission to receive notifications, do you want to enable it?", + "keepDisabled": "No, Keep it Disabled" + }, + "assigneeCard": { + "loading": "Loading tasks by assignee...", + "title": "Tasks by Assignee", + "scheduled": "Scheduled", + "pendingReview": "Pending Review" + }, + "filter": { + "status": { + "inProgress": "In Progress" + } + }, + "impersonate": { + "viewAs": "View tasks as", + "switchTitle": "Switch to user view", + "switchHint": "Tasks will be filtered to show only assignments for selected user", + "chooseUser": "Choose User", + "changeUser": "Change User" + }, + "modals": { + "changeDueDate": "Change due date", + "completePast": "Save Chore that you completed in the past", + "delegate": "Delegate to someone else", + "selectPerformer": "Select a performer", + "addNote": "Add note to attach to this completion:", + "complete": "Complete" + }, + "shortcuts": { + "allSelectedTitle": "βœ… All Tasks Selected", + "someSelectedTitle": "🎯 Tasks Selected", + "allSelectedAltTitle": "🎯 All Tasks Selected" + }, + "overview": { + "title": "Chores Overviews", + "search": "Search", + "newChore": "New Chore", + "unassigned": "Unassigned", + "changeDueDate": "Change due date" + }, + "dataError": { + "attachmentsFailed": "Failed to fetch attachments" + }, + "nudge": { + "title": "Send Nudge", + "customMessage": "Custom Message (optional)", + "messagePlaceholder": "Add a personal message with your nudge...", + "notifyAll": "Notify All Assignees", + "notifyAllHint": "If enabled, all members who can see this task will be notified. Otherwise, only the assigned person will receive the nudge." + }, + "nfc": { + "errWrite": "Error writing to NFC tag. Please try again.", + "errUnsupported": "NFC is not supported by this browser. Copy the URL and write it to an NFC tag using a compatible device.", + "titleSuccess": "Tag written!", + "titleError": "Something went wrong", + "titleWaiting": "Hold near NFC tag", + "subSuccess": "Your NFC tag is ready to use.", + "subWaiting": "Keep your device near the tag until complete.", + "subIdle": "Encode this task link onto any NFC tag.", + "tagUrl": "Tag URL", + "copyUrl": "Copy URL", + "autoComplete": "Auto-complete on scan", + "autoCompleteHint": "Mark task done when tag is tapped" + }, + "duePicker": { + "today": "Today", + "tomorrow": "Tomorrow", + "weekend": "Weekend", + "nextWeek": "Next week" + }, + "photoTask": { + "title": "Scan photo to create task", + "cameraUnavailable": "Camera not available", + "uploadPhoto": "Upload Photo", + "scanDocument": "Scan Document", + "capture": "Capture", + "processNatively": "Process Natively", + "processImage": "Process Image", + "createTask": "Create Task", + "failedScan": "Failed scan" + }, + "descriptionPlaceholder": "Enter description...", + "notifTemplate": { + "onDueDate": "On due date", + "beforeDue": "{{count}} {{unit}} before due", + "afterDue": "{{count}} {{unit}} after due", + "errDuplicate": "This notification setting already exists. Please use a different timing.", + "errOneDue": "Only one \"Due Alert\" notification is allowed.", + "errAllConfigured": "All common {{type}} times are already configured.", + "reminder": "Reminder", + "dueAlert": "Due Alert", + "followUp": "Follow-up", + "rememberFuture": "Remember for Future Tasks" } } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 6f55fb1..d52124d 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -131,7 +131,14 @@ "tooLargeTitle": "File Too Large", "tooLargeMessage": "The file you are trying to upload is too large.", "deniedTitle": "Permission Denied", - "deniedMessage": "You do not have permission to upload files." + "deniedMessage": "You do not have permission to upload files.", + "plusFeatureTitle": "Plus Feature", + "plusFeatureMessage": "Image uploads are not available in the Basic plan. Upgrade to Plus to add images to your content.", + "upgradeTitle": "Upgrade Required", + "upgradeMessage": "Image uploads are only available for Plus accounts.", + "failedTitle": "Upload Failed", + "failedMessage": "Failed to upload image.", + "processingMessage": "An error occurred while processing the image." }, "getStarted": "Get Started!", "imageLoadFailed": "Failed to load image.", @@ -191,5 +198,7 @@ "allSynced": "All changes synced", "willSync": "Will sync when back online", "cancelAll": "Cancel All" - } + }, + "attachments": "Attachments", + "fileNumbered": "File {{index}}" } diff --git a/src/components/NotificationTemplate.jsx b/src/components/NotificationTemplate.jsx index 73013b6..d942681 100644 --- a/src/components/NotificationTemplate.jsx +++ b/src/components/NotificationTemplate.jsx @@ -13,26 +13,29 @@ import Option from '@mui/joy/Option' import Select from '@mui/joy/Select' import Typography from '@mui/joy/Typography' import { useCallback, useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' import { NOTIFICATION_TYPE, TASK_COLOR } from '../utils/Colors' import { TIME_UNITS } from '../utils/DurationUtils' const timeUnits = TIME_UNITS const timingOptions = [ - { label: 'Before', value: 'before' }, - { label: 'Due', value: 'ondue' }, - { label: 'After', value: 'after' }, + { value: 'before' }, + { value: 'ondue' }, + { value: 'after' }, ] -function getRelativeLabel(notification) { +function getRelativeLabel(notification, t) { const { value, unit } = notification const numericValue = Number(value) if (numericValue === 0) { - return 'On due date' + return t('notifTemplate.onDueDate') } - const unitName = unit === 'm' ? 'minutes' : unit === 'h' ? 'hours' : 'days' + const unitName = t(`notifTemplate.unitName.${unit}`) const absValue = Math.abs(numericValue) - return `${absValue} ${unitName} ${numericValue < 0 ? 'before' : 'after'} due` + return numericValue < 0 + ? t('notifTemplate.beforeDue', { count: absValue, unit: unitName }) + : t('notifTemplate.afterDue', { count: absValue, unit: unitName }) } // Helper functions to convert between internal value and UI representation @@ -71,6 +74,7 @@ const NotificationTemplate = ({ value, showTimeline = true, }) => { + const { t } = useTranslation('chores') const [notifications, setNotifications] = useState( value?.templates || JSON.parse(localStorage.getItem('defaultNotificationTemplate')) || @@ -218,9 +222,7 @@ const NotificationTemplate = ({ if (!currentNotification) return if (isDuplicate(currentNotification, idx, currentList)) { - setError( - 'This notification setting already exists. Please use a different timing.', - ) + setError(t('notifTemplate.errDuplicate')) return } } @@ -232,14 +234,14 @@ const NotificationTemplate = ({ if (type === 'due') { if (notificationsRef.current.some(n => Number(n.value) === 0)) { - setError('Only one "Due Alert" notification is allowed.') + setError(t('notifTemplate.errOneDue')) return } newNotification = { value: 0, unit: 'm' } } else { newNotification = getSmartSuggestion(type) if (!newNotification) { - setError(`All common ${type} times are already configured.`) + setError(t('notifTemplate.errAllConfigured', { type })) return } } @@ -364,7 +366,7 @@ const NotificationTemplate = ({ fontSize: '0.6rem', }} > - Due Date + {t('group.dueDate')} @@ -398,7 +400,7 @@ const NotificationTemplate = ({ zIndex: 10, }, }} - title={getRelativeLabel(n)} + title={getRelativeLabel(n, t)} > - {getRelativeLabel(n)} + {getRelativeLabel(n, t)} @@ -575,7 +577,7 @@ const NotificationTemplate = ({ value={opt.value} disabled={opt.value === 'ondue' && hasOnDueElsewhere} > - {opt.label} + {t(`notifTemplate.timing.${opt.value}`)} ))} @@ -638,7 +640,7 @@ const NotificationTemplate = ({ > {timeUnits.map(opt => ( ))} @@ -688,7 +690,7 @@ const NotificationTemplate = ({ }, }} > - Reminder + {t('notifTemplate.reminder')} {showSaveDefault && ( @@ -762,7 +764,7 @@ const NotificationTemplate = ({ setShowSaveDefault(false) }} > - Remember for Future Tasks + {t('notifTemplate.rememberFuture')} )} diff --git a/src/views/Chores/ArchivedTasks.jsx b/src/views/Chores/ArchivedTasks.jsx index 7e5a9f4..46321bc 100644 --- a/src/views/Chores/ArchivedTasks.jsx +++ b/src/views/Chores/ArchivedTasks.jsx @@ -27,6 +27,7 @@ import { import { useQueryClient } from '@tanstack/react-query' import Fuse from 'fuse.js' import { useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import EmptyState from '../../components/common/EmptyState' @@ -92,6 +93,7 @@ const applyPendingArchivedState = async chores => { } const ArchivedTasks = () => { + const { t } = useTranslation('chores') const { data: userProfile, isLoading: isUserProfileLoading } = useUserProfile() const { showError, showSuccess } = useNotification() @@ -132,7 +134,7 @@ const ArchivedTasks = () => { () => [ { id: 'assignee', - label: 'Assignee', + label: t('toolbar.type.assignee'), type: 'multi-select', icon: , options: performers.map(p => ({ @@ -144,7 +146,7 @@ const ArchivedTasks = () => { }, { id: 'priority', - label: 'Priority', + label: t('priority'), type: 'multi-select', icon: , options: Priorities.map(p => ({ @@ -159,7 +161,7 @@ const ArchivedTasks = () => { ? [ { id: 'label', - label: 'Labels', + label: t('labels.label'), type: 'multi-select', icon: