feat: initial support for i18n
This commit is contained in:
Generated
+5
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import en from './en'
|
||||||
|
|
||||||
|
export default { en }
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
+18
-10
@@ -1,11 +1,19 @@
|
|||||||
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 = {
|
const Player = () => {
|
||||||
|
const translate = useTranslate()
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
bounds: 'body',
|
bounds: 'body',
|
||||||
mode: 'full',
|
mode: 'full',
|
||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
@@ -17,27 +25,27 @@ const defaultOptions = {
|
|||||||
glassBg: false,
|
glassBg: false,
|
||||||
showThemeSwitch: false,
|
showThemeSwitch: false,
|
||||||
playModeText: {
|
playModeText: {
|
||||||
order: 'order',
|
order: translate('player.playModeText.order'),
|
||||||
orderLoop: 'orderLoop',
|
orderLoop: translate('player.playModeText.orderLoop'),
|
||||||
singleLoop: 'singleLoop',
|
singleLoop: translate('player.playModeText.singleLoop'),
|
||||||
shufflePlay: 'shufflePlay'
|
shufflePlay: translate('player.playModeText.shufflePlay')
|
||||||
},
|
},
|
||||||
|
panelTitle: translate('player.panelTitle'),
|
||||||
defaultPosition: {
|
defaultPosition: {
|
||||||
top: 300,
|
top: 300,
|
||||||
left: 120
|
left: 120
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addQueueToOptions = (queue) => {
|
const addQueueToOptions = (queue) => {
|
||||||
return {
|
return {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
autoPlay: true,
|
autoPlay: true,
|
||||||
clearPriorAudioLists: queue.clear,
|
clearPriorAudioLists: queue.clear,
|
||||||
audioLists: queue.queue.map((item) => item)
|
audioLists: queue.queue.map((item) => item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Player = () => {
|
|
||||||
const dataProvider = useDataProvider()
|
const dataProvider = useDataProvider()
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const queue = useSelector((state) => state.queue)
|
const queue = useSelector((state) => state.queue)
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user