From d37351610a13f3f02ab7d88eec0bbdaf608453c3 Mon Sep 17 00:00:00 2001 From: Deluan <deluan@deluan.com> Date: Fri, 7 Feb 2020 09:40:52 -0500 Subject: [PATCH] feat: initial support for i18n --- ui/package-lock.json | 5 +++ ui/package.json | 1 + ui/src/App.js | 10 ++++- ui/src/album/AlbumList.js | 4 +- ui/src/i18n/en.js | 47 +++++++++++++++++++++ ui/src/i18n/index.js | 3 ++ ui/src/layout/Login.js | 14 +++---- ui/src/layout/Menu.js | 2 +- ui/src/player/Player.js | 74 ++++++++++++++++++--------------- ui/src/song/AddToQueueButton.js | 13 +++++- ui/src/song/SongList.js | 10 ++--- 11 files changed, 131 insertions(+), 52 deletions(-) create mode 100644 ui/src/i18n/en.js create mode 100644 ui/src/i18n/index.js diff --git a/ui/package-lock.json b/ui/package-lock.json index 2415763b7..f33635630 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -4720,6 +4720,11 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + }, "default-gateway": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", diff --git a/ui/package.json b/ui/package.json index 9ab53e1c5..c21ee2ff6 100644 --- a/ui/package.json +++ b/ui/package.json @@ -6,6 +6,7 @@ "@testing-library/jest-dom": "^5.0.2", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^8.0.4", + "deepmerge": "^4.2.2", "jwt-decode": "^2.2.0", "md5-hex": "^3.0.1", "prop-types": "^15.7.2", diff --git a/ui/src/App.js b/ui/src/App.js index 65dad963c..ad1ebd244 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -1,7 +1,9 @@ import React from 'react' -import { Admin, Resource } from 'react-admin' +import { Admin, Resource, resolveBrowserLocale } from 'react-admin' import dataProvider from './dataProvider' import authProvider from './authProvider' +import polyglotI18nProvider from 'ra-i18n-polyglot' +import messages from './i18n' import { DarkTheme, Layout, Login } from './layout' import user from './user' import song from './song' @@ -12,6 +14,11 @@ import { Player, playQueueReducer } from './player' const theme = createMuiTheme(DarkTheme) +const i18nProvider = polyglotI18nProvider( + (locale) => (messages[locale] ? messages[locale] : messages.en), + resolveBrowserLocale() +) + const App = () => ( <> <div> @@ -20,6 +27,7 @@ const App = () => ( customReducers={{ queue: playQueueReducer }} dataProvider={dataProvider} authProvider={authProvider} + i18nProvider={i18nProvider} layout={Layout} loginPage={Login} > diff --git a/ui/src/album/AlbumList.js b/ui/src/album/AlbumList.js index ce1047639..53d70d71e 100644 --- a/ui/src/album/AlbumList.js +++ b/ui/src/album/AlbumList.js @@ -25,7 +25,7 @@ const AlbumDetails = (props) => { return ( <Show {...props} title=" "> <SimpleShowLayout> - <TextField label="Album Artist" source="albumArtist" /> + <TextField source="albumArtist" /> <TextField source="genre" /> <BooleanField source="compilation" /> <DateField source="updatedAt" showTime /> @@ -58,7 +58,7 @@ const AlbumList = (props) => ( <TextField source="artist" /> <NumberField source="songCount" /> <TextField source="year" /> - <DurationField label="Time" source="duration" /> + <DurationField source="duration" /> </Datagrid> </List> ) diff --git a/ui/src/i18n/en.js b/ui/src/i18n/en.js new file mode 100644 index 000000000..e67b12e34 --- /dev/null +++ b/ui/src/i18n/en.js @@ -0,0 +1,47 @@ +import deepmerge from 'deepmerge' +import englishMessages from 'ra-language-english' + +export default deepmerge(englishMessages, { + resources: { + song: { + fields: { + albumArtist: 'Album Artist', + duration: 'Time', + trackNumber: 'Track #' + }, + bulk: { + addToQueue: 'Play Later' + } + }, + album: { + fields: { + albumArtist: 'Album Artist', + duration: 'Time' + } + } + }, + ra: { + auth: { + welcome1: 'Thanks for installing Navidrome!', + welcome2: 'To start, create an admin user', + confirmPassword: 'Confirm Password', + buttonCreateAdmin: 'Create Admin' + }, + validation: { + invalidChars: 'Please only use letter and numbers', + passwordDoesNotMatch: 'Password does not match' + } + }, + menu: { + library: 'Library' + }, + player: { + panelTitle: 'Play Queue', + playModeText: { + order: 'In order', + orderLoop: 'Repeat', + singleLoop: 'Repeat One', + shufflePlay: 'Shuffle' + } + } +}) diff --git a/ui/src/i18n/index.js b/ui/src/i18n/index.js new file mode 100644 index 000000000..032424835 --- /dev/null +++ b/ui/src/i18n/index.js @@ -0,0 +1,3 @@ +import en from './en' + +export default { en } diff --git a/ui/src/layout/Login.js b/ui/src/layout/Login.js index 67fe5a8bb..873e5a688 100644 --- a/ui/src/layout/Login.js +++ b/ui/src/layout/Login.js @@ -149,10 +149,10 @@ const FormSignUp = ({ loading, handleSubmit, validate }) => { </Avatar> </div> <div className={classes.systemName}> - Thanks for installing Navidrome! + {translate('ra.auth.welcome1')} </div> <div className={classes.systemName}> - To start, create an admin user + {translate('ra.auth.welcome2')} </div> <div className={classes.form}> <div className={classes.input}> @@ -160,7 +160,7 @@ const FormSignUp = ({ loading, handleSubmit, validate }) => { autoFocus name="username" component={renderInput} - label={'Admin Username'} + label={translate('ra.auth.username')} disabled={loading} /> </div> @@ -177,7 +177,7 @@ const FormSignUp = ({ loading, handleSubmit, validate }) => { <Field name="confirmPassword" component={renderInput} - label={'Confirm Password'} + label={translate('ra.auth.confirmPassword')} type="password" disabled={loading} /> @@ -193,7 +193,7 @@ const FormSignUp = ({ loading, handleSubmit, validate }) => { fullWidth > {loading && <CircularProgress size={25} thickness={2} />} - {translate('Create Admin')} + {translate('ra.auth.buttonCreateAdmin')} </Button> </CardActions> </Card> @@ -242,13 +242,13 @@ const Login = ({ location }) => { const errors = validateLogin(values) const regex = /^\w+$/g if (values.username && !values.username.match(regex)) { - errors.username = translate('Please only use letter and numbers') + errors.username = translate('ra.validation.invalidChars') } if (!values.confirmPassword) { errors.confirmPassword = translate('ra.validation.required') } if (values.confirmPassword !== values.password) { - errors.confirmPassword = 'Password does not match' + errors.confirmPassword = translate('ra.validation.passwordDoesNotMatch') } return errors } diff --git a/ui/src/layout/Menu.js b/ui/src/layout/Menu.js index 85e459544..ac8201b36 100644 --- a/ui/src/layout/Menu.js +++ b/ui/src/layout/Menu.js @@ -57,7 +57,7 @@ const Menu = ({ onMenuClick, dense, logout }) => { handleToggle={() => handleToggle('menuLibrary')} isOpen={state.menuLibrary} sidebarIsOpen={open} - name="Library" + name="menu.library" icon={<LibraryMusicIcon />} dense={dense} > diff --git a/ui/src/player/Player.js b/ui/src/player/Player.js index 223eb4baf..e6f7eee32 100644 --- a/ui/src/player/Player.js +++ b/ui/src/player/Player.js @@ -1,43 +1,51 @@ import React from 'react' import { useDispatch, useSelector } from 'react-redux' -import { fetchUtils, useAuthState, useDataProvider } from 'react-admin' +import { + fetchUtils, + useAuthState, + useDataProvider, + useTranslate +} from 'react-admin' import ReactJkMusicPlayer from 'react-jinke-music-player' import 'react-jinke-music-player/assets/index.css' import { scrobble, syncQueue } from './queue' -const defaultOptions = { - bounds: 'body', - mode: 'full', - autoPlay: true, - preload: true, - autoPlayInitLoadPlayList: true, - clearPriorAudioLists: false, - showDownload: false, - showReload: false, - glassBg: false, - showThemeSwitch: false, - playModeText: { - order: 'order', - orderLoop: 'orderLoop', - singleLoop: 'singleLoop', - shufflePlay: 'shufflePlay' - }, - defaultPosition: { - top: 300, - left: 120 - } -} - -const addQueueToOptions = (queue) => { - return { - ...defaultOptions, - autoPlay: true, - clearPriorAudioLists: queue.clear, - audioLists: queue.queue.map((item) => item) - } -} - const Player = () => { + const translate = useTranslate() + + const defaultOptions = { + bounds: 'body', + mode: 'full', + autoPlay: true, + preload: true, + autoPlayInitLoadPlayList: true, + clearPriorAudioLists: false, + showDownload: false, + showReload: false, + glassBg: false, + showThemeSwitch: false, + playModeText: { + order: translate('player.playModeText.order'), + orderLoop: translate('player.playModeText.orderLoop'), + singleLoop: translate('player.playModeText.singleLoop'), + shufflePlay: translate('player.playModeText.shufflePlay') + }, + panelTitle: translate('player.panelTitle'), + defaultPosition: { + top: 300, + left: 120 + } + } + + const addQueueToOptions = (queue) => { + return { + ...defaultOptions, + autoPlay: true, + clearPriorAudioLists: queue.clear, + audioLists: queue.queue.map((item) => item) + } + } + const dataProvider = useDataProvider() const dispatch = useDispatch() const queue = useSelector((state) => state.queue) diff --git a/ui/src/song/AddToQueueButton.js b/ui/src/song/AddToQueueButton.js index e13d7e907..f4aaccbd5 100644 --- a/ui/src/song/AddToQueueButton.js +++ b/ui/src/song/AddToQueueButton.js @@ -1,5 +1,10 @@ import React from 'react' -import { Button, useDataProvider, useUnselectAll } from 'react-admin' +import { + Button, + useDataProvider, + useUnselectAll, + useTranslate +} from 'react-admin' import { useDispatch } from 'react-redux' import { addTrack } from '../player' import AddToQueueIcon from '@material-ui/icons/AddToQueue' @@ -8,6 +13,7 @@ import Tooltip from '@material-ui/core/Tooltip' const AddToQueueButton = ({ selectedIds }) => { const dispatch = useDispatch() + const translate = useTranslate() const dataProvider = useDataProvider() const unselectAll = useUnselectAll() const addToQueue = () => { @@ -23,7 +29,10 @@ const AddToQueueButton = ({ selectedIds }) => { <Button color="secondary" label={ - <Tooltip title={'Play Later'} placement="right"> + <Tooltip + title={translate('resources.song.bulk.addToQueue')} + placement="right" + > <AddToQueueIcon /> </Tooltip> } diff --git a/ui/src/song/SongList.js b/ui/src/song/SongList.js index 7295bbe2e..e31953439 100644 --- a/ui/src/song/SongList.js +++ b/ui/src/song/SongList.js @@ -40,7 +40,7 @@ const SongDetails = (props) => { <Show {...props} title=" "> <SimpleShowLayout> <TextField source="path" /> - <TextField label="Album Artist" source="albumArtist" /> + <TextField source="albumArtist" /> <TextField source="genre" /> <BooleanField source="compilation" /> <BitrateField source="bitRate" /> @@ -80,9 +80,7 @@ const SongList = (props) => { )} secondaryText={(record) => record.artist} tertiaryText={(record) => ( - <> - <DurationField record={record} source={'duration'} /> - </> + <DurationField record={record} source={'duration'} /> )} linkType={false} /> @@ -94,9 +92,9 @@ const SongList = (props) => { <TextField source="title" /> {isDesktop && <TextField source="album" />} <TextField source="artist" /> - {isDesktop && <NumberField label="Track #" source="trackNumber" />} + {isDesktop && <NumberField source="trackNumber" />} {isDesktop && <TextField source="year" />} - <DurationField label="Time" source="duration" /> + <DurationField source="duration" /> </Datagrid> )} </List>