Merge pull request #209 from everysingletear/i18n/common

i18n: extract shared strings into the existing `common` namespace (Part of #145)
This commit is contained in:
Mohamad Tarbin
2026-08-13 19:32:47 -04:00
committed by GitHub
15 changed files with 102 additions and 43 deletions

View File

@@ -115,5 +115,37 @@
"subtitle": "We've filled in an issue with your notes and version details. Nothing has been sent yet — review it and post when you're ready.", "subtitle": "We've filled in an issue with your notes and version details. Nothing has been sent yet — review it and post when you're ready.",
"open": "Open the issue" "open": "Open the issue"
} }
},
"done": "Done",
"clearAll": "Clear all",
"upload": {
"quotaTitle": "Storage Quota Exceeded",
"quotaMessage": "You have exceeded your quota for uploading files.",
"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."
},
"getStarted": "Get Started!",
"imageLoadFailed": "Failed to load image.",
"typeHere": "Type in here…",
"summaryOfChores": "This is a summary of your chores",
"loadingOffline": "You are offline",
"loadingOfflineSub": "This not available while offline. Please check your internet connection and try again.",
"loadingSlow": "This is taking longer than usual. There might be an issue.",
"navigateBack": "Navigate Back",
"bottomNav": "Bottom Navigation",
"profile": "Profile",
"autocompletePlaceholder": "Type here...",
"errorScreen": {
"title": "Something went wrong",
"fallback": "An unexpected error occurred. Try reloading — it usually fixes it.",
"tryAgain": "Try again",
"home": "Home",
"login": "Login",
"hideDetails": "Hide error details",
"showDetails": "Show error details",
"copyToClipboard": "Copy to clipboard",
"copied": "Error details copied to clipboard"
} }
} }

View File

@@ -63,11 +63,14 @@ const LogoContainer = styled(Box)({
marginBottom: '24px', marginBottom: '24px',
}) })
import { useTranslation } from 'react-i18next'
const LoadingScreen = ({ const LoadingScreen = ({
message = 'Loading...', message = null,
showLogo = true, showLogo = true,
size = 'lg', size = 'lg',
}) => { }) => {
const { t } = useTranslation('common')
return ( return (
<LoadingContainer> <LoadingContainer>
<LoadingContent> <LoadingContent>
@@ -82,7 +85,7 @@ const LoadingScreen = ({
mb: 1, mb: 1,
}} }}
> >
Done {t('done')}
<span style={{ color: '#06b6d4' }}>tick</span> <span style={{ color: '#06b6d4' }}>tick</span>
</Typography> </Typography>
</LogoContainer> </LogoContainer>
@@ -96,7 +99,7 @@ const LoadingScreen = ({
}} }}
/> />
<PulsingText level='body-md'>{message}</PulsingText> <PulsingText level='body-md'>{message ?? t('loading')}</PulsingText>
</LoadingContent> </LoadingContent>
</LoadingContainer> </LoadingContainer>
) )

View File

@@ -1,6 +1,8 @@
import { Add, Close } from '@mui/icons-material' import { Add, Close } from '@mui/icons-material'
import { Box, Button, Chip, ChipDelete, Typography } from '@mui/joy' import { Box, Button, Chip, ChipDelete, Typography } from '@mui/joy'
import { useTranslation } from 'react-i18next'
const ActiveFilterChips = ({ const ActiveFilterChips = ({
chipSize = 'md', chipSize = 'md',
chipSx, chipSx,
@@ -18,6 +20,7 @@ const ActiveFilterChips = ({
showAddChip = false, showAddChip = false,
totalCount, totalCount,
}) => { }) => {
const { t } = useTranslation('common')
if (!chips.length) { if (!chips.length) {
return null return null
} }
@@ -161,7 +164,7 @@ const ActiveFilterChips = ({
...clearButtonSx, ...clearButtonSx,
}} }}
> >
Clear all {t('clearAll')}
</Button> </Button>
)} )}
</Box> </Box>

View File

@@ -1,14 +1,16 @@
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next'
const useConfirmationModal = () => { const useConfirmationModal = () => {
const { t } = useTranslation('common')
const [confirmModalConfig, setConfirmModalConfig] = useState({}) const [confirmModalConfig, setConfirmModalConfig] = useState({})
const showConfirmation = ( const showConfirmation = (
message, message,
title, title,
onConfirm, onConfirm,
confirmText = 'Confirm', confirmText = t('confirm'),
cancelText = 'Cancel', cancelText = t('cancel'),
color = 'primary', color = 'primary',
) => { ) => {
setConfirmModalConfig({ setConfirmModalConfig({

View File

@@ -5,12 +5,14 @@ import { useUserProfile } from '../queries/UserQueries'
import { useNotification } from '../service/NotificationProvider' import { useNotification } from '../service/NotificationProvider'
import { apiClient } from '../utils/ApiClient' import { apiClient } from '../utils/ApiClient'
import { isPlusAccount, resolvePhotoURL } from '../utils/Helpers' import { isPlusAccount, resolvePhotoURL } from '../utils/Helpers'
import { useTranslation } from 'react-i18next'
export const useFileUpload = ({ export const useFileUpload = ({
draftId, draftId,
entityId, entityId,
entityType = 'chore_attachment', entityType = 'chore_attachment',
} = {}) => { } = {}) => {
const { t } = useTranslation('common')
const { showError } = useNotification() const { showError } = useNotification()
const { data: userProfile } = useUserProfile() const { data: userProfile } = useUserProfile()
@@ -58,14 +60,14 @@ export const useFileUpload = ({
if (response.status === 507) { if (response.status === 507) {
showError({ showError({
title: 'Storage Quota Exceeded', title: t('upload.quotaTitle'),
message: 'You have exceeded your quota for uploading files.', message: t('upload.quotaMessage'),
}) })
return null return null
} else if (response.status === 413) { } else if (response.status === 413) {
showError({ showError({
title: 'File Too Large', title: t('upload.tooLargeTitle'),
message: 'The file you are trying to upload is too large.', message: t('upload.tooLargeMessage'),
}) })
return null return null
} else if (response.status === 403 && !isPlusAccount(userProfile)) { } else if (response.status === 403 && !isPlusAccount(userProfile)) {
@@ -76,8 +78,8 @@ export const useFileUpload = ({
return null return null
} else if (response.status === 403) { } else if (response.status === 403) {
showError({ showError({
title: 'Permission Denied', title: t('upload.deniedTitle'),
message: 'You do not have permission to upload files.', message: t('upload.deniedMessage'),
}) })
return null return null
} else if (!response.ok) { } else if (!response.ok) {
@@ -105,7 +107,7 @@ export const useFileUpload = ({
return null return null
} }
}, },
[entityType, entityId, draftId, showError, userProfile], [entityType, entityId, draftId, showError, userProfile, t],
) )
return { uploadFile, isPlus: isPlusAccount(userProfile) } return { uploadFile, isPlus: isPlusAccount(userProfile) }

View File

@@ -18,6 +18,7 @@ import {
formatErrorReport, formatErrorReport,
} from '../service/ErrorReportService' } from '../service/ErrorReportService'
import ErrorReportModal from './Modals/ErrorReportModal' import ErrorReportModal from './Modals/ErrorReportModal'
import { useTranslation } from 'react-i18next'
const getErrorKind = error => { const getErrorKind = error => {
if (!error) if (!error)
@@ -55,6 +56,7 @@ const safeMessage = error => {
} }
const Error = () => { const Error = () => {
const { t } = useTranslation('common')
const error = useRouteError() const error = useRouteError()
const [showDetails, setShowDetails] = useState(false) const [showDetails, setShowDetails] = useState(false)
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
@@ -192,7 +194,7 @@ const Error = () => {
textAlign='center' textAlign='center'
sx={{ mb: 1.5 }} sx={{ mb: 1.5 }}
> >
Something went wrong {t('errorScreen.title')}
</Typography> </Typography>
{/* Error message */} {/* Error message */}
@@ -207,8 +209,7 @@ const Error = () => {
wordBreak: 'break-word', wordBreak: 'break-word',
}} }}
> >
{message ?? {message ?? t('errorScreen.fallback')}
'An unexpected error occurred. Try reloading — it usually fixes it.'}
</Typography> </Typography>
{/* Primary CTA */} {/* Primary CTA */}
@@ -220,7 +221,7 @@ const Error = () => {
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
sx={{ width: '100%', mb: 1.5 }} sx={{ width: '100%', mb: 1.5 }}
> >
Try again {t('errorScreen.tryAgain')}
</Button> </Button>
{/* Reporting is one tap from the failure, where the context is still {/* Reporting is one tap from the failure, where the context is still
@@ -246,7 +247,7 @@ const Error = () => {
size='lg' size='lg'
startDecorator={<HomeRounded />} startDecorator={<HomeRounded />}
> >
Home {t('errorScreen.home')}
</Button> </Button>
<Button <Button
component={Link} component={Link}
@@ -255,7 +256,7 @@ const Error = () => {
color='neutral' color='neutral'
size='lg' size='lg'
> >
Login {t('errorScreen.login')}
</Button> </Button>
</Box> </Box>
@@ -293,7 +294,9 @@ const Error = () => {
} }
sx={{ mb: 1 }} sx={{ mb: 1 }}
> >
{showDetails ? 'Hide' : 'Show'} error details {showDetails
? t('errorScreen.hideDetails')
: t('errorScreen.showDetails')}
</Button> </Button>
{showDetails && ( {showDetails && (
@@ -312,7 +315,7 @@ const Error = () => {
color='neutral' color='neutral'
onClick={handleCopy} onClick={handleCopy}
sx={{ position: 'absolute', top: 8, right: 8 }} sx={{ position: 'absolute', top: 8, right: 8 }}
title='Copy to clipboard' title={t('errorScreen.copyToClipboard')}
> >
<ContentCopyRounded fontSize='small' /> <ContentCopyRounded fontSize='small' />
</IconButton> </IconButton>
@@ -347,7 +350,7 @@ const Error = () => {
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
size='sm' size='sm'
> >
Error details copied to clipboard {t('errorScreen.copied')}
</Snackbar> </Snackbar>
</Box> </Box>
) )

View File

@@ -1,10 +1,12 @@
import { Box, Button, Container, Typography } from '@mui/joy' import { Box, Button, Container, Typography } from '@mui/joy'
import { useTranslation } from 'react-i18next'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useState } from 'react' import { useState } from 'react'
import Logo from '../Logo' import Logo from '../Logo'
const Home = () => { const Home = () => {
const { t } = useTranslation('common')
const Navigate = useNavigate() const Navigate = useNavigate()
const getCurrentUser = () => { const getCurrentUser = () => {
return JSON.parse(localStorage.getItem('user')) return JSON.parse(localStorage.getItem('user'))
@@ -36,7 +38,7 @@ const Home = () => {
Navigate('/chores') Navigate('/chores')
}} }}
> >
Get Started! {t('getStarted')}
</Button> </Button>
</Box> </Box>
</Container> </Container>

View File

@@ -1,4 +1,5 @@
import { Browser } from '@capacitor/browser' import { Browser } from '@capacitor/browser'
import { useTranslation } from 'react-i18next'
import { Capacitor } from '@capacitor/core' import { Capacitor } from '@capacitor/core'
import { Download } from '@mui/icons-material' import { Download } from '@mui/icons-material'
import { Box, CircularProgress, Typography } from '@mui/joy' import { Box, CircularProgress, Typography } from '@mui/joy'
@@ -29,6 +30,7 @@ const downloadUrl = (url, fileName) => {
} }
function AttachmentViewerModal({ config }) { function AttachmentViewerModal({ config }) {
const { t } = useTranslation('common')
const { ResponsiveModal } = useResponsiveModal() const { ResponsiveModal } = useResponsiveModal()
const [imgLoaded, setImgLoaded] = useState(false) const [imgLoaded, setImgLoaded] = useState(false)
const [imgError, setImgError] = useState(false) const [imgError, setImgError] = useState(false)
@@ -49,7 +51,7 @@ function AttachmentViewerModal({ config }) {
maxHeight='92vh' maxHeight='92vh'
footer={ footer={
<ModalActions <ModalActions
secondary={{ label: 'Close', onClick: handleClose }} secondary={{ label: t('close'), onClick: handleClose }}
primary={{ primary={{
label: 'Download', label: 'Download',
startDecorator: <Download />, startDecorator: <Download />,
@@ -73,7 +75,7 @@ function AttachmentViewerModal({ config }) {
)} )}
{imgError ? ( {imgError ? (
<Typography level='body-sm' sx={{ color: 'text.secondary' }}> <Typography level='body-sm' sx={{ color: 'text.secondary' }}>
Failed to load image. {t('imageLoadFailed')}
</Typography> </Typography>
) : ( ) : (
<Box <Box

View File

@@ -2,8 +2,10 @@ import { Input } from '@mui/joy'
import { useState } from 'react' import { useState } from 'react'
import ModalActions from '../../../components/common/ModalActions' import ModalActions from '../../../components/common/ModalActions'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal' import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { useTranslation } from 'react-i18next'
function DateModal({ isOpen, onClose, onSave, current, title }) { function DateModal({ isOpen, onClose, onSave, current, title }) {
const { t } = useTranslation('common')
const { ResponsiveModal } = useResponsiveModal() const { ResponsiveModal } = useResponsiveModal()
const [date, setDate] = useState( const [date, setDate] = useState(
current ? new Date(current).toISOString().split('T')[0] : '', current ? new Date(current).toISOString().split('T')[0] : '',
@@ -22,8 +24,8 @@ function DateModal({ isOpen, onClose, onSave, current, title }) {
title={title} title={title}
footer={ footer={
<ModalActions <ModalActions
secondary={{ label: 'Cancel', onClick: onClose }} secondary={{ label: t('cancel'), onClick: onClose }}
primary={{ label: 'Save', onClick: handleSave, disabled: !date }} primary={{ label: t('save'), onClick: handleSave, disabled: !date }}
/> />
} }
> >

View File

@@ -2,6 +2,7 @@ import { Textarea } from '@mui/joy'
import { useState } from 'react' import { useState } from 'react'
import ModalActions from '../../../components/common/ModalActions' import ModalActions from '../../../components/common/ModalActions'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal' import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { useTranslation } from 'react-i18next'
function TextModal({ function TextModal({
isOpen, isOpen,
@@ -12,6 +13,7 @@ function TextModal({
okText, okText,
cancelText, cancelText,
}) { }) {
const { t } = useTranslation('common')
const { ResponsiveModal } = useResponsiveModal() const { ResponsiveModal } = useResponsiveModal()
const [text, setText] = useState(current) const [text, setText] = useState(current)
@@ -35,7 +37,7 @@ function TextModal({
> >
<Textarea <Textarea
autoFocus autoFocus
placeholder='Type in here…' placeholder={t('typeHere')}
value={text} value={text}
onChange={event => setText(event.target.value)} onChange={event => setText(event.target.value)}
minRows={3} minRows={3}

View File

@@ -1,8 +1,10 @@
import { Avatar, Box, List, ListItem, Typography } from '@mui/joy' import { Avatar, Box, List, ListItem, Typography } from '@mui/joy'
import ModalActions from '../../../components/common/ModalActions' import ModalActions from '../../../components/common/ModalActions'
import { useResponsiveModal } from '../../../hooks/useResponsiveModal' import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
import { useTranslation } from 'react-i18next'
const UserModal = ({ isOpen, performers = [], onSelect, onClose }) => { const UserModal = ({ isOpen, performers = [], onSelect, onClose }) => {
const { t } = useTranslation('common')
const { ResponsiveModal } = useResponsiveModal() const { ResponsiveModal } = useResponsiveModal()
return ( return (
@@ -13,7 +15,7 @@ const UserModal = ({ isOpen, performers = [], onSelect, onClose }) => {
fullWidth={true} fullWidth={true}
title='Select User' title='Select User'
footer={ footer={
<ModalActions secondary={{ label: 'Cancel', onClick: onClose }} /> <ModalActions secondary={{ label: t('cancel'), onClick: onClose }} />
} }
> >
<List sx={{ mb: 2 }}> <List sx={{ mb: 2 }}>

View File

@@ -1,13 +1,15 @@
import { Card, IconButton, Typography } from '@mui/joy' import { Card, IconButton, Typography } from '@mui/joy'
import { useTranslation } from 'react-i18next'
const SummaryCard = () => { const SummaryCard = () => {
const { t } = useTranslation('common')
return ( return (
<Card> <Card>
<div className='flex justify-between'> <div className='flex justify-between'>
<div> <div>
<Typography level='h2'>Summary</Typography> <Typography level='h2'>Summary</Typography>
<Typography level='body-xs'> <Typography level='body-xs'>
This is a summary of your chores {t('summaryOfChores')}
</Typography> </Typography>
</div> </div>
<IconButton> <IconButton>

View File

@@ -1,7 +1,9 @@
import { Chip, List, ListItem, ListItemButton, Textarea } from '@mui/joy' import { Chip, List, ListItem, ListItemButton, Textarea } from '@mui/joy'
import { useTranslation } from 'react-i18next'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
const AutocompleteInput = ({ options, ref, value, onChange, ...props }) => { const AutocompleteInput = ({ options, ref, value, onChange, ...props }) => {
const { t } = useTranslation('common')
const [filteredOptions, setFilteredOptions] = useState([]) const [filteredOptions, setFilteredOptions] = useState([])
const [menuVisible, setMenuVisible] = useState(false) const [menuVisible, setMenuVisible] = useState(false)
const [highlightedIndex, setHighlightedIndex] = useState(-1) const [highlightedIndex, setHighlightedIndex] = useState(-1)
@@ -82,7 +84,7 @@ const AutocompleteInput = ({ options, ref, value, onChange, ...props }) => {
value={value} value={value}
onChange={onChange} onChange={onChange}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder='Type here...' placeholder={t('autocompletePlaceholder')}
/> />
{menuVisible && ( {menuVisible && (
<List ref={menuRef} style={{ position: 'absolute', zIndex: 1000 }}> <List ref={menuRef} style={{ position: 'absolute', zIndex: 1000 }}>

View File

@@ -1,31 +1,29 @@
import { Box, Button, CircularProgress, Container } from '@mui/joy' import { Box, Button, CircularProgress, Container } from '@mui/joy'
import { Typography } from '@mui/material' import { Typography } from '@mui/material'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Logo from '../../Logo' import Logo from '../../Logo'
import { networkManager } from '../../hooks/NetworkManager' import { networkManager } from '../../hooks/NetworkManager'
const LoadingComponent = () => { const LoadingComponent = () => {
const [message, setMessage] = useState('Loading...') const { t } = useTranslation('common')
const [message, setMessage] = useState(t('loading'))
const [subMessage, setSubMessage] = useState('') const [subMessage, setSubMessage] = useState('')
const [isOnline, setIsOnline] = useState(networkManager.isOnline) const [isOnline, setIsOnline] = useState(networkManager.isOnline)
useEffect(() => { useEffect(() => {
if (!isOnline) { if (!isOnline) {
setMessage('You are offline') setMessage(t('loadingOffline'))
setSubMessage( setSubMessage(t('loadingOfflineSub'))
'This not available while offline. Please check your internet connection and try again.',
)
} }
}, [isOnline]) }, [isOnline, t])
useEffect(() => { useEffect(() => {
networkManager.registerNetworkListener(isOnline => setIsOnline(isOnline)) networkManager.registerNetworkListener(isOnline => setIsOnline(isOnline))
// if loading took more than 5 seconds update submessage to mention there might be an error: // if loading took more than 5 seconds update submessage to mention there might be an error:
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
if (networkManager.isOnline) { if (networkManager.isOnline) {
setSubMessage( setSubMessage(t('loadingSlow'))
'This is taking longer than usual. There might be an issue.',
)
} }
}, 5000) }, 5000)
return () => clearTimeout(timeout) return () => clearTimeout(timeout)
@@ -67,7 +65,7 @@ const LoadingComponent = () => {
window.location.href = '/' // navigate back to the home page window.location.href = '/' // navigate back to the home page
}} }}
> >
Navigate Back {t('navigateBack')}
</Button> </Button>
</Box> </Box>
</Container> </Container>

View File

@@ -1,4 +1,5 @@
import * as React from 'react' import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Box from '@mui/joy/Box' import Box from '@mui/joy/Box'
import ListItemDecorator from '@mui/joy/ListItemDecorator' import ListItemDecorator from '@mui/joy/ListItemDecorator'
import Tabs from '@mui/joy/Tabs' import Tabs from '@mui/joy/Tabs'
@@ -10,6 +11,7 @@ import Search from '@mui/icons-material/Search'
import Person from '@mui/icons-material/Person' import Person from '@mui/icons-material/Person'
export default function NavBarMobile() { export default function NavBarMobile() {
const { t } = useTranslation('common')
const [index, setIndex] = React.useState(0) const [index, setIndex] = React.useState(0)
const colors = ['primary', 'danger', 'success', 'warning'] const colors = ['primary', 'danger', 'success', 'warning']
return ( return (
@@ -32,7 +34,7 @@ export default function NavBarMobile() {
> >
<Tabs <Tabs
size='lg' size='lg'
aria-label='Bottom Navigation' aria-label={t('bottomNav')}
value={index} value={index}
onChange={(event, value) => setIndex(value)} onChange={(event, value) => setIndex(value)}
sx={theme => ({ sx={theme => ({
@@ -98,7 +100,7 @@ export default function NavBarMobile() {
<ListItemDecorator> <ListItemDecorator>
<Person /> <Person />
</ListItemDecorator> </ListItemDecorator>
Profile {t('profile')}
</Tab> </Tab>
</TabList> </TabList>
</Tabs> </Tabs>