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 = () => (
<>
@@ -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 (
-
+
@@ -58,7 +58,7 @@ const AlbumList = (props) => (
-
+
)
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 }) => {
- Thanks for installing Navidrome!
+ {translate('ra.auth.welcome1')}
- To start, create an admin user
+ {translate('ra.auth.welcome2')}
@@ -160,7 +160,7 @@ const FormSignUp = ({ loading, handleSubmit, validate }) => {
autoFocus
name="username"
component={renderInput}
- label={'Admin Username'}
+ label={translate('ra.auth.username')}
disabled={loading}
/>
@@ -177,7 +177,7 @@ const FormSignUp = ({ loading, handleSubmit, validate }) => {
@@ -193,7 +193,7 @@ const FormSignUp = ({ loading, handleSubmit, validate }) => {
fullWidth
>
{loading &&
}
- {translate('Create Admin')}
+ {translate('ra.auth.buttonCreateAdmin')}
@@ -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={
}
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 }) => {