chore(release): automatic release v0.1.0

This commit is contained in:
homarr-releases[bot]
2024-11-04 18:21:54 +00:00
committed by GitHub
151 changed files with 31857 additions and 3443 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text eol=lf

View File

@@ -45,9 +45,9 @@
"@million/lint": "1.0.11",
"@t3-oss/env-nextjs": "^0.11.1",
"@tabler/icons-react": "^3.21.0",
"@tanstack/react-query": "^5.59.16",
"@tanstack/react-query-devtools": "^5.59.16",
"@tanstack/react-query-next-experimental": "5.59.16",
"@tanstack/react-query": "^5.59.19",
"@tanstack/react-query-devtools": "^5.59.19",
"@tanstack/react-query-next-experimental": "5.59.19",
"@trpc/client": "next",
"@trpc/next": "next",
"@trpc/react-query": "next",
@@ -70,7 +70,7 @@
"react-dom": "^18.3.1",
"react-error-boundary": "^4.1.2",
"react-simple-code-editor": "^0.14.1",
"sass": "^1.80.5",
"sass": "^1.80.6",
"superjson": "2.2.1",
"swagger-ui-react": "^5.17.14",
"use-deep-compare-effect": "^1.8.1"
@@ -80,13 +80,13 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "2.4.4",
"@types/node": "^22.8.6",
"@types/node": "^22.8.7",
"@types/prismjs": "^1.26.5",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/swagger-ui-react": "^4.18.3",
"concurrently": "^9.0.1",
"eslint": "^9.13.0",
"concurrently": "^9.1.0",
"eslint": "^9.14.0",
"node-loader": "^2.0.0",
"prettier": "^3.3.3",
"typescript": "^5.6.3"

View File

@@ -0,0 +1,12 @@
"use client";
import type { PropsWithChildren } from "react";
import { useSuspenseDayJsLocalization } from "@homarr/translation/dayjs";
export const DayJsLoader = ({ children }: PropsWithChildren) => {
// Load the dayjs localization for the current locale with suspense
useSuspenseDayJsLocalization();
return children;
};

View File

@@ -8,14 +8,18 @@ import dayjs from "dayjs";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { parseCookies, setClientCookie } from "@homarr/common";
import type { ColorScheme } from "@homarr/definitions";
import { colorSchemeCookieKey } from "@homarr/definitions";
export const CustomMantineProvider = ({ children }: PropsWithChildren) => {
export const CustomMantineProvider = ({
children,
defaultColorScheme,
}: PropsWithChildren<{ defaultColorScheme: ColorScheme }>) => {
const manager = useColorSchemeManager();
return (
<DirectionProvider>
<MantineProvider
defaultColorScheme="dark"
defaultColorScheme={defaultColorScheme}
colorSchemeManager={manager}
theme={createTheme({
primaryColor: "red",
@@ -28,12 +32,11 @@ export const CustomMantineProvider = ({ children }: PropsWithChildren) => {
);
};
function useColorSchemeManager(): MantineColorSchemeManager {
const key = "homarr-color-scheme";
export function useColorSchemeManager(): MantineColorSchemeManager {
const { data: session } = useSession();
const updateCookieValue = (value: Exclude<MantineColorScheme, "auto">) => {
setClientCookie(key, value, { expires: dayjs().add(1, "year").toDate(), path: "/" });
setClientCookie(colorSchemeCookieKey, value, { expires: dayjs().add(1, "year").toDate(), path: "/" });
};
const { mutate: mutateColorScheme } = clientApi.user.changeColorScheme.useMutation({
@@ -50,7 +53,7 @@ function useColorSchemeManager(): MantineColorSchemeManager {
try {
const cookies = parseCookies(document.cookie);
return (cookies[key] as MantineColorScheme | undefined) ?? defaultValue;
return (cookies[colorSchemeCookieKey] as MantineColorScheme | undefined) ?? defaultValue;
} catch {
return defaultValue;
}

View File

@@ -4,10 +4,17 @@ import type { PropsWithChildren } from "react";
import type { MantineColorsTuple } from "@mantine/core";
import { createTheme, darken, lighten, MantineProvider } from "@mantine/core";
import type { ColorScheme } from "@homarr/definitions";
import { useColorSchemeManager } from "../../_client-providers/mantine";
import { useRequiredBoard } from "./_context";
export const BoardMantineProvider = ({ children }: PropsWithChildren) => {
export const BoardMantineProvider = ({
children,
defaultColorScheme,
}: PropsWithChildren<{ defaultColorScheme: ColorScheme }>) => {
const board = useRequiredBoard();
const colorSchemeManager = useColorSchemeManager();
const theme = createTheme({
colors: {
@@ -18,7 +25,11 @@ export const BoardMantineProvider = ({ children }: PropsWithChildren) => {
autoContrast: true,
});
return <MantineProvider theme={theme}>{children}</MantineProvider>;
return (
<MantineProvider defaultColorScheme={defaultColorScheme} theme={theme} colorSchemeManager={colorSchemeManager}>
{children}
</MantineProvider>
);
};
export const generateColors = (hex: string) => {

View File

@@ -12,7 +12,7 @@ import { BoardRenameModal } from "~/components/board/modals/board-rename-modal";
import { useRequiredBoard } from "../../(content)/_context";
import classes from "./danger.module.css";
export const DangerZoneSettingsContent = () => {
export const DangerZoneSettingsContent = ({ hideVisibility }: { hideVisibility: boolean }) => {
const board = useRequiredBoard();
const t = useScopedI18n("board.setting");
const router = useRouter();
@@ -90,14 +90,18 @@ export const DangerZoneSettingsContent = () => {
buttonText={t("section.dangerZone.action.rename.button")}
onClick={onRenameClick}
/>
<Divider />
<DangerZoneRow
label={t("section.dangerZone.action.visibility.label")}
description={t(`section.dangerZone.action.visibility.description.${visibility}`)}
buttonText={t(`section.dangerZone.action.visibility.button.${visibility}`)}
onClick={onVisibilityClick}
isPending={isChangeVisibilityPending}
/>
{hideVisibility ? null : (
<>
<Divider />
<DangerZoneRow
label={t("section.dangerZone.action.visibility.label")}
description={t(`section.dangerZone.action.visibility.description.${visibility}`)}
buttonText={t(`section.dangerZone.action.visibility.button.${visibility}`)}
onClick={onVisibilityClick}
isPending={isChangeVisibilityPending}
/>
</>
)}
<Divider />
<DangerZoneRow
label={t("section.dangerZone.action.delete.label")}

View File

@@ -14,6 +14,8 @@ import { TRPCError } from "@trpc/server";
import { api } from "@homarr/api/server";
import { capitalize } from "@homarr/common";
import { db } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import type { TranslationObject } from "@homarr/translation";
import { getScopedI18n } from "@homarr/translation/server";
import type { TablerIcon } from "@homarr/ui";
@@ -63,6 +65,7 @@ const getBoardAndPermissionsAsync = async (params: Props["params"]) => {
export default async function BoardSettingsPage({ params, searchParams }: Props) {
const { board, permissions } = await getBoardAndPermissionsAsync(params);
const boardSettings = await getServerSettingByKeyAsync(db, "board");
const { hasFullAccess } = await getBoardPermissionsAsync(board);
const t = await getScopedI18n("board.setting");
@@ -92,7 +95,7 @@ export default async function BoardSettingsPage({ params, searchParams }: Props)
<BoardAccessSettings board={board} initialPermissions={permissions} />
</AccordionItemFor>
<AccordionItemFor value="dangerZone" icon={IconAlertTriangle} danger noPadding>
<DangerZoneSettingsContent />
<DangerZoneSettingsContent hideVisibility={boardSettings.defaultBoardId === board.id} />
</AccordionItemFor>
</>
)}

View File

@@ -8,6 +8,7 @@ import { logger } from "@homarr/log";
import { MainHeader } from "~/components/layout/header";
import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo";
import { ClientShell } from "~/components/layout/shell";
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
import type { Board } from "./_types";
import { BoardProvider } from "./(content)/_context";
import type { Params } from "./(content)/_creator";
@@ -37,10 +38,11 @@ export const createBoardLayout = <TParams extends Params>({
throw error;
});
const colorScheme = await getCurrentColorSchemeAsync();
return (
<BoardProvider initialBoard={initialBoard}>
<BoardMantineProvider>
<BoardMantineProvider defaultColorScheme={colorScheme}>
<CustomCss />
<ClientShell hasNavigation={false}>
<MainHeader

View File

@@ -6,7 +6,6 @@ import "@homarr/spotlight/styles.css";
import "@homarr/ui/styles.css";
import "~/styles/scroll-area.scss";
import { cookies } from "next/headers";
import { notFound } from "next/navigation";
import { NextIntlClientProvider } from "next-intl";
@@ -14,11 +13,13 @@ import { env } from "@homarr/auth/env.mjs";
import { auth } from "@homarr/auth/next";
import { ModalProvider } from "@homarr/modals";
import { Notifications } from "@homarr/notifications";
import { isLocaleSupported } from "@homarr/translation";
import { getI18nMessages, getScopedI18n } from "@homarr/translation/server";
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
import { getI18nMessages } from "@homarr/translation/server";
import { Analytics } from "~/components/layout/analytics";
import { SearchEngineOptimization } from "~/components/layout/search-engine-optimization";
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
import { DayJsLoader } from "./_client-providers/dayjs-loader";
import { JotaiProvider } from "./_client-providers/jotai";
import { CustomMantineProvider } from "./_client-providers/mantine";
import { AuthProvider } from "./_client-providers/session";
@@ -30,7 +31,8 @@ const fontSans = Inter({
variable: "--font-sans",
});
export const generateMetadata = (): Metadata => ({
// eslint-disable-next-line no-restricted-syntax
export const generateMetadata = async (): Promise<Metadata> => ({
title: "Homarr",
description:
"Simplify the management of your server with Homarr - a sleek, modern dashboard that puts all of your apps and services at your fingertips.",
@@ -49,7 +51,7 @@ export const generateMetadata = (): Metadata => ({
title: "Homarr",
capable: true,
startupImage: { url: "/logo/logo.png" },
statusBarStyle: getColorScheme() === "dark" ? "black-translucent" : "default",
statusBarStyle: (await getCurrentColorSchemeAsync()) === "dark" ? "black-translucent" : "default",
},
});
@@ -66,9 +68,8 @@ export default async function Layout(props: { children: React.ReactNode; params:
}
const session = await auth();
const colorScheme = getColorScheme();
const tCommon = await getScopedI18n("common");
const direction = tCommon("direction");
const colorScheme = await getCurrentColorSchemeAsync();
const direction = isLocaleRTL(props.params.locale) ? "rtl" : "ltr";
const i18nMessages = await getI18nMessages();
const StackedProvider = composeWrappers([
@@ -77,8 +78,9 @@ export default async function Layout(props: { children: React.ReactNode; params:
},
(innerProps) => <JotaiProvider {...innerProps} />,
(innerProps) => <TRPCReactProvider {...innerProps} />,
(innerProps) => <DayJsLoader {...innerProps} />,
(innerProps) => <NextIntlClientProvider {...innerProps} messages={i18nMessages} />,
(innerProps) => <CustomMantineProvider {...innerProps} />,
(innerProps) => <CustomMantineProvider {...innerProps} defaultColorScheme={colorScheme} />,
(innerProps) => <ModalProvider {...innerProps} />,
]);
@@ -106,7 +108,3 @@ export default async function Layout(props: { children: React.ReactNode; params:
</html>
);
}
const getColorScheme = () => {
return cookies().get("homarr-color-scheme")?.value ?? "dark";
};

View File

@@ -1,7 +1,6 @@
"use client";
import { useState } from "react";
import { useParams } from "next/navigation";
import { ActionIcon, Avatar, Button, Card, Collapse, Group, Kbd, Stack, Text } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconEye, IconEyeOff } from "@tabler/icons-react";
@@ -23,7 +22,6 @@ interface SecretCardProps {
}
export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
const params = useParams<{ locale: string }>();
const t = useI18n();
const { isPublic } = integrationSecretKindObject[secret.kind];
const [publicSecretDisplayOpened, { toggle: togglePublicSecretDisplay }] = useDisclosure(false);
@@ -45,7 +43,7 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
<Group>
<Text c="gray.6" size="sm">
{t("integration.secrets.lastUpdated", {
date: dayjs().locale(params.locale).to(dayjs(secret.updatedAt)),
date: dayjs().to(dayjs(secret.updatedAt)),
})}
</Text>
{isPublic ? (

View File

@@ -0,0 +1,52 @@
"use client";
import { Group, Text } from "@mantine/core";
import { IconMoon, IconSun } from "@tabler/icons-react";
import type { ColorScheme } from "@homarr/definitions";
import { colorSchemes } from "@homarr/definitions";
import type { ServerSettings } from "@homarr/server-settings";
import { useScopedI18n } from "@homarr/translation/client";
import { SelectWithCustomItems } from "@homarr/ui";
import { CommonSettingsForm } from "./common-form";
export const AppearanceSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["appearance"] }) => {
const tApperance = useScopedI18n("management.page.settings.section.appearance");
return (
<CommonSettingsForm settingKey="appearance" defaultValues={defaultValues}>
{(form) => (
<>
<SelectWithCustomItems
label={tApperance("defaultColorScheme.label")}
data={colorSchemes.map((scheme) => ({
value: scheme,
label: tApperance(`defaultColorScheme.options.${scheme}`),
}))}
{...form.getInputProps("defaultColorScheme")}
SelectOption={ApperanceCustomOption}
/>
</>
)}
</CommonSettingsForm>
);
};
const appearanceIcons = {
light: IconSun,
dark: IconMoon,
};
const ApperanceCustomOption = ({ value, label }: { value: ColorScheme; label: string }) => {
const Icon = appearanceIcons[value];
return (
<Group>
<Icon size={16} stroke={1.5} />
<Text fz="sm" fw={500}>
{label}
</Text>
</Group>
);
};

View File

@@ -0,0 +1,44 @@
"use client";
import { Group, Text } from "@mantine/core";
import { IconLayoutDashboard } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import type { ServerSettings } from "@homarr/server-settings";
import { useScopedI18n } from "@homarr/translation/client";
import { SelectWithCustomItems } from "@homarr/ui";
import { CommonSettingsForm } from "./common-form";
export const BoardSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["board"] }) => {
const tBoard = useScopedI18n("management.page.settings.section.board");
const [selectableBoards] = clientApi.board.getPublicBoards.useSuspenseQuery();
return (
<CommonSettingsForm settingKey="board" defaultValues={defaultValues}>
{(form) => (
<>
<SelectWithCustomItems
label={tBoard("defaultBoard.label")}
description={tBoard("defaultBoard.description")}
data={selectableBoards.map((board) => ({
value: board.id,
label: board.name,
image: board.logoImageUrl,
}))}
SelectOption={({ label, image }: { value: string; label: string; image: string | null }) => (
<Group>
{/* eslint-disable-next-line @next/next/no-img-element */}
{image ? <img width={16} height={16} src={image} alt={label} /> : <IconLayoutDashboard size={16} />}
<Text fz="sm" fw={500}>
{label}
</Text>
</Group>
)}
{...form.getInputProps("defaultBoardId")}
/>
</>
)}
</CommonSettingsForm>
);
};

View File

@@ -0,0 +1,57 @@
"use client";
import { Button, Group, Stack } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { useForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { ServerSettings } from "@homarr/server-settings";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
export const CommonSettingsForm = <TKey extends keyof ServerSettings>({
settingKey,
defaultValues,
children,
}: {
settingKey: TKey;
defaultValues: ServerSettings[TKey];
children: (form: ReturnType<typeof useForm<ServerSettings[TKey]>>) => React.ReactNode;
}) => {
const t = useI18n();
const tSettings = useScopedI18n("management.page.settings");
const { mutateAsync, isPending } = clientApi.serverSettings.saveSettings.useMutation({
onSuccess() {
showSuccessNotification({
message: tSettings("notification.success.message"),
});
},
onError() {
showErrorNotification({
message: tSettings("notification.error.message"),
});
},
});
const form = useForm({
initialValues: defaultValues,
});
const handleSubmitAsync = async (values: ServerSettings[TKey]) => {
await mutateAsync({
settingsKey: settingKey,
value: values,
});
};
return (
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
<Stack gap="sm">
{children(form)}
<Group justify="end">
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};

View File

@@ -0,0 +1,26 @@
"use client";
import type { ServerSettings } from "@homarr/server-settings";
import type { SupportedLanguage } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client";
import { LanguageCombobox } from "~/components/language/language-combobox";
import { CommonSettingsForm } from "./common-form";
export const CultureSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["culture"] }) => {
const tCulture = useScopedI18n("management.page.settings.section.culture");
return (
<CommonSettingsForm settingKey="culture" defaultValues={defaultValues}>
{(form) => (
<>
<LanguageCombobox
label={tCulture("defaultLocale.label")}
value={form.getInputProps("defaultLocale").value as SupportedLanguage}
{...form.getInputProps("defaultLocale")}
/>
</>
)}
</CommonSettingsForm>
);
};

View File

@@ -6,6 +6,9 @@ import { getScopedI18n } from "@homarr/translation/server";
import { CrawlingAndIndexingSettings } from "~/app/[locale]/manage/settings/_components/crawling-and-indexing.settings";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { AnalyticsSettings } from "./_components/analytics.settings";
import { AppearanceSettingsForm } from "./_components/appearance-settings-form";
import { BoardSettingsForm } from "./_components/board-settings-form";
import { CultureSettingsForm } from "./_components/culture-settings-form";
export async function generateMetadata() {
const t = await getScopedI18n("management");
@@ -18,14 +21,26 @@ export async function generateMetadata() {
export default async function SettingsPage() {
const serverSettings = await api.serverSettings.getAll();
const t = await getScopedI18n("management.page.settings");
const tSettings = await getScopedI18n("management.page.settings");
return (
<>
<DynamicBreadcrumb />
<Stack>
<Title order={1}>{t("title")}</Title>
<Title order={1}>{tSettings("title")}</Title>
<AnalyticsSettings initialData={serverSettings.analytics} />
<CrawlingAndIndexingSettings initialData={serverSettings.crawlingAndIndexing} />
<Stack>
<Title order={2}>{tSettings("section.board.title")}</Title>
<BoardSettingsForm defaultValues={serverSettings.board} />
</Stack>
<Stack>
<Title order={2}>{tSettings("section.appearance.title")}</Title>
<AppearanceSettingsForm defaultValues={serverSettings.appearance} />
</Stack>
<Stack>
<Title order={2}>{tSettings("section.culture.title")}</Title>
<CultureSettingsForm defaultValues={serverSettings.culture} />
</Stack>
</Stack>
</>
);

View File

@@ -6,7 +6,7 @@ import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { LanguageCombobox } from "~/components/language/language-combobox";
import { CurrentLanguageCombobox } from "~/components/language/current-language-combobox";
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
import { catchTrpcNotFound } from "~/errors/trpc-not-found";
import { createMetaTitle } from "~/metadata";
@@ -81,7 +81,7 @@ export default async function EditUserPage({ params }: Props) {
<Stack mb="lg">
<Title order={2}>{tGeneral("item.language")}</Title>
<LanguageCombobox />
<CurrentLanguageCombobox />
</Stack>
<Stack mb="lg">

View File

@@ -2,7 +2,7 @@
import { useCallback } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@mantine/core";
import { Button, useMatches } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
@@ -61,8 +61,14 @@ export const DeleteGroup = ({ group }: DeleteGroupProps) => {
});
}, [tDelete, tRoot, openConfirmModal, group.id, group.name, mutateAsync, router]);
const fullWidth = useMatches({
xs: true,
sm: true,
md: false,
});
return (
<Button variant="subtle" color="red" onClick={handleDeletion}>
<Button variant="subtle" color="red" onClick={handleDeletion} fullWidth={fullWidth}>
{tDelete("label")}
</Button>
);

View File

@@ -1,7 +1,7 @@
"use client";
import { useCallback, useState } from "react";
import { Button } from "@mantine/core";
import { Button, useMatches } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { useConfirmModal, useModalAction } from "@homarr/modals";
@@ -74,8 +74,14 @@ export const TransferGroupOwnership = ({ group }: TransferGroupOwnershipProps) =
);
}, [group.id, group.name, innerOwnerId, mutateAsync, openConfirmModal, openModal, tRoot, tTransfer]);
const fullWidth = useMatches({
xs: true,
sm: true,
md: false,
});
return (
<Button variant="subtle" color="red" onClick={handleTransfer}>
<Button variant="subtle" color="red" onClick={handleTransfer} fullWidth={fullWidth}>
{tTransfer("label")}
</Button>
);

View File

@@ -1,8 +1,9 @@
import { Stack, Title } from "@mantine/core";
import { Card, Group, Stack, Text, Title } from "@mantine/core";
import { api } from "@homarr/api/server";
import { everyoneGroup } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { UserAvatar } from "@homarr/ui";
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
import { DeleteGroup } from "./_delete-group";
@@ -30,6 +31,30 @@ export default async function GroupsDetailPage({ params }: GroupsDetailPageProps
<RenameGroupForm group={group} disabled={isReserved} />
<Title order={2}>{tGeneral("owner")}</Title>
<Card>
{group.owner ? (
<Group>
<UserAvatar user={{ name: group.owner.name, image: group.owner.image }} size={"lg"} />
<Stack align={"start"} gap={3}>
<Text fw={"bold"}>{group.owner.name}</Text>
<Text>{group.owner.email}</Text>
<Text c={"dimmed"} size={"sm"}>
{tGeneral("ownerOfGroup")}
</Text>
</Stack>
</Group>
) : (
<Group>
<Stack align={"start"} gap={3}>
<Text c={"dimmed"} size={"sm"}>
{tGeneral("ownerOfGroupDeleted")}
</Text>
</Stack>
</Group>
)}
</Card>
{!isReserved && (
<DangerZoneRoot>
<DangerZoneItem

View File

@@ -5,10 +5,23 @@ import type { SupportedAuthProvider } from "@homarr/definitions";
import { logger } from "@homarr/log";
export const GET = async (req: NextRequest) => {
return await createHandlers(extractProvider(req)).handlers.GET(reqWithTrustedOrigin(req));
return await createHandlers(extractProvider(req), isSecureCookieEnabled(req)).handlers.GET(reqWithTrustedOrigin(req));
};
export const POST = async (req: NextRequest) => {
return await createHandlers(extractProvider(req)).handlers.POST(reqWithTrustedOrigin(req));
return await createHandlers(extractProvider(req), isSecureCookieEnabled(req)).handlers.POST(
reqWithTrustedOrigin(req),
);
};
/**
* wheter to use secure cookies or not, is only supported for https.
* For http it will not add the cookie as it is not considered secure.
* @param req request containing the url
* @returns true if the request is https, false otherwise
*/
const isSecureCookieEnabled = (req: NextRequest): boolean => {
const url = new URL(req.url);
return url.protocol === "https:";
};
/**

View File

@@ -0,0 +1,12 @@
"use client";
import { useChangeLocale, useCurrentLocale } from "@homarr/translation/client";
import { LanguageCombobox } from "./language-combobox";
export const CurrentLanguageCombobox = () => {
const currentLocale = useCurrentLocale();
const { changeLocale, isPending } = useChangeLocale();
return <LanguageCombobox value={currentLocale} onChange={changeLocale} isPending={isPending} />;
};

View File

@@ -1,31 +1,35 @@
"use client";
import React from "react";
import { Combobox, Group, InputBase, Loader, Text, useCombobox } from "@mantine/core";
import { Combobox, Group, InputBase, Loader, ScrollArea, Text, useCombobox } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
import type { SupportedLanguage } from "@homarr/translation";
import { localeConfigurations, supportedLanguages } from "@homarr/translation";
import { useChangeLocale, useCurrentLocale } from "@homarr/translation/client";
import classes from "./language-combobox.module.css";
export const LanguageCombobox = () => {
interface LanguageComboboxProps {
label?: string;
value: SupportedLanguage;
onChange: (value: SupportedLanguage) => void;
isPending?: boolean;
}
export const LanguageCombobox = ({ label, value, onChange, isPending }: LanguageComboboxProps) => {
const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(),
});
const currentLocale = useCurrentLocale();
const { changeLocale, isPending } = useChangeLocale();
const handleOnOptionSubmit = React.useCallback(
(value: string) => {
if (!value) {
return;
}
changeLocale(value as SupportedLanguage);
onChange(value as SupportedLanguage);
combobox.closeDropdown();
},
[changeLocale, combobox],
[onChange, combobox],
);
const handleOnClick = React.useCallback(() => {
@@ -39,23 +43,26 @@ export const LanguageCombobox = () => {
component="button"
type="button"
pointer
label={label}
leftSection={isPending ? <Loader size={16} /> : null}
rightSection={<Combobox.Chevron />}
rightSectionPointerEvents="none"
onClick={handleOnClick}
variant="filled"
>
<OptionItem currentLocale={currentLocale} localeKey={currentLocale} />
<OptionItem currentLocale={value} localeKey={value} />
</InputBase>
</Combobox.Target>
<Combobox.Dropdown>
<Combobox.Options>
{supportedLanguages.map((languageKey) => (
<Combobox.Option value={languageKey} key={languageKey}>
<OptionItem currentLocale={currentLocale} localeKey={languageKey} showCheck />
</Combobox.Option>
))}
</Combobox.Options>
<ScrollArea h={300}>
<Combobox.Options>
{supportedLanguages.map((languageKey) => (
<Combobox.Option value={languageKey} key={languageKey}>
<OptionItem currentLocale={value} localeKey={languageKey} showCheck />
</Combobox.Option>
))}
</Combobox.Options>
</ScrollArea>
</Combobox.Dropdown>
</Combobox>
);

View File

@@ -1,11 +1,12 @@
import Script from "next/script";
import { UMAMI_WEBSITE_ID } from "@homarr/analytics";
import { api } from "@homarr/api/server";
import { db } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
export const Analytics = async () => {
// For static pages it will not find any analytics data so we do not include the script on them
const analytics = await api.serverSettings.getAnalytics().catch(() => null);
const analytics = await getServerSettingByKeyAsync(db, "analytics").catch(() => null);
if (analytics?.enableGeneral) {
return <Script src="https://umami.homarr.dev/script.js" data-website-id={UMAMI_WEBSITE_ID} defer />;

View File

@@ -1,28 +1,28 @@
import SuperJSON from "superjson";
import { db, eq } from "@homarr/db";
import { serverSettings } from "@homarr/db/schema/sqlite";
import type { defaultServerSettings } from "@homarr/server-settings";
import { db } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
export const SearchEngineOptimization = async () => {
const crawlingAndIndexingSetting = await db.query.serverSettings.findFirst({
where: eq(serverSettings.settingKey, "crawlingAndIndexing"),
});
const crawlingAndIndexingSetting = await getServerSettingByKeyAsync(db, "crawlingAndIndexing");
if (!crawlingAndIndexingSetting) {
return null;
const robotsAttributes: string[] = [];
if (crawlingAndIndexingSetting.noIndex) {
robotsAttributes.push("noindex");
}
const value = SuperJSON.parse<(typeof defaultServerSettings)["crawlingAndIndexing"]>(
crawlingAndIndexingSetting.value,
);
if (crawlingAndIndexingSetting.noFollow) {
robotsAttributes.push("nofollow");
}
const robotsAttributes = [...(value.noIndex ? ["noindex"] : []), ...(value.noIndex ? ["nofollow"] : [])];
const googleAttributes: string[] = [];
const googleAttributes = [
...(value.noSiteLinksSearchBox ? ["nositelinkssearchbox"] : []),
...(value.noTranslate ? ["notranslate"] : []),
];
if (crawlingAndIndexingSetting.noSiteLinksSearchBox) {
googleAttributes.push("nositelinkssearchbox");
}
if (crawlingAndIndexingSetting.noTranslate) {
googleAttributes.push("notranslate");
}
return (
<>

View File

@@ -15,7 +15,7 @@ export const DangerZoneRoot = async ({ children }: DangerZoneRootProps) => {
<Title c="red.8" order={2}>
{t("common.dangerZone")}
</Title>
<Card withBorder style={{ borderColor: "var(--mantine-color-red-8)" }}>
<Card withBorder style={{ borderColor: "var(--mantine-color-red-8)", borderWidth: 3 }}>
<Stack gap="sm">
{Array.isArray(children)
? children.map((child, index) => (
@@ -43,14 +43,14 @@ interface DangerZoneItemProps {
export const DangerZoneItem = ({ label, description, action }: DangerZoneItemProps) => {
return (
<Group justify="space-between" px="md">
<Group justify="space-between" px="md" w={"100%"}>
<Stack gap={0}>
<Text fw="bold" size="sm">
{label}
</Text>
<Text size="sm">{description}</Text>
</Stack>
<Group justify="end" w={{ base: "100%", xs: "auto" }}>
<Group justify="end" w={{ xs: "100%", sm: "100%", md: "auto" }}>
{action}
</Group>
</Group>

View File

@@ -24,7 +24,7 @@ import { useScopedI18n } from "@homarr/translation/client";
import "flag-icons/css/flag-icons.min.css";
import { useAuthContext } from "~/app/[locale]/_client-providers/session";
import { LanguageCombobox } from "./language/language-combobox";
import { CurrentLanguageCombobox } from "./language/current-language-combobox";
interface UserAvatarMenuProps {
children: ReactNode;
@@ -72,7 +72,7 @@ export const UserAvatarMenu = ({ children }: UserAvatarMenuProps) => {
<Menu.Divider />
<Menu.Item p={0} closeMenuOnClick={false}>
<LanguageCombobox />
<CurrentLanguageCombobox />
</Menu.Item>
<Menu.Divider />
{Boolean(session.data) && (

View File

@@ -1,9 +1,15 @@
import type { NextRequest } from "next/server";
import { I18nMiddleware } from "@homarr/translation/middleware";
import { fetchApi } from "@homarr/api/client";
import { createI18nMiddleware } from "@homarr/translation/middleware";
export function middleware(request: NextRequest) {
return I18nMiddleware(request);
export async function middleware(request: NextRequest) {
const culture = await fetchApi.serverSettings.getCulture.query();
// We don't want to fallback to accept-language header so we clear it
request.headers.set("accept-language", "");
const next = createI18nMiddleware(culture.defaultLocale);
return next(request);
}
export const config = {

View File

@@ -0,0 +1,18 @@
import { cache } from "react";
import { cookies } from "next/headers";
import { db } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import type { ColorScheme } from "@homarr/definitions";
import { colorSchemeCookieKey } from "@homarr/definitions";
export const getCurrentColorSchemeAsync = cache(async () => {
const cookieValue = cookies().get(colorSchemeCookieKey)?.value;
if (cookieValue) {
return cookieValue as ColorScheme;
}
const appearanceSettings = await getServerSettingByKeyAsync(db, "appearance");
return appearanceSettings.defaultColorScheme;
});

View File

@@ -44,9 +44,9 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.8.6",
"@types/node": "^22.8.7",
"dotenv-cli": "^7.4.2",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"prettier": "^3.3.3",
"tsx": "4.19.2",
"typescript": "^5.6.3"

View File

@@ -33,8 +33,8 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.5.12",
"eslint": "^9.13.0",
"@types/ws": "^8.5.13",
"eslint": "^9.14.0",
"prettier": "^3.3.3",
"typescript": "^5.6.3"
}

View File

@@ -37,7 +37,7 @@
"testcontainers": "^10.13.2",
"turbo": "^2.2.3",
"typescript": "^5.6.3",
"vite-tsconfig-paths": "^5.0.1",
"vite-tsconfig-paths": "^5.1.0",
"vitest": "^2.1.4"
},
"packageManager": "pnpm@9.12.3",

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -1,9 +1,9 @@
import type { UmamiEventData } from "@umami/node";
import { Umami } from "@umami/node";
import SuperJSON from "superjson";
import { count, db, eq } from "@homarr/db";
import { integrations, items, serverSettings, users } from "@homarr/db/schema/sqlite";
import { count, db } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import { integrations, items, users } from "@homarr/db/schema/sqlite";
import { logger } from "@homarr/log";
import type { defaultServerSettings } from "@homarr/server-settings";
@@ -12,18 +12,7 @@ import { UMAMI_HOST_URL, UMAMI_WEBSITE_ID } from "./constants";
export const sendServerAnalyticsAsync = async () => {
const stopWatch = new Stopwatch();
const setting = await db.query.serverSettings.findFirst({
where: eq(serverSettings.settingKey, "analytics"),
});
if (!setting) {
logger.info(
"Server does not know the configured state of analytics. No data will be sent. Enable analytics in the settings",
);
return;
}
const analyticsSettings = SuperJSON.parse<typeof defaultServerSettings.analytics>(setting.value);
const analyticsSettings = await getServerSettingByKeyAsync(db, "analytics");
if (!analyticsSettings.enableGeneral) {
logger.info("Analytics are disabled. No data will be sent. Enable analytics in the settings");

View File

@@ -49,7 +49,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.31",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"prettier": "^3.3.3",
"typescript": "^5.6.3"
}

View File

@@ -1,6 +1,6 @@
import { TRPCError } from "@trpc/server";
import { asc, createId, eq, like } from "@homarr/db";
import { asc, createId, eq, inArray, like } from "@homarr/db";
import { apps } from "@homarr/db/schema/sqlite";
import { validation, z } from "@homarr/validation";
@@ -55,6 +55,8 @@ export const appRouter = createTRPCRouter({
name: z.string(),
id: z.string(),
iconUrl: z.string(),
description: z.string().nullable(),
href: z.string().nullable(),
}),
),
)
@@ -72,6 +74,8 @@ export const appRouter = createTRPCRouter({
id: true,
name: true,
iconUrl: true,
description: true,
href: true,
},
orderBy: asc(apps.name),
});
@@ -102,6 +106,11 @@ export const appRouter = createTRPCRouter({
return app;
}),
byIds: publicProcedure.input(z.array(z.string())).query(async ({ ctx, input }) => {
return await ctx.db.query.apps.findMany({
where: inArray(apps.id, input),
});
}),
create: protectedProcedure
.input(validation.app.manage)
.output(z.void())

View File

@@ -4,6 +4,7 @@ import superjson from "superjson";
import { constructBoardPermissions } from "@homarr/auth/shared";
import type { Database, SQL } from "@homarr/db";
import { and, createId, eq, inArray, like, or } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import {
boardGroupPermissions,
boards,
@@ -41,6 +42,16 @@ export const boardRouter = createTRPCRouter({
throw error;
}
}),
getPublicBoards: publicProcedure.query(async ({ ctx }) => {
return await ctx.db.query.boards.findMany({
columns: {
id: true,
name: true,
logoImageUrl: true,
},
where: eq(boards.isPublic, true),
});
}),
getAllBoards: publicProcedure.query(async ({ ctx }) => {
const userId = ctx.session?.user.id;
const permissionsOfCurrentUserWhenPresent = await ctx.db.query.boardUserPermissions.findMany({
@@ -216,6 +227,14 @@ export const boardRouter = createTRPCRouter({
.input(validation.board.changeVisibility)
.mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "full");
const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board");
if (input.visibility !== "public" && boardSettings.defaultBoardId === input.id) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Cannot make default board private",
});
}
await ctx.db
.update(boards)
@@ -240,7 +259,22 @@ export const boardRouter = createTRPCRouter({
})
: null;
const boardWhere = user?.homeBoardId ? eq(boards.id, user.homeBoardId) : eq(boards.name, "home");
// 1. user home board, 2. default board, 3. not found
let boardWhere: SQL<unknown> | null = null;
if (user?.homeBoardId) {
boardWhere = eq(boards.id, user.homeBoardId);
} else {
const boardSettings = await getServerSettingByKeyAsync(ctx.db, "board");
boardWhere = boardSettings.defaultBoardId ? eq(boards.id, boardSettings.defaultBoardId) : null;
}
if (!boardWhere) {
throw new TRPCError({
code: "NOT_FOUND",
message: "No home board found",
});
}
await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);

View File

@@ -75,6 +75,14 @@ export const groupRouter = createTRPCRouter({
permission: true,
},
},
owner: {
columns: {
id: true,
name: true,
image: true,
email: true,
},
},
},
});

View File

@@ -1,47 +1,16 @@
import SuperJSON from "superjson";
import { eq } from "@homarr/db";
import { serverSettings } from "@homarr/db/schema/sqlite";
import { logger } from "@homarr/log";
import type { defaultServerSettings, ServerSettings } from "@homarr/server-settings";
import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
import type { ServerSettings } from "@homarr/server-settings";
import { defaultServerSettingsKeys } from "@homarr/server-settings";
import { z } from "@homarr/validation";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
export const serverSettingsRouter = createTRPCRouter({
// this must be public so anonymous users also get analytics
getAnalytics: publicProcedure.query(async ({ ctx }) => {
const setting = await ctx.db.query.serverSettings.findFirst({
where: eq(serverSettings.settingKey, "analytics"),
});
if (!setting) {
logger.info(
"Server settings for analytics is currently undefined. Using default values instead. If this persists, there may be an issue with the server settings",
);
return {
enableGeneral: true,
enableIntegrationData: false,
enableUserData: false,
enableWidgetData: false,
} as (typeof defaultServerSettings)["analytics"];
}
return SuperJSON.parse<(typeof defaultServerSettings)["analytics"]>(setting.value);
getCulture: publicProcedure.query(async ({ ctx }) => {
return await getServerSettingByKeyAsync(ctx.db, "culture");
}),
getAll: protectedProcedure.query(async ({ ctx }) => {
const settings = await ctx.db.query.serverSettings.findMany();
const data = {} as ServerSettings;
defaultServerSettingsKeys.forEach((key) => {
const settingValue = settings.find((setting) => setting.settingKey === key)?.value;
if (!settingValue) {
return;
}
data[key] = SuperJSON.parse(settingValue);
});
return data;
return await getServerSettingsAsync(ctx.db);
}),
saveSettings: protectedProcedure
.input(
@@ -51,12 +20,10 @@ export const serverSettingsRouter = createTRPCRouter({
}),
)
.mutation(async ({ ctx, input }) => {
const databaseRunResult = await ctx.db
.update(serverSettings)
.set({
value: SuperJSON.stringify(input.value),
})
.where(eq(serverSettings.settingKey, input.settingsKey));
return databaseRunResult.changes === 1;
await updateServerSettingByKeyAsync(
ctx.db,
input.settingsKey,
input.value as ServerSettings[keyof ServerSettings],
);
}),
});

View File

@@ -15,6 +15,7 @@ import {
integrations,
items,
sections,
serverSettings,
users,
} from "@homarr/db/schema/sqlite";
import { createDb } from "@homarr/db/test";
@@ -473,13 +474,19 @@ describe("deleteBoard should delete board", () => {
});
describe("getHomeBoard should return home board", () => {
it("should return home board", async () => {
test("should return user home board when user has one", async () => {
// Arrange
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
const db = createDb();
const caller = boardRouter.createCaller({ db, session: defaultSession });
const fullBoardProps = await createFullBoardAsync(db, "home");
await db
.update(users)
.set({
homeBoardId: fullBoardProps.boardId,
})
.where(eq(users.id, defaultCreatorId));
// Act
const result = await caller.getHomeBoard();
@@ -491,6 +498,40 @@ describe("getHomeBoard should return home board", () => {
});
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "view");
});
test("should return global home board when user doesn't have one", async () => {
// Arrange
const spy = vi.spyOn(boardAccess, "throwIfActionForbiddenAsync");
const db = createDb();
const caller = boardRouter.createCaller({ db, session: defaultSession });
const fullBoardProps = await createFullBoardAsync(db, "home");
await db.insert(serverSettings).values({
settingKey: "board",
value: SuperJSON.stringify({ defaultBoardId: fullBoardProps.boardId }),
});
// Act
const result = await caller.getHomeBoard();
// Assert
expectInputToBeFullBoardWithName(result, {
name: "home",
...fullBoardProps,
});
expect(spy).toHaveBeenCalledWith(expect.anything(), expect.anything(), "view");
});
test("should throw error when home board not configured in serverSettings", async () => {
// Arrange
const db = createDb();
const caller = boardRouter.createCaller({ db, session: defaultSession });
await createFullBoardAsync(db, "home");
// Act
const actAsync = async () => await caller.getHomeBoard();
// Assert
await expect(actAsync()).rejects.toThrowError("No home board found");
});
});
describe("getBoardByName should return board by name", () => {

View File

@@ -40,56 +40,20 @@ describe("getAll server settings", () => {
await expect(actAsync()).rejects.toThrow();
});
test("getAll should return server", async () => {
test("getAll should return default server settings when nothing in database", async () => {
const db = createDb();
const caller = serverSettingsRouter.createCaller({
db,
session: defaultSession,
});
await db.insert(serverSettings).values([
{
settingKey: defaultServerSettingsKeys[0],
value: SuperJSON.stringify(defaultServerSettings.analytics),
},
]);
const result = await caller.getAll();
expect(result).toStrictEqual({
analytics: {
enableGeneral: true,
enableWidgetData: false,
enableIntegrationData: false,
enableUserData: false,
},
});
expect(result).toStrictEqual(defaultServerSettings);
});
});
describe("saveSettings", () => {
test("saveSettings should return false when it did not update one", async () => {
const db = createDb();
const caller = serverSettingsRouter.createCaller({
db,
session: defaultSession,
});
const result = await caller.saveSettings({
settingsKey: "analytics",
value: {
enableGeneral: true,
enableWidgetData: true,
enableIntegrationData: true,
enableUserData: true,
},
});
expect(result).toBe(false);
const dbSettings = await db.select().from(serverSettings);
expect(dbSettings.length).toBe(0);
});
test("saveSettings should update settings and return true when it updated only one", async () => {
const db = createDb();
const caller = serverSettingsRouter.createCaller({
@@ -104,7 +68,7 @@ describe("saveSettings", () => {
},
]);
const result = await caller.saveSettings({
await caller.saveSettings({
settingsKey: "analytics",
value: {
enableGeneral: true,
@@ -114,8 +78,6 @@ describe("saveSettings", () => {
},
});
expect(result).toBe(true);
const dbSettings = await db.select().from(serverSettings);
expect(dbSettings).toStrictEqual([
{

View File

@@ -18,7 +18,11 @@ import { createRedirectUri } from "./redirect";
import { expireDateAfter, generateSessionToken, sessionTokenCookieName } from "./session";
// See why it's unknown in the [...nextauth]/route.ts file
export const createConfiguration = (provider: SupportedAuthProvider | "unknown", headers: ReadonlyHeaders | null) => {
export const createConfiguration = (
provider: SupportedAuthProvider | "unknown",
headers: ReadonlyHeaders | null,
useSecureCookies: boolean,
) => {
const adapter = createAdapter(db, provider);
return NextAuth({
logger: {
@@ -37,12 +41,6 @@ export const createConfiguration = (provider: SupportedAuthProvider | "unknown",
cookies: {
sessionToken: {
name: sessionTokenCookieName,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: true,
},
},
},
adapter,
@@ -81,7 +79,7 @@ export const createConfiguration = (provider: SupportedAuthProvider | "unknown",
expires: expires,
httpOnly: true,
sameSite: "lax",
secure: true,
secure: useSecureCookies,
});
return true;

View File

@@ -5,7 +5,7 @@ import type { NextAuthConfig } from "next-auth";
import { and, eq, inArray } from "@homarr/db";
import type { Database } from "@homarr/db";
import { groupMembers, groups, users } from "@homarr/db/schema/sqlite";
import { everyoneGroup } from "@homarr/definitions";
import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions";
import { logger } from "@homarr/log";
import { env } from "./env.mjs";
@@ -51,8 +51,10 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
);
}
logger.info(`User '${dbUser.name}' logged in at ${dayjs().format()}`);
// We use a cookie as localStorage is not shared with server (otherwise flickering would occur)
cookies().set("homarr-color-scheme", dbUser.colorScheme, {
cookies().set(colorSchemeCookieKey, dbUser.colorScheme, {
path: "/",
expires: dayjs().add(1, "year").toDate(),
});

View File

@@ -20,6 +20,7 @@ declare module "next-auth" {
export * from "./security";
// See why it's unknown in the [...nextauth]/route.ts file
export const createHandlers = (provider: SupportedAuthProvider | "unknown") => createConfiguration(provider, headers());
export const createHandlers = (provider: SupportedAuthProvider | "unknown", useSecureCookies: boolean) =>
createConfiguration(provider, headers(), useSecureCookies);
export { getSessionFromTokenAsync as getSessionFromToken, sessionTokenCookieName } from "./session";

View File

@@ -2,7 +2,7 @@ import { cache } from "react";
import { createConfiguration } from "./configuration";
const { auth: defaultAuth } = createConfiguration("unknown", null);
const { auth: defaultAuth } = createConfiguration("unknown", null, false);
/**
* This is the main way to get session data for your RSCs.

View File

@@ -23,8 +23,8 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@auth/core": "^0.37.2",
"@auth/drizzle-adapter": "^1.7.2",
"@auth/core": "^0.37.3",
"@auth/drizzle-adapter": "^1.7.3",
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
@@ -45,7 +45,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "5.0.2",
"@types/cookies": "0.9.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"prettier": "^3.3.3",
"typescript": "^5.6.3"
}

View File

@@ -7,7 +7,7 @@ import { eq } from "@homarr/db";
import type { Database } from "@homarr/db";
import { groupMembers, groups, users } from "@homarr/db/schema/sqlite";
import { createDb } from "@homarr/db/test";
import { everyoneGroup } from "@homarr/definitions";
import { colorSchemeCookieKey, everyoneGroup } from "@homarr/definitions";
import { createSignInEventHandler } from "../events";
@@ -224,7 +224,7 @@ describe("createSignInEventHandler should create signInEventHandler", () => {
});
expect(dbUser?.name).toBe("test-new");
});
test("signInEventHandler should set homarr-color-scheme cookie", async () => {
test("signInEventHandler should set color-scheme cookie", async () => {
// Arrange
const db = createDb();
await createUserAsync(db);
@@ -239,7 +239,7 @@ describe("createSignInEventHandler should create signInEventHandler", () => {
// Assert
expect(cookies().set).toHaveBeenCalledWith(
"homarr-color-scheme",
colorSchemeCookieKey,
"dark",
expect.objectContaining({
path: "/",

View File

@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -29,13 +29,13 @@
"dayjs": "^1.11.13",
"next": "^14.2.16",
"react": "^18.3.1",
"tldts": "^6.1.57"
"tldts": "^6.1.58"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -1,25 +1,23 @@
"use client";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime);
const calculateTimeAgo = (timestamp: Date, locale: string) => {
return dayjs().locale(locale).to(timestamp);
const calculateTimeAgo = (timestamp: Date) => {
return dayjs().to(timestamp);
};
export const useTimeAgo = (timestamp: Date) => {
const { locale } = useParams<{ locale: string }>();
const [timeAgo, setTimeAgo] = useState(calculateTimeAgo(timestamp, locale));
const [timeAgo, setTimeAgo] = useState(calculateTimeAgo(timestamp));
useEffect(() => {
const intervalId = setInterval(() => setTimeAgo(calculateTimeAgo(timestamp, locale)), 1000); // update every second
const intervalId = setInterval(() => setTimeAgo(calculateTimeAgo(timestamp)), 1000); // update every second
return () => clearInterval(intervalId); // clear interval on hook unmount
}, [timestamp, locale]);
}, [timestamp]);
return timeAgo;
};

View File

@@ -30,7 +30,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -32,7 +32,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -41,7 +41,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -1,27 +1,16 @@
import SuperJSON from "superjson";
import { sendServerAnalyticsAsync } from "@homarr/analytics";
import { EVERY_WEEK } from "@homarr/cron-jobs-core/expressions";
import { db, eq } from "@homarr/db";
import { serverSettings } from "@homarr/db/schema/sqlite";
import type { defaultServerSettings } from "@homarr/server-settings";
import { db } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import { createCronJob } from "../lib";
export const analyticsJob = createCronJob("analytics", EVERY_WEEK, {
runOnStart: true,
}).withCallback(async () => {
const analyticSetting = await db.query.serverSettings.findFirst({
where: eq(serverSettings.settingKey, "analytics"),
});
const analyticSetting = await getServerSettingByKeyAsync(db, "analytics");
if (!analyticSetting) {
return;
}
const value = SuperJSON.parse<(typeof defaultServerSettings)["analytics"]>(analyticSetting.value);
if (!value.enableGeneral) {
if (!analyticSetting.enableGeneral) {
return;
}

View File

@@ -62,7 +62,7 @@ export const iconsUpdaterJob = createCronJob("iconsUpdater", EVERY_WEEK, {
id: createId(),
checksum: icon.checksum,
name: icon.fileNameWithExtension,
url: icon.imageUrl.href,
url: icon.imageUrl,
iconRepositoryId: repositoryIconGroupId,
});
countInserted++;

View File

@@ -1,5 +1,6 @@
import SuperJSON from "superjson";
import { objectKeys } from "@homarr/common";
import { everyoneGroup } from "@homarr/definitions";
import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings";
@@ -32,21 +33,33 @@ const seedEveryoneGroupAsync = async (db: Database) => {
const seedServerSettingsAsync = async (db: Database) => {
const serverSettingsData = await db.query.serverSettings.findMany();
let insertedSettingsCount = 0;
for (const settingsKey of defaultServerSettingsKeys) {
if (serverSettingsData.some((setting) => setting.settingKey === settingsKey)) {
return;
const currentDbEntry = serverSettingsData.find((setting) => setting.settingKey === settingsKey);
if (!currentDbEntry) {
await db.insert(serverSettings).values({
settingKey: settingsKey,
value: SuperJSON.stringify(defaultServerSettings[settingsKey]),
});
console.log(`Created serverSetting through seed key=${settingsKey}`);
continue;
}
await db.insert(serverSettings).values({
settingKey: settingsKey,
value: SuperJSON.stringify(defaultServerSettings[settingsKey]),
});
insertedSettingsCount++;
}
const currentSettings = SuperJSON.parse<Record<string, unknown>>(currentDbEntry.value);
const defaultSettings = defaultServerSettings[settingsKey];
const missingKeys = objectKeys(defaultSettings).filter((key) => !(key in currentSettings));
if (insertedSettingsCount > 0) {
console.info(`Inserted ${insertedSettingsCount} missing settings`);
if (missingKeys.length === 0) {
console.info(`Skipping seeding for serverSetting as it already exists key=${settingsKey}`);
continue;
}
await db
.update(serverSettings)
.set({
value: SuperJSON.stringify({ ...defaultSettings, ...currentSettings }), // Add missing keys
})
.where(eq(serverSettings.settingKey, settingsKey));
console.log(`Updated serverSetting through seed key=${settingsKey}`);
}
};

View File

@@ -35,7 +35,7 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@auth/core": "^0.37.2",
"@auth/core": "^0.37.3",
"@homarr/common": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
@@ -54,7 +54,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "7.6.11",
"dotenv-cli": "^7.4.2",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"prettier": "^3.3.3",
"tsx": "4.19.2",
"typescript": "^5.6.3"

View File

@@ -1 +1,2 @@
export * from "./item";
export * from "./server-setting";

View File

@@ -0,0 +1,52 @@
import SuperJSON from "superjson";
import type { ServerSettings } from "@homarr/server-settings";
import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings";
import type { Database } from "..";
import { eq } from "..";
import { serverSettings } from "../schema/sqlite";
export const getServerSettingsAsync = async (db: Database) => {
const settings = await db.query.serverSettings.findMany();
return defaultServerSettingsKeys.reduce((acc, settingKey) => {
const setting = settings.find((setting) => setting.settingKey === settingKey);
if (!setting) {
// Typescript is not happy because the key is a union and it does not know that they are the same
acc[settingKey] = defaultServerSettings[settingKey] as never;
return acc;
}
acc[settingKey] = {
...defaultServerSettings[settingKey],
...SuperJSON.parse(setting.value),
};
return acc;
}, {} as ServerSettings);
};
export const getServerSettingByKeyAsync = async <TKey extends keyof ServerSettings>(db: Database, key: TKey) => {
const dbSettings = await db.query.serverSettings.findFirst({
where: eq(serverSettings.settingKey, key),
});
if (!dbSettings) {
return defaultServerSettings[key];
}
return SuperJSON.parse<ServerSettings[TKey]>(dbSettings.value);
};
export const updateServerSettingByKeyAsync = async <TKey extends keyof ServerSettings>(
db: Database,
key: TKey,
value: ServerSettings[TKey],
) => {
await db
.update(serverSettings)
.set({
value: SuperJSON.stringify(value),
})
.where(eq(serverSettings.settingKey, key));
};

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -0,0 +1,2 @@
export const colorSchemeCookieKey = "homarr.color-scheme";
export const localeCookieKey = "homarr.locale";

View File

@@ -8,3 +8,4 @@ export * from "./auth";
export * from "./user";
export * from "./group";
export * from "./docs";
export * from "./cookie";

View File

@@ -15,6 +15,7 @@ export const widgetKinds = [
"mediaRequests-requestList",
"mediaRequests-requestStats",
"rssFeed",
"bookmarks",
"indexerManager",
"healthMonitoring",
] as const;

View File

@@ -30,7 +30,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -23,13 +23,14 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -1,5 +1,6 @@
import { GitHubIconRepository } from "./repositories/github.icon-repository";
import { JsdelivrIconRepository } from "./repositories/jsdelivr.icon-repository";
import { LocalIconRepository } from "./repositories/local.icon-repository";
import type { RepositoryIconGroup } from "./types";
const repositories = [
@@ -43,6 +44,7 @@ const repositories = [
new URL("https://data.jsdelivr.com/v1/packages/gh/loganmarchione/homelab-svg-assets@main?structure=flat"),
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/{0}",
),
new LocalIconRepository(),
];
export const fetchIconsAsync = async (): Promise<RepositoryIconGroup[]> => {

View File

@@ -35,11 +35,8 @@ export class GitHubIconRepository extends IconRepository {
.map(({ path, size: sizeInBytes, sha: checksum }) => {
const file = parse(path);
const fileNameWithExtension = file.base;
const imageUrl = new URL(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.repositoryBlobUrlTemplate!.replace("{0}", path).replace("{1}", file.name),
);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const imageUrl = this.repositoryBlobUrlTemplate!.replace("{0}", path).replace("{1}", file.name);
return {
imageUrl,
fileNameWithExtension,

View File

@@ -33,7 +33,7 @@ export class JsdelivrIconRepository extends IconRepository {
const fileNameWithExtension = file.base;
return {
imageUrl: new URL(this.repositoryBlobUrlTemplate.replace("{0}", path).replace("{1}", file.name)),
imageUrl: this.repositoryBlobUrlTemplate.replace("{0}", path).replace("{1}", file.name),
fileNameWithExtension,
local: false,
sizeInBytes,

View File

@@ -0,0 +1,26 @@
import { createHash } from "crypto";
import { db } from "@homarr/db";
import type { RepositoryIconGroup } from "../types";
import { IconRepository } from "./icon-repository";
export class LocalIconRepository extends IconRepository {
constructor() {
super("Local", "local", undefined, undefined, undefined, undefined);
}
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
const medias = await db.query.medias.findMany();
return {
success: true,
icons: medias.map((media) => ({
local: true,
fileNameWithExtension: media.name,
imageUrl: `/api/user-medias/${media.id}`,
checksum: createHash("md5").update(media.content).digest("hex"),
sizeInBytes: media.size,
})),
slug: "local",
};
}
}

View File

@@ -1,7 +1,7 @@
export interface RepositoryIcon {
fileNameWithExtension: string;
sizeInBytes?: number;
imageUrl: URL;
imageUrl: string;
local: boolean;
checksum: string;
}

View File

@@ -41,7 +41,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/xml2js": "^0.4.14",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -28,13 +28,13 @@
"dependencies": {
"ioredis": "5.4.1",
"superjson": "2.2.1",
"winston": "3.15.0"
"winston": "3.16.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -40,7 +40,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
},
"prettier": "@homarr/prettier-config"

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -31,7 +31,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
},
"prettier": "@homarr/prettier-config"

View File

@@ -1,49 +1,57 @@
import { createId, inArray } from "@homarr/db";
import type { Database, InferInsertModel } from "@homarr/db";
import type { Database, InferInsertModel, InferSelectModel } from "@homarr/db";
import { apps as appsTable } from "@homarr/db/schema/sqlite";
import { logger } from "@homarr/log";
import type { OldmarrApp } from "@homarr/old-schema";
import type { BookmarkApp } from "./widgets/definitions/bookmark";
type DbAppWithoutId = Omit<InferSelectModel<typeof appsTable>, "id">;
interface AppMapping extends DbAppWithoutId {
ids: string[];
newId: string;
exists: boolean;
}
export const insertAppsAsync = async (
db: Database,
apps: OldmarrApp[],
bookmarkApps: BookmarkApp[],
distinctAppsByHref: boolean,
configName: string,
) => {
logger.info(
`Importing old homarr apps configuration=${configName} distinctAppsByHref=${distinctAppsByHref} apps=${apps.length}`,
);
const existingAppsWithHref = distinctAppsByHref
? await db.query.apps.findMany({
where: inArray(appsTable.href, [...new Set(apps.map((app) => app.url))]),
where: inArray(appsTable.href, [
...new Set(apps.map((app) => app.url).concat(bookmarkApps.map((app) => app.href))),
]),
})
: [];
logger.debug(`Found existing apps with href count=${existingAppsWithHref.length}`);
const mappedApps = apps.map((app) => ({
// Use id of existing app when it has the same href and distinctAppsByHref is true
newId: distinctAppsByHref
? (existingAppsWithHref.find(
(existingApp) =>
existingApp.href === (app.behaviour.externalUrl === "" ? app.url : app.behaviour.externalUrl) &&
existingApp.name === app.name &&
existingApp.iconUrl === app.appearance.iconUrl,
)?.id ?? createId())
: createId(),
...app,
}));
// Generate mappings for all apps from old to new ids
const appMappings: AppMapping[] = [];
addMappingFor(apps, appMappings, existingAppsWithHref, convertApp);
addMappingFor(bookmarkApps, appMappings, existingAppsWithHref, convertBookmarkApp);
const appsToCreate = mappedApps
.filter((app) => !existingAppsWithHref.some((existingApp) => existingApp.id === app.newId))
logger.debug(`Mapping apps count=${appMappings.length}`);
const appsToCreate = appMappings
.filter((app) => !app.exists)
.map(
(app) =>
({
id: app.newId,
name: app.name,
iconUrl: app.appearance.iconUrl,
href: app.behaviour.externalUrl === "" ? app.url : app.behaviour.externalUrl,
description: app.behaviour.tooltipDescription,
iconUrl: app.iconUrl,
href: app.href,
description: app.description,
}) satisfies InferInsertModel<typeof appsTable>,
);
@@ -55,5 +63,95 @@ export const insertAppsAsync = async (
logger.info(`Imported apps count=${appsToCreate.length}`);
return mappedApps;
// Generates a map from old key to new key for all apps
return new Map(
appMappings
.map((app) => app.ids.map((id) => ({ id, newId: app.newId })))
.flat()
.map(({ id, newId }) => [id, newId]),
);
};
/**
* Creates a callback to be used in a find method that compares the old app with the new app
* @param app either an oldmarr app or a bookmark app
* @param convertApp a function that converts the app to a new app
* @returns a callback that compares the old app with the new app and returns true if they are the same
*/
const createFindCallback = <TApp extends OldmarrApp | BookmarkApp>(
app: TApp,
convertApp: (app: TApp) => DbAppWithoutId,
) => {
const oldApp = convertApp(app);
return (dbApp: DbAppWithoutId) =>
oldApp.href === dbApp.href &&
oldApp.name === dbApp.name &&
oldApp.iconUrl === dbApp.iconUrl &&
oldApp.description === dbApp.description;
};
/**
* Adds mappings for the given apps to the appMappings array
* @param apps apps to add mappings for
* @param appMappings existing app mappings
* @param existingAppsWithHref existing apps with href
* @param convertApp a function that converts the app to a new app
*/
const addMappingFor = <TApp extends OldmarrApp | BookmarkApp>(
apps: TApp[],
appMappings: AppMapping[],
existingAppsWithHref: InferSelectModel<typeof appsTable>[],
convertApp: (app: TApp) => DbAppWithoutId,
) => {
for (const app of apps) {
const previous = appMappings.find(createFindCallback(app, convertApp));
if (previous) {
previous.ids.push(app.id);
continue;
}
const existing = existingAppsWithHref.find(createFindCallback(app, convertApp));
if (existing) {
appMappings.push({
ids: [app.id],
newId: existing.id,
name: existing.name,
href: existing.href,
iconUrl: existing.iconUrl,
description: existing.description,
exists: true,
});
continue;
}
appMappings.push({
ids: [app.id],
newId: createId(),
...convertApp(app),
exists: false,
});
}
};
/**
* Converts an oldmarr app to a new app
* @param app oldmarr app
* @returns new app
*/
const convertApp = (app: OldmarrApp): DbAppWithoutId => ({
name: app.name,
href: app.behaviour.externalUrl === "" ? app.url : app.behaviour.externalUrl,
iconUrl: app.appearance.iconUrl,
description: app.behaviour.tooltipDescription ?? null,
});
/**
* Converts a bookmark app to a new app
* @param app bookmark app
* @returns new app
*/
const convertBookmarkApp = (app: BookmarkApp): DbAppWithoutId => ({
...app,
description: null,
});

View File

@@ -15,11 +15,12 @@ import { mapOptions } from "./widgets/options";
export const insertItemsAsync = async (
db: Database,
widgets: OldmarrWidget[],
mappedApps: (OldmarrApp & { newId: string })[],
apps: OldmarrApp[],
appsMap: Map<string, string>,
sectionIdMaps: Map<string, string>,
configuration: OldmarrImportConfiguration,
) => {
logger.info(`Importing old homarr items widgets=${widgets.length} apps=${mappedApps.length}`);
logger.info(`Importing old homarr items widgets=${widgets.length} apps=${apps.length}`);
for (const widget of widgets) {
// All items should have been moved to the last wrapper
@@ -54,13 +55,13 @@ export const insertItemsAsync = async (
xOffset: screenSizeShape.location.x,
yOffset: screenSizeShape.location.y,
kind,
options: SuperJSON.stringify(mapOptions(kind, widget.properties)),
options: SuperJSON.stringify(mapOptions(kind, widget.properties, appsMap)),
});
logger.debug(`Inserted widget id=${widget.id} sectionId=${sectionId}`);
}
for (const app of mappedApps) {
for (const app of apps) {
// All items should have been moved to the last wrapper
if (app.area.type === "sidebar") {
continue;
@@ -85,7 +86,9 @@ export const insertItemsAsync = async (
yOffset: screenSizeShape.location.y,
kind: "app",
options: SuperJSON.stringify({
appId: app.newId,
// it's safe to assume that the app exists in the map
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
appId: appsMap.get(app.id)!,
openInNewTab: app.behaviour.isOpeningNewTab,
pingEnabled: app.network.enabledStatusChecker,
showDescriptionTooltip: app.behaviour.tooltipDescription !== "",

View File

@@ -9,12 +9,24 @@ import { OldHomarrImportError, OldHomarrScreenSizeError } from "./import-error";
import { insertItemsAsync } from "./import-items";
import { insertSectionsAsync } from "./import-sections";
import { moveWidgetsAndAppsIfMerge } from "./move-widgets-and-apps-merge";
import type { BookmarkApp } from "./widgets/definitions/bookmark";
export const importAsync = async (db: Database, old: OldmarrConfig, configuration: OldmarrImportConfiguration) => {
const bookmarkApps = old.widgets
.filter((widget) => widget.type === "bookmark")
.map((widget) => widget.properties.items)
.flat() as BookmarkApp[];
if (configuration.onlyImportApps) {
await db
.transaction(async (trasaction) => {
await insertAppsAsync(trasaction, old.apps, configuration.distinctAppsByHref, old.configProperties.name);
await insertAppsAsync(
trasaction,
old.apps,
bookmarkApps,
configuration.distinctAppsByHref,
old.configProperties.name,
);
})
.catch((error) => {
throw new OldHomarrImportError(old, error);
@@ -29,13 +41,14 @@ export const importAsync = async (db: Database, old: OldmarrConfig, configuratio
const boardId = await insertBoardAsync(trasaction, old, configuration);
const sectionIdMaps = await insertSectionsAsync(trasaction, categories, wrappers, boardId);
const mappedApps = await insertAppsAsync(
const appsMap = await insertAppsAsync(
trasaction,
apps,
bookmarkApps,
configuration.distinctAppsByHref,
old.configProperties.name,
);
await insertItemsAsync(trasaction, widgets, mappedApps, sectionIdMaps, configuration);
await insertItemsAsync(trasaction, widgets, apps, appsMap, sectionIdMaps, configuration);
})
.catch((error) => {
if (error instanceof OldHomarrScreenSizeError) {

View File

@@ -16,3 +16,5 @@ export type OldmarrBookmarkDefinition = CommonOldmarrWidgetDefinition<
layout: "autoGrid" | "horizontal" | "vertical";
}
>;
export type BookmarkApp = OldmarrBookmarkDefinition["options"]["items"][number];

View File

@@ -66,6 +66,7 @@ export const widgetKindMapping = {
"mediaRequests-requestList": "media-requests-list",
"mediaRequests-requestStats": "media-requests-stats",
indexerManager: "indexer-manager",
bookmarks: "bookmark",
healthMonitoring: "health-monitoring",
} satisfies Record<WidgetKind, OldmarrWidgetDefinitions["id"] | null>;
// Use null for widgets that did not exist in oldmarr

View File

@@ -13,6 +13,7 @@ type OptionMapping = {
: {
[OptionsKey in keyof WidgetComponentProps<WidgetKey>["options"]]: (
oldOptions: Extract<OldmarrWidgetDefinitions, { id: WidgetMapping[WidgetKey] }>["options"],
appsMap: Map<string, string>,
) => WidgetComponentProps<WidgetKey>["options"][OptionsKey] | undefined;
};
};
@@ -22,6 +23,22 @@ const optionMapping: OptionMapping = {
linksTargetNewTab: (oldOptions) => oldOptions.openInNewTab,
},
"mediaRequests-requestStats": {},
bookmarks: {
title: (oldOptions) => oldOptions.name,
// It's safe to assume that the app exists, because the app is always created before the widget
// And the mapping is created in insertAppsAsync
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
items: (oldOptions, appsMap) => oldOptions.items.map((item) => appsMap.get(item.id)!),
layout: (oldOptions) => {
const mappedLayouts: Record<typeof oldOptions.layout, WidgetComponentProps<"bookmarks">["options"]["layout"]> = {
autoGrid: "grid",
horizontal: "row",
vertical: "column",
};
return mappedLayouts[oldOptions.layout];
},
},
calendar: {
releaseType: (oldOptions) => [oldOptions.radarrReleaseType],
filterFutureMonths: () => undefined,
@@ -118,11 +135,13 @@ const optionMapping: OptionMapping = {
* Maps the oldmarr options to the newmarr options
* @param kind item kind to map
* @param oldOptions oldmarr options for this item
* @param appsMap map of old app ids to new app ids
* @returns newmarr options for this item or null if the item did not exist in oldmarr
*/
export const mapOptions = <K extends WidgetKind>(
kind: K,
oldOptions: Extract<OldmarrWidgetDefinitions, { id: WidgetMapping[K] }>["options"],
appsMap: Map<string, string>,
) => {
logger.debug(`Mapping old homarr options for widget kind=${kind} options=${JSON.stringify(oldOptions)}`);
if (optionMapping[kind] === null) {
@@ -132,7 +151,7 @@ export const mapOptions = <K extends WidgetKind>(
const mapping = optionMapping[kind];
return objectEntries(mapping).reduce(
(acc, [key, value]) => {
const newValue = value(oldOptions as never);
const newValue = value(oldOptions as never, appsMap);
logger.debug(`Mapping old homarr option kind=${kind} key=${key as string} newValue=${newValue as string}`);
if (newValue !== undefined) {
acc[key as string] = newValue;

View File

@@ -27,7 +27,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
},
"prettier": "@homarr/prettier-config"

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -21,11 +21,15 @@
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -1,4 +1,13 @@
export const defaultServerSettingsKeys = ["analytics", "crawlingAndIndexing"] as const;
import type { ColorScheme } from "@homarr/definitions";
import type { SupportedLanguage } from "@homarr/translation";
export const defaultServerSettingsKeys = [
"analytics",
"crawlingAndIndexing",
"board",
"appearance",
"culture",
] as const;
export type ServerSettingsRecord = Record<(typeof defaultServerSettingsKeys)[number], Record<string, unknown>>;
@@ -15,6 +24,15 @@ export const defaultServerSettings = {
noTranslate: true,
noSiteLinksSearchBox: false,
},
board: {
defaultBoardId: null as string | null,
},
appearance: {
defaultColorScheme: "light" as ColorScheme,
},
culture: {
defaultLocale: "en" as SupportedLanguage,
},
} satisfies ServerSettingsRecord;
export type ServerSettings = typeof defaultServerSettings;

View File

@@ -43,7 +43,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
},
"prettier": "@homarr/prettier-config"

View File

@@ -9,7 +9,8 @@
"./client": "./src/client/index.ts",
"./server": "./src/server.ts",
"./middleware": "./src/middleware.ts",
"./request": "./src/request.ts"
"./request": "./src/request.ts",
"./dayjs": "./src/dayjs.ts"
},
"typesVersions": {
"*": {
@@ -27,7 +28,9 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"dayjs": "^1.11.13",
"deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.7",
"next": "^14.2.16",
"next-intl": "3.24.0",
@@ -37,7 +40,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.13.0",
"eslint": "^9.14.0",
"typescript": "^5.6.3"
}
}

View File

@@ -1,15 +1,342 @@
import type { MRT_Localization } from "mantine-react-table";
import { objectKeys } from "@homarr/common";
export const localeConfigurations = {
cn: {
name: "中文",
translatedName: "Chinese (Simplified)",
flagIcon: "cn",
importMrtLocalization() {
return import("mantine-react-table/locales/zh-Hans/index.esm.mjs").then(
(module) => module.MRT_Localization_ZH_HANS,
);
},
importDayJsLocale() {
return import("dayjs/locale/zh-cn").then((module) => module.default);
},
},
cs: {
name: "Čeština",
translatedName: "Czech",
flagIcon: "cz",
importMrtLocalization() {
return import("mantine-react-table/locales/cs/index.esm.mjs").then((module) => module.MRT_Localization_CS);
},
importDayJsLocale() {
return import("dayjs/locale/cs").then((module) => module.default);
},
},
da: {
name: "Dansk",
translatedName: "Danish",
flagIcon: "dk",
importMrtLocalization() {
return import("mantine-react-table/locales/da/index.esm.mjs").then((module) => module.MRT_Localization_DA);
},
importDayJsLocale() {
return import("dayjs/locale/da").then((module) => module.default);
},
},
de: {
name: "Deutsch",
translatedName: "German",
flagIcon: "de",
importMrtLocalization() {
return import("mantine-react-table/locales/de/index.esm.mjs").then((module) => module.MRT_Localization_DE);
},
importDayJsLocale() {
return import("dayjs/locale/de").then((module) => module.default);
},
},
en: {
name: "English",
translatedName: "English",
flagIcon: "us",
importMrtLocalization() {
return import("mantine-react-table/locales/en/index.esm.mjs").then((module) => module.MRT_Localization_EN);
},
importDayJsLocale() {
return import("dayjs/locale/en").then((module) => module.default);
},
},
el: {
name: "Ελληνικά",
translatedName: "Greek",
flagIcon: "gr",
importMrtLocalization() {
return import("mantine-react-table/locales/el/index.esm.mjs").then((module) => module.MRT_Localization_EL);
},
importDayJsLocale() {
return import("dayjs/locale/el").then((module) => module.default);
},
},
es: {
name: "Español",
translatedName: "Spanish",
flagIcon: "es",
importMrtLocalization() {
return import("mantine-react-table/locales/es/index.esm.mjs").then((module) => module.MRT_Localization_ES);
},
importDayJsLocale() {
return import("dayjs/locale/es").then((module) => module.default);
},
},
et: {
name: "Eesti",
translatedName: "Estonian",
flagIcon: "ee",
importMrtLocalization() {
return import("mantine-react-table/locales/et/index.esm.mjs").then((module) => module.MRT_Localization_ET);
},
importDayJsLocale() {
return import("dayjs/locale/et").then((module) => module.default);
},
},
fr: {
name: "Français",
translatedName: "French",
flagIcon: "fr",
importMrtLocalization() {
return import("mantine-react-table/locales/fr/index.esm.mjs").then((module) => module.MRT_Localization_FR);
},
importDayJsLocale() {
return import("dayjs/locale/fr").then((module) => module.default);
},
},
he: {
name: "עברית",
translatedName: "Hebrew",
flagIcon: "il",
isRTL: true,
importMrtLocalization() {
return import("mantine-react-table/locales/he/index.esm.mjs").then((module) => module.MRT_Localization_HE);
},
importDayJsLocale() {
return import("dayjs/locale/he").then((module) => module.default);
},
},
hr: {
name: "Hrvatski",
translatedName: "Croatian",
flagIcon: "hr",
importMrtLocalization() {
return import("mantine-react-table/locales/hr/index.esm.mjs").then((module) => module.MRT_Localization_HR);
},
importDayJsLocale() {
return import("dayjs/locale/hr").then((module) => module.default);
},
},
hu: {
name: "Magyar",
translatedName: "Hungarian",
flagIcon: "hu",
importMrtLocalization() {
return import("mantine-react-table/locales/hu/index.esm.mjs").then((module) => module.MRT_Localization_HU);
},
importDayJsLocale() {
return import("dayjs/locale/hu").then((module) => module.default);
},
},
it: {
name: "Italiano",
translatedName: "Italian",
flagIcon: "it",
importMrtLocalization() {
return import("mantine-react-table/locales/it/index.esm.mjs").then((module) => module.MRT_Localization_IT);
},
importDayJsLocale() {
return import("dayjs/locale/it").then((module) => module.default);
},
},
ja: {
name: "日本語",
translatedName: "Japanese",
flagIcon: "jp",
importMrtLocalization() {
return import("mantine-react-table/locales/ja/index.esm.mjs").then((module) => module.MRT_Localization_JA);
},
importDayJsLocale() {
return import("dayjs/locale/ja").then((module) => module.default);
},
},
ko: {
name: "한국어",
translatedName: "Korean",
flagIcon: "kr",
importMrtLocalization() {
return import("mantine-react-table/locales/ko/index.esm.mjs").then((module) => module.MRT_Localization_KO);
},
importDayJsLocale() {
return import("dayjs/locale/ko").then((module) => module.default);
},
},
lt: {
name: "Lietuvių",
translatedName: "Lithuanian",
flagIcon: "lt",
importMrtLocalization() {
return import("./mantine-react-table/lt.json");
},
importDayJsLocale() {
return import("dayjs/locale/lt").then((module) => module.default);
},
},
lv: {
name: "Latviešu",
translatedName: "Latvian",
flagIcon: "lv",
importMrtLocalization() {
return import("./mantine-react-table/lv.json");
},
importDayJsLocale() {
return import("dayjs/locale/lv").then((module) => module.default);
},
},
nl: {
name: "Nederlands",
translatedName: "Dutch",
flagIcon: "nl",
importMrtLocalization() {
return import("mantine-react-table/locales/nl/index.esm.mjs").then((module) => module.MRT_Localization_NL);
},
importDayJsLocale() {
return import("dayjs/locale/nl").then((module) => module.default);
},
},
no: {
name: "Norsk",
translatedName: "Norwegian",
flagIcon: "no",
importMrtLocalization() {
return import("mantine-react-table/locales/no/index.esm.mjs").then((module) => module.MRT_Localization_NO);
},
importDayJsLocale() {
return import("dayjs/locale/nb").then((module) => module.default);
},
},
pl: {
name: "Polski",
translatedName: "Polish",
flagIcon: "pl",
importMrtLocalization() {
return import("mantine-react-table/locales/pl/index.esm.mjs").then((module) => module.MRT_Localization_PL);
},
importDayJsLocale() {
return import("dayjs/locale/pl").then((module) => module.default);
},
},
pt: {
name: "Português",
translatedName: "Portuguese",
flagIcon: "pt",
importMrtLocalization() {
return import("mantine-react-table/locales/pt/index.esm.mjs").then((module) => module.MRT_Localization_PT);
},
importDayJsLocale() {
return import("dayjs/locale/pt").then((module) => module.default);
},
},
ro: {
name: "Românesc",
translatedName: "Romanian",
flagIcon: "ro",
importMrtLocalization() {
return import("mantine-react-table/locales/ro/index.esm.mjs").then((module) => module.MRT_Localization_RO);
},
importDayJsLocale() {
return import("dayjs/locale/ro").then((module) => module.default);
},
},
ru: {
name: "Русский",
translatedName: "Russian",
flagIcon: "ru",
importMrtLocalization() {
return import("mantine-react-table/locales/ru/index.esm.mjs").then((module) => module.MRT_Localization_RU);
},
importDayJsLocale() {
return import("dayjs/locale/ru").then((module) => module.default);
},
},
sk: {
name: "Slovenčina",
translatedName: "Slovak",
flagIcon: "sk",
importMrtLocalization() {
return import("mantine-react-table/locales/sk/index.esm.mjs").then((module) => module.MRT_Localization_SK);
},
importDayJsLocale() {
return import("dayjs/locale/sk").then((module) => module.default);
},
},
sl: {
name: "Slovenščina",
translatedName: "Slovenian",
flagIcon: "si",
importMrtLocalization() {
return import("./mantine-react-table/sl.json");
},
importDayJsLocale() {
return import("dayjs/locale/sl").then((module) => module.default);
},
},
sv: {
name: "Svenska",
translatedName: "Swedish",
flagIcon: "se",
importMrtLocalization() {
return import("mantine-react-table/locales/sv/index.esm.mjs").then((module) => module.MRT_Localization_SV);
},
importDayJsLocale() {
return import("dayjs/locale/sv").then((module) => module.default);
},
},
tr: {
name: "Türkçe",
translatedName: "Turkish",
flagIcon: "tr",
importMrtLocalization() {
return import("mantine-react-table/locales/tr/index.esm.mjs").then((module) => module.MRT_Localization_TR);
},
importDayJsLocale() {
return import("dayjs/locale/tr").then((module) => module.default);
},
},
tw: {
name: "中文",
translatedName: "Chinese (Traditional)",
flagIcon: "tw",
importMrtLocalization() {
return import("mantine-react-table/locales/zh-Hant/index.esm.mjs").then(
(module) => module.MRT_Localization_ZH_HANT,
);
},
importDayJsLocale() {
return import("dayjs/locale/zh-tw").then((module) => module.default);
},
},
uk: {
name: "Українська",
translatedName: "Ukrainian",
flagIcon: "ua",
importMrtLocalization() {
return import("mantine-react-table/locales/uk/index.esm.mjs").then((module) => module.MRT_Localization_UK);
},
importDayJsLocale() {
return import("dayjs/locale/uk").then((module) => module.default);
},
},
vi: {
name: "Tiếng Việt",
translatedName: "Vietnamese",
flagIcon: "vn",
importMrtLocalization() {
return import("mantine-react-table/locales/vi/index.esm.mjs").then((module) => module.MRT_Localization_VI);
},
importDayJsLocale() {
return import("dayjs/locale/vi").then((module) => module.default);
},
},
} satisfies Record<
string,
@@ -17,10 +344,16 @@ export const localeConfigurations = {
name: string;
translatedName: string;
flagIcon: string;
importMrtLocalization: () => Promise<MRT_Localization>;
importDayJsLocale: () => Promise<ILocale>;
isRTL?: boolean;
}
>;
export const supportedLanguages = objectKeys(localeConfigurations);
export type SupportedLanguage = (typeof supportedLanguages)[number];
export const defaultLocale = "en" satisfies SupportedLanguage;
export const fallbackLocale = "en" satisfies SupportedLanguage;
export const isLocaleRTL = (locale: SupportedLanguage) =>
"isRTL" in localeConfigurations[locale] && localeConfigurations[locale].isRTL;

View File

@@ -0,0 +1,48 @@
import { useParams } from "next/navigation";
import dayjs from "dayjs";
import type { SupportedLanguage } from "./config";
import { localeConfigurations } from "./config";
let promise: Promise<void> | null = null;
let loading = true;
let previousLocale: SupportedLanguage | null = null;
const load = () => {
if (loading) {
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw promise;
}
};
const dayJsLocalization = (locale: SupportedLanguage) => {
if (promise && previousLocale === locale) {
return {
load,
};
}
promise = localeConfigurations[locale]
.importDayJsLocale()
.then((dayJsLocale) => {
dayjs.locale(dayJsLocale);
loading = false;
previousLocale = locale;
})
.catch(() => {
loading = false;
});
return {
load,
};
};
/**
* Load the dayjs localization for the current locale with suspense
* This allows us to have the loading spinner shown until the localization is loaded and applied.
* Suspense works by throwing a promise, which is caught by the nearest Suspense boundary.
*/
export const useSuspenseDayJsLocalization = () => {
const { locale } = useParams<{ locale: SupportedLanguage }>();
const resource = dayJsLocalization(locale);
resource.load();
};

View File

@@ -0,0 +1,911 @@
{
"user": {
"title": "用户",
"name": "用户",
"field": {
"email": {
"label": "邮箱"
},
"username": {
"label": "用户名"
},
"password": {
"label": "密码",
"requirement": {
"lowercase": "包括小写字母",
"uppercase": "包含大写字母",
"number": "包含数字"
}
},
"passwordConfirm": {
"label": "确认密码"
}
},
"action": {
"login": {
"label": "登录"
},
"register": {
"label": "创建账号",
"notification": {
"success": {
"title": "账号已创建"
}
}
},
"create": "创建用户"
}
},
"group": {
"field": {
"name": "名称"
},
"permission": {
"admin": {
"title": "管理员"
},
"board": {
"title": "面板"
}
}
},
"app": {
"page": {
"list": {
"title": "应用"
}
},
"field": {
"name": {
"label": "名称"
}
}
},
"integration": {
"field": {
"name": {
"label": "名称"
}
},
"testConnection": {
"notification": {
"invalidUrl": {
"title": "无效链接"
}
}
},
"secrets": {
"kind": {
"username": {
"label": "用户名"
},
"password": {
"label": "密码",
"newLabel": "新密码"
}
}
}
},
"media": {
"field": {
"name": "名称",
"size": "大小",
"creator": "创建者"
}
},
"common": {
"error": "错误",
"action": {
"add": "添加",
"apply": "应用",
"create": "创建",
"edit": "编辑",
"insert": "插入",
"remove": "删除",
"save": "保存",
"saveChanges": "保存更改",
"cancel": "取消",
"delete": "删除",
"confirm": "确认",
"previous": "上一步",
"next": "下一步",
"tryAgain": "请再试一次"
},
"information": {
"hours": "时",
"minutes": "分"
},
"userAvatar": {
"menu": {
"preferences": "您的首选项",
"login": "登录"
}
},
"dangerZone": "危险",
"noResults": "未找到结果",
"zod": {
"errors": {
"default": "该字段无效",
"required": "此字段为必填",
"string": {
"startsWith": "该字段必须以 {startsWith} 开头",
"endsWith": "该字段必须以 {endsWith} 结尾",
"includes": "该字段必须包含 {includes}"
},
"tooSmall": {
"string": "该字段的长度必须至少为 {minimum} 个字符",
"number": "该字段必须大于或等于 {minimum}"
},
"tooBig": {
"string": "该字段的长度不得超过 {maximum} 个字符",
"number": "该字段必须小于或等于 {maximum}"
}
}
}
},
"section": {
"category": {
"field": {
"name": {
"label": "名称"
}
},
"action": {
"moveUp": "上移",
"moveDown": "下移"
},
"menu": {
"label": {
"changePosition": "换位"
}
}
}
},
"item": {
"menu": {
"label": {
"settings": "设置"
}
},
"moveResize": {
"field": {
"width": {
"label": "宽度"
},
"height": {
"label": "高度"
}
}
}
},
"widget": {
"app": {
"option": {
"openInNewTab": {
"label": "在新标签页中打开"
}
}
},
"dnsHoleSummary": {
"option": {
"layout": {
"label": "显示布局",
"option": {
"row": {
"label": "横向"
},
"column": {
"label": "垂直"
}
}
}
},
"data": {
"adsBlockedToday": "今日屏蔽",
"adsBlockedTodayPercentage": "今日屏蔽",
"dnsQueriesToday": "今日查询"
}
},
"dnsHoleControls": {
"description": "从您的面板控制 PiHole 或 AdGuard",
"option": {
"layout": {
"label": "显示布局",
"option": {
"row": {
"label": "横向"
},
"column": {
"label": "垂直"
}
}
}
},
"controls": {
"set": "设置",
"enabled": "已启用",
"disabled": "已禁用",
"hours": "时",
"minutes": "分"
}
},
"clock": {
"description": "显示当前的日期和时间。",
"option": {
"timezone": {
"label": "时区"
}
}
},
"notebook": {
"name": "笔记本",
"option": {
"showToolbar": {
"label": "显示帮助您写下 Markdown 的工具栏"
},
"allowReadOnlyCheck": {
"label": "允许在只读模式中检查"
},
"content": {
"label": "笔记本的内容"
}
},
"controls": {
"bold": "粗体",
"italic": "斜体",
"strikethrough": "删除线",
"underline": "下划线",
"colorText": "文字颜色",
"colorHighlight": "彩色高亮文本",
"code": "代码",
"clear": "清除格式",
"heading": "标题 {level}",
"align": "对齐文本: {position}",
"blockquote": "引用",
"horizontalLine": "横线",
"bulletList": "符号列表",
"orderedList": "顺序列表",
"checkList": "检查列表",
"increaseIndent": "增加缩进",
"decreaseIndent": "减小缩进",
"link": "链接",
"unlink": "删除链接",
"image": "嵌入图片",
"addTable": "添加表格",
"deleteTable": "删除表格",
"colorCell": "单元格颜色",
"mergeCell": "切换单元格合并",
"addColumnLeft": "在前面添加列",
"addColumnRight": "在后面添加列",
"deleteColumn": "删除整列",
"addRowTop": "在前面添加行",
"addRowBelow": "在后面添加行",
"deleteRow": "删除整行"
},
"align": {
"left": "左边",
"center": "居中",
"right": "右边"
},
"popover": {
"clearColor": "清除颜色",
"source": "来源",
"widthPlaceholder": "百分比或像素值",
"columns": "列数",
"rows": "行数",
"width": "宽度",
"height": "高度"
}
},
"iframe": {
"name": "iFrame",
"description": "嵌入互联网上的任何内容。某些网站可能限制访问。",
"option": {
"embedUrl": {
"label": "嵌入地址"
},
"allowFullScreen": {
"label": "允许全屏"
},
"allowTransparency": {
"label": "允许透明"
},
"allowScrolling": {
"label": "允许滚动"
},
"allowPayment": {
"label": "允许支付"
},
"allowAutoPlay": {
"label": "允许自动播放"
},
"allowMicrophone": {
"label": "允许麦克风"
},
"allowCamera": {
"label": "允许摄像头"
},
"allowGeolocation": {
"label": "允许地理位置"
}
},
"error": {
"noBrowerSupport": "您的浏览器不支持 iframe。请更新您的浏览器。"
}
},
"smartHome-entityState": {
"option": {
"entityId": {
"label": "实体 ID"
}
}
},
"smartHome-executeAutomation": {
"option": {
"displayName": {
"label": "显示名称"
},
"automationId": {
"label": "自动化 ID"
}
}
},
"calendar": {
"name": "日历",
"option": {
"releaseType": {
"label": "Radarr发布类型"
}
}
},
"weather": {
"name": "天气",
"description": "显示指定位置的当前天气信息。",
"option": {
"location": {
"label": "天气位置"
}
},
"kind": {
"clear": "晴朗",
"mainlyClear": "晴朗为主",
"fog": "雾",
"drizzle": "细雨",
"freezingDrizzle": "冻毛毛雨",
"rain": "雨",
"freezingRain": "冻雨",
"snowFall": "降雪",
"snowGrains": "霰",
"rainShowers": "阵雨",
"snowShowers": "阵雪",
"thunderstorm": "雷暴",
"thunderstormWithHail": "雷暴夹冰雹",
"unknown": "未知"
}
},
"indexerManager": {
"name": "索引器管理状态",
"title": "索引器管理",
"testAll": "测试全部"
},
"healthMonitoring": {
"name": "系统健康监测",
"description": "显示系统运行状况和状态的信息。",
"option": {
"fahrenheit": {
"label": "CPU 温度(华氏度)"
},
"cpu": {
"label": "显示CPU信息"
},
"memory": {
"label": "显示内存信息"
},
"fileSystem": {
"label": "显示文件系统信息"
}
},
"popover": {
"available": "可用"
}
},
"common": {
"location": {
"search": "搜索",
"table": {
"header": {},
"action": {},
"population": {
"fallback": "未知"
}
}
}
},
"video": {
"name": "视频流",
"description": "嵌入来自相机或网站的视频流或视频",
"option": {
"feedUrl": {
"label": "订阅网址"
},
"hasAutoPlay": {
"label": "自动播放"
}
}
},
"downloads": {
"items": {
"added": {
"detailsTitle": "日期已添加"
},
"downSpeed": {
"columnTitle": "下载",
"detailsTitle": "下载速度"
},
"integration": {
"columnTitle": "集成"
},
"progress": {
"columnTitle": "进度"
},
"ratio": {
"columnTitle": "分享率"
},
"state": {
"columnTitle": "状态"
},
"upSpeed": {
"columnTitle": "上传"
}
},
"states": {
"downloading": "正在下载",
"queued": "排队中",
"paused": "已暂停",
"completed": "已完成",
"unknown": "未知"
}
},
"mediaRequests-requestList": {
"description": "查看 Overr 或 Jellyseerr 实例中的所有媒体请求列表",
"option": {
"linksTargetNewTab": {
"label": "在新标签页中打开链接"
}
},
"availability": {
"unknown": "未知",
"partiallyAvailable": "部分",
"available": "可用"
}
},
"mediaRequests-requestStats": {
"description": "您的媒体请求统计",
"titles": {
"stats": {
"main": "媒体统计",
"approved": "已经批准",
"pending": "等待批准",
"tv": "电视请求",
"movie": "电影请求",
"total": "请求总计"
},
"users": {
"main": "用户排行"
}
}
}
},
"board": {
"action": {
"oldImport": {
"form": {
"apps": {
"label": "应用"
},
"screenSize": {
"option": {
"sm": "小号",
"md": "中号",
"lg": "大号"
}
}
}
}
},
"field": {
"backgroundImageAttachment": {
"label": "背景图片附件"
},
"backgroundImageSize": {
"label": "背景图像大小"
},
"primaryColor": {
"label": "主体色"
},
"secondaryColor": {
"label": "辅助色"
},
"customCss": {
"description": "只推荐有经验的用户使用 CSS 自定义面板"
},
"name": {
"label": "名称"
},
"isPublic": {
"label": "公开"
}
},
"setting": {
"section": {
"general": {
"title": "通用"
},
"layout": {
"title": "显示布局"
},
"background": {
"title": "背景"
},
"access": {
"permission": {
"item": {
"view": {
"label": "查看面板"
}
}
}
},
"dangerZone": {
"title": "危险",
"action": {
"delete": {
"confirm": {
"title": "删除面板"
}
}
}
}
}
}
},
"management": {
"navbar": {
"items": {
"home": "首页",
"boards": "面板",
"apps": "应用",
"users": {
"label": "用户",
"items": {
"manage": "管理中心",
"invites": "邀请"
}
},
"tools": {
"label": "工具",
"items": {
"docker": "Docker",
"api": "API"
}
},
"settings": "设置",
"help": {
"label": "帮助",
"items": {
"documentation": "文档",
"discord": "Discord 社区"
}
},
"about": "关于"
}
},
"page": {
"home": {
"statistic": {
"board": "面板",
"user": "用户",
"invite": "邀请",
"app": "应用"
},
"statisticLabel": {
"boards": "面板"
}
},
"board": {
"title": "您的面板",
"action": {
"settings": {
"label": "设置"
},
"setHomeBoard": {
"badge": {
"label": "首页"
}
},
"delete": {
"label": "永久删除",
"confirm": {
"title": "删除面板"
}
}
},
"modal": {
"createBoard": {
"field": {
"name": {
"label": "名称"
}
}
}
}
},
"user": {
"setting": {
"general": {
"title": "通用",
"item": {
"firstDayOfWeek": "一周的第一天",
"accessibility": "无障碍服务"
}
},
"security": {
"title": "安全"
},
"board": {
"title": "面板"
}
},
"list": {
"metaTitle": "管理用户",
"title": "用户"
},
"create": {
"metaTitle": "创建用户",
"step": {
"security": {
"label": "安全"
}
}
},
"invite": {
"title": "管理用户邀请",
"action": {
"new": {
"description": "过期后,邀请会失效,被邀请的收件人将无法创建账号。"
},
"copy": {
"link": "邀请链接"
},
"delete": {
"title": "删除邀请",
"description": "你确定要删除这个邀请吗? 使用此链接的用户将不能再使用该链接创建账号。"
}
},
"field": {
"id": {
"label": "ID"
},
"creator": {
"label": "创建者"
},
"expirationDate": {
"label": "过期时间"
},
"token": {
"label": "Token"
}
}
}
},
"group": {
"setting": {
"general": {
"title": "通用"
}
}
},
"settings": {
"title": "设置"
},
"tool": {
"tasks": {
"status": {
"running": "运行中",
"error": "错误"
},
"job": {
"mediaServer": {
"label": "媒体服务"
},
"mediaRequests": {
"label": "媒体请求"
}
}
},
"api": {
"title": "API",
"tab": {
"documentation": {
"label": "文档"
},
"apiKey": {
"table": {
"header": {
"id": "ID"
}
}
}
}
}
}
}
},
"docker": {
"title": "容器",
"field": {
"name": {
"label": "名称"
},
"state": {
"label": "状态",
"option": {
"created": "已创建",
"running": "运行中",
"paused": "已暂停",
"restarting": "正在重启",
"removing": "删除中"
}
},
"containerImage": {
"label": "镜像"
},
"ports": {
"label": "端口"
}
},
"action": {
"start": {
"label": "开始"
},
"stop": {
"label": "停止"
},
"restart": {
"label": "重启"
},
"remove": {
"label": "删除"
}
}
},
"permission": {
"tab": {
"user": "用户"
},
"field": {
"user": {
"label": "用户"
}
}
},
"navigationStructure": {
"manage": {
"label": "管理中心",
"boards": {
"label": "面板"
},
"integrations": {
"edit": {
"label": "编辑"
}
},
"search-engines": {
"edit": {
"label": "编辑"
}
},
"apps": {
"label": "应用",
"edit": {
"label": "编辑"
}
},
"users": {
"label": "用户",
"create": {
"label": "创建"
},
"general": "通用",
"security": "安全",
"board": "面板",
"invites": {
"label": "邀请"
}
},
"tools": {
"label": "工具",
"docker": {
"label": "Docker"
}
},
"settings": {
"label": "设置"
},
"about": {
"label": "关于"
}
}
},
"search": {
"mode": {
"appIntegrationBoard": {
"group": {
"app": {
"title": "应用"
},
"board": {
"title": "面板"
}
}
},
"external": {
"group": {
"searchEngine": {
"option": {
"torrent": {
"name": "Torrents"
}
}
}
}
},
"help": {
"group": {
"help": {
"title": "帮助",
"option": {
"documentation": {
"label": "文档"
},
"discord": {
"label": "Discord 社区"
}
}
}
}
},
"page": {
"group": {
"page": {
"option": {
"manageUser": {
"label": "管理用户"
},
"about": {
"label": "关于"
},
"preferences": {
"label": "您的首选项"
}
}
}
}
},
"userGroup": {
"group": {
"user": {
"title": "用户"
}
}
}
},
"engine": {
"field": {
"name": {
"label": "名称"
}
}
}
}
}

View File

@@ -0,0 +1,911 @@
{
"user": {
"title": "Uživatelé",
"name": "Uživatel",
"field": {
"email": {
"label": "E-mail"
},
"username": {
"label": "Uživatelské jméno"
},
"password": {
"label": "Heslo",
"requirement": {
"lowercase": "Obsahuje malé písmeno",
"uppercase": "Obsahuje velké písmeno",
"number": "Obsahuje číslo"
}
},
"passwordConfirm": {
"label": "Potvrďte heslo"
}
},
"action": {
"login": {
"label": "Přihlásit se"
},
"register": {
"label": "Vytvořit účet",
"notification": {
"success": {
"title": "Účet byl vytvořen"
}
}
},
"create": "Vytvořit uživatele"
}
},
"group": {
"field": {
"name": "Název"
},
"permission": {
"admin": {
"title": "Administrátor"
},
"board": {
"title": "Plochy"
}
}
},
"app": {
"page": {
"list": {
"title": "Aplikace"
}
},
"field": {
"name": {
"label": "Název"
}
}
},
"integration": {
"field": {
"name": {
"label": "Název"
}
},
"testConnection": {
"notification": {
"invalidUrl": {
"title": "Neplatná URL adresa"
}
}
},
"secrets": {
"kind": {
"username": {
"label": "Uživatelské jméno"
},
"password": {
"label": "Heslo",
"newLabel": "Nové heslo"
}
}
}
},
"media": {
"field": {
"name": "Název",
"size": "Velikost",
"creator": "Vytvořil/a"
}
},
"common": {
"error": "Chyba",
"action": {
"add": "Přidat",
"apply": "Použít",
"create": "Vytvořit",
"edit": "Upravit",
"insert": "Vložit",
"remove": "Odstranit",
"save": "Uložit",
"saveChanges": "Uložit změny",
"cancel": "Zrušit",
"delete": "Odstranit",
"confirm": "Potvrdit",
"previous": "Zpět",
"next": "Další",
"tryAgain": "Zkusit znovu"
},
"information": {
"hours": "Hodin",
"minutes": "Minut"
},
"userAvatar": {
"menu": {
"preferences": "Vaše předvolby",
"login": "Přihlásit se"
}
},
"dangerZone": "Nebezpečná zóna",
"noResults": "Nebyly nalezeny žádné výsledky",
"zod": {
"errors": {
"default": "Toto pole je neplatné",
"required": "Toto pole je nutné vyplnit",
"string": {
"startsWith": "Toto pole musí začínat {startsWith}",
"endsWith": "Toto pole musí končit {endsWith}",
"includes": "Toto pole musí obsahovat {includes}"
},
"tooSmall": {
"string": "Toto pole musí obsahovat alespoň {minimum} znaků",
"number": "Toto pole musí být větší nebo rovno {minimum}"
},
"tooBig": {
"string": "Toto pole může být maximálně {maximum} znaků dlouhé",
"number": "Toto pole musí být menší nebo rovno {maximum}"
}
}
}
},
"section": {
"category": {
"field": {
"name": {
"label": "Název"
}
},
"action": {
"moveUp": "Posunout nahoru",
"moveDown": "Posunout dolů"
},
"menu": {
"label": {
"changePosition": "Změnit pozici"
}
}
}
},
"item": {
"menu": {
"label": {
"settings": "Nastavení"
}
},
"moveResize": {
"field": {
"width": {
"label": "Šířka"
},
"height": {
"label": "Výška"
}
}
}
},
"widget": {
"app": {
"option": {
"openInNewTab": {
"label": "Otevřít na nové kartě"
}
}
},
"dnsHoleSummary": {
"option": {
"layout": {
"label": "Rozložení",
"option": {
"row": {
"label": "Horizontální"
},
"column": {
"label": "Vertikální"
}
}
}
},
"data": {
"adsBlockedToday": "Zablokováno dnes",
"adsBlockedTodayPercentage": "Zablokováno dnes",
"dnsQueriesToday": "Dotazů dnes"
}
},
"dnsHoleControls": {
"description": "Ovládejte PiHole nebo AdGuard z plochy",
"option": {
"layout": {
"label": "Rozložení",
"option": {
"row": {
"label": "Horizontální"
},
"column": {
"label": "Vertikální"
}
}
}
},
"controls": {
"set": "Nastavit",
"enabled": "Zapnuto",
"disabled": "Vypnuto",
"hours": "Hodin",
"minutes": "Minut"
}
},
"clock": {
"description": "Zobrazuje aktuální datum a čas.",
"option": {
"timezone": {
"label": "Časové pásmo"
}
}
},
"notebook": {
"name": "Zápisník",
"option": {
"showToolbar": {
"label": "Zobrazovat panel nástroju, který Vám pomůže s formátováním textu"
},
"allowReadOnlyCheck": {
"label": "Povolit kontrolu v režimu pouze pro čtení"
},
"content": {
"label": "Obsah zápisníku"
}
},
"controls": {
"bold": "Tučné",
"italic": "Kurzíva",
"strikethrough": "Přeškrtnuté",
"underline": "Podtržení",
"colorText": "Barva písma",
"colorHighlight": "Barevné zvýraznění textu",
"code": "Kód",
"clear": "Vymazat formátování",
"heading": "Nadpis {level}",
"align": "Zarovnat text: {position}",
"blockquote": "Citace",
"horizontalLine": "Vodorovná čára",
"bulletList": "Odrážkový seznam",
"orderedList": "Číslovaný seznam",
"checkList": "Zaškrtávací seznam",
"increaseIndent": "Zvětšit odsazení",
"decreaseIndent": "Zmenšit odsazení",
"link": "Vložit odkaz",
"unlink": "Odstranit odkaz",
"image": "Vložit obrázek",
"addTable": "Přidat tabulku",
"deleteTable": "Odstranit tabulku",
"colorCell": "Barva výplně",
"mergeCell": "Sloučit buňky",
"addColumnLeft": "Přidat sloupec před",
"addColumnRight": "Přidat sloupec za",
"deleteColumn": "Odstranit sloupec",
"addRowTop": "Přidat řádek nad",
"addRowBelow": "Přidat řádek pod",
"deleteRow": "Odstranit řádek"
},
"align": {
"left": "Vlevo",
"center": "Střed",
"right": "Vpravo"
},
"popover": {
"clearColor": "Odstranit barvu",
"source": "Zdroj",
"widthPlaceholder": "Hodnota v % nebo pixelech",
"columns": "Sloupce",
"rows": "Řádky",
"width": "Šířka",
"height": "Výška"
}
},
"iframe": {
"name": "iFrame",
"description": "Vložte jakýkoli obsah z internetu. Některé webové stránky mohou omezit přístup.",
"option": {
"embedUrl": {
"label": "Embed URL"
},
"allowFullScreen": {
"label": "Povolit celou obrazovku"
},
"allowTransparency": {
"label": "Povolit průhlednost"
},
"allowScrolling": {
"label": "Povolit posouvání"
},
"allowPayment": {
"label": "Povolit platbu"
},
"allowAutoPlay": {
"label": "Povolit automatické přehrávání"
},
"allowMicrophone": {
"label": "Povolit mikrofon"
},
"allowCamera": {
"label": "Povolit kameru"
},
"allowGeolocation": {
"label": "Povolit polohu"
}
},
"error": {
"noBrowerSupport": "Váš prohlížeč nepodporuje iFrame. Aktualizujte, prosím, svůj prohlížeč."
}
},
"smartHome-entityState": {
"option": {
"entityId": {
"label": "ID entity"
}
}
},
"smartHome-executeAutomation": {
"option": {
"displayName": {
"label": "Zobrazovaný název"
},
"automationId": {
"label": "ID automatizace"
}
}
},
"calendar": {
"name": "Kalendář",
"option": {
"releaseType": {
"label": "Typ vydání filmu pro Radarr"
}
}
},
"weather": {
"name": "Počasí",
"description": "Zobrazuje aktuální informace o počasí na nastaveném místě.",
"option": {
"location": {
"label": "Lokalita pro počasí"
}
},
"kind": {
"clear": "Jasno",
"mainlyClear": "Převážně jasno",
"fog": "Mlha",
"drizzle": "Mrholení",
"freezingDrizzle": "Mrznoucí mrholení",
"rain": "Déšť",
"freezingRain": "Mrznoucí déšť",
"snowFall": "Sněžení",
"snowGrains": "Sněhová zrna",
"rainShowers": "Dešťové přeháňky",
"snowShowers": "Sněhové přeháňky",
"thunderstorm": "Bouřka",
"thunderstormWithHail": "Bouřka s krupobitím",
"unknown": "Neznámý"
}
},
"indexerManager": {
"name": "Stav správce indexeru",
"title": "Správce indexeru",
"testAll": "Otestovat vše"
},
"healthMonitoring": {
"name": "Monitorování stavu systému",
"description": "Zobrazuje informace o stavu a kondici Vašeho systému (systémů).",
"option": {
"fahrenheit": {
"label": "Teplota CPU ve stupních Fahrenheit"
},
"cpu": {
"label": "Zobrazit info o CPU"
},
"memory": {
"label": "Zobrazit informace o paměti"
},
"fileSystem": {
"label": "Zobrazit informace o souborovém systému"
}
},
"popover": {
"available": "K dispozici"
}
},
"common": {
"location": {
"search": "Vyhledat",
"table": {
"header": {},
"action": {},
"population": {
"fallback": "Neznámý"
}
}
}
},
"video": {
"name": "Streamování videa",
"description": "Vložte video stream nebo video z kamery nebo webové stránky",
"option": {
"feedUrl": {
"label": "URL zdroje"
},
"hasAutoPlay": {
"label": "Automatické přehrávání"
}
}
},
"downloads": {
"items": {
"added": {
"detailsTitle": "Datum přidání"
},
"downSpeed": {
"columnTitle": "Stahování",
"detailsTitle": "Rychlost stahování"
},
"integration": {
"columnTitle": "Integrace"
},
"progress": {
"columnTitle": "Postup"
},
"ratio": {
"columnTitle": "Poměr"
},
"state": {
"columnTitle": "Status"
},
"upSpeed": {
"columnTitle": "Nahrávání"
}
},
"states": {
"downloading": "Stahování",
"queued": "Ve frontě",
"paused": "Pozastaveno",
"completed": "Hotovo",
"unknown": "Neznámý"
}
},
"mediaRequests-requestList": {
"description": "Podívejte se na seznam všech požadavků na média z vaší instance Overseerr nebo Jellyseerr",
"option": {
"linksTargetNewTab": {
"label": "Otevírat odkazy v nové kartě"
}
},
"availability": {
"unknown": "Neznámý",
"partiallyAvailable": "Částečně dostupné",
"available": "K dispozici"
}
},
"mediaRequests-requestStats": {
"description": "Statistiky vašich požadavků na média",
"titles": {
"stats": {
"main": "Statistiky médií",
"approved": "Již schváleno",
"pending": "Čeká na schválení",
"tv": "Požadavky seriálů",
"movie": "Požadavky filmů",
"total": "Celkem"
},
"users": {
"main": "Top uživatelé"
}
}
}
},
"board": {
"action": {
"oldImport": {
"form": {
"apps": {
"label": "Aplikace"
},
"screenSize": {
"option": {
"sm": "Malé",
"md": "Střední",
"lg": "Velké"
}
}
}
}
},
"field": {
"backgroundImageAttachment": {
"label": "Příloha obrázku na pozadí"
},
"backgroundImageSize": {
"label": "Velikost obrázku na pozadí"
},
"primaryColor": {
"label": "Primární barva"
},
"secondaryColor": {
"label": "Doplňková barva"
},
"customCss": {
"description": "Dále si můžete přizpůsobit ovládací panel pomocí CSS, doporučujeme pouze zkušeným uživatelům"
},
"name": {
"label": "Název"
},
"isPublic": {
"label": "Veřejné"
}
},
"setting": {
"section": {
"general": {
"title": "Obecné"
},
"layout": {
"title": "Rozložení"
},
"background": {
"title": "Pozadí"
},
"access": {
"permission": {
"item": {
"view": {
"label": "Zobrazit plochu"
}
}
}
},
"dangerZone": {
"title": "Nebezpečná zóna",
"action": {
"delete": {
"confirm": {
"title": "Odstranit plochu"
}
}
}
}
}
}
},
"management": {
"navbar": {
"items": {
"home": "Domovská stránka",
"boards": "Plochy",
"apps": "Aplikace",
"users": {
"label": "Uživatelé",
"items": {
"manage": "Spravovat",
"invites": "Pozvánky"
}
},
"tools": {
"label": "Nástroje",
"items": {
"docker": "Docker",
"api": "API"
}
},
"settings": "Nastavení",
"help": {
"label": "Nápověda",
"items": {
"documentation": "Dokumentace",
"discord": "Komunitní Discord"
}
},
"about": "O aplikaci"
}
},
"page": {
"home": {
"statistic": {
"board": "Plochy",
"user": "Uživatelé",
"invite": "Pozvánky",
"app": "Aplikace"
},
"statisticLabel": {
"boards": "Plochy"
}
},
"board": {
"title": "Vaše plochy",
"action": {
"settings": {
"label": "Nastavení"
},
"setHomeBoard": {
"badge": {
"label": "Domovská stránka"
}
},
"delete": {
"label": "Trvale smazat",
"confirm": {
"title": "Odstranit plochu"
}
}
},
"modal": {
"createBoard": {
"field": {
"name": {
"label": "Název"
}
}
}
}
},
"user": {
"setting": {
"general": {
"title": "Obecné",
"item": {
"firstDayOfWeek": "První den v týdnu",
"accessibility": "Přístupnost"
}
},
"security": {
"title": "Bezpečnost"
},
"board": {
"title": "Plochy"
}
},
"list": {
"metaTitle": "Správa uživatelů",
"title": "Uživatelé"
},
"create": {
"metaTitle": "Vytvořit uživatele",
"step": {
"security": {
"label": "Bezpečnost"
}
}
},
"invite": {
"title": "Správa pozvánek uživatelů",
"action": {
"new": {
"description": "Po vypršení platnosti pozvánka přestane být platná a příjemce pozvánky si nebude moci vytvořit účet."
},
"copy": {
"link": "Odkaz na pozvánku"
},
"delete": {
"title": "Odstranit pozvánku",
"description": "Jste si jisti, že chcete tuto pozvánku smazat? Uživatelé s tímto odkazem již nebudou moci pomocí tohoto odkazu vytvořit účet."
}
},
"field": {
"id": {
"label": "ID"
},
"creator": {
"label": "Vytvořil/a"
},
"expirationDate": {
"label": "Datum konce platnosti"
},
"token": {
"label": "Token"
}
}
}
},
"group": {
"setting": {
"general": {
"title": "Obecné"
}
}
},
"settings": {
"title": "Nastavení"
},
"tool": {
"tasks": {
"status": {
"running": "Běží",
"error": "Chyba"
},
"job": {
"mediaServer": {
"label": "Mediální server"
},
"mediaRequests": {
"label": "Žádosti o média"
}
}
},
"api": {
"title": "API",
"tab": {
"documentation": {
"label": "Dokumentace"
},
"apiKey": {
"table": {
"header": {
"id": "ID"
}
}
}
}
}
}
}
},
"docker": {
"title": "Kontejnery",
"field": {
"name": {
"label": "Název"
},
"state": {
"label": "Status",
"option": {
"created": "Vytvořený",
"running": "Běží",
"paused": "Pozastaveno",
"restarting": "Restartování",
"removing": "Odstraňování"
}
},
"containerImage": {
"label": "Obraz"
},
"ports": {
"label": "Porty"
}
},
"action": {
"start": {
"label": "Spustit"
},
"stop": {
"label": "Zastavit"
},
"restart": {
"label": "Restartovat"
},
"remove": {
"label": "Odstranit"
}
}
},
"permission": {
"tab": {
"user": "Uživatelé"
},
"field": {
"user": {
"label": "Uživatel"
}
}
},
"navigationStructure": {
"manage": {
"label": "Spravovat",
"boards": {
"label": "Plochy"
},
"integrations": {
"edit": {
"label": "Upravit"
}
},
"search-engines": {
"edit": {
"label": "Upravit"
}
},
"apps": {
"label": "Aplikace",
"edit": {
"label": "Upravit"
}
},
"users": {
"label": "Uživatelé",
"create": {
"label": "Vytvořit"
},
"general": "Obecné",
"security": "Bezpečnost",
"board": "Plochy",
"invites": {
"label": "Pozvánky"
}
},
"tools": {
"label": "Nástroje",
"docker": {
"label": "Docker"
}
},
"settings": {
"label": "Nastavení"
},
"about": {
"label": "O aplikaci"
}
}
},
"search": {
"mode": {
"appIntegrationBoard": {
"group": {
"app": {
"title": "Aplikace"
},
"board": {
"title": "Plochy"
}
}
},
"external": {
"group": {
"searchEngine": {
"option": {
"torrent": {
"name": "Torrenty"
}
}
}
}
},
"help": {
"group": {
"help": {
"title": "Nápověda",
"option": {
"documentation": {
"label": "Dokumentace"
},
"discord": {
"label": "Komunitní Discord"
}
}
}
}
},
"page": {
"group": {
"page": {
"option": {
"manageUser": {
"label": "Správa uživatelů"
},
"about": {
"label": "O aplikaci"
},
"preferences": {
"label": "Vaše předvolby"
}
}
}
}
},
"userGroup": {
"group": {
"user": {
"title": "Uživatelé"
}
}
}
},
"engine": {
"field": {
"name": {
"label": "Název"
}
}
}
}
}

View File

@@ -0,0 +1,911 @@
{
"user": {
"title": "Brugere",
"name": "Bruger",
"field": {
"email": {
"label": "E-mail"
},
"username": {
"label": "Brugernavn"
},
"password": {
"label": "Adgangskode",
"requirement": {
"lowercase": "Inkluderer små bogstaver",
"uppercase": "Inkluderer store bogstaver",
"number": "Inkluderer nummer"
}
},
"passwordConfirm": {
"label": "Bekræft kodeord"
}
},
"action": {
"login": {
"label": "Log ind"
},
"register": {
"label": "Opret konto",
"notification": {
"success": {
"title": "Konto oprettet"
}
}
},
"create": "Opret bruger"
}
},
"group": {
"field": {
"name": "Navn"
},
"permission": {
"admin": {
"title": "Admin"
},
"board": {
"title": "Boards"
}
}
},
"app": {
"page": {
"list": {
"title": "Apps"
}
},
"field": {
"name": {
"label": "Navn"
}
}
},
"integration": {
"field": {
"name": {
"label": "Navn"
}
},
"testConnection": {
"notification": {
"invalidUrl": {
"title": "Ugyldig URL"
}
}
},
"secrets": {
"kind": {
"username": {
"label": "Brugernavn"
},
"password": {
"label": "Adgangskode",
"newLabel": "Nyt kodeord"
}
}
}
},
"media": {
"field": {
"name": "Navn",
"size": "Størrelse",
"creator": "Skaber"
}
},
"common": {
"error": "Fejl",
"action": {
"add": "Tilføj",
"apply": "Anvend",
"create": "Opret",
"edit": "Rediger",
"insert": "Indsæt",
"remove": "Fjern",
"save": "Gem",
"saveChanges": "Gem ændringer",
"cancel": "Annuller",
"delete": "Slet",
"confirm": "Bekræft",
"previous": "Forrige",
"next": "Næste",
"tryAgain": "Prøv igen"
},
"information": {
"hours": "Timer",
"minutes": "Minutter"
},
"userAvatar": {
"menu": {
"preferences": "Dine indstillinger",
"login": "Log ind"
}
},
"dangerZone": "Farezone",
"noResults": "Ingen resultater fundet",
"zod": {
"errors": {
"default": "Dette felt er ugyldigt",
"required": "Dette felt er påkrævet",
"string": {
"startsWith": "Dette felt skal starte med {startsWith}",
"endsWith": "Dette felt skal slutte med {endsWith}",
"includes": "Dette felt skal indeholde {includes}"
},
"tooSmall": {
"string": "Dette felt skal være mindst {minimum} tegn langt",
"number": "Dette felt skal være større end eller lig med {minimum}"
},
"tooBig": {
"string": "Dette felt må højst være på {maximum} tegn",
"number": "Dette felt skal være mindre end eller lig med {maximum}"
}
}
}
},
"section": {
"category": {
"field": {
"name": {
"label": "Navn"
}
},
"action": {
"moveUp": "Flyt op",
"moveDown": "Flyt ned"
},
"menu": {
"label": {
"changePosition": "Ændre placering"
}
}
}
},
"item": {
"menu": {
"label": {
"settings": "Indstillinger"
}
},
"moveResize": {
"field": {
"width": {
"label": "Bredde"
},
"height": {
"label": "Højde"
}
}
}
},
"widget": {
"app": {
"option": {
"openInNewTab": {
"label": "Åbn i nyt faneblad"
}
}
},
"dnsHoleSummary": {
"option": {
"layout": {
"label": "Layout",
"option": {
"row": {
"label": "Horisontal"
},
"column": {
"label": "Vertikal"
}
}
}
},
"data": {
"adsBlockedToday": "Blokeret i dag",
"adsBlockedTodayPercentage": "Blokeret i dag",
"dnsQueriesToday": "Forespørgsler i dag"
}
},
"dnsHoleControls": {
"description": "Kontroller PiHole eller AdGuard fra dit dashboard",
"option": {
"layout": {
"label": "Layout",
"option": {
"row": {
"label": "Horisontal"
},
"column": {
"label": "Vertikal"
}
}
}
},
"controls": {
"set": "Indstil",
"enabled": "Aktiveret",
"disabled": "Deaktiveret",
"hours": "Timer",
"minutes": "Minutter"
}
},
"clock": {
"description": "Viser aktuel dag og klokkeslæt.",
"option": {
"timezone": {
"label": "Tidszone"
}
}
},
"notebook": {
"name": "Notesbog",
"option": {
"showToolbar": {
"label": "Vis værktøjslinjen, der hjælper dig med at skrive markdown"
},
"allowReadOnlyCheck": {
"label": "Tillad tjek i skrivebeskyttet tilstand"
},
"content": {
"label": "Indholdet af notesbogen"
}
},
"controls": {
"bold": "Fed",
"italic": "Kursiv",
"strikethrough": "Gennemstreget",
"underline": "Understreget",
"colorText": "Tekst i farver",
"colorHighlight": "Farvet fremhævning af tekst",
"code": "Kode",
"clear": "Ryd formatering",
"heading": "Overskrift {level}",
"align": "Juster tekst: {position}",
"blockquote": "Citatblok",
"horizontalLine": "Horisontal linje",
"bulletList": "Punktopstillet liste",
"orderedList": "Sorteret liste",
"checkList": "Tjekliste",
"increaseIndent": "Forøg indrykning",
"decreaseIndent": "Formindsk indrykning",
"link": "Link",
"unlink": "Fjern link",
"image": "Integrer billede",
"addTable": "Tilføj tabel",
"deleteTable": "Slet tabel",
"colorCell": "Farvecelle",
"mergeCell": "Slå cellefletning til/fra",
"addColumnLeft": "Tilføj kolonne før",
"addColumnRight": "Tilføj kolonne efter",
"deleteColumn": "Slet kolonne",
"addRowTop": "Tilføj række før",
"addRowBelow": "Tilføj række efter",
"deleteRow": "Slet række"
},
"align": {
"left": "Venstre",
"center": "Centrer",
"right": "Højre"
},
"popover": {
"clearColor": "Ryd farve",
"source": "Kilde",
"widthPlaceholder": "Værdi i % eller pixels",
"columns": "Kolonner",
"rows": "Rækker",
"width": "Bredde",
"height": "Højde"
}
},
"iframe": {
"name": "indlejret dokument (iframe)",
"description": "Indlejr ethvert indhold fra internettet. Nogle websteder kan begrænse adgang.",
"option": {
"embedUrl": {
"label": "Indlejr URL"
},
"allowFullScreen": {
"label": "Tillad fuld skærm"
},
"allowTransparency": {
"label": "Tillad gennemsigtighed"
},
"allowScrolling": {
"label": "Tillad rulning"
},
"allowPayment": {
"label": "Tillad betaling"
},
"allowAutoPlay": {
"label": "Tillad automatisk afspilning"
},
"allowMicrophone": {
"label": "Tillad mikrofon"
},
"allowCamera": {
"label": "Tillad kamera"
},
"allowGeolocation": {
"label": "Tillad geolokalisering"
}
},
"error": {
"noBrowerSupport": "Din browser understøtter ikke iframes. Opdater venligst din browser."
}
},
"smartHome-entityState": {
"option": {
"entityId": {
"label": "Entitet ID"
}
}
},
"smartHome-executeAutomation": {
"option": {
"displayName": {
"label": "Visningsnavn"
},
"automationId": {
"label": "Automatiserings ID"
}
}
},
"calendar": {
"name": "Kalender",
"option": {
"releaseType": {
"label": "Radarr udgivelsestype"
}
}
},
"weather": {
"name": "Vejr",
"description": "Viser de aktuelle vejroplysninger for en bestemt placering.",
"option": {
"location": {
"label": "Vejr lokation"
}
},
"kind": {
"clear": "Skyfrit",
"mainlyClear": "Hovedsageligt skyfrit",
"fog": "Tåge",
"drizzle": "Støvregn",
"freezingDrizzle": "Støvregn med isslag",
"rain": "Regn",
"freezingRain": "Isslag",
"snowFall": "Snefald",
"snowGrains": "Mildt snefald",
"rainShowers": "Regnbyger",
"snowShowers": "Snebyger",
"thunderstorm": "Tordenvejr",
"thunderstormWithHail": "Tordenvejr med hagl",
"unknown": "Ukendt"
}
},
"indexerManager": {
"name": "Indekserings manager status",
"title": "Indexer-manager",
"testAll": "Test alle"
},
"healthMonitoring": {
"name": "Systemsundhedsovervågning",
"description": "Viser oplysninger om dit/dine systems tilstand og status.",
"option": {
"fahrenheit": {
"label": "CPU-temperatur i Fahrenheit"
},
"cpu": {
"label": "Vis CPU-info"
},
"memory": {
"label": "Vis hukommelsesoplysninger"
},
"fileSystem": {
"label": "Vis information om filsystemet"
}
},
"popover": {
"available": "Tilgængelig"
}
},
"common": {
"location": {
"search": "Søg",
"table": {
"header": {},
"action": {},
"population": {
"fallback": "Ukendt"
}
}
}
},
"video": {
"name": "Video Stream",
"description": "Indlejr en video stream eller video fra et kamera eller et website",
"option": {
"feedUrl": {
"label": "Feed URL"
},
"hasAutoPlay": {
"label": "Auto-afspilning"
}
}
},
"downloads": {
"items": {
"added": {
"detailsTitle": "Dato tilføjet"
},
"downSpeed": {
"columnTitle": "Down",
"detailsTitle": "Download hastighed"
},
"integration": {
"columnTitle": "Integration"
},
"progress": {
"columnTitle": "Fremskridt"
},
"ratio": {
"columnTitle": "Delingsforhold"
},
"state": {
"columnTitle": "Tilstand"
},
"upSpeed": {
"columnTitle": "Up"
}
},
"states": {
"downloading": "Downloader",
"queued": "I kø",
"paused": "På pause",
"completed": "Fuldført",
"unknown": "Ukendt"
}
},
"mediaRequests-requestList": {
"description": "Se en liste over alle medieforespørgsler fra din Overseerr eller Jellyseerr instans",
"option": {
"linksTargetNewTab": {
"label": "Åbn links i ny fane"
}
},
"availability": {
"unknown": "Ukendt",
"partiallyAvailable": "Delvis",
"available": "Tilgængelig"
}
},
"mediaRequests-requestStats": {
"description": "Statistik over dine medieanmodninger",
"titles": {
"stats": {
"main": "Mediestatistik",
"approved": "Allerede godkendt",
"pending": "Afventer godkendelse",
"tv": "TV-anmodninger",
"movie": "Film anmodninger",
"total": "Total"
},
"users": {
"main": "Topbrugere"
}
}
}
},
"board": {
"action": {
"oldImport": {
"form": {
"apps": {
"label": "Apps"
},
"screenSize": {
"option": {
"sm": "Lille",
"md": "Mellem",
"lg": "Stor"
}
}
}
}
},
"field": {
"backgroundImageAttachment": {
"label": "Vedhæftning af baggrundsbillede"
},
"backgroundImageSize": {
"label": "Baggrundsbilledets størrelse"
},
"primaryColor": {
"label": "Primær farve"
},
"secondaryColor": {
"label": "Sekundær farve"
},
"customCss": {
"description": "Yderligere, tilpasse dit dashboard ved hjælp af CSS, anbefales kun til erfarne brugere"
},
"name": {
"label": "Navn"
},
"isPublic": {
"label": "Offentlig"
}
},
"setting": {
"section": {
"general": {
"title": "Generelt"
},
"layout": {
"title": "Layout"
},
"background": {
"title": "Baggrund"
},
"access": {
"permission": {
"item": {
"view": {
"label": "Se board"
}
}
}
},
"dangerZone": {
"title": "Farezone",
"action": {
"delete": {
"confirm": {
"title": "Slet board"
}
}
}
}
}
}
},
"management": {
"navbar": {
"items": {
"home": "Hjem",
"boards": "Boards",
"apps": "Apps",
"users": {
"label": "Brugere",
"items": {
"manage": "Administrer",
"invites": "Invitationer"
}
},
"tools": {
"label": "Værktøjer",
"items": {
"docker": "Docker",
"api": "API"
}
},
"settings": "Indstillinger",
"help": {
"label": "Hjælp",
"items": {
"documentation": "Dokumentation",
"discord": "Discordfællesskab"
}
},
"about": "Om"
}
},
"page": {
"home": {
"statistic": {
"board": "Boards",
"user": "Brugere",
"invite": "Invitationer",
"app": "Apps"
},
"statisticLabel": {
"boards": "Boards"
}
},
"board": {
"title": "Dine boards",
"action": {
"settings": {
"label": "Indstillinger"
},
"setHomeBoard": {
"badge": {
"label": "Hjem"
}
},
"delete": {
"label": "Slet permanent",
"confirm": {
"title": "Slet board"
}
}
},
"modal": {
"createBoard": {
"field": {
"name": {
"label": "Navn"
}
}
}
}
},
"user": {
"setting": {
"general": {
"title": "Generelt",
"item": {
"firstDayOfWeek": "Første ugedag",
"accessibility": "Hjælpefunktioner"
}
},
"security": {
"title": "Sikkerhed"
},
"board": {
"title": "Boards"
}
},
"list": {
"metaTitle": "Administrér brugere",
"title": "Brugere"
},
"create": {
"metaTitle": "Opret bruger",
"step": {
"security": {
"label": "Sikkerhed"
}
}
},
"invite": {
"title": "Administrer brugerinvitationer",
"action": {
"new": {
"description": "Efter udløb vil en invitation ikke længere være gyldig, og modtageren af invitationen vil ikke være i stand til at oprette en konto."
},
"copy": {
"link": "Invitationslink"
},
"delete": {
"title": "Slet invitation",
"description": "Er du sikker på, at du vil slette denne invitation? Brugere med dette link vil ikke længere kunne oprette en konto ved hjælp af dette link."
}
},
"field": {
"id": {
"label": "ID"
},
"creator": {
"label": "Skaber"
},
"expirationDate": {
"label": "Udløbsdato"
},
"token": {
"label": "Token"
}
}
}
},
"group": {
"setting": {
"general": {
"title": "Generelt"
}
}
},
"settings": {
"title": "Indstillinger"
},
"tool": {
"tasks": {
"status": {
"running": "Kører",
"error": "Fejl"
},
"job": {
"mediaServer": {
"label": "Medieserver"
},
"mediaRequests": {
"label": "Medieforespørgsler"
}
}
},
"api": {
"title": "API",
"tab": {
"documentation": {
"label": "Dokumentation"
},
"apiKey": {
"table": {
"header": {
"id": "ID"
}
}
}
}
}
}
}
},
"docker": {
"title": "Containere",
"field": {
"name": {
"label": "Navn"
},
"state": {
"label": "Tilstand",
"option": {
"created": "Oprettet",
"running": "Kører",
"paused": "På pause",
"restarting": "Genstarter",
"removing": "Fjerner"
}
},
"containerImage": {
"label": "Image"
},
"ports": {
"label": "Porte"
}
},
"action": {
"start": {
"label": "Start"
},
"stop": {
"label": "Stop"
},
"restart": {
"label": "Genstart"
},
"remove": {
"label": "Fjern"
}
}
},
"permission": {
"tab": {
"user": "Brugere"
},
"field": {
"user": {
"label": "Bruger"
}
}
},
"navigationStructure": {
"manage": {
"label": "Administrer",
"boards": {
"label": "Boards"
},
"integrations": {
"edit": {
"label": "Rediger"
}
},
"search-engines": {
"edit": {
"label": "Rediger"
}
},
"apps": {
"label": "Apps",
"edit": {
"label": "Rediger"
}
},
"users": {
"label": "Brugere",
"create": {
"label": "Opret"
},
"general": "Generelt",
"security": "Sikkerhed",
"board": "Boards",
"invites": {
"label": "Invitationer"
}
},
"tools": {
"label": "Værktøjer",
"docker": {
"label": "Docker"
}
},
"settings": {
"label": "Indstillinger"
},
"about": {
"label": "Om"
}
}
},
"search": {
"mode": {
"appIntegrationBoard": {
"group": {
"app": {
"title": "Apps"
},
"board": {
"title": "Boards"
}
}
},
"external": {
"group": {
"searchEngine": {
"option": {
"torrent": {
"name": "Torrents"
}
}
}
}
},
"help": {
"group": {
"help": {
"title": "Hjælp",
"option": {
"documentation": {
"label": "Dokumentation"
},
"discord": {
"label": "Discordfællesskab"
}
}
}
}
},
"page": {
"group": {
"page": {
"option": {
"manageUser": {
"label": "Administrér brugere"
},
"about": {
"label": "Om"
},
"preferences": {
"label": "Dine indstillinger"
}
}
}
}
},
"userGroup": {
"group": {
"user": {
"title": "Brugere"
}
}
}
},
"engine": {
"field": {
"name": {
"label": "Navn"
}
}
}
}
}

View File

@@ -0,0 +1,911 @@
{
"user": {
"title": "Benutzer",
"name": "Benutzer",
"field": {
"email": {
"label": "E-Mail"
},
"username": {
"label": "Benutzername"
},
"password": {
"label": "Passwort",
"requirement": {
"lowercase": "Enthält Kleinbuchstaben",
"uppercase": "Enthält Großbuchstaben",
"number": "Enthält Ziffern"
}
},
"passwordConfirm": {
"label": "Passwort bestätigen"
}
},
"action": {
"login": {
"label": "Anmelden"
},
"register": {
"label": "Account erstellen",
"notification": {
"success": {
"title": "Account erstellt"
}
}
},
"create": "Benutzer erstellen"
}
},
"group": {
"field": {
"name": "Name"
},
"permission": {
"admin": {
"title": "Admin"
},
"board": {
"title": "Boards"
}
}
},
"app": {
"page": {
"list": {
"title": "Apps"
}
},
"field": {
"name": {
"label": "Name"
}
}
},
"integration": {
"field": {
"name": {
"label": "Name"
}
},
"testConnection": {
"notification": {
"invalidUrl": {
"title": "Ungültige URL"
}
}
},
"secrets": {
"kind": {
"username": {
"label": "Benutzername"
},
"password": {
"label": "Passwort",
"newLabel": "Neues Passwort"
}
}
}
},
"media": {
"field": {
"name": "Name",
"size": "Größe",
"creator": "Ersteller"
}
},
"common": {
"error": "Fehler",
"action": {
"add": "Hinzufügen",
"apply": "Übernehmen",
"create": "Erstellen",
"edit": "Bearbeiten",
"insert": "Einfügen",
"remove": "Entfernen",
"save": "Speichern",
"saveChanges": "Änderungen speichern",
"cancel": "Abbrechen",
"delete": "Löschen",
"confirm": "Bestätigen",
"previous": "Zurück",
"next": "Weiter",
"tryAgain": "Erneut versuchen"
},
"information": {
"hours": "Stunden",
"minutes": "Minuten"
},
"userAvatar": {
"menu": {
"preferences": "Ihre Einstellungen",
"login": "Anmelden"
}
},
"dangerZone": "Gefahrenzone",
"noResults": "Die Suche ergab keine Treffer",
"zod": {
"errors": {
"default": "Dieses Feld ist ungültig",
"required": "Dieses Feld ist erforderlich",
"string": {
"startsWith": "Dieses Feld muss mit {startsWith} beginnen",
"endsWith": "Dieses Feld muss mit {endsWith} enden",
"includes": "Dieses Feld muss {includes} beinhalten"
},
"tooSmall": {
"string": "Dieses Feld muss mindestens {minimum} Zeichen lang sein",
"number": "Dieses Feld muss größer oder gleich {minimum} sein"
},
"tooBig": {
"string": "Dieses Feld muss mindestens {maximum} Zeichen lang sein",
"number": "Dieses Feld muss größer oder gleich {maximum} sein"
}
}
}
},
"section": {
"category": {
"field": {
"name": {
"label": "Name"
}
},
"action": {
"moveUp": "Nach oben bewegen",
"moveDown": "Nach unten bewegen"
},
"menu": {
"label": {
"changePosition": "Position wechseln"
}
}
}
},
"item": {
"menu": {
"label": {
"settings": "Einstellungen"
}
},
"moveResize": {
"field": {
"width": {
"label": "Breite"
},
"height": {
"label": "Höhe"
}
}
}
},
"widget": {
"app": {
"option": {
"openInNewTab": {
"label": "In neuem Tab öffnen"
}
}
},
"dnsHoleSummary": {
"option": {
"layout": {
"label": "Ansicht",
"option": {
"row": {
"label": "Horizontal"
},
"column": {
"label": "Vertikal"
}
}
}
},
"data": {
"adsBlockedToday": "Heute blockiert",
"adsBlockedTodayPercentage": "Heute blockiert",
"dnsQueriesToday": "Heutige Anfragen"
}
},
"dnsHoleControls": {
"description": "Steuern Sie PiHole oder AdGuard von Ihrem Dashboard aus",
"option": {
"layout": {
"label": "Ansicht",
"option": {
"row": {
"label": "Horizontal"
},
"column": {
"label": "Vertikal"
}
}
}
},
"controls": {
"set": "Speichern",
"enabled": "Aktiviert",
"disabled": "Deaktiviert",
"hours": "Stunden",
"minutes": "Minuten"
}
},
"clock": {
"description": "Zeigt das aktuelle Datum und die Uhrzeit an.",
"option": {
"timezone": {
"label": "Zeitzone"
}
}
},
"notebook": {
"name": "Notizbuch",
"option": {
"showToolbar": {
"label": "Zeigt die Symbolleiste an, um Ihnen beim Schreiben der Markdown zu assistieren"
},
"allowReadOnlyCheck": {
"label": "Prüfung im Nur-Lese-Modus zulassen"
},
"content": {
"label": "Der Inhalt des Notizbuchs"
}
},
"controls": {
"bold": "Fett",
"italic": "Kursiv",
"strikethrough": "Durchgestrichen",
"underline": "Unterstrichen",
"colorText": "Farbiger Text",
"colorHighlight": "Farbig hervorgehobener Text",
"code": "Code",
"clear": "Formatierung entfernen",
"heading": "Überschrift {level}",
"align": "Text ausrichten: {position}",
"blockquote": "Blockzitat",
"horizontalLine": "Horizontale Linie",
"bulletList": "Aufzählung",
"orderedList": "Geordnete Liste",
"checkList": "Checkliste",
"increaseIndent": "Einzug vergrößern",
"decreaseIndent": "Einzug verkleinern",
"link": "Link",
"unlink": "Link entfernen",
"image": "Bild einbetten",
"addTable": "Tabelle hinzufügen",
"deleteTable": "Tabelle entfernen",
"colorCell": "Farbe der Tabellen Zelle",
"mergeCell": "Zellen-Zusammenführung umschalten",
"addColumnLeft": "Spalte davor hinzufügen",
"addColumnRight": "Spalte danach hinzufügen",
"deleteColumn": "Spalte löschen",
"addRowTop": "Zeile davor hinzufügen",
"addRowBelow": "Zeile danach hinzufügen",
"deleteRow": "Zeile löschen"
},
"align": {
"left": "Links",
"center": "Mittig",
"right": "Rechts"
},
"popover": {
"clearColor": "Farbe entfernen",
"source": "Quelle",
"widthPlaceholder": "Wert in % oder Pixel",
"columns": "Spalten",
"rows": "Zeilen",
"width": "Breite",
"height": "Höhe"
}
},
"iframe": {
"name": "iFrame",
"description": "Einbetten von Inhalten aus dem Internet. Einige Websites können den Zugriff einschränken.",
"option": {
"embedUrl": {
"label": "URL einbetten"
},
"allowFullScreen": {
"label": "Vollbildmodus zulassen"
},
"allowTransparency": {
"label": "Erlaube Transparenz"
},
"allowScrolling": {
"label": "Scrollen zulassen"
},
"allowPayment": {
"label": "Zahlung zulassen"
},
"allowAutoPlay": {
"label": "Automatische Wiedergabe zulassen"
},
"allowMicrophone": {
"label": "Mikrofonzugriff erlauben"
},
"allowCamera": {
"label": "Kamera freigeben"
},
"allowGeolocation": {
"label": "Geolokalisierung zulassen"
}
},
"error": {
"noBrowerSupport": "Ihr Browser unterstützt keine iframes. Bitte aktualisieren Sie Ihren Browser."
}
},
"smartHome-entityState": {
"option": {
"entityId": {
"label": "Eintrag-ID"
}
}
},
"smartHome-executeAutomation": {
"option": {
"displayName": {
"label": "Anzeigename"
},
"automationId": {
"label": "Automatisierungs-ID"
}
}
},
"calendar": {
"name": "Kalender",
"option": {
"releaseType": {
"label": "Radarr Veröffentlichungs Typ"
}
}
},
"weather": {
"name": "Wetter",
"description": "Zeigt die aktuellen Wetterinformationen für einen bestimmten Ort an.",
"option": {
"location": {
"label": "Wetterstandort"
}
},
"kind": {
"clear": "Klar",
"mainlyClear": "Überwiegend klar",
"fog": "Nebel",
"drizzle": "Niesel",
"freezingDrizzle": "Eisiger Nieselregen",
"rain": "Regen",
"freezingRain": "Eisiger Regen",
"snowFall": "Schneefall",
"snowGrains": "Schneekörner",
"rainShowers": "Regenschauer",
"snowShowers": "Schneeschauer",
"thunderstorm": "Gewitter",
"thunderstormWithHail": "Gewitter mit Hagel",
"unknown": "Unbekannt"
}
},
"indexerManager": {
"name": "Status des Indexer-Managers",
"title": "Indexer-Manager",
"testAll": "Alle testen"
},
"healthMonitoring": {
"name": "Überwachung des Systemzustands",
"description": "Zeigt Informationen zum Zustand und Status Ihres/Ihrer Systeme(s) an.",
"option": {
"fahrenheit": {
"label": "CPU-Temperatur in Fahrenheit"
},
"cpu": {
"label": "CPU-Info anzeigen"
},
"memory": {
"label": "Speicher-Info anzeigen"
},
"fileSystem": {
"label": "Dateisystem Info anzeigen"
}
},
"popover": {
"available": "Verfügbar"
}
},
"common": {
"location": {
"search": "Suchen",
"table": {
"header": {},
"action": {},
"population": {
"fallback": "Unbekannt"
}
}
}
},
"video": {
"name": "Videostream",
"description": "Einbetten eines Videostreams oder eines Videos von einer Kamera oder einer Website",
"option": {
"feedUrl": {
"label": "Feed-URL"
},
"hasAutoPlay": {
"label": "Automatische Wiedergabe"
}
}
},
"downloads": {
"items": {
"added": {
"detailsTitle": "Hinzugefügt am"
},
"downSpeed": {
"columnTitle": "Down",
"detailsTitle": "Download Geschwindigkeit"
},
"integration": {
"columnTitle": "Integration"
},
"progress": {
"columnTitle": "Fortschritt"
},
"ratio": {
"columnTitle": "Verhältnis"
},
"state": {
"columnTitle": "Staat"
},
"upSpeed": {
"columnTitle": "Up"
}
},
"states": {
"downloading": "Herunterladen",
"queued": "In der Warteschlange",
"paused": "Pausiert",
"completed": "Abgeschlossen",
"unknown": "Unbekannt"
}
},
"mediaRequests-requestList": {
"description": "Sehen Sie eine Liste aller Medienanfragen von Ihrer Overseerr- oder Jellyseerr-Instanz",
"option": {
"linksTargetNewTab": {
"label": "Links in neuem Tab öffnen"
}
},
"availability": {
"unknown": "Unbekannt",
"partiallyAvailable": "Teilweise",
"available": "Verfügbar"
}
},
"mediaRequests-requestStats": {
"description": "Statistiken über Ihre Medienanfragen",
"titles": {
"stats": {
"main": "Medien-Statistiken",
"approved": "Bereits genehmigt",
"pending": "Ausstehende Freigaben",
"tv": "TV-Anfragen",
"movie": "Film-Anfragen",
"total": "Gesamt"
},
"users": {
"main": "Top-Nutzer"
}
}
}
},
"board": {
"action": {
"oldImport": {
"form": {
"apps": {
"label": "Apps"
},
"screenSize": {
"option": {
"sm": "Klein",
"md": "Mittel",
"lg": "Groß"
}
}
}
}
},
"field": {
"backgroundImageAttachment": {
"label": "Anhang des Hintergrundbildes"
},
"backgroundImageSize": {
"label": "Hintergrundbild-Größe"
},
"primaryColor": {
"label": "Primärfarbe"
},
"secondaryColor": {
"label": "Sekundärfarbe"
},
"customCss": {
"description": "Außerdem können Sie Ihr Dashboard mittels CSS anpassen, dies wird nur für erfahrene Benutzer empfohlen"
},
"name": {
"label": "Name"
},
"isPublic": {
"label": "Öffentlich sichtbar"
}
},
"setting": {
"section": {
"general": {
"title": "Allgemein"
},
"layout": {
"title": "Ansicht"
},
"background": {
"title": "Hintergrund"
},
"access": {
"permission": {
"item": {
"view": {
"label": "Board anzeigen"
}
}
}
},
"dangerZone": {
"title": "Gefahrenzone",
"action": {
"delete": {
"confirm": {
"title": "Board löschen"
}
}
}
}
}
}
},
"management": {
"navbar": {
"items": {
"home": "Startseite",
"boards": "Boards",
"apps": "Apps",
"users": {
"label": "Benutzer",
"items": {
"manage": "Verwalten",
"invites": "Einladungen"
}
},
"tools": {
"label": "Werkzeuge",
"items": {
"docker": "Docker",
"api": "API"
}
},
"settings": "Einstellungen",
"help": {
"label": "Hilfe",
"items": {
"documentation": "Dokumentation",
"discord": "Community Discord"
}
},
"about": "Über"
}
},
"page": {
"home": {
"statistic": {
"board": "Boards",
"user": "Benutzer",
"invite": "Einladungen",
"app": "Apps"
},
"statisticLabel": {
"boards": "Boards"
}
},
"board": {
"title": "Deine Boards",
"action": {
"settings": {
"label": "Einstellungen"
},
"setHomeBoard": {
"badge": {
"label": "Startseite"
}
},
"delete": {
"label": "Dauerhaft löschen",
"confirm": {
"title": "Board löschen"
}
}
},
"modal": {
"createBoard": {
"field": {
"name": {
"label": "Name"
}
}
}
}
},
"user": {
"setting": {
"general": {
"title": "Allgemein",
"item": {
"firstDayOfWeek": "Erster Tag der Woche",
"accessibility": "Barrierefreiheit"
}
},
"security": {
"title": "Sicherheit"
},
"board": {
"title": "Boards"
}
},
"list": {
"metaTitle": "Verwaltung von Benutzern",
"title": "Benutzer"
},
"create": {
"metaTitle": "Benutzer erstellen",
"step": {
"security": {
"label": "Sicherheit"
}
}
},
"invite": {
"title": "Verwalten von Benutzereinladungen",
"action": {
"new": {
"description": "Nach Ablauf der Frist ist eine Einladung nicht mehr gültig und der Empfänger der Einladung kann kein Konto erstellen."
},
"copy": {
"link": "Link zur Einladung"
},
"delete": {
"title": "Einladung löschen",
"description": "Sind Sie sicher, dass Sie diese Einladung löschen möchten? Benutzer mit diesem Link können dann kein Konto mehr über diesen Link erstellen."
}
},
"field": {
"id": {
"label": "ID"
},
"creator": {
"label": "Ersteller"
},
"expirationDate": {
"label": "Ablaufdatum"
},
"token": {
"label": "Token"
}
}
}
},
"group": {
"setting": {
"general": {
"title": "Allgemein"
}
}
},
"settings": {
"title": "Einstellungen"
},
"tool": {
"tasks": {
"status": {
"running": "Aktiv",
"error": "Fehler"
},
"job": {
"mediaServer": {
"label": "Medien Server"
},
"mediaRequests": {
"label": "Medienanfragen"
}
}
},
"api": {
"title": "API",
"tab": {
"documentation": {
"label": "Dokumentation"
},
"apiKey": {
"table": {
"header": {
"id": "ID"
}
}
}
}
}
}
}
},
"docker": {
"title": "Container",
"field": {
"name": {
"label": "Name"
},
"state": {
"label": "Staat",
"option": {
"created": "Erstellt",
"running": "Aktiv",
"paused": "Pausiert",
"restarting": "Startet neu",
"removing": "Wird entfernt"
}
},
"containerImage": {
"label": "Image"
},
"ports": {
"label": "Ports"
}
},
"action": {
"start": {
"label": "Starten"
},
"stop": {
"label": "Stopp"
},
"restart": {
"label": "Neustarten"
},
"remove": {
"label": "Entfernen"
}
}
},
"permission": {
"tab": {
"user": "Benutzer"
},
"field": {
"user": {
"label": "Benutzer"
}
}
},
"navigationStructure": {
"manage": {
"label": "Verwalten",
"boards": {
"label": "Boards"
},
"integrations": {
"edit": {
"label": "Bearbeiten"
}
},
"search-engines": {
"edit": {
"label": "Bearbeiten"
}
},
"apps": {
"label": "Apps",
"edit": {
"label": "Bearbeiten"
}
},
"users": {
"label": "Benutzer",
"create": {
"label": "Erstellen"
},
"general": "Allgemein",
"security": "Sicherheit",
"board": "Boards",
"invites": {
"label": "Einladungen"
}
},
"tools": {
"label": "Werkzeuge",
"docker": {
"label": "Docker"
}
},
"settings": {
"label": "Einstellungen"
},
"about": {
"label": "Über"
}
}
},
"search": {
"mode": {
"appIntegrationBoard": {
"group": {
"app": {
"title": "Apps"
},
"board": {
"title": "Boards"
}
}
},
"external": {
"group": {
"searchEngine": {
"option": {
"torrent": {
"name": "Torrents"
}
}
}
}
},
"help": {
"group": {
"help": {
"title": "Hilfe",
"option": {
"documentation": {
"label": "Dokumentation"
},
"discord": {
"label": "Community Discord"
}
}
}
}
},
"page": {
"group": {
"page": {
"option": {
"manageUser": {
"label": "Verwaltung von Benutzern"
},
"about": {
"label": "Über"
},
"preferences": {
"label": "Ihre Einstellungen"
}
}
}
}
},
"userGroup": {
"group": {
"user": {
"title": "Benutzer"
}
}
}
},
"engine": {
"field": {
"name": {
"label": "Name"
}
}
}
}
}

View File

@@ -1,196 +0,0 @@
import "dayjs/locale/de";
import dayjs from "dayjs";
import { MRT_Localization_DE } from "mantine-react-table/locales/de/index.cjs";
dayjs.locale("de");
export default {
user: {
page: {
login: {
title: "Melde dich bei deinem Konto an",
subtitle: "Willkommen zurück! Bitte gib deine Zugangsdaten ein",
},
init: {
title: "Neue Homarr Installation",
subtitle: "Bitte erstelle den initialen Administrator Benutzer",
},
},
field: {
username: {
label: "Benutzername",
},
password: {
label: "Passwort",
},
passwordConfirm: {
label: "Passwort bestätigen",
},
},
action: {
login: "Anmelden",
create: "Benutzer erstellen",
},
},
integration: {
page: {
list: {
title: "Integrationen",
search: "Integration suchen",
empty: "Keine Integrationen gefunden",
},
create: {
title: "Neue {name} Integration erstellen",
notification: {
success: {
title: "Erstellung erfolgreich",
message: "Die Integration wurde erfolgreich erstellt",
},
error: {
title: "Erstellung fehlgeschlagen",
message: "Die Integration konnte nicht erstellt werden",
},
},
},
edit: {
title: "{name} Integration bearbeiten",
notification: {
success: {
title: "Änderungen erfolgreich angewendet",
message: "Die Integration wurde erfolgreich gespeichert",
},
error: {
title: "Änderungen konnten nicht angewendet werden",
message: "Die Integration konnte nicht gespeichert werden",
},
},
},
delete: {
title: "Integration entfernen",
message: "Möchtest du die Integration {name} wirklich entfernen?",
notification: {
success: {
title: "Entfernen erfolgreich",
message: "Die Integration wurde erfolgreich entfernt",
},
error: {
title: "Entfernen fehlgeschlagen",
message: "Die Integration konnte nicht entfernt werden",
},
},
},
},
field: {
name: {
label: "Name",
},
url: {
label: "Url",
},
},
action: {
create: "Neue Integration",
},
testConnection: {
action: "Verbindung überprüfen",
alertNotice: "Der Button zum Speichern wird aktiviert, sobald die Verbindung erfolgreich überprüft wurde",
notification: {
success: {
title: "Verbindung erfolgreich",
message: "Die Verbindung wurde erfolgreich hergestellt",
},
invalidUrl: {
title: "Ungültige URL",
message: "Die URL ist ungültig",
},
notAllSecretsProvided: {
title: "Fehlende Zugangsdaten",
message: "Es wurden nicht alle Zugangsdaten angegeben",
},
invalidCredentials: {
title: "Ungültige Zugangsdaten",
message: "Die Zugangsdaten sind ungültig",
},
commonError: {
title: "Verbindung fehlgeschlagen",
message: "Die Verbindung konnte nicht hergestellt werden",
},
},
},
secrets: {
title: "Zugangsdaten",
lastUpdated: "Zuletzt geändert {date}",
secureNotice: "Diese Zugangsdaten können nach der Erstellung nicht mehr ausgelesen werden",
reset: {
title: "Zugangsdaten zurücksetzen",
message: "Möchtest du diese Zugangsdaten wirklich zurücksetzen?",
},
kind: {
username: {
label: "Benutzername",
newLabel: "Neuer Benutzername",
},
apiKey: {
label: "API Key",
newLabel: "Neuer API Key",
},
password: {
label: "Passwort",
newLabel: "Neues Passwort",
},
},
},
},
common: {
rtl: "{value}{symbol}",
action: {
backToOverview: "Zurück zur Übersicht",
create: "Erstellen",
edit: "Bearbeiten",
save: "Speichern",
cancel: "Abbrechen",
confirm: "Bestätigen",
},
multiSelect: {
placeholder: "Wähle eine oder mehrere Optionen aus",
},
noResults: "Keine Ergebnisse gefunden",
mantineReactTable: MRT_Localization_DE,
},
widget: {
editModal: {
integrations: {
label: "Integrationen",
},
},
clock: {
option: {
is24HourFormat: {
label: "24-Stunden Format",
description: "Verwende das 24-Stunden Format anstelle des 12-Stunden Formats",
},
isLocaleTime: {
label: "Lokale Zeit verwenden",
},
timezone: {
label: "Zeitzone",
},
},
},
weather: {
option: {
location: {
label: "Standort",
},
showCity: {
label: "Stadt anzeigen",
},
},
},
},
search: {
placeholder: "Suche nach etwas",
nothingFound: "Nichts gefunden",
},
} as const;

View File

@@ -0,0 +1,911 @@
{
"user": {
"title": "Χρήστες",
"name": "Χρήστης",
"field": {
"email": {
"label": "E-Mail"
},
"username": {
"label": "Όνομα Χρήστη"
},
"password": {
"label": "Κωδικός",
"requirement": {
"lowercase": "Περιλαμβάνει πεζό γράμμα",
"uppercase": "Περιλαμβάνει κεφαλαίο γράμμα",
"number": "Περιλαμβάνει αριθμό"
}
},
"passwordConfirm": {
"label": "Επιβεβαίωση κωδικού"
}
},
"action": {
"login": {
"label": "Σύνδεση"
},
"register": {
"label": "Δημιουργία λογαριασμού",
"notification": {
"success": {
"title": "Ο λογαριασμός δημιουργήθηκε"
}
}
},
"create": "Δημιουργία χρήστη"
}
},
"group": {
"field": {
"name": "Όνομα"
},
"permission": {
"admin": {
"title": "Διαχειριστής"
},
"board": {
"title": "Πίνακες"
}
}
},
"app": {
"page": {
"list": {
"title": "Εφαρμογές"
}
},
"field": {
"name": {
"label": "Όνομα"
}
}
},
"integration": {
"field": {
"name": {
"label": "Όνομα"
}
},
"testConnection": {
"notification": {
"invalidUrl": {
"title": "Μη Έγκυρος Σύνδεσμος"
}
}
},
"secrets": {
"kind": {
"username": {
"label": "Όνομα Χρήστη"
},
"password": {
"label": "Κωδικός",
"newLabel": "Νέος κωδικός"
}
}
}
},
"media": {
"field": {
"name": "Όνομα",
"size": "Μέγεθος",
"creator": "Δημιουργός"
}
},
"common": {
"error": "Σφάλμα",
"action": {
"add": "Προσθήκη",
"apply": "Εφαρμογή",
"create": "Δημιουργία",
"edit": "Επεξεργασία",
"insert": "Εισαγωγή",
"remove": "Αφαίρεση",
"save": "Αποθήκευση",
"saveChanges": "Αποθήκευση αλλαγών",
"cancel": "Ακύρωση",
"delete": "Διαγραφή",
"confirm": "Επιβεβαίωση",
"previous": "Προηγούμενο",
"next": "Επόμενο",
"tryAgain": "Προσπαθήστε ξανά"
},
"information": {
"hours": "Ώρες",
"minutes": "Λεπτά"
},
"userAvatar": {
"menu": {
"preferences": "Οι ρυθμίσεις σας",
"login": "Σύνδεση"
}
},
"dangerZone": "Επικίνδυνη Περιοχή",
"noResults": "Δεν βρέθηκαν αποτελέσματα",
"zod": {
"errors": {
"default": "Το πεδίο δεν είναι έγκυρο",
"required": "Αυτό το πεδίο είναι υποχρεωτικό",
"string": {
"startsWith": "Αυτό το πεδίο πρέπει να ξεκινά με {startsWith}",
"endsWith": "Το πεδίο αυτό πρέπει να τελειώνει με {endsWith}",
"includes": "Το πεδίο αυτό πρέπει να περιλαμβάνει το {includes}"
},
"tooSmall": {
"string": "Το πεδίο αυτό πρέπει να έχει μήκος τουλάχιστον {minimum} χαρακτήρες",
"number": "Το πεδίο αυτό πρέπει να είναι μεγαλύτερο ή ίσο του {minimum}"
},
"tooBig": {
"string": "Το πεδίο αυτό πρέπει να έχει μήκος το πολύ {maximum} χαρακτήρες",
"number": "Το πεδίο αυτό πρέπει να είναι μικρότερο ή ίσο του {maximum}"
}
}
}
},
"section": {
"category": {
"field": {
"name": {
"label": "Όνομα"
}
},
"action": {
"moveUp": "Μετακίνηση επάνω",
"moveDown": "Μετακίνηση κάτω"
},
"menu": {
"label": {
"changePosition": "Αλλαγή θέσης"
}
}
}
},
"item": {
"menu": {
"label": {
"settings": "Ρυθμίσεις"
}
},
"moveResize": {
"field": {
"width": {
"label": "Πλάτος"
},
"height": {
"label": "Ύψος"
}
}
}
},
"widget": {
"app": {
"option": {
"openInNewTab": {
"label": "Άνοιγμα σε νέα καρτέλα"
}
}
},
"dnsHoleSummary": {
"option": {
"layout": {
"label": "Διάταξη",
"option": {
"row": {
"label": "Οριζόντια"
},
"column": {
"label": "Κατακόρυφα"
}
}
}
},
"data": {
"adsBlockedToday": "Σημερινοί αποκλεισμοί",
"adsBlockedTodayPercentage": "Σημερινοί αποκλεισμοί",
"dnsQueriesToday": "Σημερινά queries"
}
},
"dnsHoleControls": {
"description": "Ελέγξτε το PiHole ή το AdGuard από το dashboard σας",
"option": {
"layout": {
"label": "Διάταξη",
"option": {
"row": {
"label": "Οριζόντια"
},
"column": {
"label": "Κατακόρυφα"
}
}
}
},
"controls": {
"set": "Ορισμός",
"enabled": "Ενεργοποιημένο",
"disabled": "Απενεργοποιημένο",
"hours": "Ώρες",
"minutes": "Λεπτά"
}
},
"clock": {
"description": "Εμφανίζει την τρέχουσα ημερομηνία και ώρα.",
"option": {
"timezone": {
"label": "Ζώνη ώρας"
}
}
},
"notebook": {
"name": "Σημειωματάριο",
"option": {
"showToolbar": {
"label": "Εμφάνιση γραμμής εργαλείων για να σας βοηθήσει να γράψετε σημάνσεις"
},
"allowReadOnlyCheck": {
"label": "Να επιτρέπεται η επιλογή σε λειτουργία μόνο ανάγνωσης"
},
"content": {
"label": "Το περιεχόμενο του σημειωματάριου"
}
},
"controls": {
"bold": "Έντονη γραφή",
"italic": "Πλάγια γραφή",
"strikethrough": "Διαγραμμισμένο Κείμενο",
"underline": "Υπογραμμισμένο Κείμενο",
"colorText": "Έγχρωμο κείμενο",
"colorHighlight": "Έγχρωμο κείμενο επισήμανσης",
"code": "Κωδικός",
"clear": "Εκκαθάριση μορφοποίησης",
"heading": "Επικεφαλίδα {level}",
"align": "Στοίχιση κειμένου: {position}",
"blockquote": "Μπλοκ κειμένου παράθεσης",
"horizontalLine": "Οριζόντια γραμμή",
"bulletList": "Λίστα με κουκκίδες",
"orderedList": "Ταξινομημένη λίστα",
"checkList": "Λίστα ελέγχου",
"increaseIndent": "Αύξηση εσοχής",
"decreaseIndent": "Μείωση εσοχής",
"link": "Σύνδεσμος",
"unlink": "Αφαίρεση συνδέσμου",
"image": "Ενσωμάτωση εικόνας",
"addTable": "Προσθήκη πίνακα",
"deleteTable": "Διαγραφή πίνακα",
"colorCell": "Χρώμα κελιού",
"mergeCell": "Εναλλαγή συγχώνευσης κελιού",
"addColumnLeft": "Προσθήκη στήλης πριν",
"addColumnRight": "Προσθήκη στήλης μετά",
"deleteColumn": "Διαγραφή στήλης",
"addRowTop": "Προσθήκη γραμμής πριν",
"addRowBelow": "Προσθήκη γραμμής μετά",
"deleteRow": "Διαγραφή γραμμής"
},
"align": {
"left": "Αριστερά",
"center": "Κέντρο",
"right": "Δεξιά"
},
"popover": {
"clearColor": "Καθαρισμός χρώματος",
"source": "Πηγή",
"widthPlaceholder": "Τιμή σε % ή εικονοστοιχεία",
"columns": "Στήλες",
"rows": "Γραμμές",
"width": "Πλάτος",
"height": "Ύψος"
}
},
"iframe": {
"name": "iframe",
"description": "Ενσωματώστε οποιοδήποτε περιεχόμενο από το διαδίκτυο. Ορισμένοι ιστότοποι ενδέχεται να περιορίζουν την πρόσβαση.",
"option": {
"embedUrl": {
"label": "URL ενσωμάτωσης"
},
"allowFullScreen": {
"label": "Επιτρέψτε την πλήρη οθόνη"
},
"allowTransparency": {
"label": "Να επιτρέπεται η διαφάνεια"
},
"allowScrolling": {
"label": "Επιτρέπεται η κύλιση"
},
"allowPayment": {
"label": "Επιτρέπονται πληρωμές"
},
"allowAutoPlay": {
"label": "Επιτρέπεται η αυτόματη αναπαραγωγή"
},
"allowMicrophone": {
"label": "Πρόσβαση στο μικρόφωνο"
},
"allowCamera": {
"label": "Πρόσβαση στην κάμερα"
},
"allowGeolocation": {
"label": "Επιτρέπεται ο γεωεντοπισμός"
}
},
"error": {
"noBrowerSupport": "Ο περιηγητής σας δεν υποστηρίζει iframes. Παρακαλούμε ενημερώστε το πρόγραμμα περιήγησης."
}
},
"smartHome-entityState": {
"option": {
"entityId": {
"label": "Αναγνωριστικό οντότητας"
}
}
},
"smartHome-executeAutomation": {
"option": {
"displayName": {
"label": "Εμφανιζόμενο όνομα"
},
"automationId": {
"label": "Αναγνωριστικό αυτοματισμού"
}
}
},
"calendar": {
"name": "Ημερολόγιο",
"option": {
"releaseType": {
"label": "Τύπος κυκλοφορίας Radarr"
}
}
},
"weather": {
"name": "Καιρός",
"description": "Εμφανίζει τις τρέχουσες πληροφορίες καιρού μιας καθορισμένης τοποθεσίας.",
"option": {
"location": {
"label": "Τοποθεσία καιρού"
}
},
"kind": {
"clear": "Καθαρός",
"mainlyClear": "Κυρίως καθαρός",
"fog": "Ομίχλη",
"drizzle": "Ψιχάλες",
"freezingDrizzle": "Παγωμένο ψιλόβροχο",
"rain": "Βροχή",
"freezingRain": "Παγωμένη βροχή",
"snowFall": "Χιονόπτωση",
"snowGrains": "Κόκκοι χιονιού",
"rainShowers": "Βροχοπτώσεις",
"snowShowers": "Χιονοπτώσεις",
"thunderstorm": "Καταιγίδα",
"thunderstormWithHail": "Καταιγίδα με χαλάζι",
"unknown": "Άγνωστο"
}
},
"indexerManager": {
"name": "Κατάσταση διαχειριστή indexer",
"title": "Διαχειριστής indexer",
"testAll": "Δοκιμή όλων"
},
"healthMonitoring": {
"name": "Παρακολούθηση της υγείας του συστήματος",
"description": "Εμφανίζει πληροφορίες που δείχνουν την κατάσταση και την υγεία του/ων συστήματος/ων σας.",
"option": {
"fahrenheit": {
"label": "Θερμοκρασία CPU σε Φαρενάιτ"
},
"cpu": {
"label": "Εμφάνιση πληροφοριών επεξεργαστή"
},
"memory": {
"label": "Εμφάνιση Πληροφοριών Μνήμης"
},
"fileSystem": {
"label": "Εμφάνιση Πληροφοριών Συστήματος Αρχείων"
}
},
"popover": {
"available": "Διαθέσιμο"
}
},
"common": {
"location": {
"search": "Αναζήτηση",
"table": {
"header": {},
"action": {},
"population": {
"fallback": "Άγνωστο"
}
}
}
},
"video": {
"name": "Ροή Βίντεο",
"description": "Ενσωματώστε μια ροή βίντεο ή βίντεο από μια κάμερα ή έναν ιστότοπο",
"option": {
"feedUrl": {
"label": "URL τροφοδοσίας"
},
"hasAutoPlay": {
"label": "Αυτόματη αναπαραγωγή"
}
}
},
"downloads": {
"items": {
"added": {
"detailsTitle": "Ημερομηνία Προσθήκης"
},
"downSpeed": {
"columnTitle": "Κάτω",
"detailsTitle": "Ταχύτητα Λήψης"
},
"integration": {
"columnTitle": "Ενσωμάτωση"
},
"progress": {
"columnTitle": "Πρόοδος"
},
"ratio": {
"columnTitle": "Αναλογία"
},
"state": {
"columnTitle": "Κατάσταση"
},
"upSpeed": {
"columnTitle": "Πάνω"
}
},
"states": {
"downloading": "Λήψη",
"queued": "Στην ουρά",
"paused": "Σε παύση",
"completed": "Ολοκληρώθηκε",
"unknown": "Άγνωστο"
}
},
"mediaRequests-requestList": {
"description": "Δείτε μια λίστα με όλα τα αιτήματα μέσων ενημέρωσης από την περίπτωση Overseerr ή Jellyseerr",
"option": {
"linksTargetNewTab": {
"label": "Άνοιγμα συνδέσμων σε νέα καρτέλα"
}
},
"availability": {
"unknown": "Άγνωστο",
"partiallyAvailable": "Μερικώς",
"available": "Διαθέσιμο"
}
},
"mediaRequests-requestStats": {
"description": "Στατιστικά στοιχεία σχετικά με τα αιτήματά σας για τα μέσα ενημέρωσης",
"titles": {
"stats": {
"main": "Στατιστικά Πολυμέσων",
"approved": "Ήδη εγκεκριμένα",
"pending": "Εκκρεμείς εγκρίσεις",
"tv": "Αιτήσεις TV",
"movie": "Αιτήσεις ταινιών",
"total": "Σύνολο"
},
"users": {
"main": "Κορυφαίοι Χρήστες"
}
}
}
},
"board": {
"action": {
"oldImport": {
"form": {
"apps": {
"label": "Εφαρμογές"
},
"screenSize": {
"option": {
"sm": "Μικρό",
"md": "Μεσαίο",
"lg": "Μεγάλο"
}
}
}
}
},
"field": {
"backgroundImageAttachment": {
"label": "Συνημμένη εικόνα φόντου"
},
"backgroundImageSize": {
"label": "Μέγεθος εικόνας φόντου"
},
"primaryColor": {
"label": "Βασικό χρώμα"
},
"secondaryColor": {
"label": "Δευτερεύον χρώμα"
},
"customCss": {
"description": "Περαιτέρω, προσαρμόστε τον πίνακα ελέγχου σας χρησιμοποιώντας CSS, συνιστάται μόνο για έμπειρους χρήστες"
},
"name": {
"label": "Όνομα"
},
"isPublic": {
"label": "Δημόσιο"
}
},
"setting": {
"section": {
"general": {
"title": "Γενικά"
},
"layout": {
"title": "Διάταξη"
},
"background": {
"title": "Φόντο"
},
"access": {
"permission": {
"item": {
"view": {
"label": "Προβολή πίνακα"
}
}
}
},
"dangerZone": {
"title": "Επικίνδυνη Περιοχή",
"action": {
"delete": {
"confirm": {
"title": "Διαγραφή πίνακα"
}
}
}
}
}
}
},
"management": {
"navbar": {
"items": {
"home": "Αρχική",
"boards": "Πίνακες",
"apps": "Εφαρμογές",
"users": {
"label": "Χρήστες",
"items": {
"manage": "Διαχείριση",
"invites": "Προσκλήσεις"
}
},
"tools": {
"label": "Εργαλεία",
"items": {
"docker": "Docker",
"api": "API"
}
},
"settings": "Ρυθμίσεις",
"help": {
"label": "Βοήθεια",
"items": {
"documentation": "Τεκμηρίωση",
"discord": "Κοινότητα Discord"
}
},
"about": "Σχετικά"
}
},
"page": {
"home": {
"statistic": {
"board": "Πίνακες",
"user": "Χρήστες",
"invite": "Προσκλήσεις",
"app": "Εφαρμογές"
},
"statisticLabel": {
"boards": "Πίνακες"
}
},
"board": {
"title": "Οι πίνακές σας",
"action": {
"settings": {
"label": "Ρυθμίσεις"
},
"setHomeBoard": {
"badge": {
"label": "Αρχική"
}
},
"delete": {
"label": "Οριστική διαγραφή",
"confirm": {
"title": "Διαγραφή πίνακα"
}
}
},
"modal": {
"createBoard": {
"field": {
"name": {
"label": "Όνομα"
}
}
}
}
},
"user": {
"setting": {
"general": {
"title": "Γενικά",
"item": {
"firstDayOfWeek": "Πρώτη ημέρα της εβδομάδας",
"accessibility": "Προσβασιμότητα"
}
},
"security": {
"title": "Ασφάλεια"
},
"board": {
"title": "Πίνακες"
}
},
"list": {
"metaTitle": "Διαχείριση χρηστών",
"title": "Χρήστες"
},
"create": {
"metaTitle": "Δημιουργία χρήστη",
"step": {
"security": {
"label": "Ασφάλεια"
}
}
},
"invite": {
"title": "Διαχείριση προσκλήσεων χρηστών",
"action": {
"new": {
"description": "Μετά τη λήξη, μια πρόσκληση δε θα είναι πλέον έγκυρη και ο παραλήπτης της πρόσκλησης δε θα είναι σε θέση να δημιουργήσει λογαριασμό."
},
"copy": {
"link": "Σύνδεσμος πρόσκλησης"
},
"delete": {
"title": "Διαγραφή πρόσκλησης",
"description": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτή την πρόσκληση; Οι χρήστες με αυτόν τον σύνδεσμο δεν θα μπορούν πλέον να δημιουργήσουν λογαριασμό χρησιμοποιώντας αυτόν τον σύνδεσμο."
}
},
"field": {
"id": {
"label": "Αναγνωριστικό (ID)"
},
"creator": {
"label": "Δημιουργός"
},
"expirationDate": {
"label": "Ημερομηνία λήξης"
},
"token": {
"label": "Token"
}
}
}
},
"group": {
"setting": {
"general": {
"title": "Γενικά"
}
}
},
"settings": {
"title": "Ρυθμίσεις"
},
"tool": {
"tasks": {
"status": {
"running": "Εκτελείται",
"error": "Σφάλμα"
},
"job": {
"mediaServer": {
"label": "Διακομιστής πολυμέσων"
},
"mediaRequests": {
"label": "Αιτήματα μέσων ενημέρωσης"
}
}
},
"api": {
"title": "API",
"tab": {
"documentation": {
"label": "Τεκμηρίωση"
},
"apiKey": {
"table": {
"header": {
"id": "Αναγνωριστικό (ID)"
}
}
}
}
}
}
}
},
"docker": {
"title": "Containers",
"field": {
"name": {
"label": "Όνομα"
},
"state": {
"label": "Κατάσταση",
"option": {
"created": "Δημιουργήθηκε",
"running": "Εκτελείται",
"paused": "Σε παύση",
"restarting": "Γίνεται επανεκκίνηση",
"removing": "Αφαιρείται"
}
},
"containerImage": {
"label": "Εικόνα"
},
"ports": {
"label": "Θύρες"
}
},
"action": {
"start": {
"label": "Έναρξη"
},
"stop": {
"label": "Διακοπή"
},
"restart": {
"label": "Επανεκκίνηση"
},
"remove": {
"label": "Αφαίρεση"
}
}
},
"permission": {
"tab": {
"user": "Χρήστες"
},
"field": {
"user": {
"label": "Χρήστης"
}
}
},
"navigationStructure": {
"manage": {
"label": "Διαχείριση",
"boards": {
"label": "Πίνακες"
},
"integrations": {
"edit": {
"label": "Επεξεργασία"
}
},
"search-engines": {
"edit": {
"label": "Επεξεργασία"
}
},
"apps": {
"label": "Εφαρμογές",
"edit": {
"label": "Επεξεργασία"
}
},
"users": {
"label": "Χρήστες",
"create": {
"label": "Δημιουργία"
},
"general": "Γενικά",
"security": "Ασφάλεια",
"board": "Πίνακες",
"invites": {
"label": "Προσκλήσεις"
}
},
"tools": {
"label": "Εργαλεία",
"docker": {
"label": "Docker"
}
},
"settings": {
"label": "Ρυθμίσεις"
},
"about": {
"label": "Σχετικά"
}
}
},
"search": {
"mode": {
"appIntegrationBoard": {
"group": {
"app": {
"title": "Εφαρμογές"
},
"board": {
"title": "Πίνακες"
}
}
},
"external": {
"group": {
"searchEngine": {
"option": {
"torrent": {
"name": "Τόρρεντ"
}
}
}
}
},
"help": {
"group": {
"help": {
"title": "Βοήθεια",
"option": {
"documentation": {
"label": "Τεκμηρίωση"
},
"discord": {
"label": "Κοινότητα Discord"
}
}
}
}
},
"page": {
"group": {
"page": {
"option": {
"manageUser": {
"label": "Διαχείριση χρηστών"
},
"about": {
"label": "Σχετικά"
},
"preferences": {
"label": "Οι ρυθμίσεις σας"
}
}
}
}
},
"userGroup": {
"group": {
"user": {
"title": "Χρήστες"
}
}
}
},
"engine": {
"field": {
"name": {
"label": "Όνομα"
}
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,911 @@
{
"user": {
"title": "Usuarios",
"name": "Usuario",
"field": {
"email": {
"label": "Correo electrónico"
},
"username": {
"label": "Nombre de usuario"
},
"password": {
"label": "Contraseña",
"requirement": {
"lowercase": "Incluye letra minúscula",
"uppercase": "Incluye letra mayúscula",
"number": "Incluye número"
}
},
"passwordConfirm": {
"label": "Confirmar contraseña"
}
},
"action": {
"login": {
"label": "Iniciar sesión"
},
"register": {
"label": "Crear cuenta",
"notification": {
"success": {
"title": "Cuenta creada"
}
}
},
"create": "Crear usuario"
}
},
"group": {
"field": {
"name": "Nombre"
},
"permission": {
"admin": {
"title": "Administrador"
},
"board": {
"title": "Tableros"
}
}
},
"app": {
"page": {
"list": {
"title": "Aplicaciones"
}
},
"field": {
"name": {
"label": "Nombre"
}
}
},
"integration": {
"field": {
"name": {
"label": "Nombre"
}
},
"testConnection": {
"notification": {
"invalidUrl": {
"title": "URL invalida"
}
}
},
"secrets": {
"kind": {
"username": {
"label": "Nombre de usuario"
},
"password": {
"label": "Contraseña",
"newLabel": "Nueva contraseña"
}
}
}
},
"media": {
"field": {
"name": "Nombre",
"size": "Tamaño",
"creator": "Creador"
}
},
"common": {
"error": "Error",
"action": {
"add": "Añadir",
"apply": "Aplicar",
"create": "Crear",
"edit": "Editar",
"insert": "Insertar",
"remove": "Eliminar",
"save": "Guardar",
"saveChanges": "Guardar cambios",
"cancel": "Cancelar",
"delete": "Eliminar",
"confirm": "Confirmar",
"previous": "Anterior",
"next": "Siguiente",
"tryAgain": "Inténtalo de nuevo"
},
"information": {
"hours": "",
"minutes": ""
},
"userAvatar": {
"menu": {
"preferences": "Tus preferencias",
"login": "Iniciar sesión"
}
},
"dangerZone": "Zona de riesgo",
"noResults": "No se han encontrado resultados",
"zod": {
"errors": {
"default": "Este campo no es válido",
"required": "Este campo es obligatorio",
"string": {
"startsWith": "Este campo debe empezar con {startsWith}",
"endsWith": "Este campo debe terminar con {endsWith}",
"includes": "Este campo debe incluir {includes}"
},
"tooSmall": {
"string": "Este campo debe tener al menos {minimum} caracteres",
"number": "Este campo debe ser mayor o igual a {minimum}"
},
"tooBig": {
"string": "Este campo debe tener como máximo {maximum} caracteres",
"number": "Este campo debe ser menor o igual a {maximum}"
}
}
}
},
"section": {
"category": {
"field": {
"name": {
"label": "Nombre"
}
},
"action": {
"moveUp": "Mover hacia arriba",
"moveDown": "Mover hacia abajo"
},
"menu": {
"label": {
"changePosition": "Cambiar posición"
}
}
}
},
"item": {
"menu": {
"label": {
"settings": "Ajustes"
}
},
"moveResize": {
"field": {
"width": {
"label": "Ancho"
},
"height": {
"label": "Alto"
}
}
}
},
"widget": {
"app": {
"option": {
"openInNewTab": {
"label": "Abrir en una pestaña nueva"
}
}
},
"dnsHoleSummary": {
"option": {
"layout": {
"label": "Diseño",
"option": {
"row": {
"label": "Horizontal"
},
"column": {
"label": "Vertical"
}
}
}
},
"data": {
"adsBlockedToday": "Bloqueados hoy",
"adsBlockedTodayPercentage": "Bloqueados hoy",
"dnsQueriesToday": "Consultas de hoy"
}
},
"dnsHoleControls": {
"description": "Controla Pihole o AdGuard desde tu panel",
"option": {
"layout": {
"label": "Diseño",
"option": {
"row": {
"label": "Horizontal"
},
"column": {
"label": "Vertical"
}
}
}
},
"controls": {
"set": "",
"enabled": "Activado",
"disabled": "Desactivado",
"hours": "",
"minutes": ""
}
},
"clock": {
"description": "Muestra la fecha y hora actual.",
"option": {
"timezone": {
"label": "Zona horaria"
}
}
},
"notebook": {
"name": "Bloc de notas",
"option": {
"showToolbar": {
"label": "Muestra la barra de herramientas para ayudarte a escribir Markdown"
},
"allowReadOnlyCheck": {
"label": "Permitir verificación en modo solo lectura"
},
"content": {
"label": "El contenido del Bloc de notas"
}
},
"controls": {
"bold": "Negrita",
"italic": "Cursiva",
"strikethrough": "Tachado",
"underline": "Subrayado",
"colorText": "Color de texto",
"colorHighlight": "Texto resaltado en color",
"code": "Código",
"clear": "Borrar formato",
"heading": "Encabezado {level}",
"align": "Alinear texto: {position}",
"blockquote": "Cita",
"horizontalLine": "Línea horizontal",
"bulletList": "Lista sin ordenar",
"orderedList": "Lista ordenada",
"checkList": "Lista de control",
"increaseIndent": "Aumentar sangría",
"decreaseIndent": "Disminuir sangría",
"link": "Enlace",
"unlink": "Eliminar enlace",
"image": "Insertar imagen",
"addTable": "Añadir tabla",
"deleteTable": "Eliminar tabla",
"colorCell": "Color de celda",
"mergeCell": "Alternar combinación de celdas",
"addColumnLeft": "Añadir columna a la izquierda",
"addColumnRight": "Añadir columna a la derecha",
"deleteColumn": "Eliminar columna",
"addRowTop": "Añadir fila encima",
"addRowBelow": "Añadir fila debajo",
"deleteRow": "Eliminar fila"
},
"align": {
"left": "Izquierda",
"center": "Centrar",
"right": "Derecha"
},
"popover": {
"clearColor": "Eliminar color",
"source": "Fuente",
"widthPlaceholder": "Valor en % o píxeles",
"columns": "Columnas",
"rows": "Filas",
"width": "Ancho",
"height": "Alto"
}
},
"iframe": {
"name": "iFrame",
"description": "Incrusta cualquier contenido de Internet. Algunos sitios web pueden restringir el acceso.",
"option": {
"embedUrl": {
"label": "URL incrustada"
},
"allowFullScreen": {
"label": "Permitir pantalla completa"
},
"allowTransparency": {
"label": "Permitir transparencia"
},
"allowScrolling": {
"label": "Permitir desplazamiento"
},
"allowPayment": {
"label": "Permitir pago"
},
"allowAutoPlay": {
"label": "Permitir reproducción automática"
},
"allowMicrophone": {
"label": "Permitir micrófono"
},
"allowCamera": {
"label": "Permitir cámara"
},
"allowGeolocation": {
"label": "Permitir geolocalización"
}
},
"error": {
"noBrowerSupport": "Tu navegador no soporta iframes. Por favor, actualice tu navegador."
}
},
"smartHome-entityState": {
"option": {
"entityId": {
"label": "ID de la entidad"
}
}
},
"smartHome-executeAutomation": {
"option": {
"displayName": {
"label": "Nombre a mostrar"
},
"automationId": {
"label": "ID de automatización"
}
}
},
"calendar": {
"name": "Calendario",
"option": {
"releaseType": {
"label": "Tipo de lanzamiento de Radarr"
}
}
},
"weather": {
"name": "El Tiempo",
"description": "Muestra la información meteorológica actual de la ubicación establecida.",
"option": {
"location": {
"label": "Ubicación"
}
},
"kind": {
"clear": "Despejado",
"mainlyClear": "Mayormente despejado",
"fog": "Niebla",
"drizzle": "Llovizna",
"freezingDrizzle": "Llovizna helada",
"rain": "Lluvia",
"freezingRain": "Lluvia helada",
"snowFall": "Nevada",
"snowGrains": "Granos de nieve",
"rainShowers": "Chubascos",
"snowShowers": "Chubascos de nieve",
"thunderstorm": "Tormenta eléctrica",
"thunderstormWithHail": "Tormenta con granizo",
"unknown": "Desconocido"
}
},
"indexerManager": {
"name": "",
"title": "",
"testAll": ""
},
"healthMonitoring": {
"name": "Monitorización de Salud del Sistema",
"description": "Muestra información sobre la salud y el estado de tu(s) sistema(s).",
"option": {
"fahrenheit": {
"label": "Temperatura de la CPU en grados Fahrenheit"
},
"cpu": {
"label": "Mostrar información de la CPU"
},
"memory": {
"label": "Mostrar información de la memoria"
},
"fileSystem": {
"label": "Mostrar información del sistema de archivos"
}
},
"popover": {
"available": "Disponible"
}
},
"common": {
"location": {
"search": "Buscar",
"table": {
"header": {},
"action": {},
"population": {
"fallback": "Desconocido"
}
}
}
},
"video": {
"name": "Video en directo",
"description": "Incrusta una transmisión de video o un video de una cámara o un sitio web",
"option": {
"feedUrl": {
"label": "Fuente URL"
},
"hasAutoPlay": {
"label": "Auto-reproducción"
}
}
},
"downloads": {
"items": {
"added": {
"detailsTitle": ""
},
"downSpeed": {
"columnTitle": "Descarga",
"detailsTitle": "Velocidad de Descarga"
},
"integration": {
"columnTitle": "Integración"
},
"progress": {
"columnTitle": "Completado %"
},
"ratio": {
"columnTitle": "Ratio"
},
"state": {
"columnTitle": "Estado"
},
"upSpeed": {
"columnTitle": "Subida"
}
},
"states": {
"downloading": "Descargando",
"queued": "",
"paused": "Pausado",
"completed": "Completado",
"unknown": "Desconocido"
}
},
"mediaRequests-requestList": {
"description": "Mostrar una lista de todas las solicitudes multimedia de tu instancia de Overseerr o Jellyseerr",
"option": {
"linksTargetNewTab": {
"label": "Abrir enlaces en una pestaña nueva"
}
},
"availability": {
"unknown": "Desconocido",
"partiallyAvailable": "Parcial",
"available": "Disponible"
}
},
"mediaRequests-requestStats": {
"description": "Estadísticas sobre tus solicitudes multimedia",
"titles": {
"stats": {
"main": "Estadísticas Multimedia",
"approved": "Ya aprobado",
"pending": "Aprobaciones pendientes",
"tv": "Solicitudes de TV",
"movie": "Solicitudes de películas",
"total": "Total"
},
"users": {
"main": "Mejores usuarios"
}
}
}
},
"board": {
"action": {
"oldImport": {
"form": {
"apps": {
"label": "Aplicaciones"
},
"screenSize": {
"option": {
"sm": "Pequeño",
"md": "Mediano",
"lg": "Grande"
}
}
}
}
},
"field": {
"backgroundImageAttachment": {
"label": "Adjuntar imagen de fondo"
},
"backgroundImageSize": {
"label": "Tamaño de la imagen de fondo"
},
"primaryColor": {
"label": "Color primario"
},
"secondaryColor": {
"label": "Color secundario"
},
"customCss": {
"description": "Además, personaliza tu panel usando CSS, solo recomendado para usuarios avanzados"
},
"name": {
"label": "Nombre"
},
"isPublic": {
"label": "Pública"
}
},
"setting": {
"section": {
"general": {
"title": "General"
},
"layout": {
"title": "Diseño"
},
"background": {
"title": "Fondo"
},
"access": {
"permission": {
"item": {
"view": {
"label": "Ver tablero"
}
}
}
},
"dangerZone": {
"title": "Zona de riesgo",
"action": {
"delete": {
"confirm": {
"title": "Eliminar tablero"
}
}
}
}
}
}
},
"management": {
"navbar": {
"items": {
"home": "Inicio",
"boards": "Tableros",
"apps": "Aplicaciones",
"users": {
"label": "Usuarios",
"items": {
"manage": "Administrar",
"invites": "Invitaciones"
}
},
"tools": {
"label": "Herramientas",
"items": {
"docker": "Docker",
"api": "API"
}
},
"settings": "Ajustes",
"help": {
"label": "Ayuda",
"items": {
"documentation": "Documentación",
"discord": "Comunidad Discord"
}
},
"about": "Acerca de"
}
},
"page": {
"home": {
"statistic": {
"board": "Tableros",
"user": "Usuarios",
"invite": "Invitaciones",
"app": "Aplicaciones"
},
"statisticLabel": {
"boards": "Tableros"
}
},
"board": {
"title": "Tus tableros",
"action": {
"settings": {
"label": "Ajustes"
},
"setHomeBoard": {
"badge": {
"label": "Inicio"
}
},
"delete": {
"label": "Eliminar permanentemente",
"confirm": {
"title": "Eliminar tablero"
}
}
},
"modal": {
"createBoard": {
"field": {
"name": {
"label": "Nombre"
}
}
}
}
},
"user": {
"setting": {
"general": {
"title": "General",
"item": {
"firstDayOfWeek": "Primer día de la semana",
"accessibility": "Accesibilidad"
}
},
"security": {
"title": "Seguridad"
},
"board": {
"title": "Tableros"
}
},
"list": {
"metaTitle": "Administrar usuarios",
"title": "Usuarios"
},
"create": {
"metaTitle": "Crear usuario",
"step": {
"security": {
"label": "Seguridad"
}
}
},
"invite": {
"title": "Administrar invitaciones de usuario",
"action": {
"new": {
"description": "Después de la caducidad, una invitación ya no será válida y el destinatario de la invitación no podrá crear una cuenta."
},
"copy": {
"link": "Link de invitación"
},
"delete": {
"title": "Eliminar invitación",
"description": "¿Estás seguro de que deseas eliminar esta invitación? Los usuarios con este enlace ya no podrán crear una cuenta usando ese enlace."
}
},
"field": {
"id": {
"label": "ID"
},
"creator": {
"label": "Creador"
},
"expirationDate": {
"label": "Fecha de caducidad"
},
"token": {
"label": "Token"
}
}
}
},
"group": {
"setting": {
"general": {
"title": "General"
}
}
},
"settings": {
"title": "Ajustes"
},
"tool": {
"tasks": {
"status": {
"running": "En ejecución",
"error": "Error"
},
"job": {
"mediaServer": {
"label": "Servidor Multimedia"
},
"mediaRequests": {
"label": "Solicitudes multimedia"
}
}
},
"api": {
"title": "API",
"tab": {
"documentation": {
"label": "Documentación"
},
"apiKey": {
"table": {
"header": {
"id": "ID"
}
}
}
}
}
}
}
},
"docker": {
"title": "",
"field": {
"name": {
"label": "Nombre"
},
"state": {
"label": "Estado",
"option": {
"created": "Creado",
"running": "En ejecución",
"paused": "Pausado",
"restarting": "Reiniciando",
"removing": "Eliminando"
}
},
"containerImage": {
"label": "Imagen"
},
"ports": {
"label": "Puertos"
}
},
"action": {
"start": {
"label": "Iniciar"
},
"stop": {
"label": "Detener"
},
"restart": {
"label": "Reiniciar"
},
"remove": {
"label": "Eliminar"
}
}
},
"permission": {
"tab": {
"user": "Usuarios"
},
"field": {
"user": {
"label": "Usuario"
}
}
},
"navigationStructure": {
"manage": {
"label": "Administrar",
"boards": {
"label": "Tableros"
},
"integrations": {
"edit": {
"label": "Editar"
}
},
"search-engines": {
"edit": {
"label": "Editar"
}
},
"apps": {
"label": "Aplicaciones",
"edit": {
"label": "Editar"
}
},
"users": {
"label": "Usuarios",
"create": {
"label": "Crear"
},
"general": "General",
"security": "Seguridad",
"board": "Tableros",
"invites": {
"label": "Invitaciones"
}
},
"tools": {
"label": "Herramientas",
"docker": {
"label": "Docker"
}
},
"settings": {
"label": "Ajustes"
},
"about": {
"label": "Acerca de"
}
}
},
"search": {
"mode": {
"appIntegrationBoard": {
"group": {
"app": {
"title": "Aplicaciones"
},
"board": {
"title": "Tableros"
}
}
},
"external": {
"group": {
"searchEngine": {
"option": {
"torrent": {
"name": "Torrents"
}
}
}
}
},
"help": {
"group": {
"help": {
"title": "Ayuda",
"option": {
"documentation": {
"label": "Documentación"
},
"discord": {
"label": "Comunidad Discord"
}
}
}
}
},
"page": {
"group": {
"page": {
"option": {
"manageUser": {
"label": "Administrar usuarios"
},
"about": {
"label": "Acerca de"
},
"preferences": {
"label": "Tus preferencias"
}
}
}
}
},
"userGroup": {
"group": {
"user": {
"title": "Usuarios"
}
}
}
},
"engine": {
"field": {
"name": {
"label": "Nombre"
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More