Add translation for module, fix language changer

This commit is contained in:
ajnart
2022-08-25 11:07:25 +02:00
parent 53500ffabc
commit 2ad51411f5
42 changed files with 138 additions and 117 deletions

View File

@@ -1,7 +0,0 @@
{
"states": {
"online": "Online {{response}}",
"offline": "Offline {{response}}",
"loading": "Loading..."
}
}

View File

@@ -0,0 +1,11 @@
{
"descriptor": {
"name": "Ping",
"description": "Allows you to check if the service is up or returns a specific HTTP status code."
},
"states": {
"online": "Online {{response}}",
"offline": "Offline {{response}}",
"loading": "Loading..."
}
}

View File

@@ -126,7 +126,7 @@ const AppShelf = (props: any) => {
const noCategory = config.services.filter(
(e) => e.category === undefined || e.category === null
);
const downloadEnabled = config.modules?.[DownloadsModule.title]?.enabled ?? false;
const downloadEnabled = config.modules?.[DownloadsModule.id]?.enabled ?? false;
// Create an item with 0: true, 1: true, 2: true... For each category
return (
// TODO: Style accordion so that the bar is transparent to the user settings

View File

@@ -4,14 +4,17 @@ import { showNotification } from '@mantine/notifications';
import { forwardRef, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { getCookie, setCookie } from 'cookies-next';
import { getLanguageByCode, Language } from '../../languages/language';
export default function LanguageSwitch() {
const { t, i18n } = useTranslation('settings/general/internationalization');
const { changeLanguage } = i18n;
const configLocale = getCookie('config-locale');
const { locale, locales } = useRouter();
const [selectedLanguage, setSelectedLanguage] = useState<string | null | undefined>(locale);
const [selectedLanguage, setSelectedLanguage] = useState<string | undefined>(
(configLocale as string) ?? locale
);
const data = locales
? locales.map((localeItem) => ({
@@ -26,9 +29,13 @@ export default function LanguageSwitch() {
setSelectedLanguage(value);
const newLanguage = getLanguageByCode(value);
changeLanguage(value)
.then(() => {
setCookie('config-locale', value, {
maxAge: 60 * 60 * 24 * 30,
sameSite: 'strict',
});
showNotification({
title: 'Language changed',
message: `You changed the language to '${newLanguage.originalName}'`,

View File

@@ -1,5 +1,4 @@
import { Checkbox, Popover, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { Checkbox, HoverCard, SimpleGrid, Stack, Text, Title } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import * as Modules from '../../modules';
import { IModule } from '../../modules/ModuleTypes';
@@ -11,9 +10,9 @@ export default function ModuleEnabler(props: any) {
return (
<Stack>
<Title order={4}>{t('title')}</Title>
<SimpleGrid cols={3} spacing="xs">
<SimpleGrid cols={3} spacing="sm">
{modules.map((module) => (
<ModuleToggle module={module} />
<ModuleToggle key={module.id} module={module} />
))}
</SimpleGrid>
</Stack>
@@ -22,18 +21,16 @@ export default function ModuleEnabler(props: any) {
const ModuleToggle = ({ module }: { module: IModule }) => {
const { config, setConfig } = useConfig();
const { t: translationModules } = useTranslation(module.translationNamespace);
const [opened, { close, open }] = useDisclosure(false);
const { t } = useTranslation(`modules/${module.id}`);
return (
<Popover opened={opened} withArrow withinPortal width={200}>
<Popover.Target>
<div onMouseEnter={open} onMouseLeave={close}>
<HoverCard withArrow withinPortal width={200} shadow="md" openDelay={200}>
<HoverCard.Target>
<Checkbox
key={module.title}
key={module.id}
size="md"
checked={config.modules?.[module.title]?.enabled ?? false}
label={translationModules('descriptor.name', {
checked={config.modules?.[module.id]?.enabled ?? false}
label={t('descriptor.name', {
defaultValue: 'Unknown',
})}
onChange={(e) => {
@@ -41,20 +38,19 @@ const ModuleToggle = ({ module }: { module: IModule }) => {
...config,
modules: {
...config.modules,
[module.title]: {
...config.modules?.[module.title],
[module.id]: {
...config.modules?.[module.id],
enabled: e.currentTarget.checked,
},
},
});
}}
/>
</div>
</Popover.Target>
<Popover.Dropdown>
<Text weight="bold">{translationModules('descriptor.name')}</Text>
<Text>{translationModules('descriptor.description')}</Text>
</Popover.Dropdown>
</Popover>
</HoverCard.Target>
<HoverCard.Dropdown>
<Title order={4}>{t('descriptor.name')}</Title>
<Text size="sm">{t('descriptor.description')}</Text>
</HoverCard.Dropdown>
</HoverCard>
);
};

View File

@@ -6,11 +6,11 @@ import { TablerIcon } from '@tabler/icons';
// Note: Maybe use context to keep track of the modules
export interface IModule {
id: string;
title: string;
icon: TablerIcon;
component: React.ComponentType;
options?: Option;
translationNamespace: string;
}
interface Option {

View File

@@ -34,7 +34,7 @@ export const CalendarModule: IModule = {
value: false,
},
},
translationNamespace: 'modules/calendar-module',
id: 'calendar',
};
export default function CalendarComponent(props: any) {
@@ -127,7 +127,7 @@ export default function CalendarComponent(props: any) {
}, [config.services]);
const weekStartsAtSunday =
(config?.modules?.[CalendarModule.title]?.options?.sundaystart?.value as boolean) ?? false;
(config?.modules?.[CalendarModule.id]?.options?.sundaystart?.value as boolean) ?? false;
return (
<Calendar
firstDayOfWeek={weekStartsAtSunday ? 'sunday' : 'monday'}

View File

@@ -160,7 +160,7 @@ export function SonarrMediaDisplay(props: any) {
export function MediaDisplay({ media }: { media: IMedia }) {
const [opened, setOpened] = useState(false);
const { secondaryColor } = useColorTheme();
const { t } = useTranslation('modules/common-media-cards-module');
const { t } = useTranslation('modules/common-media-cards');
return (
<Group mr="xs" align="stretch" noWrap style={{ maxHeight: 200 }}>

View File

@@ -41,7 +41,7 @@ export const DashdotModule = asModule({
value: '',
},
},
translationNamespace: 'modules/dashdot-module',
id: 'dashdot',
});
const useStyles = createStyles((theme, _params) => ({
@@ -126,7 +126,7 @@ export function DashdotComponent() {
const { classes } = useStyles();
const { colorScheme } = useMantineColorScheme();
const dashConfig = config.modules?.[DashdotModule.title]
const dashConfig = config.modules?.[DashdotModule.id]
.options as typeof DashdotModule['options'];
const isCompact = dashConfig?.useCompactView?.value ?? false;
const dashdotService: serviceItem | undefined = config.services.filter(
@@ -148,7 +148,7 @@ export function DashdotComponent() {
const totalSize =
(info?.storage?.layout as any[])?.reduce((acc, curr) => (curr.size ?? 0) + acc, 0) ?? 0;
const { t } = useTranslation('modules/dashdot-module');
const { t } = useTranslation('modules/dashdot');
const graphs = [
{

View File

@@ -16,14 +16,14 @@ export const DateModule: IModule = {
value: true,
},
},
translationNamespace: 'modules/date-module',
id: 'date',
};
export default function DateComponent(props: any) {
const [date, setDate] = useState(new Date());
const setSafeInterval = useSetSafeInterval();
const { config } = useConfig();
const isFullTime = config?.modules?.[DateModule.title]?.options?.full?.value ?? true;
const isFullTime = config?.modules?.[DateModule.id]?.options?.full?.value ?? true;
const formatString = isFullTime ? 'HH:mm' : 'h:mm A';
// Change date on minute change
// Note: Using 10 000ms instead of 1000ms to chill a little :)

View File

@@ -22,7 +22,7 @@ function sendDockerCommand(
containerName: string,
reload: () => void
) {
const { t } = useTranslation('modules/docker-module');
const { t } = useTranslation('modules/docker');
showNotification({
id: containerId,
@@ -64,7 +64,7 @@ export interface ContainerActionBarProps {
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) {
const [opened, setOpened] = useState<boolean>(false);
const { t } = useTranslation('modules/docker-module');
const { t } = useTranslation('modules/docker');
return (
<Group>

View File

@@ -9,7 +9,7 @@ export interface ContainerStateProps {
export default function ContainerState(props: ContainerStateProps) {
const { state } = props;
const { t } = useTranslation('modules/docker-module');
const { t } = useTranslation('modules/docker');
const options: {
size: MantineSize;

View File

@@ -15,7 +15,7 @@ export const DockerModule: IModule = {
title: 'Docker',
icon: IconBrandDocker,
component: DockerMenuButton,
translationNamespace: 'modules/docker-module',
id: 'docker',
};
export default function DockerMenuButton(props: any) {
@@ -23,9 +23,9 @@ export default function DockerMenuButton(props: any) {
const [containers, setContainers] = useState<Docker.ContainerInfo[]>([]);
const [selection, setSelection] = useState<Docker.ContainerInfo[]>([]);
const { config } = useConfig();
const moduleEnabled = config.modules?.[DockerModule.title]?.enabled ?? false;
const moduleEnabled = config.modules?.[DockerModule.id]?.enabled ?? false;
const { t } = useTranslation('modules/docker-module');
const { t } = useTranslation('modules/docker');
useEffect(() => {
reload();
@@ -54,7 +54,7 @@ export default function DockerMenuButton(props: any) {
);
}, 300);
}
const exists = config.modules?.[DockerModule.title]?.enabled ?? false;
const exists = config.modules?.[DockerModule.id]?.enabled ?? false;
if (!exists) {
return null;
}

View File

@@ -28,7 +28,7 @@ export default function DockerTable({
const { classes, cx } = useStyles();
const [search, setSearch] = useState('');
const { t } = useTranslation('modules/docker-module');
const { t } = useTranslation('modules/docker');
useEffect(() => {
setContainers(containers);

View File

@@ -8,6 +8,7 @@ import {
Skeleton,
ScrollArea,
Center,
Stack,
} from '@mantine/core';
import { IconDownload as Download } from '@tabler/icons';
import { useEffect, useState } from 'react';
@@ -32,7 +33,7 @@ export const DownloadsModule: IModule = {
value: false,
},
},
translationNamespace: 'modules/downloads-module',
id: 'torrents-status',
};
export default function DownloadComponent() {
@@ -46,12 +47,12 @@ export default function DownloadComponent() {
service.type === 'Deluge'
) ?? [];
const hideComplete: boolean =
(config?.modules?.[DownloadsModule.title]?.options?.hidecomplete?.value as boolean) ?? false;
(config?.modules?.[DownloadsModule.id]?.options?.hidecomplete?.value as boolean) ?? false;
const [torrents, setTorrents] = useState<NormalizedTorrent[]>([]);
const setSafeInterval = useSetSafeInterval();
const [isLoading, setIsLoading] = useState(true);
const { t } = useTranslation('modules/downloads-module');
const { t } = useTranslation(`modules/${DownloadsModule.id}`);
useEffect(() => {
setIsLoading(true);
@@ -85,13 +86,13 @@ export default function DownloadComponent() {
if (downloadServices.length === 0) {
return (
<Group>
<Stack>
<Title order={3}>{t('card.errors.noDownloadClients.title')}</Title>
<Group>
<Text>{t('card.errors.noDownloadClients.text')}</Text>
<AddItemShelfButton />
</Group>
</Group>
</Stack>
);
}

View File

@@ -18,7 +18,7 @@ export const TotalDownloadsModule: IModule = {
title: 'Download Speed',
icon: Download,
component: TotalDownloadsComponent,
translationNamespace: 'modules/total-downloads-module',
id: 'dlspeed',
};
interface torrentHistory {
@@ -37,7 +37,7 @@ export default function TotalDownloadsComponent() {
service.type === 'Transmission' ||
service.type === 'Deluge'
) ?? [];
const { t } = useTranslation('modules/downloads-module');
const { t } = useTranslation(`modules/${TotalDownloadsModule.id}`);
const [torrentHistory, torrentHistoryHandlers] = useListState<torrentHistory>([]);
const [torrents, setTorrents] = useState<NormalizedTorrent[]>([]);

View File

@@ -18,7 +18,7 @@ import { IModule } from './ModuleTypes';
function getItems(module: IModule) {
const { config, setConfig } = useConfig();
const { t } = useTranslation(module.translationNamespace);
const { t } = useTranslation([module.id, 'common']);
const items: JSX.Element[] = [];
if (module.options) {
@@ -28,8 +28,8 @@ function getItems(module: IModule) {
const types = values.map((v) => typeof v.value);
// Loop over all the types with a for each loop
types.forEach((type, index) => {
const optionName = `${module.title}.${keys[index]}`;
const moduleInConfig = config.modules?.[module.title];
const optionName = `${module.id}.${keys[index]}`;
const moduleInConfig = config.modules?.[module.id];
if (type === 'object') {
items.push(
<MultiSelect
@@ -46,7 +46,7 @@ function getItems(module: IModule) {
...config,
modules: {
...config.modules,
[module.title]: {
[module.id]: {
...moduleInConfig,
options: {
...moduleInConfig?.options,
@@ -71,12 +71,12 @@ function getItems(module: IModule) {
...config,
modules: {
...config.modules,
[module.title]: {
...config.modules[module.title],
[module.id]: {
...config.modules[module.id],
options: {
...config.modules[module.title].options,
...config.modules[module.id].options,
[keys[index]]: {
...config.modules[module.title].options?.[keys[index]],
...config.modules[module.id].options?.[keys[index]],
value: (e.target as any)[0].value,
},
},
@@ -99,7 +99,7 @@ function getItems(module: IModule) {
onChange={(e) => {}}
/>
<Button type="submit">Save</Button>
<Button type="submit">{t('actions.save')}</Button>
</Group>
</form>
);
@@ -120,12 +120,12 @@ function getItems(module: IModule) {
...config,
modules: {
...config.modules,
[module.title]: {
...config.modules[module.title],
[module.id]: {
...config.modules[module.id],
options: {
...config.modules[module.title].options,
...config.modules[module.id].options,
[keys[index]]: {
...config.modules[module.title].options?.[keys[index]],
...config.modules[module.id].options?.[keys[index]],
value: e.currentTarget.checked,
},
},
@@ -148,7 +148,7 @@ export function ModuleWrapper(props: any) {
const { config, setConfig } = useConfig();
const enabledModules = config.modules ?? {};
// Remove 'Module' from enabled modules titles
const isShown = enabledModules[module.title]?.enabled ?? false;
const isShown = enabledModules[module.id]?.enabled ?? false;
//TODO: fix the hover problem
const [hovering, setHovering] = useState(false);
const { t } = useTranslation('modules');
@@ -160,7 +160,7 @@ export function ModuleWrapper(props: any) {
return (
<Card
{...props}
key={module.title}
key={module.id}
hidden={!isShown}
withBorder
radius="lg"
@@ -195,7 +195,7 @@ export function ModuleMenu(props: any) {
<>
{module.options && (
<Menu
key={module.title}
key={module.id}
withinPortal
width="lg"
shadow="xl"

View File

@@ -6,7 +6,7 @@ export const OverseerrModule: IModule = {
title: 'Overseerr',
icon: IconEyeglass,
component: OverseerrMediaDisplay,
translationNamespace: 'modules/overseerr-module',
id: 'overseerr',
};
export interface OverseerSearchProps {

View File

@@ -10,6 +10,7 @@ import { useColorTheme } from '../../tools/color';
import { MovieResult } from './Movie.d';
import { MediaType, Result } from './SearchResult.d';
import { TvShowResult, TvShowResultSeason } from './TvShow.d';
interface RequestModalProps {
base: Result;
opened: boolean;
@@ -57,7 +58,7 @@ export function MovieRequestModal({
setOpened: (opened: boolean) => void;
}) {
const { secondaryColor } = useColorTheme();
const { t } = useTranslation('modules/overseerr-module');
const { t } = useTranslation('modules/overseerr');
return (
<Modal
@@ -114,7 +115,7 @@ export function TvRequestModal({
}) {
const [selection, setSelection] = useState<TvShowResultSeason[]>(result.seasons);
const { classes, cx } = useStyles();
const { t } = useTranslation('modules/overseerr-module');
const { t } = useTranslation('modules/overseerr');
const toggleRow = (container: TvShowResultSeason) =>
setSelection((current: TvShowResultSeason[]) =>

View File

@@ -11,7 +11,7 @@ export const PingModule: IModule = {
title: 'Ping Services',
icon: Plug,
component: PingComponent,
translationNamespace: 'modules/ping-module',
id: 'ping',
};
export default function PingComponent(props: any) {
@@ -21,9 +21,9 @@ export default function PingComponent(props: any) {
const { url }: { url: string } = props;
const [isOnline, setOnline] = useState<State>('loading');
const [response, setResponse] = useState(500);
const exists = config.modules?.[PingModule.title]?.enabled ?? false;
const exists = config.modules?.[PingModule.id]?.enabled ?? false;
const { t } = useTranslation('modules/ping-module');
const { t } = useTranslation('modules/ping');
function statusCheck(response: AxiosResponse) {
const { status }: { status: string[] } = props;
@@ -54,7 +54,7 @@ export default function PingComponent(props: any) {
.catch((error) => {
statusCheck(error.response);
});
}, [config.modules?.[PingModule.title]?.enabled]);
}, [config.modules?.[PingModule.id]?.enabled]);
if (!exists) {
return null;
}

View File

@@ -30,15 +30,15 @@ export const SearchModule: IModule = {
title: 'Search',
icon: Search,
component: SearchBar,
translationNamespace: 'modules/search-module',
id: 'search',
};
export default function SearchBar(props: any) {
const { classes, cx } = useStyles();
// Config
const { config } = useConfig();
const isModuleEnabled = config.modules?.[SearchModule.title]?.enabled ?? false;
const isOverseerrEnabled = config.modules?.[OverseerrModule.title]?.enabled ?? false;
const isModuleEnabled = config.modules?.[SearchModule.id]?.enabled ?? false;
const isOverseerrEnabled = config.modules?.[OverseerrModule.id]?.enabled ?? false;
const OverseerrService = config.services.find(
(service) => service.type === 'Overseerr' || service.type === 'Jellyseerr'
);
@@ -60,7 +60,7 @@ export default function SearchBar(props: any) {
},
});
const [debounced, cancel] = useDebouncedValue(form.values.query, 250);
const { t } = useTranslation('modules/search-module');
const { t } = useTranslation('modules/search');
useEffect(() => {
if (OverseerrService === undefined && isOverseerrEnabled) {

View File

@@ -32,7 +32,7 @@ export const WeatherModule: IModule = {
value: 'Paris',
},
},
translationNamespace: 'modules/weather-module',
id: 'weather',
};
// 0 Clear sky
@@ -49,7 +49,7 @@ export const WeatherModule: IModule = {
// 95 *Thunderstorm: Slight or moderate
// 96, 99 *Thunderstorm with slight and heavy hail
export function WeatherIcon(props: any) {
const { t } = useTranslation('modules/weather-module');
const { t } = useTranslation('modules/weather');
const { code } = props;
let data: { icon: any; name: string };
@@ -146,9 +146,9 @@ export default function WeatherComponent(props: any) {
const { config } = useConfig();
const [weather, setWeather] = useState({} as WeatherResponse);
const cityInput: string =
(config?.modules?.[WeatherModule.title]?.options?.location?.value as string) ?? 'Paris';
(config?.modules?.[WeatherModule.id]?.options?.location?.value as string) ?? 'Paris';
const isFahrenheit: boolean =
(config?.modules?.[WeatherModule.title]?.options?.freedomunit?.value as boolean) ?? false;
(config?.modules?.[WeatherModule.id]?.options?.freedomunit?.value as boolean) ?? false;
useEffect(() => {
axios

View File

@@ -10,6 +10,8 @@ import {
} from '@mantine/core';
import { NextLink } from '@mantine/next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
const useStyles = createStyles((theme) => ({
root: {
paddingTop: 80,
@@ -92,3 +94,12 @@ export default function Custom404() {
</Container>
);
}
export async function getStaticProps({ locale }: { locale: string }) {
return {
props: {
...(await serverSideTranslations(locale, ['404'])),
// Will be passed to the page component as props
},
};
}

View File

@@ -17,18 +17,19 @@ export async function getServerSideProps({
res,
locale,
}: GetServerSidePropsContext): Promise<{ props: { config: Config } }> {
let cookie = getCookie('config-name', { req, res });
if (!cookie) {
let configName = getCookie('config-name', { req, res });
const configLocale = getCookie('config-locale', { req, res });
if (!configName) {
setCookie('config-name', 'default', {
req,
res,
maxAge: 60 * 60 * 24 * 30,
sameSite: 'strict',
});
cookie = 'default';
configName = 'default';
}
const translations = await serverSideTranslations(locale as string, [
const translations = await serverSideTranslations((configLocale ?? locale) as string, [
'common',
'layout/app-shelf',
'layout/add-service-app-shelf',
@@ -46,19 +47,19 @@ export async function getServerSideProps({
'settings/customization/app-width',
'settings/customization/opacity-selector',
'modules/common',
'modules/date-module',
'modules/calendar-module',
'modules/total-downloads-module',
'modules/search-module',
'modules/downloads-module',
'modules/weather-module',
'modules/ping-module',
'modules/docker-module',
'modules/dashdot-module',
'modules/overseerr-module',
'modules/common-media-cards-module',
'modules/date',
'modules/calendar',
'modules/dlspeed',
'modules/search',
'modules/torrents-status',
'modules/weather',
'modules/ping',
'modules/docker',
'modules/dashdot',
'modules/overseerr',
'modules/common-media-cards',
]);
return getConfig(cookie as string, translations);
return getConfig(configName as string, translations);
}
export default function HomePage(props: any) {