Skip to content

Commit

Permalink
Add support for internationalization
Browse files Browse the repository at this point in the history
Make the whole thing multilingual by using FormatJS. Currently only has
English and Finnish translations.
  • Loading branch information
RauliL committed Nov 2, 2024
1 parent 161e642 commit f502a12
Show file tree
Hide file tree
Showing 17 changed files with 339 additions and 110 deletions.
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@
"devDependencies": {
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@formatjs/ts-transformer": "^3.13.20",
"@mui/icons-material": "^5.2.5",
"@mui/material": "^5.2.5",
"@types/express": "^4.17.11",
"@types/lodash": "^4.14.168",
"@types/morgan": "^1.9.2",
"@types/node": "^14.14.41",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.3",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/yup": "^0.29.11",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
Expand All @@ -56,8 +57,9 @@
"nodemon": "^3.0.1",
"postcss": "^8.2.10",
"prettier": "^2.2.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-intl": "^6.8.4",
"rimraf": "^6.0.1",
"swr": "^2.2.4",
"terser-webpack-plugin": "^5.3.9",
Expand Down
63 changes: 34 additions & 29 deletions src/frontend/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import CssBaseline from '@mui/material/CssBaseline';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import React, { FunctionComponent, useEffect, useState } from 'react';
import { IntlProvider } from 'react-intl';
import { mutate } from 'swr';

import { deleteEntry, patchEntry } from '../api';
import { useAllEntries, usePreferDarkMode } from '../hooks';
import { getBrowserLanguage, translations } from '../i18n';
import { EntryType, SavedEntry } from '../types';

import { Content } from './Content';
Expand Down Expand Up @@ -32,6 +34,7 @@ export const App: FunctionComponent = () => {
const theme = createTheme({
palette: { mode: preferDarkMode ? 'dark' : 'light' },
});
const language = getBrowserLanguage();

const handleTabChange = (selectedTab: EntryType) => {
setState((oldState) => ({
Expand Down Expand Up @@ -127,35 +130,37 @@ export const App: FunctionComponent = () => {

return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Toolbar
preferDarkMode={preferDarkMode}
onAddEntry={handleAddEntryButtonClick}
onTabChange={handleTabChange}
selectedTab={state.selectedTab}
/>
<Content
doneEntries={doneEntries}
onDeleteAllDoneEntries={handleDeleteAllDoneEntries}
onEntryDelete={handleEntryDelete}
onEntrySelect={handleEntrySelect}
onEntryToggle={handleEntryToggle}
selectedTab={state.selectedTab}
todoEntries={todoEntries}
/>
<AddEntryDialog
open={state.addEntryDialogOpen}
onClose={handleAddEntryDialogClose}
/>
<EditEntryDialog
entry={state.selectedEntry}
open={state.editEntryDialogOpen}
onClose={handleEditEntryDialogClose}
/>
<ErrorSnackbar
onClose={handleErrorSnackbarClose}
open={state.errorSnackbarOpen}
/>
<IntlProvider locale={language} messages={translations[language]}>
<CssBaseline />
<Toolbar
preferDarkMode={preferDarkMode}
onAddEntry={handleAddEntryButtonClick}
onTabChange={handleTabChange}
selectedTab={state.selectedTab}
/>
<Content
doneEntries={doneEntries}
onDeleteAllDoneEntries={handleDeleteAllDoneEntries}
onEntryDelete={handleEntryDelete}
onEntrySelect={handleEntrySelect}
onEntryToggle={handleEntryToggle}
selectedTab={state.selectedTab}
todoEntries={todoEntries}
/>
<AddEntryDialog
open={state.addEntryDialogOpen}
onClose={handleAddEntryDialogClose}
/>
<EditEntryDialog
entry={state.selectedEntry}
open={state.editEntryDialogOpen}
onClose={handleEditEntryDialogClose}
/>
<ErrorSnackbar
onClose={handleErrorSnackbarClose}
open={state.errorSnackbarOpen}
/>
</IntlProvider>
</ThemeProvider>
);
};
Expand Down
13 changes: 9 additions & 4 deletions src/frontend/components/DeleteAllEntriesListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import IconButton from '@mui/material/IconButton';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import DeleteIcon from '@mui/icons-material/Delete';
import React, { FunctionComponent, useState } from 'react';
import { FormattedMessage } from 'react-intl';

import { DeleteAllConfirmationDialog } from './dialog';

Expand Down Expand Up @@ -32,14 +33,18 @@ export const DeleteAllEntriesListItem: FunctionComponent<DeleteAllEntriesListIte

return (
<>
<ListItem button onClick={handleClick} disabled={disabled}>
<ListItemButton onClick={handleClick} disabled={disabled}>
<ListItemIcon>
<IconButton onClick={handleClick} disabled={disabled}>
<DeleteIcon />
</IconButton>
</ListItemIcon>
<ListItemText primary="Delete all" />
</ListItem>
<ListItemText
primary={
<FormattedMessage id="deleteAll" defaultMessage="Delete all" />
}
/>
</ListItemButton>
<DeleteAllConfirmationDialog
onAnswer={handleConfirmationAnswer}
open={confirmationDialogOpen}
Expand Down
6 changes: 3 additions & 3 deletions src/frontend/components/EntryListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import IconButton from '@mui/material/IconButton';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import ListItemText from '@mui/material/ListItemText';
Expand Down Expand Up @@ -52,7 +52,7 @@ export const EntryListItem: FunctionComponent<EntryListItemProps> = ({
};

return (
<ListItem button onDoubleClick={onSelect}>
<ListItemButton onDoubleClick={onSelect}>
<ListItemIcon>
<IconButton
onClick={handleButtonClick(onToggle, 'toggle')}
Expand Down Expand Up @@ -85,7 +85,7 @@ export const EntryListItem: FunctionComponent<EntryListItemProps> = ({
<DeleteIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
</ListItemButton>
);
};

Expand Down
4 changes: 2 additions & 2 deletions src/frontend/components/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Typography from '@mui/material/Typography';
import AddIcon from '@mui/icons-material/Add';
import CheckBoxOutlineBlankOutlinedIcon from '@mui/icons-material/CheckBoxOutlineBlankOutlined';
import CheckBoxOutlinedIcon from '@mui/icons-material/CheckBoxOutlined';
import React, { ChangeEvent, FunctionComponent } from 'react';
import React, { FunctionComponent, SyntheticEvent } from 'react';

import { EntryType } from '../types';

Expand All @@ -24,7 +24,7 @@ export const Toolbar: FunctionComponent<ToolbarProps> = ({
onTabChange,
selectedTab,
}) => {
const handleTabChange = (ev: ChangeEvent<unknown>, selectedTab: EntryType) =>
const handleTabChange = (ev: SyntheticEvent, selectedTab: EntryType) =>
onTabChange(selectedTab);

return (
Expand Down
5 changes: 4 additions & 1 deletion src/frontend/components/dialog/AddEntryDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { FunctionComponent } from 'react';
import { FormattedMessage } from 'react-intl';

import { createEntry } from '../../api';

Expand All @@ -21,7 +22,9 @@ export const AddEntryDialog: FunctionComponent<AddEntryDialogProps> = ({
onClose={onClose}
onSubmit={handleSubmit}
open={open}
title="Add new entry"
title={
<FormattedMessage id="addNewEntry" defaultMessage="Add new entry" />
}
/>
);
};
Expand Down
10 changes: 7 additions & 3 deletions src/frontend/components/dialog/DeleteAllConfirmationDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import React, { FunctionComponent } from 'react';
import { FormattedMessage } from 'react-intl';

export type DeleteAllConfirmationDialogProps = {
onAnswer: (answer: boolean) => void;
Expand All @@ -18,14 +19,17 @@ export const DeleteAllConfirmationDialog: FunctionComponent<DeleteAllConfirmatio
return (
<Dialog open={open} fullWidth>
<DialogContent>
Do you really want to delete all entries marked as done?
<FormattedMessage
id="deleteAllConfirmation"
defaultMessage="Do you really want to delete all entries marked as done?"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleOnAnswer(true)} color="primary">
Yes
<FormattedMessage id="yes" defaultMessage="Yes" />
</Button>
<Button onClick={handleOnAnswer(false)} color="primary">
No
<FormattedMessage id="no" defaultMessage="No" />
</Button>
</DialogActions>
</Dialog>
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/components/dialog/EditEntryDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { patchEntry } from '../../api';
import { SavedEntry } from '../../types';

import { EntryDialogBase, EntryDialogValues } from './EntryDialogBase';
import { FormattedMessage } from 'react-intl';

export type EditEntryDialogProps = {
entry?: SavedEntry;
Expand Down Expand Up @@ -31,7 +32,7 @@ export const EditEntryDialog: FunctionComponent<EditEntryDialogProps> = ({
onClose={onClose}
onSubmit={handleSubmit}
open={open}
title="Edit entry"
title={<FormattedMessage id="editEntry" defaultMessage="Edit entry" />}
/>
);
};
Expand Down
17 changes: 11 additions & 6 deletions src/frontend/components/dialog/EntryDialogBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import React, {
ChangeEvent,
FormEvent,
FunctionComponent,
ReactNode,
useEffect,
useState,
} from 'react';
import { FormattedMessage } from 'react-intl';
import { mutate } from 'swr';

export type EntryDialogValues = {
Expand All @@ -24,7 +26,7 @@ export type EntryDialogBaseProps = {
onClose: () => void;
onSubmit: (values: EntryDialogValues) => Promise<void>;
open: boolean;
title: string;
title: ReactNode;
};

export const EntryDialogBase: FunctionComponent<EntryDialogBaseProps> = ({
Expand Down Expand Up @@ -77,12 +79,15 @@ export const EntryDialogBase: FunctionComponent<EntryDialogBaseProps> = ({
<DialogContent>
{error && (
<DialogContentText color="error">
Unable to save changes.
<FormattedMessage
id="unableToSaveChanges"
defaultMessage="Unable to save changes."
/>
</DialogContentText>
)}
<TextField
autoFocus
label="Text"
label={<FormattedMessage id="text" defaultMessage="Text" />}
fullWidth
required
value={text}
Expand All @@ -92,7 +97,7 @@ export const EntryDialogBase: FunctionComponent<EntryDialogBaseProps> = ({
/>
<TextField
type="url"
label="URL"
label={<FormattedMessage id="url" defaultMessage="URL" />}
fullWidth
value={url}
onChange={handleURLChange}
Expand All @@ -101,10 +106,10 @@ export const EntryDialogBase: FunctionComponent<EntryDialogBaseProps> = ({
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
Cancel
<FormattedMessage id="cancel" defaultMessage="Cancel" />
</Button>
<Button type="submit" color="primary">
Add
<FormattedMessage id="add" defaultMessage="Add" />
</Button>
</DialogActions>
</form>
Expand Down
8 changes: 7 additions & 1 deletion src/frontend/components/snackbar/ErrorSnackbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import IconButton from '@mui/material/IconButton';
import Snackbar from '@mui/material/Snackbar';
import CloseIcon from '@mui/icons-material/Close';
import React, { FunctionComponent, SyntheticEvent } from 'react';
import { FormattedMessage } from 'react-intl';

export type ErrorSnackbarProps = {
onClose: () => void;
Expand Down Expand Up @@ -36,7 +37,12 @@ export const ErrorSnackbar: FunctionComponent<ErrorSnackbarProps> = ({
autoHideDuration={6000}
onClose={handleClose}
>
<Alert severity="error">API returned erroneous response.</Alert>
<Alert severity="error">
<FormattedMessage
id="apiError"
defaultMessage="API returned erroneous response."
/>
</Alert>
</Snackbar>
);
};
Expand Down
14 changes: 14 additions & 0 deletions src/frontend/i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"add": "Add",
"addNewEntry": "Add new entry",
"apiError": "API returned erroneous response.",
"cancel": "Cancel",
"deleteAll": "Delete all",
"deleteAllConfirmation": "Do you really want to delete all entries marked as done?",
"editEntry": "Edit entry",
"no": "No",
"text": "Text",
"url": "URL",
"unableToSaveChanges": "Unable to save changes.",
"yes": "Yes"
}
14 changes: 14 additions & 0 deletions src/frontend/i18n/fi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"add": "Lisää",
"addNewEntry": "Lisää uusi kohta",
"apiError": "Palvelin palautti virheen.",
"cancel": "Peruuta",
"deleteAll": "Poista kaikki",
"deleteAllConfirmation": "Haluatko varmasti poistaa kaikki valmiiksi merkityt kohteet?",
"editEntry": "Muokkaa",
"no": "Ei",
"text": "Teksti",
"url": "Verkko-osoite",
"unableToSaveChanges": "Tallentaminen epäonnistui.",
"yes": "Kyllä"
}
18 changes: 18 additions & 0 deletions src/frontend/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import enTranslations from './en.json';
import fiTranslations from './fi.json';

const SUPPORTED_LANGUAGES = new Set<string>(['en', 'fi']);
const DEFAULT_LANGUAGE = 'en';

export const translations: Record<string, Record<string, string>> = {
en: enTranslations,
fi: fiTranslations,
};

export const getBrowserLanguage = (): string => {
const browserLanguageCode = navigator.language.split(/[-_]/)[0];

return SUPPORTED_LANGUAGES.has(browserLanguageCode)
? browserLanguageCode
: DEFAULT_LANGUAGE;
};
Loading

0 comments on commit f502a12

Please sign in to comment.