From eb5ad541d9a14695aa7dcde0b697a8c5c079c40e Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 26 Oct 2023 03:59:05 -0700 Subject: [PATCH] Add initial translation keys --- src/renderer/api/controller.ts | 18 +- src/renderer/api/navidrome/navidrome-api.ts | 16 +- src/renderer/api/subsonic/subsonic-api.ts | 3 +- .../components/feature-carousel/index.tsx | 4 +- .../components/virtual-table/index.tsx | 47 +-- .../virtual-table/table-config-dropdown.tsx | 317 ++++++++++++++---- .../routes/action-required-route.tsx | 8 +- .../components/album-detail-content.tsx | 11 +- .../components/album-list-header-filters.tsx | 133 ++++++-- .../albums/components/album-list-header.tsx | 10 +- .../components/jellyfin-album-filters.tsx | 12 +- .../components/navidrome-album-filters.tsx | 17 +- ...um-artist-detail-top-songs-list-header.tsx | 8 +- .../components/album-artist-list-header.tsx | 8 +- .../context-menu/context-menu-provider.tsx | 70 ++-- .../components/genre-list-header-filters.tsx | 38 ++- .../genres/components/genre-list-header.tsx | 8 +- .../features/home/routes/home-route.tsx | 14 +- .../lyrics/components/lyrics-search-form.tsx | 15 +- .../features/lyrics/lyrics-actions.tsx | 10 +- .../player/components/center-controls.tsx | 55 ++- .../player/components/full-screen-player.tsx | 93 ++++- .../player/components/left-controls.tsx | 13 +- .../player/components/right-controls.tsx | 13 +- .../player/components/shuffle-all-modal.tsx | 3 +- .../player/hooks/use-center-controls.ts | 10 +- .../player/hooks/use-handle-playqueue-add.ts | 19 +- .../add-to-playlist-context-modal.tsx | 33 +- .../components/create-playlist-form.tsx | 32 +- .../components/playlist-detail-content.tsx | 11 +- ...aylist-detail-song-list-header-filters.tsx | 27 +- .../playlist-detail-song-list-header.tsx | 6 +- .../playlist-list-header-filters.tsx | 30 +- .../components/playlist-list-header.tsx | 13 +- .../components/playlist-query-builder.tsx | 20 +- .../components/save-as-playlist-form.tsx | 30 +- .../components/update-playlist-form.tsx | 35 +- .../playlist-detail-song-list-route.tsx | 4 +- .../search/components/go-to-commands.tsx | 38 ++- .../search/components/home-commands.tsx | 28 +- .../components/library-command-item.tsx | 20 +- .../search/components/server-commands.tsx | 18 +- .../servers/components/add-server-form.tsx | 63 ++-- .../servers/components/edit-server-form.tsx | 58 +++- .../servers/components/server-list.tsx | 14 +- .../general/application-settings.tsx | 61 +++- .../components/general/control-settings.tsx | 109 ++++-- .../components/general/remote-settings.tsx | 65 ++-- .../components/general/sidebar-settings.tsx | 23 +- .../components/general/theme-settings.tsx | 37 +- .../hotkeys/hotkey-manager-settings.tsx | 88 +++-- .../hotkeys/window-hotkey-settings.tsx | 11 +- .../components/playback/audio-settings.tsx | 71 +++- .../components/playback/lyric-settings.tsx | 32 +- .../components/playback/mpv-settings.tsx | 137 ++++++-- .../components/playback/playback-tab.tsx | 8 +- .../components/playback/scrobble-settings.tsx | 39 ++- .../settings/components/settings-content.tsx | 20 +- .../settings/components/settings-header.tsx | 16 +- .../settings/components/settings-section.tsx | 2 +- .../components/window/discord-settings.tsx | 48 ++- .../components/window/update-settings.tsx | 9 +- .../components/window/window-settings.tsx | 32 +- .../shared/components/order-toggle-button.tsx | 10 +- src/renderer/features/shared/utils.ts | 7 +- .../sidebar/components/action-bar.tsx | 1 - .../sidebar/components/collapsed-sidebar.tsx | 6 +- .../components/sidebar-playlist-list.tsx | 17 +- .../features/sidebar/components/sidebar.tsx | 31 +- .../components/jellyfin-song-filters.tsx | 12 +- .../components/navidrome-song-filters.tsx | 10 +- .../components/song-list-header-filters.tsx | 42 ++- .../songs/components/song-list-header.tsx | 6 +- .../features/titlebar/components/app-menu.tsx | 27 +- src/renderer/store/settings.store.ts | 56 +++- src/renderer/utils/index.ts | 1 + src/renderer/utils/sentence-case.ts | 3 + 77 files changed, 1798 insertions(+), 692 deletions(-) create mode 100644 src/renderer/utils/sentence-case.ts diff --git a/src/renderer/api/controller.ts b/src/renderer/api/controller.ts index 90b360a48..196a848c1 100644 --- a/src/renderer/api/controller.ts +++ b/src/renderer/api/controller.ts @@ -54,6 +54,7 @@ import { DeletePlaylistResponse, RandomSongListArgs } from './types'; import { ndController } from '/@/renderer/api/navidrome/navidrome-controller'; import { ssController } from '/@/renderer/api/subsonic/subsonic-controller'; import { jfController } from '/@/renderer/api/jellyfin/jellyfin-controller'; +import i18n from '/@/i18n/i18n'; export type ControllerEndpoint = Partial<{ addToPlaylist: (args: AddToPlaylistArgs) => Promise; @@ -212,7 +213,12 @@ const apiController = (endpoint: keyof ControllerEndpoint, type?: ServerType) => const serverType = type || useAuthStore.getState().currentServer?.type; if (!serverType) { - toast.error({ message: 'No server selected', title: 'Unable to route request' }); + toast.error({ + message: i18n.t('error.serverNotSelectedError', { + postProcess: 'sentenceCase', + }) as string, + title: i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' }) as string, + }); throw new Error(`No server selected`); } @@ -221,10 +227,16 @@ const apiController = (endpoint: keyof ControllerEndpoint, type?: ServerType) => if (typeof controllerFn !== 'function') { toast.error({ message: `Endpoint ${endpoint} is not implemented for ${serverType}`, - title: 'Unable to route request', + title: i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' }) as string, }); - throw new Error(`Endpoint ${endpoint} is not implemented for ${serverType}`); + throw new Error( + i18n.t('error.endpointNotImplementedError', { + endpoint, + postProcess: 'sentenceCase', + serverType, + }) as string, + ); } return endpoints[serverType][endpoint]; diff --git a/src/renderer/api/navidrome/navidrome-api.ts b/src/renderer/api/navidrome/navidrome-api.ts index 32852e93c..25cb1939b 100644 --- a/src/renderer/api/navidrome/navidrome-api.ts +++ b/src/renderer/api/navidrome/navidrome-api.ts @@ -9,6 +9,7 @@ import { authenticationFailure, resultWithHeaders } from '/@/renderer/api/utils' import { useAuthStore } from '/@/renderer/store'; import { ServerListItem } from '/@/renderer/types'; import { toast } from '/@/renderer/components'; +import i18n from '/@/i18n/i18n'; const localSettings = isElectron() ? window.electron.localSettings : null; @@ -276,9 +277,12 @@ axiosClient.interceptors.response.use( if (res.status === 429) { toast.error({ - message: - 'you have exceeded the number of allowed login requests. Please wait before logging, or consider tweaking AuthRequestLimit', - title: 'Your session has expired.', + message: i18n.t('error.loginRateError', { + postProcess: 'sentenceCase', + }) as string, + title: i18n.t('error.sessionExpiredError', { + postProcess: 'sentenceCase', + }) as string, }); const serverId = currentServer.id; @@ -292,7 +296,11 @@ axiosClient.interceptors.response.use( throw TIMEOUT_ERROR; } if (res.status !== 200) { - throw new Error('Failed to authenticate'); + throw new Error( + i18n.t('error.authenticatedFailed', { + postProcess: 'sentenceCase', + }) as string, + ); } const newCredential = res.data.token; diff --git a/src/renderer/api/subsonic/subsonic-api.ts b/src/renderer/api/subsonic/subsonic-api.ts index edd7ee3b2..858172f9a 100644 --- a/src/renderer/api/subsonic/subsonic-api.ts +++ b/src/renderer/api/subsonic/subsonic-api.ts @@ -6,6 +6,7 @@ import { z } from 'zod'; import { ssType } from '/@/renderer/api/subsonic/subsonic-types'; import { ServerListItem } from '/@/renderer/api/types'; import { toast } from '/@/renderer/components/toast/index'; +import i18n from '/@/i18n/i18n'; const c = initContract(); @@ -106,7 +107,7 @@ axiosClient.interceptors.response.use( if (data['subsonic-response'].error.code !== 0) { toast.error({ message: data['subsonic-response'].error.message, - title: 'Issue from Subsonic API', + title: i18n.t('error.genericError', { postProcess: 'sentenceCase' }) as string, }); } } diff --git a/src/renderer/components/feature-carousel/index.tsx b/src/renderer/components/feature-carousel/index.tsx index 625879e7b..2502db833 100644 --- a/src/renderer/components/feature-carousel/index.tsx +++ b/src/renderer/components/feature-carousel/index.tsx @@ -3,6 +3,7 @@ import { useState } from 'react'; import { Group, Image, Stack } from '@mantine/core'; import type { Variants } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri'; import { Link, generatePath } from 'react-router-dom'; import styled from 'styled-components'; @@ -109,6 +110,7 @@ interface FeatureCarouselProps { } export const FeatureCarousel = ({ data }: FeatureCarouselProps) => { + const { t } = useTranslation(); const handlePlayQueueAdd = usePlayQueueAdd(); const [itemIndex, setItemIndex] = useState(0); const [direction, setDirection] = useState(0); @@ -224,7 +226,7 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => { }); }} > - Play + {t('player.play', { postProcess: 'titleCase' })} - Display type + + {t('table.config.general.displayType', { postProcess: 'titleCase' })} + - Card + {t('table.config.view.card', { postProcess: 'titleCase' })} - Poster + {t('table.config.view.poster', { postProcess: 'titleCase' })} - Table + {t('table.config.view.table', { postProcess: 'titleCase' })} - Item size + + {t('table.config.general.size', { postProcess: 'titleCase' })} + - Table Columns + + {t('table.config.general.tableColumns', { + postProcess: 'titleCase', + })} + - Auto Fit Columns + + {t('table.config.general.autoFitColumns', { + postProcess: 'titleCase', + })} + ; @@ -25,6 +26,7 @@ interface GenreListHeaderProps { } export const GenreListHeader = ({ itemCount, gridRef, tableRef }: GenreListHeaderProps) => { + const { t } = useTranslation(); const cq = useContainerQuery(); const server = useCurrentServer(); const { pageKey } = useListContext(); @@ -66,7 +68,9 @@ export const GenreListHeader = ({ itemCount, gridRef, tableRef }: GenreListHeade w="100%" > - Genres + + {t('page.genreList.title', { postProcess: 'titleCase' })} + diff --git a/src/renderer/features/home/routes/home-route.tsx b/src/renderer/features/home/routes/home-route.tsx index e49f7554a..25b8bb10a 100644 --- a/src/renderer/features/home/routes/home-route.tsx +++ b/src/renderer/features/home/routes/home-route.tsx @@ -11,9 +11,11 @@ import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel import { Platform } from '/@/renderer/types'; import { useQueryClient } from '@tanstack/react-query'; import { queryKeys } from '/@/renderer/api/query-keys'; +import { useTranslation } from 'react-i18next'; import { RiRefreshLine } from 'react-icons/ri'; const HomeRoute = () => { + const { t } = useTranslation(); const queryClient = useQueryClient(); const scrollAreaRef = useRef(null); const server = useCurrentServer(); @@ -105,7 +107,7 @@ const HomeRoute = () => { data: random?.data?.items, sortBy: AlbumListSort.RANDOM, sortOrder: SortOrder.ASC, - title: 'Explore from your library', + title: t('page.home.explore', { postProcess: 'sentenceCase' }), uniqueId: 'random', }, { @@ -115,7 +117,7 @@ const HomeRoute = () => { }, sortBy: AlbumListSort.RECENTLY_PLAYED, sortOrder: SortOrder.DESC, - title: 'Recently played', + title: t('page.home.recentlyPlayed', { postProcess: 'sentenceCase' }), uniqueId: 'recentlyPlayed', }, { @@ -125,7 +127,7 @@ const HomeRoute = () => { }, sortBy: AlbumListSort.RECENTLY_ADDED, sortOrder: SortOrder.DESC, - title: 'Newly added releases', + title: t('page.home.newlyAdded', { postProcess: 'sentenceCase' }), uniqueId: 'recentlyAdded', }, { @@ -135,7 +137,7 @@ const HomeRoute = () => { }, sortBy: AlbumListSort.PLAY_COUNT, sortOrder: SortOrder.DESC, - title: 'Most played', + title: t('page.home.mostPlayed', { postProcess: 'sentenceCase' }), uniqueId: 'mostPlayed', }, ]; @@ -148,7 +150,9 @@ const HomeRoute = () => { backgroundColor: 'var(--titlebar-bg)', children: ( - Home + + {t('page.home.title', { postProcess: 'titleCase' })} + ), offset: 200, diff --git a/src/renderer/features/lyrics/components/lyrics-search-form.tsx b/src/renderer/features/lyrics/components/lyrics-search-form.tsx index b04fd5ab5..7555ded54 100644 --- a/src/renderer/features/lyrics/components/lyrics-search-form.tsx +++ b/src/renderer/features/lyrics/components/lyrics-search-form.tsx @@ -4,6 +4,7 @@ import { useForm } from '@mantine/form'; import { useDebouncedValue } from '@mantine/hooks'; import { openModal } from '@mantine/modals'; import orderBy from 'lodash/orderBy'; +import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { InternetProviderLyricSearchResponse, @@ -12,6 +13,7 @@ import { } from '../../../api/types'; import { useLyricSearch } from '../queries/lyric-search-query'; import { ScrollArea, Spinner, Text, TextInput } from '/@/renderer/components'; +import i18n from '/@/i18n/i18n'; const SearchItem = styled.button` all: unset; @@ -84,6 +86,7 @@ interface LyricSearchFormProps { } export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearchFormProps) => { + const { t } = useTranslation(); const form = useForm({ initialValues: { artist: artist || '', @@ -117,11 +120,17 @@ export const LyricsSearchForm = ({ artist, name, onSearchOverride }: LyricSearch @@ -170,6 +179,6 @@ export const openLyricSearchModal = ({ artist, name, onSearchOverride }: LyricSe /> ), size: 'lg', - title: 'Lyrics Search', + title: i18n.t('form.lyricSearch.title', { postProcess: 'titleCase' }) as string, }); }; diff --git a/src/renderer/features/lyrics/lyrics-actions.tsx b/src/renderer/features/lyrics/lyrics-actions.tsx index f6455ce15..5944e1fd6 100644 --- a/src/renderer/features/lyrics/lyrics-actions.tsx +++ b/src/renderer/features/lyrics/lyrics-actions.tsx @@ -1,5 +1,6 @@ import { Box, Group } from '@mantine/core'; import isElectron from 'is-electron'; +import { useTranslation } from 'react-i18next'; import { RiAddFill, RiSubtractFill } from 'react-icons/ri'; import { LyricsOverride } from '/@/renderer/api/types'; import { Button, NumberInput, Tooltip } from '/@/renderer/components'; @@ -22,6 +23,7 @@ export const LyricsActions = ({ onResetLyric, onSearchOverride, }: LyricsActionsProps) => { + const { t } = useTranslation(); const currentSong = useCurrentSong(); const { setSettings } = useSettingsStoreActions(); const { delayMs, sources } = useLyricsSettings(); @@ -54,7 +56,7 @@ export const LyricsActions = ({ }) } > - Search + {t('glossary.search', { postProcess: 'titleCase' })} ) : null} - Reset + {t('glossary.reset', { postProcess: 'sentenceCase' })} ) : null} @@ -104,7 +106,7 @@ export const LyricsActions = ({ variant="subtle" onClick={onRemoveLyric} > - Clear + {t('glossary.clear', { postProcess: 'sentenceCase' })} ) : null} diff --git a/src/renderer/features/player/components/center-controls.tsx b/src/renderer/features/player/components/center-controls.tsx index 4c56b6ecc..28e7b53f5 100644 --- a/src/renderer/features/player/components/center-controls.tsx +++ b/src/renderer/features/player/components/center-controls.tsx @@ -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) => { } 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) => { /> } - 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) => { } 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) => { } 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) => { )} } - 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) => { } tooltip={{ - label: 'Shuffle all', + label: t('player.playRandom', { postProcess: 'sentenceCase' }), openDelay: 500, }} variant="tertiary" diff --git a/src/renderer/features/player/components/full-screen-player.tsx b/src/renderer/features/player/components/full-screen-player.tsx index cd55646a4..e63b7b118 100644 --- a/src/renderer/features/player/components/full-screen-player.tsx +++ b/src/renderer/features/player/components/full-screen-player.tsx @@ -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 = () => { @@ -462,7 +468,7 @@ export const PlaylistQueryBuilder = forwardRef( icon={} onClick={handleSave} > - Save and replace + {t('common.saveAndReplace', { postProcess: 'titleCase' })} diff --git a/src/renderer/features/playlists/components/save-as-playlist-form.tsx b/src/renderer/features/playlists/components/save-as-playlist-form.tsx index a0f87d081..5196b2456 100644 --- a/src/renderer/features/playlists/components/save-as-playlist-form.tsx +++ b/src/renderer/features/playlists/components/save-as-playlist-form.tsx @@ -4,6 +4,7 @@ import { CreatePlaylistBody, CreatePlaylistResponse, ServerType } from '/@/rende import { Button, Switch, TextInput, toast } from '/@/renderer/components'; import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation'; import { useCurrentServer } from '/@/renderer/store'; +import { useTranslation } from 'react-i18next'; interface SaveAsPlaylistFormProps { body: Partial; @@ -18,6 +19,7 @@ export const SaveAsPlaylistForm = ({ onSuccess, onCancel, }: SaveAsPlaylistFormProps) => { + const { t } = useTranslation(); const mutation = useCreatePlaylist({}); const server = useCurrentServer(); @@ -40,10 +42,15 @@ export const SaveAsPlaylistForm = ({ { body: values, serverId }, { onError: (err) => { - toast.error({ message: err.message, title: 'Error creating playlist' }); + toast.error({ + message: err.message, + title: t('error.genericError', { postProcess: 'sentenceCase' }), + }); }, onSuccess: (data) => { - toast.success({ message: `Playlist has been created` }); + toast.success({ + message: t('form.createPlaylist.success', { postProcess: 'sentenceCase' }), + }); onSuccess(data); onCancel(); }, @@ -60,16 +67,25 @@ export const SaveAsPlaylistForm = ({ {isPublicDisplayed && ( )} @@ -78,7 +94,7 @@ export const SaveAsPlaylistForm = ({ variant="subtle" onClick={onCancel} > - Cancel + {t('common.cancel', { postProcess: 'titleCase' })} diff --git a/src/renderer/features/playlists/components/update-playlist-form.tsx b/src/renderer/features/playlists/components/update-playlist-form.tsx index 4dea0a700..54cca3d9c 100644 --- a/src/renderer/features/playlists/components/update-playlist-form.tsx +++ b/src/renderer/features/playlists/components/update-playlist-form.tsx @@ -18,6 +18,8 @@ import { Button, Select, Switch, TextInput, toast } from '/@/renderer/components import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation'; import { queryClient } from '/@/renderer/lib/react-query'; import { useCurrentServer } from '/@/renderer/store'; +import { useTranslation } from 'react-i18next'; +import i18n from '/@/i18n/i18n'; interface UpdatePlaylistFormProps { body: Partial; @@ -27,6 +29,7 @@ interface UpdatePlaylistFormProps { } export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlaylistFormProps) => { + const { t } = useTranslation(); const mutation = useUpdatePlaylist({}); const server = useCurrentServer(); @@ -60,10 +63,12 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl }, { onError: (err) => { - toast.error({ message: err.message, title: 'Error updating playlist' }); + toast.error({ + message: err.message, + title: t('error.genericError', { postProcess: 'sentenceCase' }), + }); }, onSuccess: () => { - toast.success({ message: `Playlist has been saved` }); onCancel(); }, }, @@ -80,23 +85,35 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl {isOwnerDisplayed && ( @@ -112,9 +150,12 @@ export const ControlSettings = () => { } /> ), - description: 'The default behavior of the play button when adding songs to the queue', + description: t('setting.playButtonBehavior', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: false, - title: 'Play button behavior', + title: t('setting.playButtonBehavior', { postProcess: 'sentenceCase' }), }, { control: ( @@ -131,9 +172,12 @@ export const ControlSettings = () => { }} /> ), - description: 'The style of the sidebar play queue', + description: t('setting.sidePlayQueueStyle', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: false, - title: 'Side play queue style', + title: t('setting.sidePlayQueueStyle', { postProcess: 'sentenceCase' }), }, { control: ( @@ -149,10 +193,12 @@ export const ControlSettings = () => { }} /> ), - description: - 'Display a hover icon on the right side of the application view the play queue', + description: t('setting.sidePlayQueueStyle', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: false, - title: 'Show floating queue hover area', + title: t('setting.floatingQueueArea', { postProcess: 'sentenceCase' }), }, { control: ( @@ -171,10 +217,12 @@ export const ControlSettings = () => { }} /> ), - description: - 'The amount of volume to change when scrolling the mouse wheel on the volume slider', + description: t('setting.volumeWheelStep', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: false, - title: 'Volume wheel step', + title: t('setting.volumeWheelStep', { postProcess: 'sentenceCase' }), }, { control: ( @@ -191,9 +239,12 @@ export const ControlSettings = () => { }} /> ), - description: 'When exiting, save the current play queue and restore it when reopening', + description: t('setting.savePlayQueue', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !isElectron(), - title: 'Save play queue', + title: t('setting.savePlayQueue', { postProcess: 'sentenceCase' }), }, { control: ( @@ -210,10 +261,12 @@ export const ControlSettings = () => { } /> ), - description: - 'When navigating to a playlist, go to the playlist song list page instead of the default page', + description: t('setting.skipPlaylistPage', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: false, - title: 'Go to playlist songs page by default', + title: t('setting.skipPlaylistPage', { postProcess: 'sentenceCase' }), }, ]; diff --git a/src/renderer/features/settings/components/general/remote-settings.tsx b/src/renderer/features/settings/components/general/remote-settings.tsx index 798c52af9..0178b676c 100644 --- a/src/renderer/features/settings/components/general/remote-settings.tsx +++ b/src/renderer/features/settings/components/general/remote-settings.tsx @@ -3,10 +3,12 @@ import { SettingsSection } from '/@/renderer/features/settings/components/settin import { useRemoteSettings, useSettingsStoreActions } from '/@/renderer/store'; import { NumberInput, Switch, Text, TextInput, toast } from '/@/renderer/components'; import debounce from 'lodash/debounce'; +import { useTranslation } from 'react-i18next'; const remote = isElectron() ? window.electron.remote : null; export const RemoteSettings = () => { + const { t } = useTranslation(); const settings = useRemoteSettings(); const { setSettings } = useSettingsStoreActions(); @@ -25,7 +27,9 @@ export const RemoteSettings = () => { } else { toast.error({ message: errorMsg, - title: enabled ? 'Error enabling remote' : 'Error disabling remote', + title: enabled + ? t('error.remoteEnableError', { postProcess: 'sentenceCase' }) + : t('error.remoteDisableError', { postProcess: 'sentenceCase' }), }); } }, 50); @@ -40,12 +44,12 @@ export const RemoteSettings = () => { }, }); toast.warn({ - message: 'To have your port change take effect, stop and restart the server', + message: t('error.remotePortWarning', { postProcess: 'sentenceCase' }), }); } else { toast.error({ message: errorMsg, - title: 'Error setting port', + title: t('error.remotePortError', { postProcess: 'sentenceCase' }), }); } }, 100); @@ -56,7 +60,6 @@ export const RemoteSettings = () => { { control: ( { const enabled = e.currentTarget.checked; @@ -65,8 +68,15 @@ export const RemoteSettings = () => { /> ), description: ( -
- Start an HTTP server to remotely control Feishin. This will listen on{' '} + + {t('setting.enableRemote', { + context: 'description', + postProcess: 'sentenceCase', + })}{' '} { > {url} -
+ ), isHidden, - title: 'Enable remote control', + title: t('setting.enableRemote', { postProcess: 'sentenceCase' }), }, { control: ( { @@ -92,15 +101,16 @@ export const RemoteSettings = () => { }} /> ), - description: - 'Remote server port. Changes here only take effect when you enable the remote', + description: t('setting.remotePort', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden, - title: 'Remove server port', + title: t('setting.remotePort', { postProcess: 'sentenceCase' }), }, { control: ( { const username = e.currentTarget.value; @@ -115,15 +125,16 @@ export const RemoteSettings = () => { }} /> ), - description: - 'Username that must be provided to access remote. If both username and password are empty, disable authentication', + description: t('setting.remoteUsername', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden, - title: 'Remote username', + title: t('setting.remoteUsername', { postProcess: 'sentenceCase' }), }, { control: ( { const password = e.currentTarget.value; @@ -138,22 +149,14 @@ export const RemoteSettings = () => { }} /> ), - description: 'Password to access remote', + description: t('setting.remotePassword', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden, - title: 'Remote password', + title: t('setting.remotePassword', { postProcess: 'sentenceCase' }), }, ]; - return ( - <> - - - - NOTE: these credentials are by default transferred insecurely. Do not use a - password you care about. Changing username/password will disconnect clients and - require them to reauthenticate - - - - ); + return ; }; diff --git a/src/renderer/features/settings/components/general/sidebar-settings.tsx b/src/renderer/features/settings/components/general/sidebar-settings.tsx index cb9952f15..169ddffab 100644 --- a/src/renderer/features/settings/components/general/sidebar-settings.tsx +++ b/src/renderer/features/settings/components/general/sidebar-settings.tsx @@ -2,6 +2,7 @@ import { ChangeEvent, useCallback, useState } from 'react'; import { Group } from '@mantine/core'; import { Reorder, useDragControls } from 'framer-motion'; import isEqual from 'lodash/isEqual'; +import { useTranslation } from 'react-i18next'; import { MdDragIndicator } from 'react-icons/md'; import { Button, Checkbox, Switch } from '/@/renderer/components'; import { useSettingsStoreActions, useGeneralSettings } from '../../../../store/settings.store'; @@ -54,6 +55,7 @@ const DraggableSidebarItem = ({ item, handleChangeDisabled }: DraggableSidebarIt }; export const SidebarSettings = () => { + const { t } = useTranslation(); const settings = useGeneralSettings(); const { setSidebarItems, setSettings } = useSettingsStoreActions(); @@ -107,8 +109,11 @@ export const SidebarSettings = () => { onChange={handleSetSidebarPlaylistList} /> } - description="Show playlist list in sidebar" - title="Sidebar playlist list" + description={t('setting.sidebarPlaylistList', { + context: 'description', + postProcess: 'sentenceCase', + })} + title={t('setting.sidebarPlaylistList', { postProcess: 'sentenceCase' })} /> { onChange={handleSetSidebarCollapsedNavigation} /> } - description="Show navigation buttons in the collapsed sidebar" - title="Sidebar (collapsed) navigation" + description={t('setting.sidebarPlaylistList', { + context: 'description', + postProcess: 'sentenceCase', + })} + title={t('setting.sidebarCollapsedNavigation', { postProcess: 'sentenceCase' })} /> { Save sidebar configuration } - description="Select the items and order in which they appear in the sidebar" - title="Sidebar configuration" + description={t('setting.sidebarCollapsedNavigation', { + context: 'description', + postProcess: 'sentenceCase', + })} + title={t('setting.sidebarConfiguration', { postProcess: 'sentenceCase' })} /> { + const { t } = useTranslation(); const settings = useGeneralSettings(); const { setSettings } = useSettingsStoreActions(); @@ -27,9 +29,12 @@ export const ThemeSettings = () => { }} /> ), - description: 'Follows the system-defined light or dark preference', + description: t('setting.useSystemTheme', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: false, - title: 'Use system theme', + title: t('setting.useSystemTheme', { postProcess: 'sentenceCase' }), }, { control: ( @@ -46,9 +51,12 @@ export const ThemeSettings = () => { }} /> ), - description: 'Sets the default theme', + description: t('setting.theme', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: settings.followSystemTheme, - title: 'Theme', + title: t('setting.theme', { postProcess: 'sentenceCase' }), }, { control: ( @@ -65,9 +73,12 @@ export const ThemeSettings = () => { }} /> ), - description: 'Sets the dark theme', + description: t('setting.themeDark', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !settings.followSystemTheme, - title: 'Theme (dark)', + title: t('setting.themeDark', { postProcess: 'sentenceCase' }), }, { control: ( @@ -84,9 +95,12 @@ export const ThemeSettings = () => { }} /> ), - description: 'Sets the light theme', + description: t('setting.themeLight', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !settings.followSystemTheme, - title: 'Theme (light)', + title: t('setting.themeLight', { postProcess: 'sentenceCase' }), }, { control: ( @@ -114,8 +128,11 @@ export const ThemeSettings = () => { {settings.accent} ), - description: 'Sets the accent color', - title: 'Accent color', + description: t('setting.accentColor', { + context: 'description', + postProcess: 'sentenceCase', + }), + title: t('setting.accentColor', { postProcess: 'sentenceCase' }), }, ]; diff --git a/src/renderer/features/settings/components/hotkeys/hotkey-manager-settings.tsx b/src/renderer/features/settings/components/hotkeys/hotkey-manager-settings.tsx index 9e95991df..9c0bc8ddb 100644 --- a/src/renderer/features/settings/components/hotkeys/hotkey-manager-settings.tsx +++ b/src/renderer/features/settings/components/hotkeys/hotkey-manager-settings.tsx @@ -2,48 +2,74 @@ import { useCallback, useMemo, useState, KeyboardEvent, ChangeEvent } from 'reac import { Group } from '@mantine/core'; import isElectron from 'is-electron'; import debounce from 'lodash/debounce'; +import { useTranslation } from 'react-i18next'; import { RiDeleteBinLine, RiEditLine, RiKeyboardBoxLine } from 'react-icons/ri'; import styled from 'styled-components'; import { Button, TextInput, Checkbox } from '/@/renderer/components'; import { BindingActions, useHotkeySettings, useSettingsStoreActions } from '/@/renderer/store'; import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option'; +import i18n from '/@/i18n/i18n'; const ipc = isElectron() ? window.electron.ipc : null; const BINDINGS_MAP: Record = { - browserBack: 'Browser back', - browserForward: 'Browser forward', + browserBack: i18n.t('setting.hotkey', { context: 'browserBack', postProcess: 'sentenceCase' }), + browserForward: i18n.t('setting.hotkey', { + context: 'browserForward', + postProcess: 'sentenceCase', + }), favoriteCurrentAdd: 'Favorite current song', favoriteCurrentRemove: 'Unfavorite current song', favoriteCurrentToggle: 'Toggle current song favorite', favoritePreviousAdd: 'Favorite previous song', favoritePreviousRemove: 'Unfavorite previous song', favoritePreviousToggle: 'Toggle previous song favorite', - globalSearch: 'Global search', - localSearch: 'In-page search', - next: 'Next track', - pause: 'Pause', - play: 'Play', - playPause: 'Play / Pause', - previous: 'Previous track', - rate0: 'Rating clear', - rate1: 'Rating 1 star', - rate2: 'Rating 2 star', - rate3: 'Rating 3 star', - rate4: 'Rating 4 star', - rate5: 'Rating 5 star', - skipBackward: 'Skip backward', - skipForward: 'Skip forward', - stop: 'Stop', - toggleFullscreenPlayer: 'Toggle fullscreen player', - toggleQueue: 'Toggle queue', - toggleRepeat: 'Toggle repeat', - toggleShuffle: 'Toggle shuffle', - volumeDown: 'Volume down', - volumeMute: 'Volume mute', - volumeUp: 'Volume up', - zoomIn: 'Zoom in', - zoomOut: 'Zoom out', + globalSearch: i18n.t('setting.hotkey', { + context: 'globalSearch', + postProcess: 'sentenceCase', + }), + localSearch: i18n.t('setting.hotkey', { context: 'localSearch', postProcess: 'sentenceCase' }), + next: i18n.t('setting.hotkey', { context: 'playbackNext', postProcess: 'sentenceCase' }), + pause: i18n.t('setting.hotkey', { context: 'playbackPause', postProcess: 'sentenceCase' }), + play: i18n.t('setting.hotkey', { context: 'playbackPlay', postProcess: 'sentenceCase' }), + playPause: i18n.t('setting.hotkey', { + context: 'playbackPlayPause', + postProcess: 'sentenceCase', + }), + previous: i18n.t('setting.hotkey', { + context: 'playbackPrevious', + postProcess: 'sentenceCase', + }), + rate0: i18n.t('setting.hotkey', { context: 'rate0', postProcess: 'sentenceCase' }), + rate1: i18n.t('setting.hotkey', { context: 'rate1', postProcess: 'sentenceCase' }), + rate2: i18n.t('setting.hotkey', { context: 'rate2', postProcess: 'sentenceCase' }), + rate3: i18n.t('setting.hotkey', { context: 'rate3', postProcess: 'sentenceCase' }), + rate4: i18n.t('setting.hotkey', { context: 'rate4', postProcess: 'sentenceCase' }), + rate5: i18n.t('setting.hotkey', { context: 'rate5', postProcess: 'sentenceCase' }), + skipBackward: i18n.t('setting.hotkey', { + context: 'skipBackward', + postProcess: 'sentenceCase', + }), + skipForward: i18n.t('setting.hotkey', { context: 'skipForward', postProcess: 'sentenceCase' }), + stop: i18n.t('setting.hotkey', { context: 'playbackStop', postProcess: 'sentenceCase' }), + toggleFullscreenPlayer: i18n.t('setting.hotkey', { + context: 'toggleFullScreenPlayer', + postProcess: 'sentenceCase', + }), + toggleQueue: i18n.t('setting.hotkey', { context: 'toggleQueue', postProcess: 'sentenceCase' }), + toggleRepeat: i18n.t('setting.hotkey', { + context: 'toggleRepeat', + postProcess: 'sentenceCase', + }), + toggleShuffle: i18n.t('setting.hotkey', { + context: 'toggleShuffle', + postProcess: 'sentenceCase', + }), + volumeDown: i18n.t('setting.hotkey', { context: 'volumeDown', postProcess: 'sentenceCase' }), + volumeMute: i18n.t('setting.hotkey', { context: 'volumeMute', postProcess: 'sentenceCase' }), + volumeUp: i18n.t('setting.hotkey', { context: 'volumeUp', postProcess: 'sentenceCase' }), + zoomIn: i18n.t('setting.hotkey', { context: 'zoomIn', postProcess: 'sentenceCase' }), + zoomOut: i18n.t('setting.hotkey', { context: 'zoomOut', postProcess: 'sentenceCase' }), }; const HotkeysContainer = styled.div` @@ -59,6 +85,7 @@ const HotkeysContainer = styled.div` `; export const HotkeyManagerSettings = () => { + const { t } = useTranslation(); const { bindings, globalMediaHotkeys } = useHotkeySettings(); const { setSettings } = useSettingsStoreActions(); const [selected, setSelected] = useState(null); @@ -175,8 +202,11 @@ export const HotkeyManagerSettings = () => { <> } - description="Configure application hotkeys. Toggle the checkbox to set as a global hotkey (desktop only)" - title="Application hotkeys" + description={t('setting.applicationHotkeys', { + context: 'description', + postProcess: 'sentenceCase', + })} + title={t('setting.applicationHotkeys', { postProcess: 'sentenceCase' })} /> {Object.keys(bindings) diff --git a/src/renderer/features/settings/components/hotkeys/window-hotkey-settings.tsx b/src/renderer/features/settings/components/hotkeys/window-hotkey-settings.tsx index 00d23ed84..f6b98dabe 100644 --- a/src/renderer/features/settings/components/hotkeys/window-hotkey-settings.tsx +++ b/src/renderer/features/settings/components/hotkeys/window-hotkey-settings.tsx @@ -1,4 +1,5 @@ import isElectron from 'is-electron'; +import { useTranslation } from 'react-i18next'; import { SettingOption, SettingsSection } from '../settings-section'; import { Switch } from '/@/renderer/components'; import { useHotkeySettings, useSettingsStoreActions } from '/@/renderer/store'; @@ -6,6 +7,7 @@ import { useHotkeySettings, useSettingsStoreActions } from '/@/renderer/store'; const localSettings = isElectron() ? window.electron.localSettings : null; export const WindowHotkeySettings = () => { + const { t } = useTranslation(); const settings = useHotkeySettings(); const { setSettings } = useSettingsStoreActions(); @@ -13,7 +15,6 @@ export const WindowHotkeySettings = () => { { control: ( { @@ -33,10 +34,12 @@ export const WindowHotkeySettings = () => { }} /> ), - description: - 'Enable or disable the usage of your system media hotkeys to control the audio player', + description: t('setting.globalMediaHotkeys', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !isElectron(), - title: 'Global media hotkeys', + title: t('setting.globalMediaHotkeys', { postProcess: 'sentenceCase' }), }, ]; diff --git a/src/renderer/features/settings/components/playback/audio-settings.tsx b/src/renderer/features/settings/components/playback/audio-settings.tsx index 7d9467615..285752797 100644 --- a/src/renderer/features/settings/components/playback/audio-settings.tsx +++ b/src/renderer/features/settings/components/playback/audio-settings.tsx @@ -9,6 +9,7 @@ import { import { useCurrentStatus, usePlayerStore } from '/@/renderer/store'; import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store'; import { PlaybackType, PlayerStatus, PlaybackStyle, CrossfadeStyle } from '/@/renderer/types'; +import { useTranslation } from 'react-i18next'; const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; @@ -18,6 +19,7 @@ const getAudioDevice = async () => { }; export const AudioSettings = () => { + const { t } = useTranslation(); const settings = usePlaybackSettings(); const { setSettings } = useSettingsStoreActions(); const status = useCurrentStatus(); @@ -30,13 +32,17 @@ export const AudioSettings = () => { .then((dev) => setAudioDevices(dev.map((d) => ({ label: d.label, value: d.deviceId }))), ) - .catch(() => toast.error({ message: 'Error fetching audio devices' })); + .catch(() => + toast.error({ + message: t('error.audioDeviceFetchError', { postProcess: 'sentenceCase' }), + }), + ); }; if (settings.type === PlaybackType.WEB) { getAudioDevices(); } - }, [settings.type]); + }, [settings.type, t]); const audioOptions: SettingOption[] = [ { @@ -61,10 +67,16 @@ export const AudioSettings = () => { }} /> ), - description: 'The audio player to use for playback', + description: t('setting.audioPlayer', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !isElectron(), - note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined, - title: 'Audio player', + note: + status === PlayerStatus.PLAYING + ? t('common.playerMustBePaused', { postProcess: 'sentenceCase' }) + : undefined, + title: t('setting.audioPlayer', { postProcess: 'sentenceCase' }), }, { control: ( @@ -76,16 +88,31 @@ export const AudioSettings = () => { onChange={(e) => setSettings({ playback: { ...settings, audioDeviceId: e } })} /> ), - description: 'The audio device to use for playback (web player only)', + description: t('setting.audioDevice', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: !isElectron() || settings.type !== PlaybackType.WEB, - title: 'Audio device', + title: t('setting.audioDevice', { postProcess: 'sentenceCase' }), }, { control: ( handleSetMpvProperty('gaplessAudio', e)} /> ), - description: - 'Try to play consecutive audio files with no silence or disruption at the point of file change (--gapless-audio)', + description: t('setting.gaplessAudio', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: settings.type !== PlaybackType.LOCAL, - title: 'Gapless audio', + title: t('setting.gaplessAudio', { postProcess: 'sentenceCase' }), }, { control: ( @@ -193,10 +214,12 @@ export const MpvSettings = () => { }} /> ), - description: - 'Select the output sample rate to be used if the sample frequency selected is different from that of the current media', + description: t('setting.sampleRate', { + context: 'description', + postProcess: 'sentenceCase', + }), note: 'Page refresh required for web player', - title: 'Sample rate', + title: t('setting.sampleRate', { postProcess: 'sentenceCase' }), }, { control: ( @@ -211,10 +234,12 @@ export const MpvSettings = () => { /> ), - description: - 'Enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio (--audio-exclusive)', + description: t('setting.audioExclusiveMode', { + context: 'description', + postProcess: 'sentenceCase', + }), isHidden: settings.type !== PlaybackType.LOCAL, - title: 'Audio exclusive mode', + title: t('setting.audioExclusiveMode', { postProcess: 'sentenceCase' }), }, ]; @@ -223,18 +248,42 @@ export const MpvSettings = () => { control: (