diff --git a/package.json b/package.json index 8f412a0..147924b 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,9 @@ "build-cf": "rm -rf package-lock.json && npm install && npm install --force @rollup/rollup-linux-x64-gnu@4.34.9 @swc/core-linux-x64-gnu && vite build", "build-selfhosted": "rm -rf package-lock.json && npm install && npm install --force @rollup/rollup-linux-x64-gnu@4.34.9 @swc/core-linux-x64-gnu && vite build --mode selfhosted", "build-win": "del package-lock.json && npm install && npm install --force @rollup/rollup-win32-x64-msvc @swc/core-win32-x64-msvc && vite build --mode selfhosted", - "lint": "eslint && prettier -c .", - "lint:ci": "eslint -f gha && prettier -c .", + "lint": "eslint && prettier -c . && npm run lint:i18n", + "lint:i18n": "node scripts/i18n-audit.mjs", + "lint:ci": "eslint -f gha && prettier -c . && npm run lint:i18n", "lint:fix": "eslint --fix && prettier -w .", "preview": "vite preview", "setup-m1": "rm -rf node_modules package-lock.json && npm install && npm install --force @rollup/rollup-darwin-arm64 @swc/core-darwin-arm64", diff --git a/public/locales/en/chores.json b/public/locales/en/chores.json index 98675d7..589ee85 100644 --- a/public/locales/en/chores.json +++ b/public/locales/en/chores.json @@ -259,7 +259,18 @@ "clearFilters": "Clear filters", "count_one": "{{count}} archived task", "count_other": "{{count}} archived tasks", - "matching": " matching \"{{term}}\"" + "matching": " matching \"{{term}}\"", + "filtersTitle": "Filters", + "sortArchivedDate": "Archived date", + "sortName": "Name", + "sortPriority": "Priority", + "sortDueDate": "Due date", + "noMatchTitle": "No archived tasks match", + "noMatchSearch": "Nothing in the archive matches \"{{term}}\".", + "noMatchFilters": "There are archived tasks, but none fit the filters that are currently on.", + "emptyTitle": "Nothing archived", + "emptyDescription": "Archiving hides a task without deleting it. Anything you archive from your task list shows up here, ready to restore.", + "backToTasks": "Back to tasks" }, "edit": { "deleteConfirm": "Are you sure you want to delete this chore?" @@ -374,7 +385,11 @@ "search": "Search", "newChore": "New Chore", "unassigned": "Unassigned", - "changeDueDate": "Change due date" + "changeDueDate": "Change due date", + "colDue": "Due", + "colChore": "Chore", + "colAssignee": "Assignee", + "colAction": "Action" }, "dataError": { "attachmentsFailed": "Failed to fetch attachments" @@ -404,7 +419,21 @@ "today": "Today", "tomorrow": "Tomorrow", "weekend": "Weekend", - "nextWeek": "Next week" + "nextWeek": "Next week", + "title": "Due Date", + "apply": "Apply", + "remove": "Remove", + "quickDate": "Quick date", + "quickTime": "Quick time", + "nextMonth": "Next month", + "morning": "Morning", + "noon": "Noon", + "afternoon": "Afternoon", + "evening": "Evening", + "night": "Night", + "customTime": "Custom time", + "anytime": "Anytime", + "specificTime": "Specific time" }, "photoTask": { "title": "Scan photo to create task", @@ -415,7 +444,13 @@ "processNatively": "Process Natively", "processImage": "Process Image", "createTask": "Create Task", - "failedScan": "Failed scan" + "failedScan": "Failed scan", + "retake": "Retake", + "upload": "Upload", + "keepAsAttachment": "Keep photo as attachment", + "altCaptured": "Captured document", + "altProcessing": "Processing", + "taskIdentified": "Task identified" }, "descriptionPlaceholder": "Enter description...", "notificationTemplate": { diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 6af526c..800e237 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -338,6 +338,50 @@ "tagHome": "Home", "tagPets": "Pets", "tagCooking": "Cooking" + }, + "slides": { + "problemTitle": "Stop forgetting the little things", + "problemBody": "Donetick remembers the last time it happened, and when it's due next. So nobody else has to.", + "captureTitle": "Add it before you forget it", + "captureBody": "Type it, say it out loud, or snap a photo. Donetick fills in the due date, priority, labels, and who's on it.", + "scheduleTitle": "It comes back exactly when it should", + "scheduleBody": "Every week, every 3 months, or a month after you actually did it. Finishing late doesn't throw the rest of the year off.", + "circleTitle": "Everyone gets a turn, No more arguing", + "circleBody": "Rotate in the order you pick, or at random. Donetick remembers who went last, so turns stay fair.", + "remindersTitle": "A nudge right when it matters & Widgets!", + "remindersBody": "Get a reminder before something's due, or send a heads-up when someone forgets theirs. Today's list sits on your home screen too." + }, + "carousel": { + "skip": "Skip", + "ariaLabel": "What you can do with Donetick", + "goToStep": "Go to step {{number}}", + "slidePosition": "{{index}} of {{total}}", + "turnOnReminders": "Turn on reminders", + "notNow": "Not now", + "next": "Next" + }, + "heardAbout": { + "title": "Where'd you hear about Donetick?", + "subtitle": "Helps us know what's working. Totally optional.", + "detailPlaceholder": "Tell us where", + "continue": "Continue", + "skip": "Skip", + "sources": { + "appStore": "App Store / Google Play", + "reddit": "Reddit or a forum", + "friend": "Friend or family", + "video": "YouTube or TikTok", + "search": "Search engine", + "other": "Something else" + } + }, + "privacy": { + "title": "Help us improve Donetick", + "subtitle": "Your server, your data. Both of these are off from this point unless you turn them on.", + "crashReportsLabel": "Share crash reports", + "crashReportsDescription": "When Donetick crashes, send us the error and what led to it (device, app version, the screen you were on) no task content. It's how we catch bugs that never show up in our own testing and ship fixes faster.", + "analyticsLabel": "Share anonymous usage analytics", + "analyticsDescription": "Send anonymous usage events which features get used, which get ignored so we know what to build next instead of guessing. No task content, no personal data, nothing tied back to you." } }, "errorReport": { diff --git a/public/locales/en/projects.json b/public/locales/en/projects.json index 97c2f62..d76e093 100644 --- a/public/locales/en/projects.json +++ b/public/locales/en/projects.json @@ -60,6 +60,11 @@ "errorUpdate": "Failed to update project", "errorCreate": "Failed to create project", "namePlaceholder": "Enter project name...", - "descriptionPlaceholder": "Optional project description..." + "descriptionPlaceholder": "Optional project description...", + "nameLabel": "Project Name", + "descriptionLabel": "Description", + "iconLabel": "Project Icon", + "selectIcon": "Select Icon", + "colorLabel": "Project Color" } } diff --git a/public/locales/en/settings.json b/public/locales/en/settings.json index 02714c5..eab55df 100644 --- a/public/locales/en/settings.json +++ b/public/locales/en/settings.json @@ -537,7 +537,13 @@ }, "passwordChange": { "minLength": "Password must be at least 8 characters", - "maxLength": "Password must be less than 64 characters" + "maxLength": "Password must be less than 64 characters", + "description": "Choose a password between 8 and 64 characters.", + "mismatch": "Passwords do not match", + "newPassword": "New password", + "newPasswordPlaceholder": "Enter password", + "confirmPassword": "Confirm password", + "confirmPasswordPlaceholder": "Repeat password" }, "payment": { "cancelledTitle": "Payment has been cancelled", @@ -569,7 +575,12 @@ "includeAssets": "Include uploaded files and assets", "warningLabel": "Warning:", "restoreWarning": "Restoring a backup will replace all your current data. This action cannot be undone.", - "restoreKeyPlaceholder": "Enter the encryption key used for this backup" + "restoreKeyPlaceholder": "Enter the encryption key used for this backup", + "title": "Backup & Restore", + "createTab": "Create Backup", + "restoreTab": "Restore Backup", + "creating": "Creating backup...", + "restoring": "Restoring backup..." }, "deletion": { "passwordRequired": "Please enter your password to continue", @@ -579,10 +590,25 @@ "transferIntro": "You own circles that require ownership transfer before deletion. Please select new owners:", "selectOwner": "Select new owner", "confirmPrompt": "Please enter your password and type DELETE to confirm", - "typeDelete": "DELETE" + "typeDelete": "DELETE", + "warningLead": "This action cannot be undone.", + "warningIntro": "Deleting your account will permanently remove:", + "removesProfile": "Your user profile and authentication data", + "removesChores": "All your chores, chore history, and time tracking sessions", + "removesTokens": "API tokens, MFA sessions, and password reset tokens", + "removesStorage": "Storage files and usage data", + "removesPoints": "Points history and notifications", + "removesCircles": "Circle memberships and relationships", + "passwordToContinue": "Enter your password to continue", + "circleLabel": "Circle: {{name}}", + "newOwner": "New Owner", + "logoutNotice": "On successful deletion, you will be logged out and redirected to the login page.", + "passwordLabel": "Password", + "typeToConfirm": "Type \"{{word}}\" to confirm" }, "realtime": { - "titleSse": "Real-time Updates (SSE)" + "titleSse": "Real-time Updates (SSE)", + "subtitleSse": "Get instant notifications via Server-Sent Events" }, "nativeCancel": { "title": "Cancel Subscription", diff --git a/public/locales/en/timer.json b/public/locales/en/timer.json index a448c49..fdf06ef 100644 --- a/public/locales/en/timer.json +++ b/public/locales/en/timer.json @@ -35,5 +35,11 @@ "endTime": "End Time", "leaveEmpty": "Leave empty if session is ongoing", "noSessionForChore": "No timer session found for this chore.", - "saveChanges": "Save Changes" + "saveChanges": "Save Changes", + "emptySessionsTitle": "No work sessions yet", + "emptySessionsDescription": "Start the timer on this task and each session lands here.", + "percentActive": "{{percent}}% active", + "activityTimeline": "Activity Timeline", + "liveSession": "Live Session", + "live": "Live" } diff --git a/src/components/SSESettings.jsx b/src/components/SSESettings.jsx index 679206d..8532067 100644 --- a/src/components/SSESettings.jsx +++ b/src/components/SSESettings.jsx @@ -81,12 +81,12 @@ const SSESettings = () => { {t('realtime.titleSse')} {!isPlusAccount(userProfile) && ( - Plus Feature + {t('common.plusFeature')} )} - Get instant notifications via Server-Sent Events + {t('realtime.subtitleSse')} {isSSEEnabled() && isPlusAccount(userProfile) && ( diff --git a/src/views/Chores/ArchivedTasks.jsx b/src/views/Chores/ArchivedTasks.jsx index 655eb34..06000a1 100644 --- a/src/views/Chores/ArchivedTasks.jsx +++ b/src/views/Chores/ArchivedTasks.jsx @@ -765,7 +765,7 @@ const ArchivedTasks = () => { borderRadius: '50%', }} onClick={() => setIsFilterSheetOpen(true)} - title='Filters' + title={t('archived.filtersTitle')} > @@ -774,10 +774,10 @@ const ArchivedTasks = () => { {/* Sort Menu */} { variant='no-results' fullHeight icon={} - title='No archived tasks match' + title={t('archived.noMatchTitle')} description={ searchTerm - ? `Nothing in the archive matches "${searchTerm}".` - : 'There are archived tasks, but none fit the filters that are currently on.' + ? t('archived.noMatchSearch', { term: searchTerm }) + : t('archived.noMatchFilters') } primaryAction={ searchTerm @@ -1069,9 +1069,9 @@ const ArchivedTasks = () => { } - title='Nothing archived' - description='Archiving hides a task without deleting it. Anything you archive from your task list shows up here, ready to restore.' - primaryAction={{ label: 'Back to tasks', to: '/chores' }} + title={t('archived.emptyTitle')} + description={t('archived.emptyDescription')} + primaryAction={{ label: t('archived.backToTasks'), to: '/chores' }} /> ) ) : ( diff --git a/src/views/ChoresOverview.jsx b/src/views/ChoresOverview.jsx index 9b715bd..30d7d92 100644 --- a/src/views/ChoresOverview.jsx +++ b/src/views/ChoresOverview.jsx @@ -180,11 +180,11 @@ const ChoresOverview = () => { {/* first column has minium size because its icon */} - Due - Chore - Assignee - Due - Action + {t('overview.colDue')} + {t('overview.colChore')} + {t('overview.colAssignee')} + {t('overview.colDue')} + {t('overview.colAction')} diff --git a/src/views/Modals/Inputs/BackupRestoreModal.jsx b/src/views/Modals/Inputs/BackupRestoreModal.jsx index 325c48b..6451160 100644 --- a/src/views/Modals/Inputs/BackupRestoreModal.jsx +++ b/src/views/Modals/Inputs/BackupRestoreModal.jsx @@ -104,7 +104,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { handleClose() } else { const errorData = await response.json() - setError(errorData.message || 'Failed to create backup') + setError(errorData.message || t('backup.createFailed')) } } catch (err) { setError(t('backup.createFailed')) @@ -156,7 +156,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { handleClose() } else { const errorData = await response.json() - setError(errorData.message || 'Failed to restore backup') + setError(errorData.message || t('backup.restoreFailed')) } } catch (err) { setError(t('backup.restoreFailed')) @@ -288,7 +288,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { size='lg' fullWidth={true} unmountDelay={250} - title='Backup & Restore' + title={t('backup.title')} closeOnBackdrop={!loading} closeOnEscape={!loading} footer={ @@ -299,7 +299,8 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { disabled: loading, }} primary={{ - label: activeTab === 0 ? 'Create Backup' : 'Restore Backup', + label: + activeTab === 0 ? t('backup.createTab') : t('backup.restoreTab'), color: activeTab === 0 ? 'primary' : 'warning', onClick: activeTab === 0 ? handleCreateBackup : handleRestore, loading, @@ -320,7 +321,7 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { > - {activeTab === 0 ? 'Creating backup...' : 'Restoring backup...'} + {activeTab === 0 ? t('backup.creating') : t('backup.restoring')} ) : ( @@ -329,8 +330,8 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) { onChange={(event, newValue) => setActiveTab(newValue)} > - Create Backup - Restore Backup + {t('backup.createTab')} + {t('backup.restoreTab')} {renderBackupTab()} diff --git a/src/views/Modals/Inputs/PasswordChangeModal.jsx b/src/views/Modals/Inputs/PasswordChangeModal.jsx index 52eeee5..9b9768c 100644 --- a/src/views/Modals/Inputs/PasswordChangeModal.jsx +++ b/src/views/Modals/Inputs/PasswordChangeModal.jsx @@ -18,7 +18,7 @@ function PasswordChangeModal({ isOpen, onClose }) { if (!passwordTouched || !confirmPasswordTouched) return if (password !== confirmPassword) { - setPasswordError('Passwords do not match') + setPasswordError(t('passwordChange.mismatch')) } else if (password.length < 8) { setPasswordError(t('passwordChange.minLength')) } else if (password.length > 64) { @@ -42,7 +42,7 @@ function PasswordChangeModal({ isOpen, onClose }) { onClose={() => handleAction(false)} size='sm' title={t('accountSettings.changePassword')} - description='Choose a password between 8 and 64 characters.' + description={t('passwordChange.description')} footer={ - New password + + {t('passwordChange.newPassword')} + { setPasswordTouched(true) @@ -74,13 +76,15 @@ function PasswordChangeModal({ isOpen, onClose }) { - Confirm password + + {t('passwordChange.confirmPassword')} + { setConfirmPasswordTouched(true) diff --git a/src/views/Modals/Inputs/ProjectModal.jsx b/src/views/Modals/Inputs/ProjectModal.jsx index b9a9211..b5ec3d6 100644 --- a/src/views/Modals/Inputs/ProjectModal.jsx +++ b/src/views/Modals/Inputs/ProjectModal.jsx @@ -151,7 +151,7 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => { {/* Project Name */} - Project Name + {t('modal.nameLabel')} setProjectName(e.target.value)} @@ -163,7 +163,7 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => { {/* Project Description */} - Description + {t('modal.descriptionLabel')}