Add localization support (#333)

* Add updated i18n config and en locale
This commit is contained in:
Jeff 2023-10-30 19:22:45 -07:00 committed by GitHub
parent 11863fd4c1
commit 8430b1ec95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
90 changed files with 2679 additions and 908 deletions

View file

@ -1,8 +1,9 @@
import { MutableRefObject, useCallback, useMemo } from 'react';
import { RowDoubleClickedEvent, RowHeightParams, RowNode } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Box, Group, Stack } from '@mantine/core';
import { useSetState } from '@mantine/hooks';
import { MutableRefObject, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { RiHeartFill, RiHeartLine, RiMoreFill, RiSettings2Fill } from 'react-icons/ri';
import { generatePath, useParams } from 'react-router';
import { Link } from 'react-router-dom';
@ -63,6 +64,7 @@ interface AlbumDetailContentProps {
}
export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentProps) => {
const { t } = useTranslation();
const { albumId } = useParams() as { albumId: string };
const server = useCurrentServer();
const detailQuery = useAlbumDetail({ query: { id: albumId }, serverId: server?.id });
@ -206,7 +208,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
handlePreviousPage: () => handlePreviousPage('artist'),
hasPreviousPage: pagination.artist > 0,
},
title: 'More from this artist',
title: t('page.albumDetail.moreFromArtist', { postProcess: 'sentenceCase' }),
uniqueId: 'mostPlayed',
},
{
@ -217,7 +219,10 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
(a) => a.id !== detailQuery?.data?.id,
).length,
loading: relatedAlbumGenresQuery?.isLoading || relatedAlbumGenresQuery.isFetching,
title: `More from ${detailQuery?.data?.genres?.[0]?.name}`,
title: t('page.albumDetail.moreFromGeneric', {
item: detailQuery?.data?.genres?.[0]?.name,
postProcess: 'sentenceCase',
}),
uniqueId: 'relatedGenres',
},
];

View file

@ -1,8 +1,9 @@
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback, useMemo } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Divider, Flex, Group, Stack } from '@mantine/core';
import { openModal } from '@mantine/modals';
import { useQueryClient } from '@tanstack/react-query';
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
RiAddBoxFill,
RiAddCircleFill,
@ -31,47 +32,112 @@ import {
useListStoreByKey,
} from '/@/renderer/store';
import { ListDisplayType, Play, ServerType, TableColumn } from '/@/renderer/types';
import i18n from '/@/i18n/i18n';
const FILTERS = {
jellyfin: [
{ defaultOrder: SortOrder.ASC, name: 'Album Artist', value: AlbumListSort.ALBUM_ARTIST },
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
value: AlbumListSort.ALBUM_ARTIST,
},
{
defaultOrder: SortOrder.DESC,
name: 'Community Rating',
name: i18n.t('filter.communityRating', { postProcess: 'titleCase' }),
value: AlbumListSort.COMMUNITY_RATING,
},
{ defaultOrder: SortOrder.DESC, name: 'Critic Rating', value: AlbumListSort.CRITIC_RATING },
{ defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumListSort.NAME },
{ defaultOrder: SortOrder.DESC, name: 'Play Count', value: AlbumListSort.PLAY_COUNT },
{ defaultOrder: SortOrder.ASC, name: 'Random', value: AlbumListSort.RANDOM },
{
defaultOrder: SortOrder.DESC,
name: 'Recently Added',
name: i18n.t('filter.criticRating', { postProcess: 'titleCase' }),
value: AlbumListSort.CRITIC_RATING,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumListSort.NAME,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.playCount', { postProcess: 'titleCase' }),
value: AlbumListSort.PLAY_COUNT,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
value: AlbumListSort.RANDOM,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_ADDED,
},
{ defaultOrder: SortOrder.DESC, name: 'Release Date', value: AlbumListSort.RELEASE_DATE },
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: AlbumListSort.RELEASE_DATE,
},
],
navidrome: [
{ defaultOrder: SortOrder.ASC, name: 'Album Artist', value: AlbumListSort.ALBUM_ARTIST },
{ defaultOrder: SortOrder.ASC, name: 'Artist', value: AlbumListSort.ARTIST },
{ defaultOrder: SortOrder.DESC, name: 'Duration', value: AlbumListSort.DURATION },
{ defaultOrder: SortOrder.DESC, name: 'Most Played', value: AlbumListSort.PLAY_COUNT },
{ defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumListSort.NAME },
{ defaultOrder: SortOrder.ASC, name: 'Random', value: AlbumListSort.RANDOM },
{ defaultOrder: SortOrder.DESC, name: 'Rating', value: AlbumListSort.RATING },
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
value: AlbumListSort.ALBUM_ARTIST,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.artist', { postProcess: 'titleCase' }),
value: AlbumListSort.ARTIST,
},
{
defaultOrder: SortOrder.DESC,
name: 'Recently Added',
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
value: AlbumListSort.DURATION,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
value: AlbumListSort.PLAY_COUNT,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumListSort.NAME,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
value: AlbumListSort.RANDOM,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
value: AlbumListSort.RATING,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_ADDED,
},
{
defaultOrder: SortOrder.DESC,
name: 'Recently Played',
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_PLAYED,
},
{ defaultOrder: SortOrder.DESC, name: 'Song Count', value: AlbumListSort.SONG_COUNT },
{ defaultOrder: SortOrder.DESC, name: 'Favorited', value: AlbumListSort.FAVORITED },
{ defaultOrder: SortOrder.DESC, name: 'Year', value: AlbumListSort.YEAR },
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.songCount', { postProcess: 'titleCase' }),
value: AlbumListSort.SONG_COUNT,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.favorited', { postProcess: 'titleCase' }),
value: AlbumListSort.FAVORITED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
value: AlbumListSort.YEAR,
},
],
};
@ -81,6 +147,7 @@ interface AlbumListHeaderFiltersProps {
}
export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFiltersProps) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { pageKey, customFilters, handlePlay } = useListContext();
const server = useCurrentServer();
@ -362,7 +429,9 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
fill: isFilterApplied ? 'var(--primary-color) !important' : undefined,
},
}}
tooltip={{ label: 'Filters' }}
tooltip={{
label: t('common.filter', { count: 2, postProcess: 'sentenceCase' }),
}}
variant="subtle"
onClick={handleOpenFiltersModal}
>
@ -372,7 +441,7 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
<Button
compact
size="md"
tooltip={{ label: 'Refresh' }}
tooltip={{ label: t('common.refresh', { postProcess: 'sentenceCase' }) }}
variant="subtle"
onClick={handleRefresh}
>
@ -394,26 +463,26 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
icon={<RiPlayFill />}
onClick={() => handlePlay?.({ playType: Play.NOW })}
>
Play
{t('player.play', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
<DropdownMenu.Item
icon={<RiAddBoxFill />}
onClick={() => handlePlay?.({ playType: Play.LAST })}
>
Add to queue
{t('player.addLast', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
<DropdownMenu.Item
icon={<RiAddCircleFill />}
onClick={() => handlePlay?.({ playType: Play.NEXT })}
>
Add to queue next
{t('player.addNext', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
<DropdownMenu.Divider />
<DropdownMenu.Item
icon={<RiRefreshLine />}
onClick={handleRefresh}
>
Refresh
{t('common.refresh', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
</DropdownMenu.Dropdown>
</DropdownMenu>
@ -430,7 +499,9 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
<Button
compact
size="md"
tooltip={{ label: 'Configure' }}
tooltip={{
label: t('common.configure', { postProcess: 'sentenceCase' }),
}}
variant="subtle"
>
<RiSettings3Fill size="1.3rem" />
@ -443,21 +514,21 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
value={ListDisplayType.CARD}
onClick={handleSetViewType}
>
Card
{t('table.config.view.card', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
<DropdownMenu.Item
$isActive={display === ListDisplayType.POSTER}
value={ListDisplayType.POSTER}
onClick={handleSetViewType}
>
Poster
{t('table.config.view.poster', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
<DropdownMenu.Item
$isActive={display === ListDisplayType.TABLE}
value={ListDisplayType.TABLE}
onClick={handleSetViewType}
>
Table
{t('table.config.view.table', { postProcess: 'sentenceCase' })}
</DropdownMenu.Item>
{/* <DropdownMenu.Item
$isActive={display === ListDisplayType.TABLE_PAGINATED}

View file

@ -1,7 +1,8 @@
import type { 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 type { ChangeEvent, MutableRefObject } from 'react';
import { useTranslation } from 'react-i18next';
import { useListFilterRefresh } from '../../../hooks/use-list-filter-refresh';
import { LibraryItem } from '/@/renderer/api/types';
import { PageHeader, SearchInput } from '/@/renderer/components';
@ -18,6 +19,7 @@ import {
usePlayButtonBehavior,
} from '/@/renderer/store';
import { ListDisplayType } from '/@/renderer/types';
import { titleCase } from '/@/renderer/utils';
interface AlbumListHeaderProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
@ -27,6 +29,7 @@ interface AlbumListHeaderProps {
}
export const AlbumListHeader = ({ itemCount, gridRef, tableRef, title }: AlbumListHeaderProps) => {
const { t } = useTranslation();
const server = useCurrentServer();
const { setFilter, setTablePagination } = useListStoreActions();
const cq = useContainerQuery();
@ -69,7 +72,10 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef, title }: AlbumLi
<LibraryHeaderBar.PlayButton
onClick={() => handlePlay?.({ playType: playButtonBehavior })}
/>
<LibraryHeaderBar.Title>{title || 'Albums'}</LibraryHeaderBar.Title>
<LibraryHeaderBar.Title>
{title ||
titleCase(t('page.albumList.title', { postProcess: 'titleCase' }))}
</LibraryHeaderBar.Title>
<LibraryHeaderBar.Badge
isLoading={itemCount === null || itemCount === undefined}
>

View file

@ -1,6 +1,7 @@
import { ChangeEvent, useMemo, useState } from 'react';
import { Divider, Group, Stack } from '@mantine/core';
import debounce from 'lodash/debounce';
import { ChangeEvent, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useListFilterByKey } from '../../../store/list.store';
import { AlbumArtistListSort, GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
import { MultiSelect, NumberInput, SpinnerIcon, Switch, Text } from '/@/renderer/components';
@ -23,6 +24,7 @@ export const JellyfinAlbumFilters = ({
pageKey,
serverId,
}: JellyfinAlbumFiltersProps) => {
const { t } = useTranslation();
const filter = useListFilterByKey({ key: pageKey });
const { setFilter } = useListStoreActions();
@ -51,7 +53,7 @@ export const JellyfinAlbumFilters = ({
const toggleFilters = [
{
label: 'Is favorited',
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
onChange: (e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = setFilter({
customFilters,
@ -193,7 +195,7 @@ export const JellyfinAlbumFilters = ({
<NumberInput
defaultValue={filter?._custom?.jellyfin?.minYear}
hideControls={false}
label="From year"
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
max={2300}
min={1700}
required={!!filter?._custom?.jellyfin?.maxYear}
@ -202,7 +204,7 @@ export const JellyfinAlbumFilters = ({
<NumberInput
defaultValue={filter?._custom?.jellyfin?.maxYear}
hideControls={false}
label="To year"
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
max={2300}
min={1700}
required={!!filter?._custom?.jellyfin?.minYear}
@ -215,7 +217,7 @@ export const JellyfinAlbumFilters = ({
searchable
data={genreList}
defaultValue={selectedGenres}
label="Genres"
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
onChange={handleGenresFilter}
/>
</Group>
@ -227,7 +229,7 @@ export const JellyfinAlbumFilters = ({
data={selectableAlbumArtists}
defaultValue={filter?._custom?.jellyfin?.AlbumArtistIds?.split(',')}
disabled={disableArtistFilter}
label="Artist"
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
limit={300}
placeholder="Type to search for an artist"
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}

View file

@ -6,6 +6,7 @@ import debounce from 'lodash/debounce';
import { useGenreList } from '/@/renderer/features/genres';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
import { AlbumArtistListSort, GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
import { useTranslation } from 'react-i18next';
interface NavidromeAlbumFiltersProps {
customFilters?: Partial<AlbumListFilter>;
@ -22,6 +23,7 @@ export const NavidromeAlbumFilters = ({
pageKey,
serverId,
}: NavidromeAlbumFiltersProps) => {
const { t } = useTranslation();
const { filter } = useListStoreByKey({ key: pageKey });
const { setFilter } = useListStoreActions();
@ -62,7 +64,7 @@ export const NavidromeAlbumFilters = ({
const toggleFilters = [
{
label: 'Is rated',
label: t('filter.isRated', { postProcess: 'sentenceCase' }),
onChange: (e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = setFilter({
customFilters,
@ -83,7 +85,7 @@ export const NavidromeAlbumFilters = ({
value: filter._custom?.navidrome?.has_rating,
},
{
label: 'Is favorited',
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
onChange: (e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = setFilter({
customFilters,
@ -104,7 +106,7 @@ export const NavidromeAlbumFilters = ({
value: filter._custom?.navidrome?.starred,
},
{
label: 'Is compilation',
label: t('filter.isCompilation', { postProcess: 'sentenceCase' }),
onChange: (e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = setFilter({
customFilters,
@ -125,7 +127,7 @@ export const NavidromeAlbumFilters = ({
value: filter._custom?.navidrome?.compilation,
},
{
label: 'Is recently played',
label: t('filter.isRecentlyPlayed', { postProcess: 'sentenceCase' }),
onChange: (e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = setFilter({
customFilters,
@ -226,7 +228,7 @@ export const NavidromeAlbumFilters = ({
<NumberInput
defaultValue={filter._custom?.navidrome?.year}
hideControls={false}
label="Year"
label={t('common.year', { postProcess: 'titleCase' })}
max={5000}
min={0}
onChange={(e) => handleYearFilter(e)}
@ -236,7 +238,7 @@ export const NavidromeAlbumFilters = ({
searchable
data={genreList}
defaultValue={filter._custom?.navidrome?.genre_id}
label="Genre"
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
onChange={handleGenresFilter}
/>
</Group>
@ -247,9 +249,8 @@ export const NavidromeAlbumFilters = ({
data={selectableAlbumArtists}
defaultValue={filter._custom?.navidrome?.artist_id}
disabled={disableArtistFilter}
label="Artist"
label={t('entity.artist', { count: 1, postProcess: 'titleCase' })}
limit={300}
placeholder="Type to search for an artist"
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
searchValue={albumArtistSearchTerm}
onChange={handleAlbumArtistFilter}