diff --git a/ui/package-lock.json b/ui/package-lock.json index 4ff349c94..058194c0d 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -13127,6 +13127,11 @@ "resolved": "https://registry.npmjs.org/ra-language-english/-/ra-language-english-3.2.0.tgz", "integrity": "sha512-/XmwYWoQoB4MBkkzBCbg/ykCuRGjHQOHLk2ik6n1aM10AWHxiiJNyRw2aoLzH7Vc5rcp4BBJQCuhT+DgfYIJ2Q==" }, + "ra-language-portuguese": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ra-language-portuguese/-/ra-language-portuguese-1.6.0.tgz", + "integrity": "sha512-9PAxgrisjmDOTRefjCe2y2ruYQw/iqXnXgUt09vOYUcjY4J0ctabJ4+joGI0jV/x9icF9c7Pui2USc5QDRTktQ==" + }, "ra-ui-materialui": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/ra-ui-materialui/-/ra-ui-materialui-3.3.3.tgz", diff --git a/ui/package.json b/ui/package.json index 4a68f7c14..44e28d62f 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,6 +12,7 @@ "md5-hex": "^3.0.1", "prop-types": "^15.7.2", "ra-data-json-server": "^3.3.3", + "ra-language-portuguese": "^1.6.0", "react": "^16.13.1", "react-admin": "^3.3.3", "react-dom": "^16.13.1", diff --git a/ui/src/album/AlbumList.js b/ui/src/album/AlbumList.js index 6a6947a0d..6649252fc 100644 --- a/ui/src/album/AlbumList.js +++ b/ui/src/album/AlbumList.js @@ -8,7 +8,8 @@ import { NumberInput, ReferenceInput, SearchInput, - Pagination + Pagination, + useTranslate } from 'react-admin' import { Title } from '../common' import { withWidth } from '@material-ui/core' @@ -17,21 +18,25 @@ import AlbumListView from './AlbumListView' import AlbumGridView from './AlbumGridView' import { ALBUM_MODE_LIST } from './albumState' -const AlbumFilter = (props) => ( - - - ({ name: [searchText] })} - > - - - - - -) +const AlbumFilter = (props) => { + const translate = useTranslate() + return ( + + + ({ name: [searchText] })} + > + + + + + + ) +} const getPerPage = (width) => { if (width === 'xs') return 12 diff --git a/ui/src/audioplayer/Player.js b/ui/src/audioplayer/Player.js index 5751137c6..a41e939a3 100644 --- a/ui/src/audioplayer/Player.js +++ b/ui/src/audioplayer/Player.js @@ -27,12 +27,30 @@ const Player = () => { glassBg: false, showThemeSwitch: false, showMediaSession: true, - panelTitle: translate('player.panelTitle'), defaultPosition: { top: 300, left: 120 }, locale: { + playListsText: translate('player.playListsText'), + openText: translate('player.openText'), + closeText: translate('player.closeText'), + notContentText: translate('player.notContentText'), + clickToPlayText: translate('player.clickToPlayText'), + clickToPauseText: translate('player.clickToPauseText'), + nextTrackText: translate('player.nextTrackText'), + previousTrackText: translate('player.previousTrackText'), + reloadText: translate('player.reloadText'), + volumeText: translate('player.volumeText'), + toggleLyricText: translate('player.toggleLyricText'), + toggleMiniModeText: translate('player.toggleMiniModeText'), + destroyText: translate('player.destroyText'), + downloadText: translate('player.downloadText'), + removeAudioListsText: translate('player.removeAudioListsText'), + controllerTitle: translate('player.controllerTitle'), + clickToDeleteText: (name) => + translate('player.clickToDeleteText', { name }), + emptyLyricText: translate('player.emptyLyricText'), playModeText: { order: translate('player.playModeText.order'), orderLoop: translate('player.playModeText.orderLoop'), diff --git a/ui/src/i18n/en.js b/ui/src/i18n/en.js index 69e8f346e..851c5fa5d 100644 --- a/ui/src/i18n/en.js +++ b/ui/src/i18n/en.js @@ -2,6 +2,7 @@ import deepmerge from 'deepmerge' import englishMessages from 'ra-language-english' export default deepmerge(englishMessages, { + languageName: 'English', resources: { song: { name: 'Song |||| Songs', @@ -45,12 +46,34 @@ export default deepmerge(englishMessages, { menu: { library: 'Library', settings: 'Settings', - personal: 'Personal', version: 'Version %{version}', - theme: 'Theme' + theme: 'Theme', + personal: { + name: 'Personal', + options: { + theme: 'Theme' + } + } }, player: { - panelTitle: 'Play Queue', + playListsText: 'Play Queue', + openText: 'Open', + closeText: 'Close', + notContentText: 'No music', + clickToPlayText: 'Click to play', + clickToPauseText: 'Click to pause', + nextTrackText: 'Next track', + previousTrackText: 'Previous track', + reloadText: 'Reload', + volumeText: 'Volume', + toggleLyricText: 'Toggle lyric', + toggleMiniModeText: 'Minimize', + destroyText: 'Destroy', + downloadText: 'Download', + removeAudioListsText: 'Delete audio lists', + controllerTitle: '', + clickToDeleteText: `Click to delete %{name}`, + emptyLyricText: 'No lyric', playModeText: { order: 'In order', orderLoop: 'Repeat', diff --git a/ui/src/i18n/index.js b/ui/src/i18n/index.js index 032424835..a63e142da 100644 --- a/ui/src/i18n/index.js +++ b/ui/src/i18n/index.js @@ -1,3 +1,13 @@ import en from './en' +import pt from './pt' -export default { en } +// When adding a new translation, import it above and add it to the list bellow + +const allLanguages = { en, pt } + +// "Hack" to make "albumSongs" resource use the same translations as "song" +Object.keys(allLanguages).forEach( + (k) => (allLanguages[k].resources.albumSong = allLanguages[k].resources.song) +) + +export default allLanguages diff --git a/ui/src/i18n/pt.js b/ui/src/i18n/pt.js new file mode 100644 index 000000000..2ea9c1b24 --- /dev/null +++ b/ui/src/i18n/pt.js @@ -0,0 +1,112 @@ +import deepmerge from 'deepmerge' +import en from './en' +import portugueseMessages from 'ra-language-portuguese' + +export default deepmerge.all([ + en, + portugueseMessages, + { + languageName: 'Português', + resources: { + song: { + name: 'Música |||| Músicas', + fields: { + title: 'Título', + artist: 'Artista', + album: 'Álbum', + path: 'Caminho', + genre: 'Gênero', + compilation: 'Coletânea', + duration: 'Duração', + year: 'Ano', + trackNumber: '#' + }, + bulk: { + addToQueue: 'Play Later' + } + }, + album: { + name: 'Álbum |||| Álbuns', + fields: { + name: 'Nome', + artist: 'Artista', + songCount: 'Songs', + genre: 'Gênero', + playCount: 'Plays', + compilation: 'Coletânea', + duration: 'Duração', + year: 'Ano' + }, + actions: { + playAll: 'Play', + playNext: 'Play Next', + addToQueue: 'Play Later', + shuffle: 'Shuffle' + } + }, + artist: { + name: 'Artista |||| Artistas', + fields: { + name: 'Nome' + } + }, + user: { + name: 'Usuário |||| Usuários', + fields: { + name: 'Nome' + } + }, + transcoding: { + name: 'Conversão |||| Conversões', + fields: { + name: 'Nome' + } + }, + player: { + name: 'Tocador |||| Tocadores', + fields: { + name: 'Nome' + } + } + }, + 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: 'Biblioteca', + settings: 'Configurações', + version: 'Versão %{version}', + personal: { + name: 'Pessoal', + options: { + theme: 'Tema' + } + } + }, + player: { + playListsText: 'Fila de Execução', + openText: 'Abrir', + closeText: 'Fechar', + clickToPlayText: 'Clique para tocar', + clickToPauseText: 'Clique para pausar', + nextTrackText: 'Próxima faixa', + previousTrackText: 'Faixa anterior', + clickToDeleteText: `Clique para remover %{name}`, + playModeText: { + order: 'Em ordem', + orderLoop: 'Repetir tudo', + singleLoop: 'Repetir', + shufflePlay: 'Aleatório' + } + } + } +]) diff --git a/ui/src/layout/PersonalMenu.js b/ui/src/layout/PersonalMenu.js index 9f8bfcf93..8f0e46d7d 100644 --- a/ui/src/layout/PersonalMenu.js +++ b/ui/src/layout/PersonalMenu.js @@ -16,7 +16,7 @@ const PersonalMenu = forwardRef(({ onClick, sidebarIsOpen, dense }, ref) => { } onClick={onClick} className={classes.menuItem} diff --git a/ui/src/personal/Personal.js b/ui/src/personal/Personal.js index dc528fe0e..1b10d5ff9 100644 --- a/ui/src/personal/Personal.js +++ b/ui/src/personal/Personal.js @@ -21,10 +21,11 @@ const Personal = () => { return ( - + <Title title={'Navidrome - ' + translate('menu.personal.name')} /> <SimpleForm toolbar={null}> <SelectInput source="theme" + label={translate('menu.personal.options.theme')} defaultValue={currentTheme} choices={themeChoices} onChange={(event) => { diff --git a/ui/src/song/SongList.js b/ui/src/song/SongList.js index 81908a14b..4e3ea35f7 100644 --- a/ui/src/song/SongList.js +++ b/ui/src/song/SongList.js @@ -6,7 +6,8 @@ import { List, NumberField, SearchInput, - TextField + TextField, + useTranslate } from 'react-admin' import { useMediaQuery } from '@material-ui/core' import { @@ -30,6 +31,7 @@ const SongFilter = (props) => ( ) const SongList = (props) => { + const translate = useTranslate() const dispatch = useDispatch() const isXsmall = useMediaQuery((theme) => theme.breakpoints.down('xs')) const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md')) @@ -63,7 +65,13 @@ const SongList = (props) => { rowClick={(id, basePath, record) => dispatch(setTrack(record))} > <TextField source="title" /> - {isDesktop && <AlbumLinkField source="albumId" sortBy="album" />} + {isDesktop && ( + <AlbumLinkField + source="albumId" + label={translate('resources.song.fields.album')} + sortBy="album" + /> + )} <TextField source="artist" /> {isDesktop && <NumberField source="trackNumber" />} {isDesktop && <NumberField source="playCount" />}