Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
# misc
.DS_Store
.env
.idea
.vscode

npm-debug.log*
yarn-debug.log*
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,12 @@ Place configuration in `.env` file and restart/rebuild the ticker-admin
```shell
TICKER_API_URL=http://localhost:8080/v1
```

## Localization

Strings are localized on the [locales](./src/i18n/locales) folder. To add more languages, please update those files:

- [i18n.ts](./src/i18n/i18n.ts) to localize all strings
- [UserListItem.tsx](./src/components/user/UserListItem.tsx) to localize `dayjs` relative times

To add a new string, please use the `t('stringKey')` notation and update all the locales.
100 changes: 98 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@
"@types/react": "^18.3.10",
"dayjs": "^1.11.13",
"emoji-mart": "^5.6.0",
"i18next": "^25.7.3",
"i18next-browser-languagedetector": "^8.2.0",
"jwt-decode": "^4.0.0",
"linkify-react": "^4.3.2",
"linkifyjs": "^4.3.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"react-i18next": "^16.5.1",
"react-markdown": "^9.0.1",
"react-router": "^7.5.2",
"react-router-dom": "^7.5.1"
Expand Down
5 changes: 4 additions & 1 deletion src/components/Loader.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { CircularProgress, Stack, Typography } from '@mui/material'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'

const Loader: FC = () => {
const { t } = useTranslation()

return (
<Stack alignItems="center" justifyContent="center" sx={{ m: 10 }}>
<CircularProgress size="3rem" />
<Typography component="span" sx={{ pt: 2 }} variant="h5">
Loading
{t('common.loading')}
</Typography>
</Stack>
)
Expand Down
4 changes: 3 additions & 1 deletion src/components/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { FC } from 'react'
import { Navigate, RouteProps } from 'react-router'
import useAuth from '../contexts/useAuth'
import { Roles } from '../contexts/AuthContext'
import { useTranslation } from 'react-i18next'

type Props = RouteProps & {
role: Roles
outlet: JSX.Element
}

const ProtectedRoute: FC<Props> = ({ role, outlet }) => {
const { t } = useTranslation()
const { user } = useAuth()

if (!user) {
Expand All @@ -19,7 +21,7 @@ const ProtectedRoute: FC<Props> = ({ role, outlet }) => {
//TODO: ErrorView
return (
<>
<h1>Permission denied</h1>
<h1>{t("error.permissionDenied")}</h1>
</>
)
}
Expand Down
6 changes: 4 additions & 2 deletions src/components/common/CopyToClipboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { Tooltip } from '@mui/material'
import { grey } from '@mui/material/colors'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'

interface Props {
text: string
}

const CopyToClipboard: FC<Props> = ({ text }) => {
const { t } = useTranslation()
const [copied, setCopied] = useState<boolean>(false)

const handleClick = async () => {
Expand All @@ -18,8 +20,8 @@ const CopyToClipboard: FC<Props> = ({ text }) => {
}

return (
<Tooltip title={copied ? 'Copied!' : 'Copy to clipboard'} arrow placement="top">
<FontAwesomeIcon icon={faCopy} onClick={handleClick} color={grey[800]} style={{ cursor: 'pointer' }} aria-label="Copy to Clipboard" />
<Tooltip title={copied ? t('common.copied') : t('action.copyToClipboard')} arrow placement="top">
<FontAwesomeIcon icon={faCopy} onClick={handleClick} color={grey[800]} style={{ cursor: 'pointer' }} aria-label={t('action.copyToClipboard')} />
</Tooltip>
)
}
Expand Down
6 changes: 4 additions & 2 deletions src/components/common/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Close } from '@mui/icons-material'
import { Box, Breakpoint, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Stack, SxProps } from '@mui/material'
import { FC, ReactNode } from 'react'
import { useTranslation } from 'react-i18next'

interface Props {
children: ReactNode
Expand Down Expand Up @@ -31,6 +32,7 @@ const Modal: FC<Props> = ({
submitting = false,
title,
}) => {
const { t } = useTranslation()
return (
<Dialog fullWidth={fullWidth} maxWidth={maxWidth} open={open}>
<DialogTitle>
Expand All @@ -46,7 +48,7 @@ const Modal: FC<Props> = ({
{submitForm && (
<Box sx={{ display: 'inline', position: 'relative' }}>
<Button color="primary" form={submitForm} onClick={onSubmitAction} type="submit" variant="contained" disabled={submitting}>
Save
{t('action.save')}
</Button>
{submitting && (
<CircularProgress
Expand All @@ -69,7 +71,7 @@ const Modal: FC<Props> = ({
</Button>
)}
<Button color="secondary" onClick={onClose}>
Close
{t('action.close')}
</Button>
</DialogActions>
</Dialog>
Expand Down
14 changes: 8 additions & 6 deletions src/components/message/MessageForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Box, Button, FormGroup, Stack, TextField } from '@mui/material'
import { useQueryClient } from '@tanstack/react-query'
import { FC, useCallback, useEffect, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { handleApiCall } from '../../api/Api'
import { postMessageApi } from '../../api/Message'
import { Ticker } from '../../api/Ticker'
Expand All @@ -26,6 +27,7 @@ interface FormValues {
}

const MessageForm: FC<Props> = ({ ticker }) => {
const { t } = useTranslation()
const { createNotification } = useNotification()
const {
formState: { isSubmitSuccessful, errors },
Expand Down Expand Up @@ -94,13 +96,13 @@ const MessageForm: FC<Props> = ({ ticker }) => {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['messages', ticker.id] })
setAttachments([])
createNotification({ content: 'Message successfully posted', severity: 'success' })
createNotification({ content: t("message.posted"), severity: 'success' })
},
onError: () => {
createNotification({ content: 'Failed to post message', severity: 'error' })
createNotification({ content: t("message.errorFailedToPost"), severity: 'error' })
},
onFailure: () => {
createNotification({ content: 'Failed to post message', severity: 'error' })
createNotification({ content: t('message.errorFailedToPost'), severity: 'error' })
},
})

Expand All @@ -116,7 +118,7 @@ const MessageForm: FC<Props> = ({ ticker }) => {
const message = watch('message')
const disabled = !ticker.active || isSubmitting
const color = disabled ? palette.action.disabled : palette.primary['main']
const placeholder = ticker.active ? 'Write a message' : "You can't post messages to inactive tickers."
const placeholder = ticker.active ? t('message.writeActive') : t('writeInactive')

return (
<Box>
Expand All @@ -130,7 +132,7 @@ const MessageForm: FC<Props> = ({ ticker }) => {
color={errors.message ? 'error' : 'primary'}
error={!!errors.message}
helperText={
errors.message?.type === 'maxLength' ? 'The message is too long.' : errors.message?.type === 'required' ? 'The message is required.' : null
errors.message?.type === 'maxLength' ? t('message.errorTooLong') : errors.message?.type === 'required' ? t('message.errorRequired') : null
}
multiline
placeholder={placeholder}
Expand All @@ -141,7 +143,7 @@ const MessageForm: FC<Props> = ({ ticker }) => {
<Stack alignItems="center" direction="row" justifyContent="space-between">
<Box display="flex">
<Button disabled={disabled} startIcon={<FontAwesomeIcon icon={faPaperPlane} />} sx={{ mr: 1 }} type="submit" variant="outlined">
Send
{t('action.send')}
</Button>
<EmojiPicker color={color} disabled={disabled} onChange={onSelectEmoji} />
<UploadButton color={color} disabled={disabled} onUpload={onUpload} ticker={ticker} />
Expand Down
6 changes: 4 additions & 2 deletions src/components/message/MessageList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Button, CircularProgress } from '@mui/material'
import { FC, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Ticker } from '../../api/Ticker'
import useAuth from '../../contexts/useAuth'
import useMessagesQuery from '../../queries/useMessagesQuery'
Expand All @@ -12,6 +13,7 @@ interface Props {
}

const MessageList: FC<Props> = ({ ticker }) => {
const { t } = useTranslation()
const { token } = useAuth()
const { data, fetchNextPage, isFetchingNextPage, hasNextPage, status } = useMessagesQuery({ token, ticker })

Expand Down Expand Up @@ -44,7 +46,7 @@ const MessageList: FC<Props> = ({ ticker }) => {
}

if (status === 'error') {
return <ErrorView queryKey={['messages', ticker.id]}>Unable to fetch messages from server.</ErrorView>
return <ErrorView queryKey={['messages', ticker.id]}>{t("message.errorUnableToFetch")}</ErrorView>
}

return (
Expand All @@ -54,7 +56,7 @@ const MessageList: FC<Props> = ({ ticker }) => {
<CircularProgress size="3rem" />
) : hasNextPage ? (
<Button disabled={!hasNextPage || isFetchingNextPage} onClick={() => fetchNextPage()} variant="outlined">
Load More
{t("message.loadMore")}
</Button>
) : null}
</>
Expand Down
Loading