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>