mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-13 00:45:47 +01:00
✨ Add translation for module, fix language changer
This commit is contained in:
2
src/modules/ModuleTypes.d.ts
vendored
2
src/modules/ModuleTypes.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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 :)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -6,7 +6,7 @@ export const OverseerrModule: IModule = {
|
||||
title: 'Overseerr',
|
||||
icon: IconEyeglass,
|
||||
component: OverseerrMediaDisplay,
|
||||
translationNamespace: 'modules/overseerr-module',
|
||||
id: 'overseerr',
|
||||
};
|
||||
|
||||
export interface OverseerSearchProps {
|
||||
|
||||
@@ -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[]) =>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user