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}
/>