Translations

This commit is contained in:
ajnart
2022-12-20 11:34:07 +09:00
parent a5d31dd3ec
commit 2cc04957f3
29 changed files with 272 additions and 342 deletions

View File

@@ -164,28 +164,30 @@
} }
} }
], ],
"widgets": [{ "widgets": [
"id": "date", {
"properties": { "id": "date",
"display24HourFormat": true
},
"area": {
"type": "wrapper",
"properties": { "properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e" "display24HourFormat": true
}
},
"shape": {
"location": {
"x": 0,
"y": 3
}, },
"size": { "area": {
"width": 4, "type": "wrapper",
"height": 2 "properties": {
"id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33e"
}
},
"shape": {
"location": {
"x": 0,
"y": 3
},
"size": {
"width": 4,
"height": 2
}
} }
} }
}], ],
"settings": { "settings": {
"common": { "common": {
"searchEngine": { "searchEngine": {

View File

@@ -2,6 +2,8 @@
"actions": { "actions": {
"save": "Save", "save": "Save",
"cancel": "Cancel", "cancel": "Cancel",
"close": "Close",
"delete": "Delete",
"ok": "Okay", "ok": "Okay",
"edit": "Edit", "edit": "Edit",
"changePosition": "Change position", "changePosition": "Change position",
@@ -11,6 +13,11 @@
"settings": "Settings", "settings": "Settings",
"dangerZone": "Danger zone" "dangerZone": "Danger zone"
}, },
"secrets": {
"apiKey": "Api key",
"username": "Username",
"password": "Password"
},
"tip": "Tip: ", "tip": "Tip: ",
"time": { "time": {
"seconds": "seconds", "seconds": "seconds",

View File

@@ -1,128 +0,0 @@
{
"actionIcon": {
"tooltip": "Add a service"
},
"modal": {
"title": "Add service",
"form": {
"validation": {
"invalidUrl": "Please enter a valid URL",
"noStatusCodeSelected": "Please select a status code"
}
},
"tabs": {
"options": {
"title": "Options",
"form": {
"serviceName": {
"label": "Service name",
"placeholder": "Plex"
},
"iconUrl": {
"label": "Icon URL"
},
"serviceUrl": {
"label": "Service URL"
},
"onClickUrl": {
"label": "On Click URL"
},
"serviceType": {
"label": "Service type",
"defaultValue": "Other",
"placeholder": "Pick one"
},
"category": {
"label": "Category",
"placeholder": "Select a category or create a new one",
"nothingFound": "Nothing found",
"createLabel": "+ Create {{query}}"
},
"integrations": {
"apiKey": {
"label": "API key",
"placeholder": "Your API key",
"validation": {
"noKey": "Invalid Key"
},
"tip": {
"text": "Get your API key",
"link": "here."
}
},
"qBittorrent": {
"username": {
"label": "Username",
"placeholder": "admin",
"validation": {
"invalidUsername": "Invalid username"
}
},
"password": {
"label": "Password",
"placeholder": "adminadmin",
"validation": {
"invalidPassword": "Invalid password"
}
}
},
"deluge": {
"password": {
"label": "Password",
"placeholder": "password",
"validation": {
"invalidPassword": "Invalid password"
}
}
},
"transmission": {
"username": {
"label": "Username",
"placeholder": "admin",
"validation": {
"invalidUsername": "Invalid username"
}
},
"password": {
"label": "Password",
"placeholder": "adminadmin",
"validation": {
"invalidPassword": "Invalid password"
}
}
},
"nzbget": {
"username": {
"label": "Username",
"placeholder": "admin",
"validation": {
"invalidUsername": "Invalid username"
}
},
"password": {
"label": "Password",
"placeholder": "password",
"validation": {
"invalidPassword": "Invalid password"
}
}
}
}
}
},
"advancedOptions": {
"title": "Advanced options",
"form": {
"openServiceInNewTab": {
"label": "Open service in new tab"
},
"buttons": {
"submit": {
"content": "Add service"
}
}
}
}
}
}
}

View File

@@ -1,18 +0,0 @@
{
"modal": {
"title": "Modify a service",
"buttons": {
"save": "Save service"
}
},
"menu": {
"labels": {
"settings": "Settings",
"dangerZone": "Danger zone"
},
"actions": {
"edit": "Edit",
"delete": "Delete"
}
}
}

View File

@@ -1,12 +0,0 @@
{
"accordions": {
"downloads": {
"text": "Your downloads",
"torrents": "Your Torrent downloads",
"usenet": "Your Usenet downloads"
},
"others": {
"text": "Others"
}
}
}

View File

@@ -1,7 +1,7 @@
{ {
"modal": { "modal": {
"title": "Add a new tile to your Dashboard", "title": "Add a new tile",
"text": "You can use tiles, to make Homarr's dashboard yours. You may choose your desired tiles from the collection below and drag them anywhere on the dashboard where you like." "text": "Tiles are the main element of homarr. They allow you to configure the dashboard and display the information you want."
}, },
"actionIcon": { "actionIcon": {
"tooltip": "Add a tile" "tooltip": "Add a tile"

View File

@@ -0,0 +1,59 @@
{
"tabs": {
"general": "General",
"behaviour": "Behaviour",
"network": "Network",
"appearance": "Appearance",
"integration": "Integration"
},
"general": {
"appname": {
"label": "App name",
"description": "Used for displaying the app on the dashboard"
},
"internalAddress": {
"label": "Internal address",
"description": "Internal IP of the app"
},
"externalAddress": {
"label": "External address",
"description": "Url that will be opened in the browser when clicking on the app"
}
},
"behaviour": {
"isOpeningNewTab": {
"label": "Open in new tab",
"description": "Open the link in a new tab"
}
},
"network": {
"statusChecker": {
"label": "Status checker",
"description": "Sends a simple HTTP / HTTPS request to check if your app is online"
},
"statusCodes": {
"label": "HTTP status codes",
"description": "Determines what response codes are allowed for this app to be 'Online'"
}
},
"appearance": {
"icon": {
"label": "App Icon",
"description": "Logo of your app displayed in your dashboard. (Must return a body content containg an image)"
}
},
"integration": {
"type": {
"label": "Integration configuration",
"description": "Treats this app as the selected integration and provides you with per-app configuration",
"placeholder": "Select an integration"
},
"secrets": {
"description": "To update a secret, enter a value and click the save button. To remove a secret, use the clear button.",
"warning": "Please note that Homarr removes secrets from the configuration for security reasons. Thus, you can only either define or unset any credentials. Your credentials act as the main access for your integrations and you should <strong>never</strong> share them with anybody else. Make sure to <strong>store and manage your secrets safely</strong>.",
"clear": "Clear secret",
"save": "Save secret",
"update": "Update secret"
}
}
}

View File

@@ -11,5 +11,19 @@
"credits": { "credits": {
"madeWithLove": "Made with ❤️ by @" "madeWithLove": "Made with ❤️ by @"
}, },
"grow": "Grow grid (take all space)" "grow": "Grow grid (take all space)",
"yayout": {
"title": "Dashboard yayout",
"main": "Main",
"sidebar": "Sidebar",
"cannotturnoff": "Cannot be turned off",
"dashboardlayout": "Dashboard layout",
"enablersidebar": "Enable right sidebar",
"enablelsidebar": "Enable left sidebar",
"enablesearchbar": "Enable search bar",
"enabledocker": "Enable docker integration",
"enableping": "Enable pings",
"enablelsidebardescription": "Optional. Can be used for apps and integrations only",
"enablersidebardescription": "Optional. Can be used for apps and integrations only"
}
} }

View File

@@ -38,8 +38,6 @@ const AppShelf = (props: any) => {
const [activeId, setActiveId] = useState(null); const [activeId, setActiveId] = useState(null);
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const { t } = useTranslation('layout/app-shelf');
const sensors = useSensors( const sensors = useSensors(
useSensor(TouchSensor, { useSensor(TouchSensor, {
activationConstraint: { activationConstraint: {

View File

@@ -1,5 +1,6 @@
import { Button, Flex, Grid, NumberInput, Select, SelectItem } from '@mantine/core'; import { Button, Flex, Grid, NumberInput, Select, SelectItem } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { useTranslation } from 'next-i18next';
import { useConfigContext } from '../../../../config/provider'; import { useConfigContext } from '../../../../config/provider';
interface ChangePositionModalProps { interface ChangePositionModalProps {
@@ -44,6 +45,8 @@ export const ChangePositionModal = ({
onSubmit(form.values.x, form.values.y, form.values.width, form.values.height); onSubmit(form.values.x, form.values.y, form.values.width, form.values.height);
}; };
const { t } = useTranslation('common');
return ( return (
<form onSubmit={form.onSubmit(handleSubmit)}> <form onSubmit={form.onSubmit(handleSubmit)}>
<Grid> <Grid>
@@ -94,9 +97,9 @@ export const ChangePositionModal = ({
<Flex justify="end" gap="sm" mt="md"> <Flex justify="end" gap="sm" mt="md">
<Button onClick={() => onCancel()} variant="light" color="gray"> <Button onClick={() => onCancel()} variant="light" color="gray">
Cancel {t('cancel')}
</Button> </Button>
<Button type="submit">Save</Button> <Button type="submit">{t('save')}</Button>
</Flex> </Flex>
</form> </form>
); );

View File

@@ -1,4 +1,4 @@
import { Alert, Button, createStyles, Group, Stack, Tabs, Text, ThemeIcon } from '@mantine/core'; import { Alert, Button, Group, Stack, Tabs, Text, ThemeIcon } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { ContextModalProps } from '@mantine/modals'; import { ContextModalProps } from '@mantine/modals';
import { import {
@@ -30,7 +30,7 @@ export const EditAppModal = ({
id, id,
innerProps, innerProps,
}: ContextModalProps<{ app: AppType; allowAppNamePropagation: boolean }>) => { }: ContextModalProps<{ app: AppType; allowAppNamePropagation: boolean }>) => {
const { t } = useTranslation(); const { t } = useTranslation(['layout/modals/add-app', 'common']);
const { name: configName, config } = useConfigContext(); const { name: configName, config } = useConfigContext();
const updateConfig = useConfigStore((store) => store.updateConfig); const updateConfig = useConfigStore((store) => store.updateConfig);
const [allowAppNamePropagation, setAllowAppNamePropagation] = useState<boolean>( const [allowAppNamePropagation, setAllowAppNamePropagation] = useState<boolean>(
@@ -137,68 +137,76 @@ export const EditAppModal = ({
</Stack> </Stack>
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
<Tabs <Stack
value={activeTab} justify="space-between"
onTabChange={(tab) => setActiveTab(tab as EditAppModalTab)} style={{
defaultValue="general" minHeight: 300,
}}
> >
<Tabs.List grow> <Tabs
<Tabs.Tab value={activeTab}
rightSection={<ValidationErrorIndicator keys={['name', 'url']} />} onTabChange={(tab) => setActiveTab(tab as EditAppModalTab)}
icon={<IconAdjustments size={14} />} defaultValue="general"
value="general" radius="md"
> >
General <Tabs.List grow>
</Tabs.Tab> <Tabs.Tab
<Tabs.Tab rightSection={<ValidationErrorIndicator keys={['name', 'url']} />}
rightSection={<ValidationErrorIndicator keys={['behaviour.onClickUrl']} />} icon={<IconAdjustments size={14} />}
icon={<IconClick size={14} />} value="general"
value="behaviour" >
> {t('tabs.general')}
Behaviour </Tabs.Tab>
</Tabs.Tab> <Tabs.Tab
<Tabs.Tab rightSection={<ValidationErrorIndicator keys={['behaviour.onClickUrl']} />}
rightSection={<ValidationErrorIndicator keys={[]} />} icon={<IconClick size={14} />}
icon={<IconAccessPoint size={14} />} value="behaviour"
value="network" >
> {t('tabs.behaviour')}
Network </Tabs.Tab>
</Tabs.Tab> <Tabs.Tab
<Tabs.Tab rightSection={<ValidationErrorIndicator keys={[]} />}
rightSection={<ValidationErrorIndicator keys={['appearance.iconUrl']} />} icon={<IconAccessPoint size={14} />}
icon={<IconBrush size={14} />} value="network"
value="appearance" >
> {t('tabs.network')}
Appearance </Tabs.Tab>
</Tabs.Tab> <Tabs.Tab
<Tabs.Tab rightSection={<ValidationErrorIndicator keys={['appearance.iconUrl']} />}
rightSection={<ValidationErrorIndicator keys={[]} />} icon={<IconBrush size={14} />}
icon={<IconPlug size={14} />} value="appearance"
value="integration" >
> {t('tabs.appearance')}
Integration </Tabs.Tab>
</Tabs.Tab> <Tabs.Tab
</Tabs.List> rightSection={<ValidationErrorIndicator keys={[]} />}
icon={<IconPlug size={14} />}
value="integration"
>
{t('tabs.integration')}
</Tabs.Tab>
</Tabs.List>
<GeneralTab form={form} openTab={(targetTab) => setActiveTab(targetTab)} /> <GeneralTab form={form} openTab={(targetTab) => setActiveTab(targetTab)} />
<BehaviourTab form={form} /> <BehaviourTab form={form} />
<NetworkTab form={form} /> <NetworkTab form={form} />
<AppearanceTab <AppearanceTab
form={form} form={form}
disallowAppNameProgagation={() => setAllowAppNamePropagation(false)} disallowAppNameProgagation={() => setAllowAppNamePropagation(false)}
allowAppNamePropagation={allowAppNamePropagation} allowAppNamePropagation={allowAppNamePropagation}
/> />
<IntegrationTab form={form} /> <IntegrationTab form={form} />
</Tabs> </Tabs>
<Group position="right" mt={100}> <Group position="right" mt="md">
<Button onClick={closeModal} px={50} variant="light" color="gray"> <Button onClick={closeModal} px={50} variant="light" color="gray">
Cancel {t('common:actions.cancel')}
</Button> </Button>
<Button disabled={!form.isValid()} px={50} type="submit"> <Button disabled={!form.isValid()} px={50} type="submit">
Save {t('common:actions.save')}
</Button> </Button>
</Group> </Group>
</Stack>
</form> </form>
</> </>
); );

View File

@@ -16,7 +16,7 @@ export const AppearanceTab = ({
disallowAppNameProgagation, disallowAppNameProgagation,
allowAppNamePropagation, allowAppNamePropagation,
}: AppearanceTabProps) => { }: AppearanceTabProps) => {
const { t } = useTranslation(''); const { t } = useTranslation('layout/modals/add-app');
const { classes } = useStyles(); const { classes } = useStyles();
return ( return (
@@ -26,8 +26,8 @@ export const AppearanceTab = ({
defaultValue={form.values.appearance.iconUrl} defaultValue={form.values.appearance.iconUrl}
className={classes.textInput} className={classes.textInput}
icon={<DebouncedAppIcon form={form} width={20} height={20} />} icon={<DebouncedAppIcon form={form} width={20} height={20} />}
label="App Icon" label={t('appearance.icon.label')}
description="Logo of your app displayed in your dashboard. Must return a body content containg an image" description={t('appearance.icon.description')}
variant="default" variant="default"
withAsterisk withAsterisk
required required

View File

@@ -9,21 +9,13 @@ interface BehaviourTabProps {
} }
export const BehaviourTab = ({ form }: BehaviourTabProps) => { export const BehaviourTab = ({ form }: BehaviourTabProps) => {
const { t } = useTranslation(''); const { t } = useTranslation('layout/modals/add-app');
return ( return (
<Tabs.Panel value="behaviour" pt="xs"> <Tabs.Panel value="behaviour" pt="xs">
<TextInput
icon={<IconClick size={16} />}
label="On click url"
description="Overrides the app URL when clicking on the app"
placeholder="URL that should be opened instead when clicking on the app"
variant="default"
mb="md"
{...form.getInputProps('behaviour.onClickUrl')}
/>
<Switch <Switch
label="Open in new tab" label={t('behaviour.isOpeningNewTab.label')}
description={t('behaviour.isOpeningNewTab.description')}
{...form.getInputProps('behaviour.isOpeningNewTab', { type: 'checkbox' })} {...form.getInputProps('behaviour.isOpeningNewTab', { type: 'checkbox' })}
/> />
</Tabs.Panel> </Tabs.Panel>

View File

@@ -1,6 +1,6 @@
import { Tabs, Text, TextInput } from '@mantine/core'; import { Tabs, TextInput } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form'; import { UseFormReturnType } from '@mantine/form';
import { IconCursorText, IconLink } from '@tabler/icons'; import { IconClick, IconCursorText, IconLink } from '@tabler/icons';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { AppType } from '../../../../../../types/app'; import { AppType } from '../../../../../../types/app';
import { EditAppModalTab } from '../type'; import { EditAppModalTab } from '../type';
@@ -11,43 +11,42 @@ interface GeneralTabProps {
} }
export const GeneralTab = ({ form, openTab }: GeneralTabProps) => { export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
const { t } = useTranslation(''); const { t } = useTranslation('layout/modals/add-app');
return ( return (
<Tabs.Panel value="general" pt="lg"> <Tabs.Panel value="general" pt="sm">
<TextInput <TextInput
icon={<IconCursorText size={16} />} icon={<IconCursorText size={16} />}
label="App name" label={t('general.appname.label')}
description="Used for displaying the app on the dashboard" description={t('general.appname.description')}
placeholder="My example app" placeholder="My example app"
variant="default" variant="default"
mb="md" mb="md"
withAsterisk withAsterisk
required
{...form.getInputProps('name')} {...form.getInputProps('name')}
/> />
<TextInput <TextInput
icon={<IconLink size={16} />} icon={<IconLink size={16} />}
label="App url" label={t('general.internalAddress.label')}
description={ description={t('general.internalAddress.description')}
<Text>
URL that will be opened when clicking on the app. Can be overwritten using
<Text
onClick={() => openTab('behaviour')}
variant="link"
span
style={{
cursor: 'pointer',
}}
>
{' '}
on click URL{' '}
</Text>
when using external URLs to enhance security.
</Text>
}
placeholder="https://google.com" placeholder="https://google.com"
variant="default" variant="default"
withAsterisk withAsterisk
required
{...form.getInputProps('url')} {...form.getInputProps('url')}
onChange={(e) => {
form.setFieldValue('behaviour.onClickUrl', e.target.value);
form.setFieldValue('url', e.target.value);
}}
/>
<TextInput
icon={<IconClick size={16} />}
label={t('general.externalAddress.label')}
description={t('general.externalAddress.description')}
placeholder="https://homarr.mywebsite.com/"
variant="default"
mb="md"
{...form.getInputProps('behaviour.onClickUrl')}
/> />
</Tabs.Panel> </Tabs.Panel>
); );

View File

@@ -11,6 +11,7 @@ import {
Title, Title,
} from '@mantine/core'; } from '@mantine/core';
import { TablerIcon } from '@tabler/icons'; import { TablerIcon } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { useState } from 'react'; import { useState } from 'react';
interface GenericSecretInputProps { interface GenericSecretInputProps {
@@ -32,6 +33,7 @@ export const GenericSecretInput = ({
const Icon = setIcon; const Icon = setIcon;
const [displayUpdateField, setDisplayUpdateField] = useState<boolean>(false); const [displayUpdateField, setDisplayUpdateField] = useState<boolean>(false);
const { t } = useTranslation(['layout/modals/add-app', 'common']);
return ( return (
<Card withBorder> <Card withBorder>
@@ -43,7 +45,7 @@ export const GenericSecretInput = ({
</ThemeIcon> </ThemeIcon>
<Stack spacing={0}> <Stack spacing={0}>
<Title className={classes.subtitle} order={6}> <Title className={classes.subtitle} order={6}>
{label} {t(label)}
</Title> </Title>
</Stack> </Stack>
</Group> </Group>
@@ -51,13 +53,13 @@ export const GenericSecretInput = ({
<Grid.Col xs={12} md={6}> <Grid.Col xs={12} md={6}>
<Flex gap={10} justify="end" align="end"> <Flex gap={10} justify="end" align="end">
<Button variant="subtle" color="gray" px="xl"> <Button variant="subtle" color="gray" px="xl">
Clear Secret {t('integration.secrets.clear')}
</Button> </Button>
{displayUpdateField === true ? ( {displayUpdateField === true ? (
<PasswordInput placeholder="new secret" width={200} {...props} /> <PasswordInput placeholder="new secret" width={200} {...props} />
) : ( ) : (
<Button onClick={() => setDisplayUpdateField(true)} variant="light"> <Button onClick={() => setDisplayUpdateField(true)} variant="light">
Set Secret {t('integration.secrets.update')}
</Button> </Button>
)} )}
</Flex> </Flex>

View File

@@ -17,7 +17,7 @@ interface IntegrationSelectorProps {
} }
export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => { export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
const { t } = useTranslation(''); const { t } = useTranslation('layout/modals/add-app');
// TODO: read this out from integrations dynamically. // TODO: read this out from integrations dynamically.
const data: SelectItem[] = [ const data: SelectItem[] = [
@@ -76,9 +76,9 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
return ( return (
<Select <Select
label="Integration configuration" label={t('integration.type.label')}
description="Treats this app as the selected integration and provides you with per-app configuration" description={t('integration.type.description')}
placeholder="Select your desired configuration" placeholder={t('integration.type.placeholder')}
itemComponent={SelectItemComponent} itemComponent={SelectItemComponent}
data={data} data={data}
maxDropdownHeight={400} maxDropdownHeight={400}

View File

@@ -1,7 +1,7 @@
import { Alert, Divider, Tabs, Text } from '@mantine/core'; import { Alert, Divider, Tabs, Text } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form'; import { UseFormReturnType } from '@mantine/form';
import { IconAlertTriangle } from '@tabler/icons'; import { IconAlertTriangle } from '@tabler/icons';
import { useTranslation } from 'next-i18next'; import { Trans, useTranslation } from 'next-i18next';
import { AppType } from '../../../../../../types/app'; import { AppType } from '../../../../../../types/app';
import { IntegrationSelector } from './Components/InputElements/IntegrationSelector'; import { IntegrationSelector } from './Components/InputElements/IntegrationSelector';
import { IntegrationOptionsRenderer } from './Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer'; import { IntegrationOptionsRenderer } from './Components/IntegrationOptionsRenderer/IntegrationOptionsRenderer';
@@ -11,7 +11,7 @@ interface IntegrationTabProps {
} }
export const IntegrationTab = ({ form }: IntegrationTabProps) => { export const IntegrationTab = ({ form }: IntegrationTabProps) => {
const { t } = useTranslation(''); const { t } = useTranslation('layout/modals/add-app');
const hasIntegrationSelected = form.values.integration?.type; const hasIntegrationSelected = form.values.integration?.type;
return ( return (
@@ -20,18 +20,19 @@ export const IntegrationTab = ({ form }: IntegrationTabProps) => {
{hasIntegrationSelected && ( {hasIntegrationSelected && (
<> <>
<Divider label="Integration Configuration" labelPosition="center" mt="xl" mb="md" /> <Divider
label={t('integration.type.label')}
labelPosition="center"
mt="xl"
mb="md"
/>
<Text size="sm" color="dimmed" mb="lg"> <Text size="sm" color="dimmed" mb="lg">
To update a secret, enter a value and click the save button. To remove a secret, use the {t('integration.secrets.description')}
clear button.
</Text> </Text>
<IntegrationOptionsRenderer form={form} /> <IntegrationOptionsRenderer form={form} />
<Alert icon={<IconAlertTriangle />} color="yellow"> <Alert icon={<IconAlertTriangle />} color="yellow">
<Text> <Text>
Please note that Homarr removes secrets from the configuration for security reasons. <Trans i18nKey="integration.secrets.warning" />
Thus, you can only either define or unset any credentials. Your credentials act as the
main access for your integrations and you should <b>never</b> share them with anybody
else. Make sure to <b>store and manage your secrets safely</b>.
</Text> </Text>
</Alert> </Alert>
</> </>

View File

@@ -9,12 +9,12 @@ interface NetworkTabProps {
} }
export const NetworkTab = ({ form }: NetworkTabProps) => { export const NetworkTab = ({ form }: NetworkTabProps) => {
const { t } = useTranslation(''); const { t } = useTranslation('layout/modals/add-app');
return ( return (
<Tabs.Panel value="network" pt="lg"> <Tabs.Panel value="network" pt="lg">
<Switch <Switch
label="Enable status checker" label={t('network.statusChecker.label')}
description="Sends a simple HTTP / HTTPS request to check if your app is online" description={t('network.statusChecker.description')}
mb="md" mb="md"
defaultChecked={form.values.network.enabledStatusChecker} defaultChecked={form.values.network.enabledStatusChecker}
{...form.getInputProps('network.enabledStatusChecker')} {...form.getInputProps('network.enabledStatusChecker')}
@@ -22,8 +22,8 @@ export const NetworkTab = ({ form }: NetworkTabProps) => {
{form.values.network.enabledStatusChecker && ( {form.values.network.enabledStatusChecker && (
<MultiSelect <MultiSelect
required required
label="HTTP status codes" label={t('network.statusCodes.label')}
description="Determines what response codes are allowed for this app to be 'Online'" description={t('network.statusCodes.description')}
data={StatusCodes} data={StatusCodes}
clearable clearable
searchable searchable

View File

@@ -1,6 +1,7 @@
import { Card, CardProps } from '@mantine/core'; import { Card, CardProps } from '@mantine/core';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { useCardStyles } from '../../layout/useCardStyles'; import { useCardStyles } from '../../layout/useCardStyles';
import { useEditModeStore } from '../Views/useEditModeStore';
interface HomarrCardWrapperProps extends CardProps { interface HomarrCardWrapperProps extends CardProps {
children: ReactNode; children: ReactNode;
@@ -11,11 +12,13 @@ export const HomarrCardWrapper = ({ ...props }: HomarrCardWrapperProps) => {
cx, cx,
classes: { card: cardClass }, classes: { card: cardClass },
} = useCardStyles(); } = useCardStyles();
const isEditMode = useEditModeStore((x) => x.enabled);
return ( return (
<Card <Card
{...props} {...props}
className={cx(props.className, cardClass)} className={cx(props.className, cardClass)}
withBorder withBorder
style={{ cursor: isEditMode ? 'move' : 'default' }}
radius="lg" radius="lg"
shadow="md" shadow="md"
/> />

View File

@@ -1,7 +1,6 @@
import { Group, Stack } from '@mantine/core'; import { Group, Stack } from '@mantine/core';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useConfigContext } from '../../../config/provider'; import { useConfigContext } from '../../../config/provider';
import { useScreenLargerThan } from '../../../tools/hooks/useScreenLargerThan';
import { useScreenSmallerThan } from '../../../tools/hooks/useScreenSmallerThan'; import { useScreenSmallerThan } from '../../../tools/hooks/useScreenSmallerThan';
import { CategoryType } from '../../../types/category'; import { CategoryType } from '../../../types/category';
import { WrapperType } from '../../../types/wrapper'; import { WrapperType } from '../../../types/wrapper';

View File

@@ -1,5 +1,3 @@
import { DashboardView } from './DashboardView'; import { DashboardView } from './DashboardView';
export const DashboardEditView = () => { export const DashboardEditView = () => <DashboardView />;
return <DashboardView />;
};

View File

@@ -1,6 +1,7 @@
import { Button, Group, TextInput } from '@mantine/core'; import { Button, Group, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { ContextModalProps } from '@mantine/modals'; import { ContextModalProps } from '@mantine/modals';
import { useTranslation } from 'next-i18next';
import { useConfigContext } from '../../../../config/provider'; import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store'; import { useConfigStore } from '../../../../config/store';
import { CategoryType } from '../../../../types/category'; import { CategoryType } from '../../../../types/category';
@@ -31,15 +32,17 @@ export const CategoryEditModal = ({
context.closeModal(id); context.closeModal(id);
}; };
const { t } = useTranslation('common');
return ( return (
<form onSubmit={form.onSubmit(handleSubmit)}> <form onSubmit={form.onSubmit(handleSubmit)}>
<TextInput data-autoFocus {...form.getInputProps('name')} label="Name of category" /> <TextInput data-autoFocus {...form.getInputProps('name')} label="Name of category" />
<Group mt="md" grow> <Group mt="md" grow>
<Button onClick={() => context.closeModal(id)} variant="light" color="gray"> <Button onClick={() => context.closeModal(id)} variant="light" color="gray">
Cancel {t('cancel')}
</Button> </Button>
<Button type="submit">Save</Button> <Button type="submit">{t('save')}</Button>
</Group> </Group>
</form> </form>
); );

View File

@@ -18,11 +18,10 @@ interface WrapperContentProps {
}; };
} }
export const WrapperContent = ({ apps, refs, widgets }: WrapperContentProps) => { export const WrapperContent = ({ apps, refs, widgets }: WrapperContentProps) => (
return (
<> <>
{apps?.map((app) => { {apps?.map((app) => {
const { component: TileComponent, ...tile } = Tiles['app']; const { component: TileComponent, ...tile } = Tiles.app;
return ( return (
<GridstackTileWrapper <GridstackTileWrapper
id={app.id} id={app.id}
@@ -61,4 +60,3 @@ export const WrapperContent = ({ apps, refs, widgets }: WrapperContentProps) =>
})} })}
</> </>
); );
};

View File

@@ -11,6 +11,7 @@ import {
Title, Title,
useMantineTheme, useMantineTheme,
} from '@mantine/core'; } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react'; import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
import { useConfigContext } from '../../../../config/provider'; import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store'; import { useConfigStore } from '../../../../config/store';
@@ -35,6 +36,7 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
const [searchBar, setSearchBar] = useState(defaultLayout?.enabledSearchbar ?? false); const [searchBar, setSearchBar] = useState(defaultLayout?.enabledSearchbar ?? false);
const { colors, colorScheme } = useMantineTheme(); const { colors, colorScheme } = useMantineTheme();
const { t } = useTranslation('settings/common');
if (!configName) return null; if (!configName) return null;
@@ -69,7 +71,7 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
return ( return (
<Stack spacing="xs"> <Stack spacing="xs">
<Title order={6}>Dashboard layout</Title> <Title order={6}>{t('layout.title')}</Title>
<Paper px="xs" py={4} withBorder> <Paper px="xs" py={4} withBorder>
<Group position="apart"> <Group position="apart">
@@ -94,7 +96,7 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
{leftSidebar && ( {leftSidebar && (
<Paper className={classes.secondaryWrapper} p="xs" withBorder> <Paper className={classes.secondaryWrapper} p="xs" withBorder>
<Flex align="center" justify="center" direction="column"> <Flex align="center" justify="center" direction="column">
<Text align="center">Sidebar</Text> <Text align="center">{t('layout.sidebar')}</Text>
<Text color="dimmed" size="xs" align="center"> <Text color="dimmed" size="xs" align="center">
Only for Only for
<br /> <br />
@@ -106,16 +108,16 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
)} )}
<Paper className={classes.primaryWrapper} p="xs" withBorder> <Paper className={classes.primaryWrapper} p="xs" withBorder>
<Text align="center">Main</Text> <Text align="center">{t('layout.main')}</Text>
<Text color="dimmed" size="xs" align="center"> <Text color="dimmed" size="xs" align="center">
Cannot be turned of. {t('cannotturnoff')}
</Text> </Text>
</Paper> </Paper>
{rightSidebar && ( {rightSidebar && (
<Paper className={classes.secondaryWrapper} p="xs" withBorder> <Paper className={classes.secondaryWrapper} p="xs" withBorder>
<Flex align="center" justify="center" direction="column"> <Flex align="center" justify="center" direction="column">
<Text align="center">Sidebar</Text> <Text align="center">{t('layout.sidebar')}</Text>
<Text color="dimmed" size="xs" align="center"> <Text color="dimmed" size="xs" align="center">
Only for Only for
<br /> <br />
@@ -129,29 +131,29 @@ export const LayoutSelector = ({ defaultLayout }: LayoutSelectorProps) => {
<Stack spacing="xs"> <Stack spacing="xs">
<Checkbox <Checkbox
label="Enable left sidebar" label={t('layout.enablelsidebar')}
description="Optional. Can be used for apps and integrations only" description={t('layout.enablelsidebardesc')}
checked={leftSidebar} checked={leftSidebar}
onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)} onChange={(ev) => handleChange('enabledLeftSidebar', ev, setLeftSidebar)}
/> />
<Checkbox <Checkbox
label="Enable right sidebar" label={t('layout.enablersidebar')}
description="Optional. Can be used for apps and integrations only" description={t('layout.enablersidebardesc')}
checked={rightSidebar} checked={rightSidebar}
onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)} onChange={(ev) => handleChange('enabledRightSidebar', ev, setRightSidebar)}
/> />
<Checkbox <Checkbox
label="Enable search bar" label={t('layout.enablesearchbar')}
checked={searchBar} checked={searchBar}
onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)} onChange={(ev) => handleChange('enabledSearchbar', ev, setSearchBar)}
/> />
<Checkbox <Checkbox
label="Enable docker" label={t('layout.enabledocker')}
checked={docker} checked={docker}
onChange={(ev) => handleChange('enabledDocker', ev, setDocker)} onChange={(ev) => handleChange('enabledDocker', ev, setDocker)}
/> />
<Checkbox <Checkbox
label="Enable pings" label={t('layout.enableping')}
checked={ping} checked={ping}
onChange={(ev) => handleChange('enabledPing', ev, setPing)} onChange={(ev) => handleChange('enabledPing', ev, setPing)}
/> />

View File

@@ -4,7 +4,7 @@ import { IconApps } from '@tabler/icons';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
export const AddElementAction = () => { export const AddElementAction = () => {
const { t } = useTranslation('layout/add-service-app-shelf'); const { t } = useTranslation('layout/element-selector/selector');
return ( return (
<Tooltip withinPortal label={t('actionIcon.tooltip')}> <Tooltip withinPortal label={t('actionIcon.tooltip')}>
@@ -16,7 +16,7 @@ export const AddElementAction = () => {
onClick={() => onClick={() =>
openContextModal({ openContextModal({
modal: 'selectElement', modal: 'selectElement',
title: 'Add an element to your dashboard', title: t('modal.title'),
size: 'xl', size: 'xl',
innerProps: {}, innerProps: {},
}) })

View File

@@ -1,12 +1,14 @@
import { ActionIcon, Menu, Tooltip } from '@mantine/core'; import { ActionIcon, Menu, Tooltip } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { IconInfoCircle, IconMenu2, IconSettings } from '@tabler/icons'; import { IconInfoCircle, IconMenu2, IconSettings } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { AboutModal } from '../../About/AboutModal'; import { AboutModal } from '../../About/AboutModal';
import { SettingsDrawer } from '../../Settings/SettingsDrawer'; import { SettingsDrawer } from '../../Settings/SettingsDrawer';
import { ColorSchemeSwitch } from './SettingsMenu/ColorSchemeSwitch'; import { ColorSchemeSwitch } from './SettingsMenu/ColorSchemeSwitch';
export const SettingsMenu = () => { export const SettingsMenu = () => {
const [drawerOpened, drawer] = useDisclosure(false); const [drawerOpened, drawer] = useDisclosure(false);
const { t } = useTranslation('common');
const [aboutModalOpened, aboutModal] = useDisclosure(false); const [aboutModalOpened, aboutModal] = useDisclosure(false);
return ( return (
@@ -22,7 +24,7 @@ export const SettingsMenu = () => {
<ColorSchemeSwitch /> <ColorSchemeSwitch />
<Menu.Divider /> <Menu.Divider />
<Menu.Item icon={<IconSettings strokeWidth={1.2} size={18} />} onClick={drawer.open}> <Menu.Item icon={<IconSettings strokeWidth={1.2} size={18} />} onClick={drawer.open}>
Homarr Settings {t('sections.settings')}
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
icon={<IconInfoCircle strokeWidth={1.2} size={18} />} icon={<IconInfoCircle strokeWidth={1.2} size={18} />}

View File

@@ -1,10 +1,8 @@
export const dashboardNamespaces = [ export const dashboardNamespaces = [
'common', 'common',
'layout/app-shelf',
'layout/add-service-app-shelf',
'layout/app-shelf-menu',
'layout/tools', 'layout/tools',
'layout/element-selector/selector', 'layout/element-selector/selector',
'layout/modals/add-app',
'layout/header/actions/toggle-edit-mode', 'layout/header/actions/toggle-edit-mode',
'settings/common', 'settings/common',
'settings/general/theme-selector', 'settings/general/theme-selector',

View File

@@ -90,16 +90,16 @@ export const integrationFieldDefinitions: {
apiKey: { apiKey: {
type: 'private', type: 'private',
icon: IconKey, icon: IconKey,
label: 'API Key', label: 'common:secrets.apiKey',
}, },
username: { username: {
type: 'public', type: 'public',
icon: IconUser, icon: IconUser,
label: 'Username', label: 'common:secrets.username',
}, },
password: { password: {
type: 'private', type: 'private',
icon: IconPassword, icon: IconPassword,
label: 'Password', label: 'common:secrets.password',
}, },
}; };

View File

@@ -4536,7 +4536,7 @@ __metadata:
"fsevents@patch:fsevents@^2.3.2#~builtin<compat/fsevents>, fsevents@patch:fsevents@~2.3.2#~builtin<compat/fsevents>": "fsevents@patch:fsevents@^2.3.2#~builtin<compat/fsevents>, fsevents@patch:fsevents@~2.3.2#~builtin<compat/fsevents>":
version: 2.3.2 version: 2.3.2
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin<compat/fsevents>::version=2.3.2&hash=df0bf1" resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin<compat/fsevents>::version=2.3.2&hash=18f3a7"
dependencies: dependencies:
node-gyp: latest node-gyp: latest
conditions: os=darwin conditions: os=darwin
@@ -7489,7 +7489,7 @@ __metadata:
"resolve@patch:resolve@^1.19.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.20.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.0#~builtin<compat/resolve>": "resolve@patch:resolve@^1.19.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.20.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.0#~builtin<compat/resolve>":
version: 1.22.1 version: 1.22.1
resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin<compat/resolve>::version=1.22.1&hash=c3c19d" resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin<compat/resolve>::version=1.22.1&hash=07638b"
dependencies: dependencies:
is-core-module: ^2.9.0 is-core-module: ^2.9.0
path-parse: ^1.0.7 path-parse: ^1.0.7
@@ -7502,7 +7502,7 @@ __metadata:
"resolve@patch:resolve@^2.0.0-next.3#~builtin<compat/resolve>": "resolve@patch:resolve@^2.0.0-next.3#~builtin<compat/resolve>":
version: 2.0.0-next.4 version: 2.0.0-next.4
resolution: "resolve@patch:resolve@npm%3A2.0.0-next.4#~builtin<compat/resolve>::version=2.0.0-next.4&hash=c3c19d" resolution: "resolve@patch:resolve@npm%3A2.0.0-next.4#~builtin<compat/resolve>::version=2.0.0-next.4&hash=07638b"
dependencies: dependencies:
is-core-module: ^2.9.0 is-core-module: ^2.9.0
path-parse: ^1.0.7 path-parse: ^1.0.7
@@ -8338,7 +8338,7 @@ __metadata:
"typescript@patch:typescript@^4.7.4#~builtin<compat/typescript>": "typescript@patch:typescript@^4.7.4#~builtin<compat/typescript>":
version: 4.9.4 version: 4.9.4
resolution: "typescript@patch:typescript@npm%3A4.9.4#~builtin<compat/typescript>::version=4.9.4&hash=d73830" resolution: "typescript@patch:typescript@npm%3A4.9.4#~builtin<compat/typescript>::version=4.9.4&hash=7ad353"
bin: bin:
tsc: bin/tsc tsc: bin/tsc
tsserver: bin/tsserver tsserver: bin/tsserver