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

@ -10,8 +10,10 @@ import {
useGeneralSettings,
useSettingsStoreActions,
} from '/@/renderer/store/settings.store';
import { useTranslation } from 'react-i18next';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { FontType } from '/@/renderer/types';
import i18n, { languages } from '/@/i18n/i18n';
const localSettings = isElectron() ? window.electron.localSettings : null;
const ipc = isElectron() ? window.electron.ipc : null;
@ -33,17 +35,32 @@ const FONT_OPTIONS: Font[] = [
{ label: 'Work Sans', value: 'Work Sans' },
];
const FONT_TYPES: Font[] = [{ label: 'Built-in font', value: FontType.BUILT_IN }];
const FONT_TYPES: Font[] = [
{
label: i18n.t('setting.fontType', {
context: 'optionBuiltIn',
postProcess: 'sentenceCase',
}),
value: FontType.BUILT_IN,
},
];
if (window.queryLocalFonts) {
FONT_TYPES.push({ label: 'System font', value: FontType.SYSTEM });
FONT_TYPES.push({
label: i18n.t('setting.fontType', { context: 'optionSystem', postProcess: 'sentenceCase' }),
value: FontType.SYSTEM,
});
}
if (isElectron()) {
FONT_TYPES.push({ label: 'Custom font', value: FontType.CUSTOM });
FONT_TYPES.push({
label: i18n.t('setting.fontType', { context: 'optionCustom', postProcess: 'sentenceCase' }),
value: FontType.CUSTOM,
});
}
export const ApplicationSettings = () => {
const { t } = useTranslation();
const settings = useGeneralSettings();
const fontSettings = useFontSettings();
const { setSettings } = useSettingsStoreActions();
@ -100,7 +117,9 @@ export const ApplicationSettings = () => {
const status = await navigator.permissions.query({ name: 'local-fonts' });
if (status.state === 'denied') {
throw new Error('Access denied to local fonts');
throw new Error(
t('error.localFontAccessDenied', { postProcess: 'sentenceCase' }),
);
}
const data = await window.queryLocalFonts();
@ -112,7 +131,7 @@ export const ApplicationSettings = () => {
);
} catch (error) {
toast.error({
message: 'An error occurred when trying to get system fonts',
message: t('error.systemFontError', { postProcess: 'sentenceCase' }),
});
setSettings({
@ -125,19 +144,32 @@ export const ApplicationSettings = () => {
}
};
getFonts();
}, [fontSettings, localFonts, setSettings]);
}, [fontSettings, localFonts, setSettings, t]);
const handleChangeLanguage = (e: string) => {
setSettings({
general: {
...settings,
language: e,
},
});
};
const options: SettingOption[] = [
{
control: (
<Select
disabled
data={[]}
data={languages}
value={settings.language}
onChange={handleChangeLanguage}
/>
),
description: 'Sets the application language',
description: t('setting.language', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: 'Language',
title: t('setting.language', { postProcess: 'sentenceCase' }),
},
{
control: (
@ -155,10 +187,12 @@ export const ApplicationSettings = () => {
}}
/>
),
description:
'What font to use. Built-in font selects one of the fonts provided by Feishin. System font allows you to select any font provided by your OS. Custom allows you to provide your own font',
description: t('setting.fontType', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: FONT_TYPES.length === 1,
title: 'Use system font',
title: t('setting.fontType', { postProcess: 'sentenceCase' }),
},
{
control: (
@ -177,9 +211,9 @@ export const ApplicationSettings = () => {
}}
/>
),
description: 'Sets the application content font',
description: t('setting.font', { context: 'description', postProcess: 'sentenceCase' }),
isHidden: localFonts && fontSettings.type !== FontType.BUILT_IN,
title: 'Font (Content)',
title: t('setting.font', { postProcess: 'sentenceCase' }),
},
{
control: (
@ -199,9 +233,9 @@ export const ApplicationSettings = () => {
}}
/>
),
description: 'Sets the application content font',
description: t('setting.font', { context: 'description', postProcess: 'sentenceCase' }),
isHidden: !localFonts || fontSettings.type !== FontType.SYSTEM,
title: 'Font (Content)',
title: t('setting.font', { postProcess: 'sentenceCase' }),
},
{
control: (
@ -219,9 +253,12 @@ export const ApplicationSettings = () => {
}
/>
),
description: 'Path to custom font',
description: t('setting.customFontPath', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: fontSettings.type !== FontType.CUSTOM,
title: 'Path to custom font',
title: t('setting.customFontPath', { postProcess: 'sentenceCase' }),
},
{
control: (
@ -244,9 +281,14 @@ export const ApplicationSettings = () => {
}}
/>
),
description: 'Sets the application zoom factor in percent',
description: t('setting.zoom', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: 'Zoom factor',
title: t('setting.zoom', {
postProcess: 'sentenceCase',
}),
},
];

View file

@ -1,5 +1,6 @@
import isElectron from 'is-electron';
import { Group } from '@mantine/core';
import { t } from 'i18next';
import isElectron from 'is-electron';
import { Select, Tooltip, NumberInput, Switch, Slider } from '/@/renderer/components';
import { SettingsSection } from '/@/renderer/features/settings/components/settings-section';
import {
@ -8,15 +9,29 @@ import {
useSettingsStoreActions,
} from '/@/renderer/store/settings.store';
import { Play } from '/@/renderer/types';
import { useTranslation } from 'react-i18next';
const localSettings = isElectron() ? window.electron.localSettings : null;
const SIDE_QUEUE_OPTIONS = [
{ label: 'Fixed', value: 'sideQueue' },
{ label: 'Floating', value: 'sideDrawerQueue' },
{
label: t('setting.sidePlayQueueStyle', {
context: 'optionAttached',
postProcess: 'sentenceCase',
}),
value: 'sideQueue',
},
{
label: t('setting.sidePlayQueueStyle', {
context: 'optionDetached',
postProcess: 'sentenceCase',
}),
value: 'sideDrawerQueue',
},
];
export const ControlSettings = () => {
const { t } = useTranslation();
const settings = useGeneralSettings();
const { setSettings } = useSettingsStoreActions();
@ -39,14 +54,17 @@ export const ControlSettings = () => {
}
/>
),
description: 'Show or hide the skip buttons on the playerbar',
description: t('setting.showSkipButtons', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: 'Show skip buttons',
title: t('setting.showSkipButtons', { postProcess: 'sentenceCase' }),
},
{
control: (
<Group>
<Tooltip label="Backward">
<Tooltip label={t('common.backward', { postProcess: 'titleCase' })}>
<NumberInput
defaultValue={settings.skipButtons.skipBackwardSeconds}
min={0}
@ -66,7 +84,7 @@ export const ControlSettings = () => {
}
/>
</Tooltip>
<Tooltip label="Forward">
<Tooltip label={t('common.forward', { postProcess: 'titleCase' })}>
<NumberInput
defaultValue={settings.skipButtons.skipForwardSeconds}
min={0}
@ -88,18 +106,38 @@ export const ControlSettings = () => {
</Tooltip>
</Group>
),
description:
'The number (in seconds) to skip forward or backward when using the skip buttons',
description: t('setting.skipDuration', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: 'Skip duration',
title: t('setting.skipDuration', { postProcess: 'sentenceCase' }),
},
{
control: (
<Select
data={[
{ label: 'Now', value: Play.NOW },
{ label: 'Next', value: Play.NEXT },
{ label: 'Last', value: Play.LAST },
{
label: t('setting.playButtonBehavior', {
context: 'optionPlay',
postProcess: 'titleCase',
}),
value: Play.NOW,
},
{
label: t('setting.playButtonBehavior', {
context: 'optionAddNext',
postProcess: 'titleCase',
}),
value: Play.NEXT,
},
{
label: t('setting.playButtonBehavior', {
context: 'optionAddLast',
postProcess: 'titleCase',
}),
value: Play.LAST,
},
]}
defaultValue={settings.playButtonBehavior}
onChange={(e) =>
@ -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' }),
},
];

View file

@ -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: (
<Switch
aria-label="Enable remote control server"
defaultChecked={settings.enabled}
onChange={async (e) => {
const enabled = e.currentTarget.checked;
@ -65,8 +68,15 @@ export const RemoteSettings = () => {
/>
),
description: (
<div>
Start an HTTP server to remotely control Feishin. This will listen on{' '}
<Text
$noSelect
$secondary
size="sm"
>
{t('setting.enableRemote', {
context: 'description',
postProcess: 'sentenceCase',
})}{' '}
<a
href={url}
rel="noreferrer noopener"
@ -74,15 +84,14 @@ export const RemoteSettings = () => {
>
{url}
</a>
</div>
</Text>
),
isHidden,
title: 'Enable remote control',
title: t('setting.enableRemote', { postProcess: 'sentenceCase' }),
},
{
control: (
<NumberInput
aria-label="Set remote port"
max={65535}
value={settings.port}
onBlur={async (e) => {
@ -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: (
<TextInput
aria-label="Set remote username"
defaultValue={settings.username}
onBlur={(e) => {
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: (
<TextInput
aria-label="Set remote password"
defaultValue={settings.password}
onBlur={(e) => {
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 (
<>
<SettingsSection options={controlOptions} />
<Text size="lg">
<b>
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
</b>
</Text>
</>
);
return <SettingsSection options={controlOptions} />;
};

View file

@ -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' })}
/>
<SettingsOptions
control={
@ -117,8 +122,11 @@ export const SidebarSettings = () => {
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' })}
/>
<SettingsOptions
control={
@ -128,11 +136,14 @@ export const SidebarSettings = () => {
variant="filled"
onClick={handleSave}
>
Save sidebar configuration
{t('common.save', { postProcess: 'titleCase' })}
</Button>
}
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' })}
/>
<Reorder.Group
axis="y"

View file

@ -7,8 +7,10 @@ import {
import { THEME_DATA } from '/@/renderer/hooks';
import { useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import { AppTheme } from '/@/renderer/themes/types';
import { useTranslation } from 'react-i18next';
export const ThemeSettings = () => {
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 = () => {
<Text>{settings.accent}</Text>
</Stack>
),
description: 'Sets the accent color',
title: 'Accent color',
description: t('setting.accentColor', {
context: 'description',
postProcess: 'sentenceCase',
}),
title: t('setting.accentColor', { postProcess: 'sentenceCase' }),
},
];