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

@ -3,6 +3,7 @@ import { useHotkeys } from '@mantine/hooks';
import { useQueryClient } from '@tanstack/react-query';
import formatDuration from 'format-duration';
import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next';
import { IoIosPause } from 'react-icons/io';
import {
RiMenuAddFill,
@ -92,6 +93,7 @@ const ControlsContainer = styled.div`
`;
export const CenterControls = ({ playersRef }: CenterControlsProps) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const [isSeeking, setIsSeeking] = useState(false);
const currentSong = useCurrentSong();
@ -171,7 +173,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
<PlayerButton
icon={<RiStopFill size={15} />}
tooltip={{
label: 'Stop',
label: t('player.stop', { postProcess: 'sentenceCase' }),
openDelay: 500,
}}
variant="tertiary"
@ -183,10 +185,11 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
tooltip={{
label:
shuffle === PlayerShuffle.NONE
? 'Shuffle disabled'
: shuffle === PlayerShuffle.TRACK
? 'Shuffle tracks'
: 'Shuffle albums',
? t('player.shuffle', {
context: 'off',
postProcess: 'sentenceCase',
})
: t('player.shuffle', { postProcess: 'sentenceCase' }),
openDelay: 500,
}}
variant="tertiary"
@ -194,7 +197,10 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
/>
<PlayerButton
icon={<RiSkipBackFill size={15} />}
tooltip={{ label: 'Previous track', openDelay: 500 }}
tooltip={{
label: t('player.previous', { postProcess: 'sentenceCase' }),
openDelay: 500,
}}
variant="secondary"
onClick={handlePrevTrack}
/>
@ -202,7 +208,10 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
<PlayerButton
icon={<RiRewindFill size={15} />}
tooltip={{
label: `Skip backwards ${skip?.skipBackwardSeconds} seconds`,
label: t('player.skip', {
context: 'back',
postProcess: 'sentenceCase',
}),
openDelay: 500,
}}
variant="secondary"
@ -218,7 +227,10 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
)
}
tooltip={{
label: status === PlayerStatus.PAUSED ? 'Play' : 'Pause',
label:
status === PlayerStatus.PAUSED
? t('player.play', { postProcess: 'sentenceCase' })
: t('player.pause', { postProcess: 'sentenceCase' }),
openDelay: 500,
}}
variant="main"
@ -228,7 +240,10 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
<PlayerButton
icon={<RiSpeedFill size={15} />}
tooltip={{
label: `Skip forwards ${skip?.skipForwardSeconds} seconds`,
label: t('player.stop', {
context: 'forward',
postProcess: 'sentenceCase',
}),
openDelay: 500,
}}
variant="secondary"
@ -237,7 +252,10 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
)}
<PlayerButton
icon={<RiSkipForwardFill size={15} />}
tooltip={{ label: 'Next track', openDelay: 500 }}
tooltip={{
label: t('player.next', { postProcess: 'sentenceCase' }),
openDelay: 500,
}}
variant="secondary"
onClick={handleNextTrack}
/>
@ -253,10 +271,19 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
tooltip={{
label: `${
repeat === PlayerRepeat.NONE
? 'Repeat disabled'
? t('player.repeat', {
context: 'off',
postProcess: 'sentenceCase',
})
: repeat === PlayerRepeat.ALL
? 'Repeat all'
: 'Repeat one'
? t('player.repeat', {
context: 'all',
postProcess: 'sentenceCase',
})
: t('player.repeat', {
context: 'one',
postProcess: 'sentenceCase',
})
}`,
openDelay: 500,
}}
@ -267,7 +294,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
<PlayerButton
icon={<RiMenuAddFill size={15} />}
tooltip={{
label: 'Shuffle all',
label: t('player.playRandom', { postProcess: 'sentenceCase' }),
openDelay: 500,
}}
variant="tertiary"

View file

@ -1,5 +1,6 @@
import { Group, Center } from '@mantine/core';
import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { HiOutlineQueueList } from 'react-icons/hi2';
import { RiFileMusicLine, RiFileTextLine, RiInformationFill } from 'react-icons/ri';
import styled from 'styled-components';
@ -50,11 +51,12 @@ const GridContainer = styled.div<TransparendGridContainerProps>`
grid-template-rows: auto minmax(0, 1fr);
grid-template-columns: 1fr;
padding: 1rem;
background: rgb(var(--main-bg-transparent), ${({ opacity }) => opacity}%);
background: rgb(var(--main-bg-transparent) ${({ opacity }) => opacity}%);
border-radius: 5px;
`;
export const FullScreenPlayerQueue = () => {
const { t } = useTranslation();
const { activeTab, opacity } = useFullScreenPlayerStore();
const { setStore } = useFullScreenPlayerStoreActions();
@ -62,19 +64,19 @@ export const FullScreenPlayerQueue = () => {
{
active: activeTab === 'queue',
icon: <RiFileMusicLine size="1.5rem" />,
label: 'Up Next',
label: t('page.fullScreenPlayer.upNext'),
onClick: () => setStore({ activeTab: 'queue' }),
},
{
active: activeTab === 'related',
icon: <HiOutlineQueueList size="1.5rem" />,
label: 'Related',
label: t('page.fullScreenPlayer.related'),
onClick: () => setStore({ activeTab: 'related' }),
},
{
active: activeTab === 'lyrics',
icon: <RiFileTextLine size="1.5rem" />,
label: 'Lyrics',
label: t('page.fullScreenPlayer.lyrics'),
onClick: () => setStore({ activeTab: 'lyrics' }),
},
];
@ -125,7 +127,7 @@ export const FullScreenPlayerQueue = () => {
order={3}
weight={700}
>
COMING SOON
{t('common.comingSoon', { postProcess: 'upperCase' })}
</TextTitle>
</Group>
</Center>

View file

@ -2,6 +2,7 @@ import { useLayoutEffect, useRef } from 'react';
import { Divider, Group } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks';
import { Variants, motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { RiArrowDownSLine, RiSettings3Line } from 'react-icons/ri';
import { useLocation } from 'react-router';
import styled from 'styled-components';
@ -70,6 +71,7 @@ const BackgroundImageOverlay = styled.div`
`;
const Controls = () => {
const { t } = useTranslation();
const { dynamicBackground, expanded, opacity, useImageAspectRatio } =
useFullScreenPlayerStore();
const { setStore } = useFullScreenPlayerStoreActions();
@ -104,7 +106,7 @@ const Controls = () => {
<Button
compact
size="sm"
tooltip={{ label: 'Minimize' }}
tooltip={{ label: t('common.minimize', { postProcess: 'titleCase' }) }}
variant="subtle"
onClick={handleToggleFullScreenPlayer}
>
@ -115,7 +117,7 @@ const Controls = () => {
<Button
compact
size="sm"
tooltip={{ label: 'Configure' }}
tooltip={{ label: t('common.configure', { postProcess: 'titleCase' }) }}
variant="subtle"
>
<RiSettings3Line size="1.5rem" />
@ -123,7 +125,11 @@ const Controls = () => {
</Popover.Target>
<Popover.Dropdown>
<Option>
<Option.Label>Dynamic Background</Option.Label>
<Option.Label>
{t('page.fullscreenPlayer.dynamicBackground', {
postProcess: 'sentenceCase',
})}
</Option.Label>
<Option.Control>
<Switch
defaultChecked={dynamicBackground}
@ -137,7 +143,11 @@ const Controls = () => {
</Option>
{dynamicBackground && (
<Option>
<Option.Label>Opacity</Option.Label>
<Option.Label>
{t('page.fullscreenPlayer.opacity', {
postProcess: 'sentenceCase',
})}
</Option.Label>
<Option.Control>
<Slider
defaultValue={opacity}
@ -151,7 +161,11 @@ const Controls = () => {
</Option>
)}
<Option>
<Option.Label>Use image aspect ratio</Option.Label>
<Option.Label>
{t('page.fullscreenPlayer.useImageAspectRatio', {
postProcess: 'sentenceCase',
})}
</Option.Label>
<Option.Control>
<Switch
checked={useImageAspectRatio}
@ -165,7 +179,11 @@ const Controls = () => {
</Option>
<Divider my="sm" />
<Option>
<Option.Label>Follow current lyrics</Option.Label>
<Option.Label>
{t('page.fullscreenPlayer.followCurrentLyric', {
postProcess: 'sentenceCase',
})}
</Option.Label>
<Option.Control>
<Switch
checked={lyricConfig.follow}
@ -176,7 +194,11 @@ const Controls = () => {
</Option.Control>
</Option>
<Option>
<Option.Label>Show lyrics provider</Option.Label>
<Option.Label>
{t('page.fullscreenPlayer.showLyricProvider', {
postProcess: 'sentenceCase',
})}
</Option.Label>
<Option.Control>
<Switch
checked={lyricConfig.showProvider}
@ -187,7 +209,11 @@ const Controls = () => {
</Option.Control>
</Option>
<Option>
<Option.Label>Show lyrics match</Option.Label>
<Option.Label>
{t('page.fullscreenPlayer.showLyricMatch', {
postProcess: 'sentenceCase',
})}
</Option.Label>
<Option.Control>
<Switch
checked={lyricConfig.showMatch}
@ -198,7 +224,11 @@ const Controls = () => {
</Option.Control>
</Option>
<Option>
<Option.Label>Lyrics size</Option.Label>
<Option.Label>
{t('page.fullscreenPlayer.lyric', {
postProcess: 'sentenceCase',
})}
</Option.Label>
<Option.Control>
<Group
noWrap
@ -206,7 +236,11 @@ const Controls = () => {
>
<Slider
defaultValue={lyricConfig.fontSize}
label={(e) => `Synchronized: ${e}px`}
label={(e) =>
`${t('page.fullscreenPlayer.synchronized', {
postProcess: 'titleCase',
})}: ${e}px`
}
max={72}
min={8}
w="100%"
@ -214,7 +248,11 @@ const Controls = () => {
/>
<Slider
defaultValue={lyricConfig.fontSize}
label={(e) => `Unsynchronized: ${e}px`}
label={(e) =>
`${t('page.fullscreenPlayer.unsynchronized', {
postProcess: 'sentenceCase',
})}: ${e}px`
}
max={72}
min={8}
w="100%"
@ -226,7 +264,11 @@ const Controls = () => {
</Option.Control>
</Option>
<Option>
<Option.Label>Lyrics gap</Option.Label>
<Option.Label>
{t('page.fullscreenPlayer.lyricGap', {
postProcess: 'sentenceCase',
})}
</Option.Label>
<Option.Control>
<Group
noWrap
@ -254,13 +296,32 @@ const Controls = () => {
</Option.Control>
</Option>
<Option>
<Option.Label>Lyrics alignment</Option.Label>
<Option.Label>
{t('page.fullscreenPlayer.lyricAlignment', {
postProcess: 'sentenceCase',
})}
</Option.Label>
<Option.Control>
<Select
data={[
{ label: 'Left', value: 'left' },
{ label: 'Center', value: 'center' },
{ label: 'Right', value: 'right' },
{
label: t('common.left', {
postProcess: 'titleCase',
}),
value: 'left',
},
{
label: t('common.center', {
postProcess: 'titleCase',
}),
value: 'center',
},
{
label: t('common.right', {
postProcess: 'titleCase',
}),
value: 'right',
},
]}
value={lyricConfig.alignment}
onChange={(e) => handleLyricsSettings('alignment', e)}

View file

@ -2,6 +2,7 @@ import React, { MouseEvent } from 'react';
import { Center, Group } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks';
import { motion, AnimatePresence, LayoutGroup } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { RiArrowUpSLine, RiDiscLine, RiMore2Fill } from 'react-icons/ri';
import { generatePath, Link } from 'react-router-dom';
import styled from 'styled-components';
@ -92,6 +93,7 @@ const LeftControlsContainer = styled.div`
`;
export const LeftControls = () => {
const { t } = useTranslation();
const { setSideBar } = useAppStoreActions();
const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore();
const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
@ -147,7 +149,9 @@ export const LeftControls = () => {
onClick={handleToggleFullScreenPlayer}
>
<Tooltip
label="Toggle fullscreen player"
label={t('player.toggleFullscreenPlayer', {
postProcess: 'sentenceCase',
})}
openDelay={500}
>
{currentSong?.imageUrl ? (
@ -182,7 +186,12 @@ export const LeftControls = () => {
right: 2,
top: 2,
}}
tooltip={{ label: 'Expand', openDelay: 500 }}
tooltip={{
label: t('common.expand', {
postProcess: 'titleCase',
}),
openDelay: 500,
}}
variant="default"
onClick={handleToggleSidebarImage}
>

View file

@ -2,6 +2,7 @@ import { useEffect } from 'react';
import { Flex, Group } from '@mantine/core';
import { useHotkeys, useMediaQuery } from '@mantine/hooks';
import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next';
import { HiOutlineQueueList } from 'react-icons/hi2';
import {
RiVolumeUpFill,
@ -34,6 +35,7 @@ const remote = isElectron() ? window.electron.remote : null;
const PLAYBACK_SPEEDS = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0];
export const RightControls = () => {
const { t } = useTranslation();
const isMinWidth = useMediaQuery('(max-width: 480px)');
const volume = useVolume();
const muted = useMuted();
@ -213,7 +215,7 @@ export const RightControls = () => {
<PlayerButton
icon={<>{speed} x</>}
tooltip={{
label: 'Playback speed',
label: t('player.playbackSpeed', { postProcess: 'sentenceCase' }),
openDelay: 500,
}}
variant="secondary"
@ -249,7 +251,9 @@ export const RightControls = () => {
},
}}
tooltip={{
label: currentSong?.userFavorite ? 'Unfavorite' : 'Favorite',
label: currentSong?.userFavorite
? t('player.unfavorite', { postProcess: 'titleCase' })
: t('player.favorite', { postProcess: 'titleCase' }),
openDelay: 500,
}}
variant="secondary"
@ -277,7 +281,10 @@ export const RightControls = () => {
<RiVolumeDownFill size="1.2rem" />
)
}
tooltip={{ label: muted ? 'Muted' : volume, openDelay: 500 }}
tooltip={{
label: muted ? t('player.muted', { postProcess: 'titleCase' }) : volume,
openDelay: 500,
}}
variant="secondary"
onClick={handleMute}
onWheel={handleVolumeWheel}

View file

@ -20,6 +20,7 @@ import { api } from '/@/renderer/api';
import { useAuthStore } from '/@/renderer/store';
import { queryKeys } from '/@/renderer/api/query-keys';
import { Play, PlayQueueAddOptions, ServerListItem } from '/@/renderer/types';
import i18n from '/@/i18n/i18n';
interface ShuffleAllSlice extends RandomSongListQuery {
actions: {
@ -260,6 +261,6 @@ export const openShuffleAllModal = async (
/>
),
size: 'sm',
title: 'Shuffle all',
title: i18n.t('player.playRandom', { postProcess: 'sentenceCase' }) as string,
});
};

View file

@ -18,6 +18,7 @@ import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
import debounce from 'lodash/debounce';
import { QueueSong } from '/@/renderer/api/types';
import { toast } from '/@/renderer/components';
import { useTranslation } from 'react-i18next';
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null;
@ -28,6 +29,7 @@ const remote = isElectron() ? window.electron.remote : null;
const mediaSession = !isElectron() || !utils?.isLinux() ? navigator.mediaSession : null;
export const useCenterControls = (args: { playersRef: any }) => {
const { t } = useTranslation();
const { playersRef } = args;
const settings = useSettingsStore((state) => state.playback);
@ -613,11 +615,15 @@ export const useCenterControls = (args: { playersRef: any }) => {
const handleError = useCallback(
(message: string) => {
toast.error({ id: 'mpv-error', message, title: 'An error occurred during playback' });
toast.error({
id: 'mpv-error',
message,
title: t('error.playbackError', { postProcess: 'sentenceCase' }),
});
pause();
mpvPlayer!.pause();
},
[pause],
[pause, t],
);
useEffect(() => {

View file

@ -28,6 +28,7 @@ import {
getGenreSongsById,
} from '/@/renderer/features/player/utils';
import { queryKeys } from '/@/renderer/api/query-keys';
import { useTranslation } from 'react-i18next';
const getRootQueryKey = (itemType: LibraryItem, serverId: string) => {
let queryKey;
@ -62,6 +63,7 @@ const remote = isElectron() ? window.electron.remote : null;
const addToQueue = usePlayerStore.getState().actions.addToQueue;
export const useHandlePlayQueueAdd = () => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const playerType = usePlayerType();
const server = useCurrentServer();
@ -86,15 +88,18 @@ export const useHandlePlayQueueAdd = () => {
toast.info({
autoClose: false,
id: fetchId,
message:
'This is taking a while... close the notification to cancel the request',
message: t('player.playbackFetchCancel', {
postProcess: 'sentenceCase',
}),
onClose: () => {
queryClient.cancelQueries({
exact: false,
queryKey: getRootQueryKey(itemType, server?.id),
});
},
title: 'Adding to queue',
title: t('player.playbackFetchInProgress', {
postProcess: 'sentenceCase',
}),
});
}, 2000),
};
@ -140,7 +145,7 @@ export const useHandlePlayQueueAdd = () => {
return toast.error({
message: err.message,
title: 'Play queue add failed',
title: t('error.genericError', { postProcess: 'sentenceCase' }) as string,
});
}
@ -152,8 +157,8 @@ export const useHandlePlayQueueAdd = () => {
if (!songs || songs?.length === 0)
return toast.warn({
message: 'The query returned no results',
title: 'No tracks added',
message: t('common.noResultsFromQuery', { postProcess: 'sentenceCase' }),
title: t('player.playbackFetchNoResults'),
});
if (initialIndex) {
@ -190,7 +195,7 @@ export const useHandlePlayQueueAdd = () => {
return null;
},
[play, playerType, queryClient, server],
[play, playerType, queryClient, server, t],
);
return handlePlayQueueAdd;