mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 10:23:33 +00:00
Add localization support (#333)
* Add updated i18n config and en locale
This commit is contained in:
parent
11863fd4c1
commit
8430b1ec95
90 changed files with 2679 additions and 908 deletions
|
|
@ -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: (
|
||||
<Select
|
||||
data={[
|
||||
{ label: 'Normal', value: PlaybackStyle.GAPLESS },
|
||||
{ label: 'Crossfade', value: PlaybackStyle.CROSSFADE },
|
||||
{
|
||||
label: t('setting.playbackStyle', {
|
||||
context: 'optionNormal',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: PlaybackStyle.GAPLESS,
|
||||
},
|
||||
{
|
||||
label: t('setting.playbackStyle', {
|
||||
context: 'optionCrossFade',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: PlaybackStyle.CROSSFADE,
|
||||
},
|
||||
]}
|
||||
defaultValue={settings.style}
|
||||
disabled={settings.type !== PlaybackType.WEB || status === PlayerStatus.PLAYING}
|
||||
|
|
@ -94,10 +121,16 @@ export const AudioSettings = () => {
|
|||
}
|
||||
/>
|
||||
),
|
||||
description: 'Adjust the playback style (web player only)',
|
||||
description: t('setting.playbackStyle', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.WEB,
|
||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||
title: 'Playback style',
|
||||
title: t('setting.playbackStyle', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -116,10 +149,15 @@ export const AudioSettings = () => {
|
|||
}
|
||||
/>
|
||||
),
|
||||
description: 'Adjust the crossfade duration (web player only)',
|
||||
description: t('setting.crossfadeDuration', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.WEB,
|
||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||
title: 'Crossfade Duration',
|
||||
title: t('setting.crossfadeDuration', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -153,10 +191,13 @@ export const AudioSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Change the crossfade algorithm (web player only)',
|
||||
description: t('setting.crossfadeStyle', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.WEB,
|
||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||
title: 'Crossfade Style',
|
||||
title: t('setting.crossfadeStyle', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { MultiSelect, MultiSelectProps, NumberInput, Switch } from '/@/renderer/
|
|||
import isElectron from 'is-electron';
|
||||
import styled from 'styled-components';
|
||||
import { LyricSource } from '/@/renderer/api/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ const WorkingButtonSelect = styled(MultiSelect)<MultiSelectProps>`
|
|||
`;
|
||||
|
||||
export const LyricSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = useLyricsSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
|
|
@ -36,8 +38,11 @@ export const LyricSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Enable or disable following of current lyric',
|
||||
title: 'Follow current lyric',
|
||||
description: t('setting.followLyric', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.followLyric', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -54,9 +59,12 @@ export const LyricSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Enable or disable fetching lyrics for the current song',
|
||||
description: t('setting.lyricFetch', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Fetch lyrics from the internet',
|
||||
title: t('setting.lyricFetch', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -77,10 +85,12 @@ export const LyricSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Lyric fetchers should be added in order of preference. This is the order in which they will be queried.',
|
||||
description: t('setting.lyricFetchProvider', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Providers to fetch lyrics',
|
||||
title: t('setting.lyricFetchProvider', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -99,10 +109,12 @@ export const LyricSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Lyric offset (in milliseconds). Positive values mean that lyrics are shown later, and negative mean that lyrics are shown earlier',
|
||||
description: t('setting.lyricOffset', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Lyric offset',
|
||||
title: t('setting.lyricOffset', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { PlaybackType } from '/@/renderer/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
|
|
@ -60,6 +61,7 @@ export const getMpvProperties = (settings: SettingsState['playback']['mpvPropert
|
|||
};
|
||||
|
||||
export const MpvSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = usePlaybackSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
|
|
@ -116,10 +118,13 @@ export const MpvSettings = () => {
|
|||
onChange={handleSetMpvPath}
|
||||
/>
|
||||
),
|
||||
description: 'The location of your mpv executable',
|
||||
description: t('setting.mpvExecutablePath', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
note: 'Restart required',
|
||||
title: 'MPV executable path',
|
||||
title: t('setting.mpvExecutablePath', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -128,9 +133,10 @@ export const MpvSettings = () => {
|
|||
autosize
|
||||
defaultValue={settings.mpvExtraParameters.join('\n')}
|
||||
minRows={4}
|
||||
placeholder={
|
||||
'(Add one per line):\n--gapless-audio=weak\n--prefetch-playlist=yes'
|
||||
}
|
||||
placeholder={`(${t('setting.mpvExtraParameters', {
|
||||
context: 'help',
|
||||
postProcess: 'sentenceCase',
|
||||
})}):\n--gapless-audio=weak\n--prefetch-playlist=yes`}
|
||||
width={225}
|
||||
onBlur={(e) => {
|
||||
handleSetExtraParameters(e.currentTarget.value.split('\n'));
|
||||
|
|
@ -145,7 +151,10 @@ export const MpvSettings = () => {
|
|||
$secondary
|
||||
size="sm"
|
||||
>
|
||||
Options to pass to the player
|
||||
{t('setting.mpvExtraParameters', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
<a
|
||||
|
|
@ -159,8 +168,12 @@ export const MpvSettings = () => {
|
|||
</Stack>
|
||||
),
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
note: 'Restart required',
|
||||
title: 'MPV parameters',
|
||||
note: t('common.restartRequired', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.mpvExtraParameters', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -169,18 +182,26 @@ export const MpvSettings = () => {
|
|||
control: (
|
||||
<Select
|
||||
data={[
|
||||
{ label: 'No', value: 'no' },
|
||||
{ label: 'Yes', value: 'yes' },
|
||||
{ label: 'Weak (recommended)', value: 'weak' },
|
||||
{ label: t('common.no', { postProcess: 'titleCase' }), value: 'no' },
|
||||
{ label: t('common.yes', { postProcess: 'titleCase' }), value: 'yes' },
|
||||
{
|
||||
label: t('setting.gaplessAudio', {
|
||||
context: 'optionWeak',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
value: 'weak',
|
||||
},
|
||||
]}
|
||||
defaultValue={settings.mpvProperties.gaplessAudio}
|
||||
onChange={(e) => 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: (
|
||||
<Select
|
||||
data={[
|
||||
{ label: 'None', value: 'no' },
|
||||
{ label: 'Track', value: 'track' },
|
||||
{ label: 'Album', value: 'album' },
|
||||
{
|
||||
label: t('setting.replayGainMode', {
|
||||
context: 'optionNone',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: 'no',
|
||||
},
|
||||
{
|
||||
label: t('setting.replayGainMode', {
|
||||
context: 'optionTrack',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: 'track',
|
||||
},
|
||||
{
|
||||
label: t('setting.replayGainMode', {
|
||||
context: 'optionAlbum',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: 'album',
|
||||
},
|
||||
]}
|
||||
defaultValue={settings.mpvProperties.replayGainMode}
|
||||
onChange={(e) => handleSetMpvProperty('replayGainMode', e)}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Adjust volume gain according to replaygain values stored in the file metadata (--replaygain)',
|
||||
note: 'Restart required',
|
||||
title: 'ReplayGain mode',
|
||||
description: t('setting.replayGainMode', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
note: t('common.restartRequired', { postProcess: 'sentenceCase' }),
|
||||
title: t('setting.replayGainMode', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -244,9 +293,15 @@ export const MpvSettings = () => {
|
|||
onChange={(e) => handleSetMpvProperty('replayGainPreampDB', e)}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Pre-amplification gain in dB to apply to the selected replaygain gain (--replaygain-preamp)',
|
||||
title: 'ReplayGain preamp (dB)',
|
||||
description: t('setting.replayGainMode', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.replayGainPreamp', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -257,9 +312,14 @@ export const MpvSettings = () => {
|
|||
}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Prevent clipping caused by replaygain by automatically lowering the gain (--replaygain-clip)',
|
||||
title: 'ReplayGain clipping',
|
||||
description: t('setting.replayGainClipping', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.replayGainClipping', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -269,9 +329,14 @@ export const MpvSettings = () => {
|
|||
onBlur={(e) => handleSetMpvProperty('replayGainFallbackDB', e)}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Gain in dB to apply if the file has no replay gain tags. This option is always applied if the replaygain logic is somehow inactive. If this is applied, no other replaygain options are applied',
|
||||
title: 'ReplayGain fallback (dB)',
|
||||
description: t('setting.replayGainFallback', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.replayGainFallback', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,8 @@ export const PlaybackTab = () => {
|
|||
<AudioSettings />
|
||||
<Suspense fallback={<></>}>{hasFancyAudio && <MpvSettings />}</Suspense>
|
||||
<Divider />
|
||||
{isElectron() && (
|
||||
<>
|
||||
<ScrobbleSettings />
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
<ScrobbleSettings />
|
||||
<Divider />
|
||||
<LyricSettings />
|
||||
</Stack>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { NumberInput, Slider, Switch, Text } from '/@/renderer/components';
|
||||
import { NumberInput, Slider, Switch } from '/@/renderer/components';
|
||||
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { SettingOption, SettingsSection } from '../settings-section';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const ScrobbleSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = usePlaybackSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
|
|
@ -25,8 +27,11 @@ export const ScrobbleSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Enable or disable scrobbling to your media server',
|
||||
title: 'Scrobble',
|
||||
description: t('setting.scrobble', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.scrobble', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -50,9 +55,11 @@ export const ScrobbleSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'The percentage of the song that must be played before submitting a scrobble',
|
||||
title: 'Minimum scrobble percentage*',
|
||||
description: t('setting.minimumScrobblePercentage', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.minimumScrobblePercentage', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -76,21 +83,13 @@ export const ScrobbleSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'The duration in seconds of a song that must be played before submitting a scrobble',
|
||||
title: 'Minimum scrobble duration (seconds)*',
|
||||
description: t('setting.minimumScrobblePercentage', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.minimumScrobbleSeconds', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSection options={scrobbleOptions} />
|
||||
<Text
|
||||
$secondary
|
||||
size="sm"
|
||||
>
|
||||
*The scrobble will be submitted if one or more of the above conditions is met
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
return <SettingsSection options={scrobbleOptions} />;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue