diff --git a/README.md b/README.md index f307c8c4..061e8ef7 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ First thing to do is check that your MPV binary path is correct. Navigate to the Feishin supports any music server that implements a [Navidrome](https://www.navidrome.org/) or [Jellyfin](https://jellyfin.org/) API. **Subsonic API is not currently supported**. This will likely be added in [later when the new Subsonic API is decided on](https://support.symfonium.app/t/subsonic-servers-participation/1233). -- [Navidrome](https://github.com/navidrome/navidrome) +- [Navidrome](https://github.com/navidrome/navidrome) version 0.48.0 and newer - [Jellyfin](https://github.com/jellyfin/jellyfin) - [Funkwhale](https://funkwhale.audio/) - TBD - Subsonic-compatible servers - TBD diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a6c7cebe..8e6f5f9c 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -254,6 +254,17 @@ } }, "page": { + "albumArtistDetail": { + "about": "About {{artist}}", + "appearsOn": "appears on", + "recentReleases": "recent releases", + "viewDiscography": "view discography", + "relatedArtists": "related $t(entity.artist_other)", + "topSongs": "top songs", + "topSongsFrom": "Top songs from {{title}}", + "viewAll": "view all", + "viewAllTracks": "view all $t(entity.track_other)" + }, "albumArtistList": { "title": "$t(entity.albumArtist_other)" }, @@ -353,6 +364,8 @@ "tracks": "$t(entity.track_other)" }, "trackList": { + "artistTracks": "Tracks by {{artist}}", + "genreTracks": "\"{{genre}}\" $t(entity.track_other)", "title": "$t(entity.track_other)" } }, diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index 8193dcd1..96b0fa61 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -421,7 +421,8 @@ export const albumListSortMap: AlbumListSortMap = { rating: NDAlbumListSort.RATING, recentlyAdded: NDAlbumListSort.RECENTLY_ADDED, recentlyPlayed: NDAlbumListSort.PLAY_DATE, - releaseDate: undefined, + // Recent versions of Navidrome support release date, but fallback to year for now + releaseDate: NDAlbumListSort.YEAR, songCount: NDAlbumListSort.SONG_COUNT, year: NDAlbumListSort.YEAR, }, diff --git a/src/renderer/features/artists/components/album-artist-detail-content.tsx b/src/renderer/features/artists/components/album-artist-detail-content.tsx index 1c9c8341..3f8beec6 100644 --- a/src/renderer/features/artists/components/album-artist-detail-content.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-content.tsx @@ -213,7 +213,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten order={2} weight={700} > - Recent releases + {t('page.albumArtistDetail.recentReleases', { + postProcess: 'sentenceCase', + })} ), @@ -238,7 +240,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten order={2} weight={700} > - Appears on + {t('page.albumArtistDetail.appearsOn', { postProcess: 'sentenceCase' })} ), uniqueId: 'compilationAlbums', @@ -252,7 +254,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten order={2} weight={700} > - Related artists + {t('page.albumArtistDetail.relatedArtists', { + postProcess: 'sentenceCase', + })} ), uniqueId: 'similarArtists', @@ -267,6 +271,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten recentAlbumsQuery?.data?.items, recentAlbumsQuery.isFetching, recentAlbumsQuery?.isLoading, + t, ]); const playButtonBehavior = usePlayButtonBehavior(); @@ -381,7 +386,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten to={artistDiscographyLink} variant="subtle" > - View discography + {t('page.albumArtistDetail.viewDiscography')} {showGenres ? ( @@ -463,7 +468,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten order={2} weight={700} > - About {detailQuery?.data?.name} + {t('page.albumArtistDetail.about', { + artist: detailQuery?.data?.name, + })} - Top Songs + {t('page.albumArtistDetail.topSongs', { + postProcess: 'sentenceCase', + })} diff --git a/src/renderer/features/artists/components/album-artist-detail-top-songs-list-header.tsx b/src/renderer/features/artists/components/album-artist-detail-top-songs-list-header.tsx index 1cbdcaf2..bec3e7dc 100644 --- a/src/renderer/features/artists/components/album-artist-detail-top-songs-list-header.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-top-songs-list-header.tsx @@ -33,7 +33,9 @@ export const AlbumArtistDetailTopSongsListHeader = ({ handlePlay(playButtonBehavior)} /> - Top songs from {title} + + {t('page.albumArtistDetail.topSongsFrom', { title })} + } onClick={() => handlePlay(Play.NOW)} > - {t('player.add', { postProcess: 'sentenceCase' })} + {t('player.play', { postProcess: 'sentenceCase' })} } diff --git a/src/renderer/features/search/components/search-header.tsx b/src/renderer/features/search/components/search-header.tsx index d86d775f..0e31a6da 100644 --- a/src/renderer/features/search/components/search-header.tsx +++ b/src/renderer/features/search/components/search-header.tsx @@ -1,7 +1,8 @@ +import { ChangeEvent, MutableRefObject } from 'react'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { Flex, Group, Stack } from '@mantine/core'; import debounce from 'lodash/debounce'; -import { ChangeEvent, MutableRefObject } from 'react'; +import { useTranslation } from 'react-i18next'; import { generatePath, Link, useParams, useSearchParams } from 'react-router-dom'; import { useCurrentServer } from '../../../store/auth.store'; import { LibraryItem } from '/@/renderer/api/types'; @@ -17,6 +18,7 @@ interface SearchHeaderProps { } export const SearchHeader = ({ tableRef, navigationId }: SearchHeaderProps) => { + const { t } = useTranslation(); const { itemType } = useParams() as { itemType: LibraryItem }; const [searchParams, setSearchParams] = useSearchParams(); const cq = useContainerQuery(); @@ -70,7 +72,7 @@ export const SearchHeader = ({ tableRef, navigationId }: SearchHeaderProps) => { }} variant={itemType === LibraryItem.SONG ? 'filled' : 'subtle'} > - Tracks + {t('entity.track_other', { postProcess: 'sentenceCase' })} diff --git a/src/renderer/features/songs/routes/song-list-route.tsx b/src/renderer/features/songs/routes/song-list-route.tsx index 61a8f070..19a20d68 100644 --- a/src/renderer/features/songs/routes/song-list-route.tsx +++ b/src/renderer/features/songs/routes/song-list-route.tsx @@ -1,6 +1,7 @@ import { useCallback, useMemo, useRef } from 'react'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import isEmpty from 'lodash/isEmpty'; +import { useTranslation } from 'react-i18next'; import { useParams, useSearchParams } from 'react-router-dom'; import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/renderer/api/types'; import { ListContext } from '/@/renderer/context/list-context'; @@ -16,6 +17,7 @@ import { titleCase } from '/@/renderer/utils'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; const TrackListRoute = () => { + const { t } = useTranslation(); const gridRef = useRef(null); const tableRef = useRef(null); const server = useCurrentServer(); @@ -132,6 +134,13 @@ const TrackListRoute = () => { }; }, [albumArtistId, customFilters, genreId, handlePlay, pageKey]); + const artist = searchParams.get('artistName'); + const title = artist + ? t('page.trackList.artistTracks', { artist }) + : genreId + ? t('page.trackList.genreTracks', { genre: titleCase(genreTitle) }) + : undefined; + return ( @@ -139,11 +148,7 @@ const TrackListRoute = () => { gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} - title={ - searchParams.get('artistName') || genreId - ? `"${titleCase(genreTitle)}" Tracks` - : undefined - } + title={title} />