feat: initial support for i18n

This commit is contained in:
Deluan
2020-02-07 09:40:52 -05:00
parent 99361c0d9f
commit d37351610a
11 changed files with 131 additions and 52 deletions
+5
View File
@@ -4720,6 +4720,11 @@
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" "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": { "default-gateway": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz",
+1
View File
@@ -6,6 +6,7 @@
"@testing-library/jest-dom": "^5.0.2", "@testing-library/jest-dom": "^5.0.2",
"@testing-library/react": "^9.3.2", "@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^8.0.4", "@testing-library/user-event": "^8.0.4",
"deepmerge": "^4.2.2",
"jwt-decode": "^2.2.0", "jwt-decode": "^2.2.0",
"md5-hex": "^3.0.1", "md5-hex": "^3.0.1",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
+9 -1
View File
@@ -1,7 +1,9 @@
import React from 'react' import React from 'react'
import { Admin, Resource } from 'react-admin' import { Admin, Resource, resolveBrowserLocale } from 'react-admin'
import dataProvider from './dataProvider' import dataProvider from './dataProvider'
import authProvider from './authProvider' import authProvider from './authProvider'
import polyglotI18nProvider from 'ra-i18n-polyglot'
import messages from './i18n'
import { DarkTheme, Layout, Login } from './layout' import { DarkTheme, Layout, Login } from './layout'
import user from './user' import user from './user'
import song from './song' import song from './song'
@@ -12,6 +14,11 @@ import { Player, playQueueReducer } from './player'
const theme = createMuiTheme(DarkTheme) const theme = createMuiTheme(DarkTheme)
const i18nProvider = polyglotI18nProvider(
(locale) => (messages[locale] ? messages[locale] : messages.en),
resolveBrowserLocale()
)
const App = () => ( const App = () => (
<> <>
<div> <div>
@@ -20,6 +27,7 @@ const App = () => (
customReducers={{ queue: playQueueReducer }} customReducers={{ queue: playQueueReducer }}
dataProvider={dataProvider} dataProvider={dataProvider}
authProvider={authProvider} authProvider={authProvider}
i18nProvider={i18nProvider}
layout={Layout} layout={Layout}
loginPage={Login} loginPage={Login}
> >
+2 -2
View File
@@ -25,7 +25,7 @@ const AlbumDetails = (props) => {
return ( return (
<Show {...props} title=" "> <Show {...props} title=" ">
<SimpleShowLayout> <SimpleShowLayout>
<TextField label="Album Artist" source="albumArtist" /> <TextField source="albumArtist" />
<TextField source="genre" /> <TextField source="genre" />
<BooleanField source="compilation" /> <BooleanField source="compilation" />
<DateField source="updatedAt" showTime /> <DateField source="updatedAt" showTime />
@@ -58,7 +58,7 @@ const AlbumList = (props) => (
<TextField source="artist" /> <TextField source="artist" />
<NumberField source="songCount" /> <NumberField source="songCount" />
<TextField source="year" /> <TextField source="year" />
<DurationField label="Time" source="duration" /> <DurationField source="duration" />
</Datagrid> </Datagrid>
</List> </List>
) )
+47
View File
@@ -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'
}
}
})
+3
View File
@@ -0,0 +1,3 @@
import en from './en'
export default { en }
+7 -7
View File
@@ -149,10 +149,10 @@ const FormSignUp = ({ loading, handleSubmit, validate }) => {
</Avatar> </Avatar>
</div> </div>
<div className={classes.systemName}> <div className={classes.systemName}>
Thanks for installing Navidrome! {translate('ra.auth.welcome1')}
</div> </div>
<div className={classes.systemName}> <div className={classes.systemName}>
To start, create an admin user {translate('ra.auth.welcome2')}
</div> </div>
<div className={classes.form}> <div className={classes.form}>
<div className={classes.input}> <div className={classes.input}>
@@ -160,7 +160,7 @@ const FormSignUp = ({ loading, handleSubmit, validate }) => {
autoFocus autoFocus
name="username" name="username"
component={renderInput} component={renderInput}
label={'Admin Username'} label={translate('ra.auth.username')}
disabled={loading} disabled={loading}
/> />
</div> </div>
@@ -177,7 +177,7 @@ const FormSignUp = ({ loading, handleSubmit, validate }) => {
<Field <Field
name="confirmPassword" name="confirmPassword"
component={renderInput} component={renderInput}
label={'Confirm Password'} label={translate('ra.auth.confirmPassword')}
type="password" type="password"
disabled={loading} disabled={loading}
/> />
@@ -193,7 +193,7 @@ const FormSignUp = ({ loading, handleSubmit, validate }) => {
fullWidth fullWidth
> >
{loading && <CircularProgress size={25} thickness={2} />} {loading && <CircularProgress size={25} thickness={2} />}
{translate('Create Admin')} {translate('ra.auth.buttonCreateAdmin')}
</Button> </Button>
</CardActions> </CardActions>
</Card> </Card>
@@ -242,13 +242,13 @@ const Login = ({ location }) => {
const errors = validateLogin(values) const errors = validateLogin(values)
const regex = /^\w+$/g const regex = /^\w+$/g
if (values.username && !values.username.match(regex)) { 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) { if (!values.confirmPassword) {
errors.confirmPassword = translate('ra.validation.required') errors.confirmPassword = translate('ra.validation.required')
} }
if (values.confirmPassword !== values.password) { if (values.confirmPassword !== values.password) {
errors.confirmPassword = 'Password does not match' errors.confirmPassword = translate('ra.validation.passwordDoesNotMatch')
} }
return errors return errors
} }
+1 -1
View File
@@ -57,7 +57,7 @@ const Menu = ({ onMenuClick, dense, logout }) => {
handleToggle={() => handleToggle('menuLibrary')} handleToggle={() => handleToggle('menuLibrary')}
isOpen={state.menuLibrary} isOpen={state.menuLibrary}
sidebarIsOpen={open} sidebarIsOpen={open}
name="Library" name="menu.library"
icon={<LibraryMusicIcon />} icon={<LibraryMusicIcon />}
dense={dense} dense={dense}
> >
+41 -33
View File
@@ -1,43 +1,51 @@
import React from 'react' import React from 'react'
import { useDispatch, useSelector } from 'react-redux' 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 ReactJkMusicPlayer from 'react-jinke-music-player'
import 'react-jinke-music-player/assets/index.css' import 'react-jinke-music-player/assets/index.css'
import { scrobble, syncQueue } from './queue' 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 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 dataProvider = useDataProvider()
const dispatch = useDispatch() const dispatch = useDispatch()
const queue = useSelector((state) => state.queue) const queue = useSelector((state) => state.queue)
+11 -2
View File
@@ -1,5 +1,10 @@
import React from 'react' 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 { useDispatch } from 'react-redux'
import { addTrack } from '../player' import { addTrack } from '../player'
import AddToQueueIcon from '@material-ui/icons/AddToQueue' import AddToQueueIcon from '@material-ui/icons/AddToQueue'
@@ -8,6 +13,7 @@ import Tooltip from '@material-ui/core/Tooltip'
const AddToQueueButton = ({ selectedIds }) => { const AddToQueueButton = ({ selectedIds }) => {
const dispatch = useDispatch() const dispatch = useDispatch()
const translate = useTranslate()
const dataProvider = useDataProvider() const dataProvider = useDataProvider()
const unselectAll = useUnselectAll() const unselectAll = useUnselectAll()
const addToQueue = () => { const addToQueue = () => {
@@ -23,7 +29,10 @@ const AddToQueueButton = ({ selectedIds }) => {
<Button <Button
color="secondary" color="secondary"
label={ label={
<Tooltip title={'Play Later'} placement="right"> <Tooltip
title={translate('resources.song.bulk.addToQueue')}
placement="right"
>
<AddToQueueIcon /> <AddToQueueIcon />
</Tooltip> </Tooltip>
} }
+4 -6
View File
@@ -40,7 +40,7 @@ const SongDetails = (props) => {
<Show {...props} title=" "> <Show {...props} title=" ">
<SimpleShowLayout> <SimpleShowLayout>
<TextField source="path" /> <TextField source="path" />
<TextField label="Album Artist" source="albumArtist" /> <TextField source="albumArtist" />
<TextField source="genre" /> <TextField source="genre" />
<BooleanField source="compilation" /> <BooleanField source="compilation" />
<BitrateField source="bitRate" /> <BitrateField source="bitRate" />
@@ -80,9 +80,7 @@ const SongList = (props) => {
)} )}
secondaryText={(record) => record.artist} secondaryText={(record) => record.artist}
tertiaryText={(record) => ( tertiaryText={(record) => (
<> <DurationField record={record} source={'duration'} />
<DurationField record={record} source={'duration'} />
</>
)} )}
linkType={false} linkType={false}
/> />
@@ -94,9 +92,9 @@ const SongList = (props) => {
<TextField source="title" /> <TextField source="title" />
{isDesktop && <TextField source="album" />} {isDesktop && <TextField source="album" />}
<TextField source="artist" /> <TextField source="artist" />
{isDesktop && <NumberField label="Track #" source="trackNumber" />} {isDesktop && <NumberField source="trackNumber" />}
{isDesktop && <TextField source="year" />} {isDesktop && <TextField source="year" />}
<DurationField label="Time" source="duration" /> <DurationField source="duration" />
</Datagrid> </Datagrid>
)} )}
</List> </List>