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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
import { Chip, List, ListItem, ListItemButton, Textarea } from '@mui/joy'
import { useTranslation } from 'react-i18next'
import React, { useEffect, useRef, useState } from 'react'
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('autocompletePlaceholder')}
/>
{menuVisible && (
<List ref={menuRef} style={{ position: 'absolute', zIndex: 1000 }}>

View File

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

View File

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