feat: enhance localization and user experience across various components

- Added new localization keys for project management, settings, and onboarding views.
- Updated labels and placeholders in project modal, password change modal, and backup/restore modal for better clarity.
- Improved user feedback messages for password mismatch and backup creation failures.
- Enhanced the timer details view with localized strings for active work, sessions, and distribution.
- Refined the due date picker modal with localized quick date and time options.
- Updated photo task modal and scan panel to include localized text for better accessibility.
- Ensured consistent use of translation function for all user-facing strings.
This commit is contained in:
Mo Tarbin
2026-08-18 01:31:16 -04:00
parent 1fa4748168
commit 0bac419c3a
19 changed files with 339 additions and 235 deletions

View File

@@ -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 }) {
>
<CircularProgress />
<Typography level='body-md' sx={{ ml: 2 }}>
{activeTab === 0 ? 'Creating backup...' : 'Restoring backup...'}
{activeTab === 0 ? t('backup.creating') : t('backup.restoring')}
</Typography>
</Box>
) : (
@@ -329,8 +330,8 @@ function BackupRestoreModal({ isOpen, onClose, showNotification }) {
onChange={(event, newValue) => setActiveTab(newValue)}
>
<TabList>
<Tab>Create Backup</Tab>
<Tab>Restore Backup</Tab>
<Tab>{t('backup.createTab')}</Tab>
<Tab>{t('backup.restoreTab')}</Tab>
</TabList>
<TabPanel value={0}>{renderBackupTab()}</TabPanel>

View File

@@ -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={
<ModalActions
secondary={{
@@ -58,13 +58,15 @@ function PasswordChangeModal({ isOpen, onClose }) {
}
>
<FormControl sx={{ mb: 2 }}>
<Typography level='body-sm'>New password</Typography>
<Typography level='body-sm'>
{t('passwordChange.newPassword')}
</Typography>
<Input
required
name='password'
type='password'
autoComplete='new-password'
placeholder='Enter password'
placeholder={t('passwordChange.newPasswordPlaceholder')}
value={password}
onChange={event => {
setPasswordTouched(true)
@@ -74,13 +76,15 @@ function PasswordChangeModal({ isOpen, onClose }) {
</FormControl>
<FormControl error={Boolean(passwordError)}>
<Typography level='body-sm'>Confirm password</Typography>
<Typography level='body-sm'>
{t('passwordChange.confirmPassword')}
</Typography>
<Input
required
name='confirmPassword'
type='password'
autoComplete='new-password'
placeholder='Repeat password'
placeholder={t('passwordChange.confirmPasswordPlaceholder')}
value={confirmPassword}
onChange={event => {
setConfirmPasswordTouched(true)

View File

@@ -151,7 +151,7 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => {
<Stack spacing={3}>
{/* Project Name */}
<FormControl required>
<FormLabel>Project Name</FormLabel>
<FormLabel>{t('modal.nameLabel')}</FormLabel>
<Input
value={projectName}
onChange={e => setProjectName(e.target.value)}
@@ -163,7 +163,7 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => {
{/* Project Description */}
<FormControl>
<FormLabel>Description</FormLabel>
<FormLabel>{t('modal.descriptionLabel')}</FormLabel>
<Textarea
value={projectDescription}
onChange={e => setProjectDescription(e.target.value)}
@@ -176,7 +176,7 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => {
{/* Icon Selection */}
<FormControl>
<FormLabel>Project Icon</FormLabel>
<FormLabel>{t('modal.iconLabel')}</FormLabel>
<Button
variant='outlined'
onClick={() => setIsIconPickerOpen(true)}
@@ -212,19 +212,20 @@ const ProjectModal = ({ isOpen, onClose, onSave, project }) => {
}
sx={{ justifyContent: 'flex-start' }}
>
{PROJECT_ICONS.find(icon => icon.value === projectIcon)?.name ||
'Select Icon'}
{PROJECT_ICONS.some(icon => icon.value === projectIcon)
? t(`icons.${projectIcon}`)
: t('modal.selectIcon')}
</Button>
</FormControl>
{/* Color Selection */}
<FormControl>
<FormLabel>Project Color</FormLabel>
<FormLabel>{t('modal.colorLabel')}</FormLabel>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{PROJECT_COLORS.map(colorOption => (
<Box
key={colorOption.value}
title={colorOption.name}
title={t(`common:colors.${colorOption.name}`)}
onClick={() => setProjectColor(colorOption.value)}
sx={{
width: 26,

View File

@@ -17,6 +17,10 @@ import ModalActions from '../../../components/common/ModalActions'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { CheckUserDeletion, DeleteUser } from '../../../utils/Fetcher'
// Sent verbatim to the delete endpoint, so it stays English in every locale —
// the label around it is what gets translated.
const CONFIRMATION_WORD = 'DELETE'
function UserDeletionModal({ isOpen, onClose }) {
const { t } = useTranslation('settings')
const { ResponsiveModal } = useResponsiveModal()
@@ -99,7 +103,7 @@ function UserDeletionModal({ isOpen, onClose }) {
}
const executeUserDeletion = async () => {
if (password.trim() === '' || confirmation !== 'DELETE') {
if (password.trim() === '' || confirmation !== CONFIRMATION_WORD) {
setError(t('deletion.passwordAndDelete'))
return
}
@@ -153,33 +157,27 @@ function UserDeletionModal({ isOpen, onClose }) {
const renderWarningStep = () => (
<>
<Typography level='body-md' mb={2}>
<strong>This action cannot be undone.</strong> Deleting your account
will permanently remove:
<strong>{t('deletion.warningLead')}</strong>{' '}
{t('deletion.warningIntro')}
</Typography>
<Box mb={3}>
<Typography level='body-sm' mb={1}>
Your user profile and authentication data
</Typography>
<Typography level='body-sm' mb={1}>
All your chores, chore history, and time tracking sessions
</Typography>
<Typography level='body-sm' mb={1}>
API tokens, MFA sessions, and password reset tokens
</Typography>
<Typography level='body-sm' mb={1}>
Storage files and usage data
</Typography>
<Typography level='body-sm' mb={1}>
Points history and notifications
</Typography>
<Typography level='body-sm' mb={1}>
Circle memberships and relationships
</Typography>
{[
'removesProfile',
'removesChores',
'removesTokens',
'removesStorage',
'removesPoints',
'removesCircles',
].map(key => (
<Typography key={key} level='body-sm' mb={1}>
{t(`deletion.${key}`)}
</Typography>
))}
</Box>
<FormControl sx={{ mb: 2 }}>
<FormLabel>Enter your password to continue</FormLabel>
<FormLabel>{t('deletion.passwordToContinue')}</FormLabel>
<Input
type='password'
value={password}
@@ -200,16 +198,15 @@ function UserDeletionModal({ isOpen, onClose }) {
<>
<Typography level='body-md' mb={3}>
{t('deletion.transferIntro')}
select new owners:
</Typography>
{circlesRequiringTransfer.map(circle => (
<Card key={circle.id} sx={{ mb: 2, p: 2 }}>
<Typography level='title-sm' mb={1}>
Circle: {circle.name}
{t('deletion.circleLabel', { name: circle.name })}
</Typography>
<FormControl>
<FormLabel>New Owner</FormLabel>
<FormLabel>{t('deletion.newOwner')}</FormLabel>
<Select
placeholder={t('deletion.selectOwner')}
value={
@@ -243,12 +240,11 @@ function UserDeletionModal({ isOpen, onClose }) {
{t('deletion.confirmPrompt')}
</Typography>
<Typography level='body-sm' mb={2}>
on successful deletion, you will be logged out and redirected to the
login page.
{t('deletion.logoutNotice')}
</Typography>
<FormControl sx={{ mb: 2 }}>
<FormLabel>Password</FormLabel>
<FormLabel>{t('deletion.passwordLabel')}</FormLabel>
<Input
type='password'
value={password}
@@ -258,7 +254,9 @@ function UserDeletionModal({ isOpen, onClose }) {
</FormControl>
<FormControl sx={{ mb: 3 }}>
<FormLabel>Type &quot;DELETE&quot; to confirm</FormLabel>
<FormLabel>
{t('deletion.typeToConfirm', { word: CONFIRMATION_WORD })}
</FormLabel>
<Input
value={confirmation}
onChange={e => setConfirmation(e.target.value)}
@@ -323,7 +321,7 @@ function UserDeletionModal({ isOpen, onClose }) {
? !password
: step === 2
? circlesRequiringTransfer.length !== transferOptions.length
: !password || confirmation !== 'DELETE',
: !password || confirmation !== CONFIRMATION_WORD,
}}
/>
)