diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6336813b0..a192e95c5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -31,6 +31,7 @@ body: label: Version description: What version of Homarr are you running? options: + - 1.12.0 - 1.11.0 - 1.10.0 - 1.9.0 diff --git a/.github/renovate.json5 b/.github/renovate.json5 index e7e82ba4b..377002605 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -6,11 +6,6 @@ matchPackagePatterns: ["^@homarr/"], enabled: false, }, - // 15.2.0 crashes with turbopack error (panic) - { - matchPackagePatterns: ["^next$", "^@next/eslint-plugin-next$"], - enabled: false, - }, { matchUpdateTypes: ["minor", "patch", "pin", "digest"], automerge: true, diff --git a/.run/All Tests.run.xml b/.run/All Tests.run.xml new file mode 100644 index 000000000..ad69ee63f --- /dev/null +++ b/.run/All Tests.run.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.vscode/i18n-ally-custom-framework.yml b/.vscode/i18n-ally-custom-framework.yml index 6cb944e00..1ee83bc7e 100644 --- a/.vscode/i18n-ally-custom-framework.yml +++ b/.vscode/i18n-ally-custom-framework.yml @@ -12,17 +12,17 @@ languageIds: # You should unescape RegEx strings in order to fit in the YAML file # To help with this, you can use https://www.freeformatter.com/json-escape.html usageMatchRegex: - # The following example shows how to detect `t("your.i18n.keys")` - # the `{key}` will be placed by a proper keypath matching regex, - # you can ignore it and use your own matching rules as well + # For direct t("your.i18n.keys") usage - "[^\\w\\d]t\\(['\"`]({key})['\"`]" + # For variable t assigned from getScopedI18n or useScopedI18n + - "\\bt\\(['\"`]({key})['\"`]\\)" # A RegEx to set a custom scope range. This scope will be used as a prefix when detecting keys # and works like how the i18next framework identifies the namespace scope from the # useTranslation() hook. # You should unescape RegEx strings in order to fit in the YAML file # To help with this, you can use https://www.freeformatter.com/json-escape.html -scopeRangeRegex: "(getScopedI18n|useScopedI18n)\\(\\s*['\"](.*?)['\"]\\)" +scopeRangeRegex: "(?:const|let|var)\\s+t\\s*=\\s*(?:await\\s+)?(?:getScopedI18n|useScopedI18n)\\(\\s*['\"](.*?)['\"]\\)" # An array of strings containing refactor templates. # The "$1" will be replaced by the keypath specified. diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 1fa69c38f..72afa9617 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -48,21 +48,21 @@ "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "@homarr/widgets": "workspace:^0.1.0", - "@mantine/colors-generator": "^7.17.2", - "@mantine/core": "^7.17.2", - "@mantine/dropzone": "^7.17.2", - "@mantine/hooks": "^7.17.2", - "@mantine/modals": "^7.17.2", - "@mantine/tiptap": "^7.17.2", + "@mantine/colors-generator": "^7.17.3", + "@mantine/core": "^7.17.3", + "@mantine/dropzone": "^7.17.3", + "@mantine/hooks": "^7.17.3", + "@mantine/modals": "^7.17.3", + "@mantine/tiptap": "^7.17.3", "@million/lint": "1.0.14", "@tabler/icons-react": "^3.31.0", - "@tanstack/react-query": "^5.69.0", - "@tanstack/react-query-devtools": "^5.69.0", - "@tanstack/react-query-next-experimental": "^5.69.0", - "@trpc/client": "^11.0.0", - "@trpc/next": "^11.0.0", - "@trpc/react-query": "^11.0.0", - "@trpc/server": "^11.0.0", + "@tanstack/react-query": "^5.70.0", + "@tanstack/react-query-devtools": "^5.70.0", + "@tanstack/react-query-next-experimental": "^5.70.0", + "@trpc/client": "^11.0.1", + "@trpc/next": "^11.0.1", + "@trpc/react-query": "^11.0.1", + "@trpc/server": "^11.0.1", "@xterm/addon-canvas": "^0.7.0", "@xterm/addon-fit": "0.10.0", "@xterm/xterm": "^5.5.0", @@ -74,7 +74,7 @@ "glob": "^11.0.1", "jotai": "^2.12.2", "mantine-react-table": "2.0.0-beta.9", - "next": "15.1.7", + "next": "15.2.4", "postcss-preset-mantine": "^1.17.0", "prismjs": "^1.30.0", "react": "19.0.0", @@ -83,7 +83,7 @@ "react-simple-code-editor": "^0.14.1", "sass": "^1.86.0", "superjson": "2.2.2", - "swagger-ui-react": "^5.20.1", + "swagger-ui-react": "^5.20.2", "use-deep-compare-effect": "^1.8.1", "zod": "^3.24.2" }, @@ -92,13 +92,13 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/chroma-js": "3.1.1", - "@types/node": "^22.13.11", + "@types/node": "^22.13.14", "@types/prismjs": "^1.26.5", "@types/react": "19.0.12", "@types/react-dom": "19.0.4", "@types/swagger-ui-react": "^5.18.0", "concurrently": "^9.1.2", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "node-loader": "^2.1.0", "prettier": "^3.5.3", "typescript": "^5.8.2" diff --git a/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx b/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx index e094ce4ce..2dd2df722 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/user/init-user-form.tsx @@ -4,6 +4,7 @@ import { Button, PasswordInput, Stack, TextInput } from "@mantine/core"; import type { z } from "zod"; import { clientApi } from "@homarr/api/client"; +import { signIn } from "@homarr/auth/client"; import { revalidatePathActionAsync } from "@homarr/common/client"; import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; @@ -30,6 +31,13 @@ export const InitUserForm = () => { title: tUser("notification.success.title"), message: tUser("notification.success.message"), }); + + await signIn("credentials", { + name: values.username, + password: values.password, + redirect: false, + }); + await revalidatePathActionAsync("/init"); }, onError: (error) => { diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/new/[id]/page.tsx similarity index 59% rename from apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx rename to apps/nextjs/src/app/[locale]/manage/integrations/new/[id]/page.tsx index dba8e5c1f..4622faa1e 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/new/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/new/[id]/page.tsx @@ -3,49 +3,55 @@ import { Container, Group, Stack, Title } from "@mantine/core"; import { z } from "zod"; import { auth } from "@homarr/auth/next"; -import type { IntegrationKind } from "@homarr/definitions"; import { getIntegrationName, integrationKinds } from "@homarr/definitions"; import { getScopedI18n } from "@homarr/translation/server"; import { IntegrationAvatar } from "@homarr/ui"; import type { validation } from "@homarr/validation"; import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; -import { NewIntegrationForm } from "./_integration-new-form"; +import { NewIntegrationForm } from "../_integration-new-form"; -interface NewIntegrationPageProps { - searchParams: Promise< - Partial> & { - kind: IntegrationKind; - } - >; +interface NewIntegrationByIdPageProps { + params: { + id: string; + }; + searchParams: Partial>; } -export default async function IntegrationsNewPage(props: NewIntegrationPageProps) { - const searchParams = await props.searchParams; +export function generateStaticParams() { + return integrationKinds.map((kind) => ({ + id: kind, + })); +} + +export default async function IntegrationNewByIdPage(props: NewIntegrationByIdPageProps) { + const { id } = props.params; const session = await auth(); + if (!session?.user.permissions.includes("integration-create")) { notFound(); } - const result = z.enum(integrationKinds).safeParse(searchParams.kind); + const result = z.enum(integrationKinds).safeParse(id); if (!result.success) { notFound(); } const tCreate = await getScopedI18n("integration.page.create"); - const currentKind = result.data; + const dynamicMappings = new Map([[id, getIntegrationName(currentKind)]]); + return ( <> - + {tCreate("title", { name: getIntegrationName(currentKind) })} - + diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-dropdown.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-dropdown.tsx index a697b9270..af94f6642 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-dropdown.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-dropdown.tsx @@ -39,7 +39,7 @@ export const IntegrationCreateDropdownContent = () => { {filteredKinds.length > 0 ? ( {filteredKinds.map((kind) => ( - + {getIntegrationName(kind)} diff --git a/apps/nextjs/src/app/[locale]/manage/tools/tasks/_components/jobs-list.tsx b/apps/nextjs/src/app/[locale]/manage/tools/tasks/_components/jobs-list.tsx index c1150622c..53058ad80 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/tasks/_components/jobs-list.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/tasks/_components/jobs-list.tsx @@ -66,15 +66,17 @@ export const JobsList = ({ initialJobs }: JobsListProps) => { {job.status && } - handleJobTrigger(job)} - disabled={job.status?.status === "running"} - variant={"default"} - size={"xl"} - radius={"xl"} - > - - + {!job.job.preventManualExecution && ( + handleJobTrigger(job)} + disabled={job.status?.status === "running"} + variant={"default"} + size={"xl"} + radius={"xl"} + > + + + )} ))} diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx index add232243..ee960b0f5 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx @@ -118,15 +118,13 @@ export default async function EditUserPage(props: Props) { - {isCredentialsUser && ( - - } - /> - - )} + + } + /> + ); } diff --git a/apps/nextjs/src/app/api/[...trpc]/route.ts b/apps/nextjs/src/app/api/[...trpc]/route.ts index 42a09e316..ea992afda 100644 --- a/apps/nextjs/src/app/api/[...trpc]/route.ts +++ b/apps/nextjs/src/app/api/[...trpc]/route.ts @@ -26,6 +26,9 @@ const handlerAsync = async (req: NextRequest) => { endpoint: "/", router: appRouter, createContext: () => createTRPCContext({ session, headers: req.headers }), + onError({ error, path, type }) { + logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause })); + }, }); }; diff --git a/apps/nextjs/src/app/api/trpc/[trpc]/route.ts b/apps/nextjs/src/app/api/trpc/[trpc]/route.ts index 66aa2ff71..f35afb7a3 100644 --- a/apps/nextjs/src/app/api/trpc/[trpc]/route.ts +++ b/apps/nextjs/src/app/api/trpc/[trpc]/route.ts @@ -31,9 +31,7 @@ const handler = auth(async (req) => { req, createContext: () => createTRPCContext({ session: req.auth, headers: req.headers }), onError({ error, path, type }) { - logger.error( - `tRPC Error with ${type} on '${path}': (${error.code}) - ${error.message}\n${error.stack}\n${error.cause}`, - ); + logger.error(new Error(`tRPC Error with ${type} on '${path}'`, { cause: error.cause })); }, }); diff --git a/apps/nextjs/src/components/board/items/actions/test/mocks/dynamic-section-mock.ts b/apps/nextjs/src/components/board/items/actions/test/mocks/dynamic-section-mock.ts index 7de8db63c..553deda9e 100644 --- a/apps/nextjs/src/components/board/items/actions/test/mocks/dynamic-section-mock.ts +++ b/apps/nextjs/src/components/board/items/actions/test/mocks/dynamic-section-mock.ts @@ -10,6 +10,7 @@ export class DynamicSectionMockBuilder { id: createId(), kind: "dynamic", options: { + title: "", borderColor: "", }, layouts: [], diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx index 709d52182..7ee901ac8 100644 --- a/apps/nextjs/src/components/board/items/item-content.tsx +++ b/apps/nextjs/src/components/board/items/item-content.tsx @@ -5,6 +5,8 @@ import combineClasses from "clsx"; import { NoIntegrationSelectedError } from "node_modules/@homarr/widgets/src/errors"; import { ErrorBoundary } from "react-error-boundary"; +import { useSession } from "@homarr/auth/client"; +import { isWidgetRestricted } from "@homarr/auth/shared"; import { useRequiredBoard } from "@homarr/boards/context"; import { useEditMode } from "@homarr/boards/edit-mode"; import { useSettings } from "@homarr/settings"; @@ -15,6 +17,7 @@ import type { SectionItem } from "~/app/[locale]/boards/_types"; import classes from "../sections/item.module.css"; import { useItemActions } from "./item-actions"; import { BoardItemMenu } from "./item-menu"; +import { RestrictedWidgetContent } from "./restricted"; interface BoardItemContentProps { item: SectionItem; @@ -59,6 +62,7 @@ interface InnerContentProps { const InnerContent = ({ item, ...dimensions }: InnerContentProps) => { const settings = useSettings(); const board = useRequiredBoard(); + const { data: session } = useSession(); const [isEditMode] = useEditMode(); const Comp = loadWidgetDynamic(item.kind); const { definition } = widgetImports[item.kind]; @@ -70,6 +74,16 @@ const InnerContent = ({ item, ...dimensions }: InnerContentProps) => { const widgetSupportsIntegrations = "supportedIntegrations" in definition && definition.supportedIntegrations.length >= 1; + if ( + isWidgetRestricted({ + definition, + user: session?.user ?? null, + check: (level) => level === "all", + }) + ) { + return ; + } + return ( {({ reset }) => ( diff --git a/apps/nextjs/src/components/board/items/item-menu.tsx b/apps/nextjs/src/components/board/items/item-menu.tsx index e7072fea4..771279679 100644 --- a/apps/nextjs/src/components/board/items/item-menu.tsx +++ b/apps/nextjs/src/components/board/items/item-menu.tsx @@ -3,6 +3,8 @@ import { ActionIcon, Menu } from "@mantine/core"; import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil, IconTrash } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; +import { useSession } from "@homarr/auth/client"; +import { isWidgetRestricted } from "@homarr/auth/shared"; import { useEditMode } from "@homarr/boards/edit-mode"; import { useConfirmModal, useModalAction } from "@homarr/modals"; import { useSettings } from "@homarr/settings"; @@ -37,6 +39,7 @@ export const BoardItemMenu = ({ const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]); const { gridstack } = useSectionContext().refs; const settings = useSettings(); + const { data: session } = useSession(); // Reset error boundary on next render if item has been edited useEffect(() => { @@ -91,6 +94,16 @@ export const BoardItemMenu = ({ }); }; + if ( + isWidgetRestricted({ + definition: currentDefinition, + user: session?.user ?? null, + check: (level) => level !== "none", + }) + ) { + return null; + } + return ( diff --git a/apps/nextjs/src/components/board/items/item-select-modal.tsx b/apps/nextjs/src/components/board/items/item-select-modal.tsx index a2fce1264..c93a59497 100644 --- a/apps/nextjs/src/components/board/items/item-select-modal.tsx +++ b/apps/nextjs/src/components/board/items/item-select-modal.tsx @@ -2,6 +2,8 @@ import { useMemo, useState } from "react"; import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core"; import { IconSearch } from "@tabler/icons-react"; +import { useSession } from "@homarr/auth/client"; +import { isWidgetRestricted } from "@homarr/auth/shared"; import { objectEntries } from "@homarr/common"; import type { WidgetKind } from "@homarr/definitions"; import { createModal } from "@homarr/modals"; @@ -15,10 +17,18 @@ export const ItemSelectModal = createModal(({ actions }) => { const [search, setSearch] = useState(""); const t = useI18n(); const { createItem } = useItemActions(); + const { data: session } = useSession(); const items = useMemo( () => objectEntries(widgetImports) + .filter(([, value]) => { + return !isWidgetRestricted({ + definition: value.definition, + user: session?.user ?? null, + check: (level) => level !== "none", + }); + }) .map(([kind, value]) => ({ kind, icon: value.definition.icon, @@ -26,7 +36,7 @@ export const ItemSelectModal = createModal(({ actions }) => { description: t(`widget.${kind}.description`), })) .sort((itemA, itemB) => itemA.name.localeCompare(itemB.name)), - [t], + [t, session?.user], ); const filteredItems = useMemo( diff --git a/apps/nextjs/src/components/board/items/restricted.tsx b/apps/nextjs/src/components/board/items/restricted.tsx new file mode 100644 index 000000000..494e90015 --- /dev/null +++ b/apps/nextjs/src/components/board/items/restricted.tsx @@ -0,0 +1,28 @@ +import { Center, Group, Stack, Text } from "@mantine/core"; +import { IconShield } from "@tabler/icons-react"; + +import type { WidgetKind } from "@homarr/definitions"; +import { useScopedI18n } from "@homarr/translation/client"; + +interface RestrictedWidgetProps { + kind: WidgetKind; +} + +export const RestrictedWidgetContent = ({ kind }: RestrictedWidgetProps) => { + const tCurrentWidget = useScopedI18n(`widget.${kind}`); + const tCommonWidget = useScopedI18n("widget.common"); + + return ( +
+ + + + + {tCommonWidget("restricted.title")} + + + {tCommonWidget("restricted.description", { name: tCurrentWidget("name") })} + +
+ ); +}; diff --git a/apps/nextjs/src/components/board/sections/dynamic-section.tsx b/apps/nextjs/src/components/board/sections/dynamic-section.tsx index 90525715a..3b5493366 100644 --- a/apps/nextjs/src/components/board/sections/dynamic-section.tsx +++ b/apps/nextjs/src/components/board/sections/dynamic-section.tsx @@ -1,4 +1,4 @@ -import { Box, Card } from "@mantine/core"; +import { Badge, Box, Card } from "@mantine/core"; import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context"; @@ -17,7 +17,12 @@ export const BoardDynamicSection = ({ section }: Props) => { const options = section.options; return ( - + { withBorder styles={{ root: { + overflow: "visible", "--opacity": board.opacity / 100, - overflow: "hidden", - "--border-color": options.borderColor !== "" ? options.borderColor : undefined, + "--border-color": options.borderColor || undefined, }, }} radius={board.itemRadius} p={0} > + {options.title && ( + + {options.title} + + )} {/* Use unique key by layout to reinitialize gridstack */} diff --git a/apps/nextjs/src/components/board/sections/dynamic/actions/add-dynamic-section.ts b/apps/nextjs/src/components/board/sections/dynamic/actions/add-dynamic-section.ts index 415d80572..c33507a73 100644 --- a/apps/nextjs/src/components/board/sections/dynamic/actions/add-dynamic-section.ts +++ b/apps/nextjs/src/components/board/sections/dynamic/actions/add-dynamic-section.ts @@ -17,6 +17,7 @@ export const addDynamicSectionCallback = () => (board: Board) => { id: createId(), kind: "dynamic", options: { + title: "", borderColor: "", }, layouts: createDynamicSectionLayouts(board, firstSection), diff --git a/apps/nextjs/src/components/board/sections/dynamic/dynamic-edit-modal.tsx b/apps/nextjs/src/components/board/sections/dynamic/dynamic-edit-modal.tsx index 001af145a..85ffc7e79 100644 --- a/apps/nextjs/src/components/board/sections/dynamic/dynamic-edit-modal.tsx +++ b/apps/nextjs/src/components/board/sections/dynamic/dynamic-edit-modal.tsx @@ -1,6 +1,6 @@ "use client"; -import { Button, CloseButton, ColorInput, Group, Stack, useMantineTheme } from "@mantine/core"; +import { Button, CloseButton, ColorInput, Group, Stack, TextInput, useMantineTheme } from "@mantine/core"; import type { z } from "zod"; import { useZodForm } from "@homarr/form"; @@ -30,6 +30,7 @@ export const DynamicSectionEditModal = createModal(({ actions, inner })} > + { diff --git a/apps/websocket/package.json b/apps/websocket/package.json index fa870e18a..2619df887 100644 --- a/apps/websocket/package.json +++ b/apps/websocket/package.json @@ -34,7 +34,7 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/ws": "^8.18.0", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "prettier": "^3.5.3", "typescript": "^5.8.2" } diff --git a/package.json b/package.json index daad56efa..1c2415a97 100644 --- a/package.json +++ b/package.json @@ -47,13 +47,13 @@ "jsdom": "^26.0.0", "prettier": "^3.5.3", "semantic-release": "^24.2.3", - "testcontainers": "^10.21.0", + "testcontainers": "^10.23.0", "turbo": "^2.4.4", "typescript": "^5.8.2", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.9" }, - "packageManager": "pnpm@10.6.5", + "packageManager": "pnpm@10.7.0", "engines": { "node": ">=22.14.0" }, @@ -69,13 +69,13 @@ "tree-sitter", "tree-sitter-json" ], - "allowNonAppliedPatches": true, "overrides": { - "proxmox-api>undici": "7.5.0" + "proxmox-api>undici": "7.6.0" }, "patchedDependencies": { "pretty-print-error": "patches/pretty-print-error.patch" }, + "allowUnusedPatches": true, "ignoredBuiltDependencies": [ "@scarf/scarf", "core-js-pure", diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 57a2d7689..7b805f092 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/api/package.json b/packages/api/package.json index 9e524a3b6..bee69ebbe 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -41,11 +41,11 @@ "@homarr/server-settings": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", "@kubernetes/client-node": "^1.1.0", - "@trpc/client": "^11.0.0", - "@trpc/react-query": "^11.0.0", - "@trpc/server": "^11.0.0", + "@trpc/client": "^11.0.1", + "@trpc/react-query": "^11.0.1", + "@trpc/server": "^11.0.1", "lodash.clonedeep": "^4.5.0", - "next": "15.1.7", + "next": "15.2.4", "pretty-print-error": "^1.1.2", "react": "19.0.0", "react-dom": "19.0.0", @@ -57,7 +57,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "prettier": "^3.5.3", "typescript": "^5.8.2" } diff --git a/packages/api/src/router/board.ts b/packages/api/src/router/board.ts index ffdff8228..4d5233b17 100644 --- a/packages/api/src/router/board.ts +++ b/packages/api/src/router/board.ts @@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server"; import superjson from "superjson"; import { z } from "zod"; -import { constructBoardPermissions } from "@homarr/auth/shared"; +import { constructBoardPermissions, isWidgetRestricted } from "@homarr/auth/shared"; import type { DeviceType } from "@homarr/common/server"; import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db"; import { and, asc, createId, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db"; @@ -40,6 +40,7 @@ import { oldmarrConfigSchema } from "@homarr/old-schema"; import type { BoardItemAdvancedOptions } from "@homarr/validation"; import { sectionSchema, sharedItemSchema, validation, zodUnionFromArray } from "@homarr/validation"; +import { widgetImports } from "../../../widgets/src"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc"; import { throwIfActionForbiddenAsync } from "./board/board-access"; import { generateResponsiveGridFor } from "./board/grid-algorithm"; @@ -251,6 +252,13 @@ export const boardRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { const boardId = createId(); + const user = await ctx.db.query.users.findFirst({ + where: eq(users.id, ctx.session.user.id), + columns: { + homeBoardId: true, + }, + }); + const createBoardCollection = createDbInsertCollectionWithoutTransaction(["boards", "sections", "layouts"]); createBoardCollection.boards.push({ @@ -275,6 +283,12 @@ export const boardRouter = createTRPCRouter({ }); await createBoardCollection.insertAllAsync(ctx.db); + + if (!user?.homeBoardId) { + await ctx.db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, ctx.session.user.id)); + } + + return { boardId }; }), duplicateBoard: permissionRequiredProcedure .requiresPermission("board-create") @@ -310,6 +324,13 @@ export const boardRouter = createTRPCRouter({ } const { sections: boardSections, items: boardItems, layouts: boardLayouts, ...boardProps } = board; + const allowedBoardItems = boardItems.filter((item) => { + return !isWidgetRestricted({ + definition: widgetImports[item.kind].definition, + user: ctx.session.user, + check: (level) => level !== "none", + }); + }); const newBoardId = createId(); @@ -357,8 +378,8 @@ export const boardRouter = createTRPCRouter({ ), ); - const itemMap = new Map(boardItems.map((item) => [item.id, createId()])); - const itemsToInsert: InferInsertModel[] = boardItems.map( + const itemMap = new Map(allowedBoardItems.map((item) => [item.id, createId()])); + const itemsToInsert: InferInsertModel[] = allowedBoardItems.map( ({ integrations: _, layouts: _layouts, ...item }) => ({ ...item, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -367,7 +388,7 @@ export const boardRouter = createTRPCRouter({ }), ); - const itemLayoutsToInsert: InferInsertModel[] = boardItems.flatMap((item) => + const itemLayoutsToInsert: InferInsertModel[] = allowedBoardItems.flatMap((item) => item.layouts.map( (layoutSection): InferInsertModel => ({ ...layoutSection, @@ -400,7 +421,7 @@ export const boardRouter = createTRPCRouter({ ) .then((result) => result.map((row) => row.id)); - const itemIntegrationsToInsert = boardItems.flatMap((item) => + const itemIntegrationsToInsert = allowedBoardItems.flatMap((item) => item.integrations // Restrict integrations to only those the user has access to .filter(({ integrationId }) => integrationIdsWithAccess.includes(integrationId) || hasAccessForAll) @@ -730,105 +751,140 @@ export const boardRouter = createTRPCRouter({ const dbBoard = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id); + const addedSections = filterAddedItems(input.sections, dbBoard.sections); + const sectionsToInsert = addedSections.map( + (section): InferInsertModel => ({ + id: section.id, + kind: section.kind, + yOffset: section.kind !== "dynamic" ? section.yOffset : null, + xOffset: section.kind === "dynamic" ? null : 0, + options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON, + name: "name" in section ? section.name : null, + boardId: dbBoard.id, + }), + ); + + const sectionLayoutsToInsert = addedSections + .filter((section) => section.kind === "dynamic") + .flatMap((section) => + section.layouts.map( + (sectionLayout): InferInsertModel => ({ + layoutId: sectionLayout.layoutId, + sectionId: section.id, + parentSectionId: sectionLayout.parentSectionId, + height: sectionLayout.height, + width: sectionLayout.width, + xOffset: sectionLayout.xOffset, + yOffset: sectionLayout.yOffset, + }), + ), + ); + + const addedItems = filterAddedItems(input.items, dbBoard.items).filter((item) => { + return !isWidgetRestricted({ + definition: widgetImports[item.kind].definition, + user: ctx.session.user, + check: (level) => level !== "none", + }); + }); + const itemsToInsert = addedItems.map( + (item): InferInsertModel => ({ + id: item.id, + kind: item.kind, + options: superjson.stringify(item.options), + advancedOptions: superjson.stringify(item.advancedOptions), + boardId: dbBoard.id, + }), + ); + + const itemLayoutsToInsert = addedItems.flatMap((item) => + item.layouts.map( + (layoutSection): InferInsertModel => ({ + layoutId: layoutSection.layoutId, + sectionId: layoutSection.sectionId, + itemId: item.id, + height: layoutSection.height, + width: layoutSection.width, + xOffset: layoutSection.xOffset, + yOffset: layoutSection.yOffset, + }), + ), + ); + + const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) => + integrationIds.map((integrationId) => ({ + integrationId, + itemId, + })), + ); + const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) => + integrationIds.map((integrationId) => ({ + integrationId, + itemId, + })), + ); + const addedIntegrationRelations = inputIntegrationRelations.filter( + (inputRelation) => + !dbIntegrationRelations.some( + (dbRelation) => + dbRelation.itemId === inputRelation.itemId && dbRelation.integrationId === inputRelation.integrationId, + ), + ); + const integrationItemsToInsert = addedIntegrationRelations.map((relation) => ({ + itemId: relation.itemId, + integrationId: relation.integrationId, + })); + + const updatedItems = filterUpdatedItems(input.items, dbBoard.items).filter((item) => { + return !isWidgetRestricted({ + definition: widgetImports[item.kind].definition, + user: ctx.session.user, + check: (level) => level !== "none", + }); + }); + const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections); + + const removedIntegrationRelations = dbIntegrationRelations.filter( + (dbRelation) => + !inputIntegrationRelations.some( + (inputRelation) => + dbRelation.itemId === inputRelation.itemId && dbRelation.integrationId === inputRelation.integrationId, + ), + ); + + const removedItems = filterRemovedItems(input.items, dbBoard.items).filter((item) => { + return !isWidgetRestricted({ + definition: widgetImports[item.kind].definition, + user: ctx.session.user, + check: (level) => level !== "none", + }); + }); + const itemIdsToRemove = removedItems.map((item) => item.id); + + const removedSections = filterRemovedItems(input.sections, dbBoard.sections); + const sectionIdsToRemove = removedSections.map((section) => section.id); + await handleTransactionsAsync(ctx.db, { async handleAsync(db, schema) { await db.transaction(async (transaction) => { - const addedSections = filterAddedItems(input.sections, dbBoard.sections); - - if (addedSections.length > 0) { - await transaction.insert(schema.sections).values( - addedSections.map((section) => ({ - id: section.id, - kind: section.kind, - yOffset: section.kind !== "dynamic" ? section.yOffset : null, - xOffset: section.kind === "dynamic" ? null : 0, - options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON, - name: "name" in section ? section.name : null, - boardId: dbBoard.id, - })), - ); - - if (addedSections.some((section) => section.kind === "dynamic")) { - await transaction.insert(schema.sectionLayouts).values( - addedSections - .filter((section) => section.kind === "dynamic") - .flatMap((section) => - section.layouts.map( - (sectionLayout): InferInsertModel => ({ - layoutId: sectionLayout.layoutId, - sectionId: section.id, - parentSectionId: sectionLayout.parentSectionId, - height: sectionLayout.height, - width: sectionLayout.width, - xOffset: sectionLayout.xOffset, - yOffset: sectionLayout.yOffset, - }), - ), - ), - ); - } + if (sectionsToInsert.length > 0) { + await transaction.insert(schema.sections).values(sectionsToInsert); } - const addedItems = filterAddedItems(input.items, dbBoard.items); - - if (addedItems.length > 0) { - await transaction.insert(schema.items).values( - addedItems.map((item) => ({ - id: item.id, - kind: item.kind, - options: superjson.stringify(item.options), - advancedOptions: superjson.stringify(item.advancedOptions), - boardId: dbBoard.id, - })), - ); - await transaction.insert(schema.itemLayouts).values( - addedItems.flatMap((item) => - item.layouts.map( - (layoutSection): InferInsertModel => ({ - layoutId: layoutSection.layoutId, - sectionId: layoutSection.sectionId, - itemId: item.id, - height: layoutSection.height, - width: layoutSection.width, - xOffset: layoutSection.xOffset, - yOffset: layoutSection.yOffset, - }), - ), - ), - ); + if (sectionLayoutsToInsert.length > 0) { + await transaction.insert(schema.sectionLayouts).values(sectionLayoutsToInsert); } - const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) => - integrationIds.map((integrationId) => ({ - integrationId, - itemId, - })), - ); - const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) => - integrationIds.map((integrationId) => ({ - integrationId, - itemId, - })), - ); - const addedIntegrationRelations = inputIntegrationRelations.filter( - (inputRelation) => - !dbIntegrationRelations.some( - (dbRelation) => - dbRelation.itemId === inputRelation.itemId && - dbRelation.integrationId === inputRelation.integrationId, - ), - ); - - if (addedIntegrationRelations.length > 0) { - await transaction.insert(schema.integrationItems).values( - addedIntegrationRelations.map((relation) => ({ - itemId: relation.itemId, - integrationId: relation.integrationId, - })), - ); + if (itemsToInsert.length > 0) { + await transaction.insert(schema.items).values(itemsToInsert); + } + if (itemLayoutsToInsert.length > 0) { + await transaction.insert(schema.itemLayouts).values(itemLayoutsToInsert); } - const updatedItems = filterUpdatedItems(input.items, dbBoard.items); + if (integrationItemsToInsert.length > 0) { + await transaction.insert(schema.integrationItems).values(integrationItemsToInsert); + } for (const item of updatedItems) { await transaction @@ -859,8 +915,6 @@ export const boardRouter = createTRPCRouter({ } } - const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections); - for (const section of updatedSections) { const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id); await transaction @@ -894,15 +948,6 @@ export const boardRouter = createTRPCRouter({ } } - const removedIntegrationRelations = dbIntegrationRelations.filter( - (dbRelation) => - !inputIntegrationRelations.some( - (inputRelation) => - dbRelation.itemId === inputRelation.itemId && - dbRelation.integrationId === inputRelation.integrationId, - ), - ); - for (const relation of removedIntegrationRelations) { await transaction .delete(schema.integrationItems) @@ -914,134 +959,36 @@ export const boardRouter = createTRPCRouter({ ); } - const removedItems = filterRemovedItems(input.items, dbBoard.items); - - const itemIds = removedItems.map((item) => item.id); - if (itemIds.length > 0) { - await transaction.delete(schema.items).where(inArray(schema.items.id, itemIds)); + if (itemIdsToRemove.length > 0) { + await transaction.delete(schema.items).where(inArray(schema.items.id, itemIdsToRemove)); } - const removedSections = filterRemovedItems(input.sections, dbBoard.sections); - const sectionIds = removedSections.map((section) => section.id); - - if (sectionIds.length > 0) { - await transaction.delete(schema.sections).where(inArray(schema.sections.id, sectionIds)); + if (sectionIdsToRemove.length > 0) { + await transaction.delete(schema.sections).where(inArray(schema.sections.id, sectionIdsToRemove)); } }); }, handleSync(db) { db.transaction((transaction) => { - const addedSections = filterAddedItems(input.sections, dbBoard.sections); - - if (addedSections.length > 0) { - transaction - .insert(sections) - .values( - addedSections.map((section) => ({ - id: section.id, - kind: section.kind, - yOffset: section.kind !== "dynamic" ? section.yOffset : null, - xOffset: section.kind === "dynamic" ? null : 0, - options: section.kind === "dynamic" ? superjson.stringify(section.options) : emptySuperJSON, - name: "name" in section ? section.name : null, - boardId: dbBoard.id, - })), - ) - .run(); - - if (addedSections.some((section) => section.kind === "dynamic")) { - transaction - .insert(sectionLayouts) - .values( - addedSections - .filter((section) => section.kind === "dynamic") - .flatMap((section) => - section.layouts.map( - (sectionLayout): InferInsertModel => ({ - layoutId: sectionLayout.layoutId, - sectionId: section.id, - parentSectionId: sectionLayout.parentSectionId, - height: sectionLayout.height, - width: sectionLayout.width, - xOffset: sectionLayout.xOffset, - yOffset: sectionLayout.yOffset, - }), - ), - ), - ) - .run(); - } + if (sectionsToInsert.length > 0) { + transaction.insert(sections).values(sectionsToInsert).run(); } - const addedItems = filterAddedItems(input.items, dbBoard.items); - - if (addedItems.length > 0) { - transaction - .insert(items) - .values( - addedItems.map((item) => ({ - id: item.id, - kind: item.kind, - options: superjson.stringify(item.options), - advancedOptions: superjson.stringify(item.advancedOptions), - boardId: dbBoard.id, - })), - ) - .run(); - transaction - .insert(itemLayouts) - .values( - addedItems.flatMap((item) => - item.layouts.map( - (layoutSection): InferInsertModel => ({ - layoutId: layoutSection.layoutId, - sectionId: layoutSection.sectionId, - itemId: item.id, - height: layoutSection.height, - width: layoutSection.width, - xOffset: layoutSection.xOffset, - yOffset: layoutSection.yOffset, - }), - ), - ), - ) - .run(); + if (sectionLayoutsToInsert.length > 0) { + transaction.insert(sectionLayouts).values(sectionLayoutsToInsert).run(); } - const inputIntegrationRelations = input.items.flatMap(({ integrationIds, id: itemId }) => - integrationIds.map((integrationId) => ({ - integrationId, - itemId, - })), - ); - const dbIntegrationRelations = dbBoard.items.flatMap(({ integrationIds, id: itemId }) => - integrationIds.map((integrationId) => ({ - integrationId, - itemId, - })), - ); - const addedIntegrationRelations = inputIntegrationRelations.filter( - (inputRelation) => - !dbIntegrationRelations.some( - (dbRelation) => - dbRelation.itemId === inputRelation.itemId && - dbRelation.integrationId === inputRelation.integrationId, - ), - ); - - if (addedIntegrationRelations.length > 0) { - transaction - .insert(integrationItems) - .values( - addedIntegrationRelations.map((relation) => ({ - itemId: relation.itemId, - integrationId: relation.integrationId, - })), - ) - .run(); + if (itemsToInsert.length > 0) { + transaction.insert(items).values(itemsToInsert).run(); } - const updatedItems = filterUpdatedItems(input.items, dbBoard.items); + if (itemLayoutsToInsert.length > 0) { + transaction.insert(itemLayouts).values(itemLayoutsToInsert).run(); + } + + if (integrationItemsToInsert.length > 0) { + transaction.insert(integrationItems).values(integrationItemsToInsert).run(); + } for (const item of updatedItems) { transaction @@ -1069,8 +1016,6 @@ export const boardRouter = createTRPCRouter({ } } - const updatedSections = filterUpdatedItems(input.sections, dbBoard.sections); - for (const section of updatedSections) { const prev = dbBoard.sections.find((dbSection) => dbSection.id === section.id); transaction @@ -1103,15 +1048,6 @@ export const boardRouter = createTRPCRouter({ } } - const removedIntegrationRelations = dbIntegrationRelations.filter( - (dbRelation) => - !inputIntegrationRelations.some( - (inputRelation) => - dbRelation.itemId === inputRelation.itemId && - dbRelation.integrationId === inputRelation.integrationId, - ), - ); - for (const relation of removedIntegrationRelations) { transaction .delete(integrationItems) @@ -1124,18 +1060,12 @@ export const boardRouter = createTRPCRouter({ .run(); } - const removedItems = filterRemovedItems(input.items, dbBoard.items); - - const itemIds = removedItems.map((item) => item.id); - if (itemIds.length > 0) { - transaction.delete(items).where(inArray(items.id, itemIds)).run(); + if (itemIdsToRemove.length > 0) { + transaction.delete(items).where(inArray(items.id, itemIdsToRemove)).run(); } - const removedSections = filterRemovedItems(input.sections, dbBoard.sections); - const sectionIds = removedSections.map((section) => section.id); - - if (sectionIds.length > 0) { - transaction.delete(sections).where(inArray(sections.id, sectionIds)).run(); + if (sectionIdsToRemove.length > 0) { + transaction.delete(sections).where(inArray(sections.id, sectionIdsToRemove)).run(); } }); }, @@ -1305,7 +1235,7 @@ export const boardRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const content = await input.file.text(); const oldmarr = oldmarrConfigSchema.parse(JSON.parse(content)); - await importOldmarrAsync(ctx.db, oldmarr, input.configuration); + await importOldmarrAsync(ctx.db, oldmarr, input.configuration, ctx.session); }), }); diff --git a/packages/api/src/router/cron-jobs.ts b/packages/api/src/router/cron-jobs.ts index e8c582f24..a4aa4bbba 100644 --- a/packages/api/src/router/cron-jobs.ts +++ b/packages/api/src/router/cron-jobs.ts @@ -1,9 +1,9 @@ import { observable } from "@trpc/server/observable"; -import { jobNameSchema, triggerCronJobAsync } from "@homarr/cron-job-runner"; +import { objectEntries } from "@homarr/common"; +import { cronJobNames, cronJobs, jobNameSchema, triggerCronJobAsync } from "@homarr/cron-job-runner"; import type { TaskStatus } from "@homarr/cron-job-status"; import { createCronJobStatusChannel } from "@homarr/cron-job-status"; -import { jobGroup } from "@homarr/cron-jobs"; import { logger } from "@homarr/log"; import { createTRPCRouter, permissionRequiredProcedure } from "../trpc"; @@ -16,18 +16,17 @@ export const cronJobsRouter = createTRPCRouter({ await triggerCronJobAsync(input); }), getJobs: permissionRequiredProcedure.requiresPermission("admin").query(() => { - const registry = jobGroup.getJobRegistry(); - return [...registry.values()].map((job) => ({ - name: job.name, - expression: job.cronExpression, + return objectEntries(cronJobs).map(([name, options]) => ({ + name, + preventManualExecution: options.preventManualExecution, })); }), subscribeToStatusUpdates: permissionRequiredProcedure.requiresPermission("admin").subscription(() => { return observable((emit) => { const unsubscribes: (() => void)[] = []; - for (const job of jobGroup.getJobRegistry().values()) { - const channel = createCronJobStatusChannel(job.name); + for (const name of cronJobNames) { + const channel = createCronJobStatusChannel(name); const unsubscribe = channel.subscribe((data) => { emit.next(data); }); diff --git a/packages/api/src/router/import/import-router.ts b/packages/api/src/router/import/import-router.ts index 4ffc5ca1a..f46cd4009 100644 --- a/packages/api/src/router/import/import-router.ts +++ b/packages/api/src/router/import/import-router.ts @@ -37,7 +37,7 @@ export const importRouter = createTRPCRouter({ .requiresStep("import") .input(importInitialOldmarrInputSchema) .mutation(async ({ ctx, input }) => { - await importInitialOldmarrAsync(ctx.db, input); + await importInitialOldmarrAsync(ctx.db, input, ctx.session); await nextOnboardingStepAsync(ctx.db, undefined); }), }); diff --git a/packages/api/src/router/integration/integration-test-connection.ts b/packages/api/src/router/integration/integration-test-connection.ts index 96ae9f4f4..cd82b8cfa 100644 --- a/packages/api/src/router/integration/integration-test-connection.ts +++ b/packages/api/src/router/integration/integration-test-connection.ts @@ -1,5 +1,3 @@ -import { formatError } from "pretty-print-error"; - import { decryptSecret } from "@homarr/common/server"; import type { Integration } from "@homarr/db/schema"; import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions"; @@ -41,7 +39,10 @@ export const testConnectionAsync = async ( }; } catch (error) { logger.warn( - `Failed to decrypt secret from database integration="${integration.name}" secretKind="${secret.kind}"\n${formatError(error)}`, + new Error( + `Failed to decrypt secret from database integration="${integration.name}" secretKind="${secret.kind}"`, + { cause: error }, + ), ); return null; } diff --git a/packages/api/src/router/kubernetes/kubernetes-client.ts b/packages/api/src/router/kubernetes/kubernetes-client.ts index c0cb907aa..29714b358 100644 --- a/packages/api/src/router/kubernetes/kubernetes-client.ts +++ b/packages/api/src/router/kubernetes/kubernetes-client.ts @@ -65,9 +65,7 @@ export class KubernetesClient { } public static getInstance(): KubernetesClient { - if (!KubernetesClient.instance) { - KubernetesClient.instance = new KubernetesClient(); - } + KubernetesClient.instance ??= new KubernetesClient(); return KubernetesClient.instance; } } diff --git a/packages/api/src/router/update-checker.ts b/packages/api/src/router/update-checker.ts index d02744f92..6920358ce 100644 --- a/packages/api/src/router/update-checker.ts +++ b/packages/api/src/router/update-checker.ts @@ -1,5 +1,3 @@ -import { formatError } from "pretty-print-error"; - import { logger } from "@homarr/log"; import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker"; @@ -12,7 +10,7 @@ export const updateCheckerRouter = createTRPCRouter({ const data = await handler.getCachedOrUpdatedDataAsync({}); return data.data.availableUpdates; } catch (error) { - logger.error(`Failed to get available updates\n${formatError(error)}`); + logger.error(new Error("Failed to get available updates", { cause: error })); return undefined; // We return undefined to not show the indicator in the UI } }), diff --git a/packages/auth/configuration.ts b/packages/auth/configuration.ts index a1bf9b665..177e7e8fb 100644 --- a/packages/auth/configuration.ts +++ b/packages/auth/configuration.ts @@ -2,7 +2,6 @@ import type { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapte import { cookies } from "next/headers"; import NextAuth from "next-auth"; import Credentials from "next-auth/providers/credentials"; -import { formatError } from "pretty-print-error"; import { db } from "@homarr/db"; import type { SupportedAuthProvider } from "@homarr/definitions"; @@ -36,8 +35,7 @@ export const createConfiguration = ( return; } - logger.error(formatError(error)); - logger.error(formatError(error.cause)); + logger.error(error); }, }, trustHost: true, diff --git a/packages/auth/env.ts b/packages/auth/env.ts index 12de5f558..e58aa9673 100644 --- a/packages/auth/env.ts +++ b/packages/auth/env.ts @@ -39,6 +39,7 @@ export const env = createEnv({ AUTH_OIDC_SCOPE_OVERWRITE: z.string().min(1).default("openid email profile groups"), AUTH_OIDC_GROUPS_ATTRIBUTE: z.string().default("groups"), // Is used in the signIn event to assign the correct groups, key is from object of decoded id_token AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE: z.string().optional(), + AUTH_OIDC_FORCE_USERINFO: createBooleanSchema(false), } : {}), ...(authProviders.includes("ldap") diff --git a/packages/auth/package.json b/packages/auth/package.json index 02bb7a58c..a99491af5 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -34,8 +34,8 @@ "@homarr/validation": "workspace:^0.1.0", "bcrypt": "^5.1.1", "cookies": "^0.9.1", - "ldapts": "7.3.1", - "next": "15.1.7", + "ldapts": "7.3.3", + "next": "15.2.4", "next-auth": "5.0.0-beta.25", "pretty-print-error": "^1.1.2", "react": "19.0.0", @@ -48,7 +48,7 @@ "@homarr/tsconfig": "workspace:^0.1.0", "@types/bcrypt": "5.0.2", "@types/cookies": "0.9.0", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "prettier": "^3.5.3", "typescript": "^5.8.2" } diff --git a/packages/auth/permissions/index.ts b/packages/auth/permissions/index.ts index dd5b9e462..83b13629a 100644 --- a/packages/auth/permissions/index.ts +++ b/packages/auth/permissions/index.ts @@ -1,2 +1,3 @@ export * from "./board-permissions"; export * from "./integration-permissions"; +export * from "./widget-restriction"; diff --git a/packages/auth/permissions/widget-restriction.ts b/packages/auth/permissions/widget-restriction.ts new file mode 100644 index 000000000..1959ffd0e --- /dev/null +++ b/packages/auth/permissions/widget-restriction.ts @@ -0,0 +1,14 @@ +import type { Session } from "next-auth"; + +import type { WidgetDefinition } from "../../widgets/src"; +import type { RestrictionLevel } from "../../widgets/src/definition"; + +export const isWidgetRestricted = (props: { + definition: TDefinition; + user: Session["user"] | null; + check: (level: RestrictionLevel) => boolean; +}) => { + if (!("restrict" in props.definition)) return false; + if (props.definition.restrict === undefined) return false; + return props.check(props.definition.restrict({ user: props.user ?? null })); +}; diff --git a/packages/auth/providers/oidc/oidc-provider.ts b/packages/auth/providers/oidc/oidc-provider.ts index d03d9c736..bd72f9359 100644 --- a/packages/auth/providers/oidc/oidc-provider.ts +++ b/packages/auth/providers/oidc/oidc-provider.ts @@ -22,6 +22,31 @@ export const OidcProvider = (headers: ReadonlyHeaders | null): OIDCConfig { + if (response.status === 401) return response; + + const newHeaders = Array.from(response.headers.entries()) + .filter(([key]) => key.toLowerCase() !== "www-authenticate") + .reduce((headers, [key, value]) => { + headers.append(key, value); + return headers; + }, new Headers()); + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: newHeaders, + }); + }, + }, + // idToken false forces the use of the userinfo endpoint + // Userinfo endpoint is required for authelia since v4.39 + // See https://github.com/homarr-labs/homarr/issues/2635 + idToken: !env.AUTH_OIDC_FORCE_USERINFO, profile(profile) { if (!profile.sub) { throw new Error(`OIDC provider did not return a sub property='${Object.keys(profile).join(",")}'`); diff --git a/packages/boards/package.json b/packages/boards/package.json index 7766e0295..944aeba51 100644 --- a/packages/boards/package.json +++ b/packages/boards/package.json @@ -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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/certificates/package.json b/packages/certificates/package.json index 6b3fae919..5dcf67224 100644 --- a/packages/certificates/package.json +++ b/packages/certificates/package.json @@ -23,13 +23,13 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^0.1.0", - "undici": "7.5.0" + "undici": "7.6.0" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/cli/package.json b/packages/cli/package.json index c1ec8720d..a666d374d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/common/package.json b/packages/common/package.json index dd2b1e377..11f02a02d 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -30,17 +30,17 @@ "@homarr/env": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0", "dayjs": "^1.11.13", - "next": "15.1.7", + "next": "15.2.4", "react": "19.0.0", "react-dom": "19.0.0", - "undici": "7.5.0", + "undici": "7.6.0", "zod": "^3.24.2" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/common/src/url.ts b/packages/common/src/url.ts index 9e974a73e..8efbb968c 100644 --- a/packages/common/src/url.ts +++ b/packages/common/src/url.ts @@ -8,12 +8,9 @@ export const extractBaseUrlFromHeaders = ( headers: ReadonlyHeaders, fallbackProtocol: "http" | "https" = "http", ): `${string}://${string}` => { - let protocol = headers.get("x-forwarded-proto"); - - // If the protocol is not set or an empty string - if (!protocol) { - protocol = fallbackProtocol; - } + // For empty string we also use the fallback protocol + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + let protocol = headers.get("x-forwarded-proto") || fallbackProtocol; // @see https://support.glitch.com/t/x-forwarded-proto-contains-multiple-protocols/17219 if (protocol.includes(",")) { diff --git a/packages/cron-job-runner/package.json b/packages/cron-job-runner/package.json index b0dd2ab14..4fb511f28 100644 --- a/packages/cron-job-runner/package.json +++ b/packages/cron-job-runner/package.json @@ -5,7 +5,8 @@ "license": "Apache-2.0", "type": "module", "exports": { - ".": "./index.ts" + ".": "./index.ts", + "./register": "./src/register.ts" }, "typesVersions": { "*": { @@ -22,15 +23,17 @@ }, "prettier": "@homarr/prettier-config", "dependencies": { + "@homarr/common": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0", - "@homarr/redis": "workspace:^0.1.0" + "@homarr/redis": "workspace:^0.1.0", + "@homarr/validation": "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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/cron-job-runner/src/index.ts b/packages/cron-job-runner/src/index.ts index caf0c185b..9042241c3 100644 --- a/packages/cron-job-runner/src/index.ts +++ b/packages/cron-job-runner/src/index.ts @@ -1,19 +1,29 @@ +import { objectKeys } from "@homarr/common"; import type { JobGroupKeys } from "@homarr/cron-jobs"; -import { jobGroup } from "@homarr/cron-jobs"; +import { createSubPubChannel } from "@homarr/redis"; +import { zodEnumFromArray } from "@homarr/validation"; -import { createSubPubChannel } from "../../redis/src/lib/channel"; -import { zodEnumFromArray } from "../../validation/src/enums"; +export const cronJobRunnerChannel = createSubPubChannel("cron-job-runner", { persist: false }); -const cronJobRunnerChannel = createSubPubChannel("cron-job-runner", { persist: false }); - -/** - * Registers the cron job runner to listen to the Redis PubSub channel. - */ -export const registerCronJobRunner = () => { - cronJobRunnerChannel.subscribe((jobName) => { - jobGroup.runManually(jobName); - }); -}; +export const cronJobs = { + analytics: { preventManualExecution: true }, + iconsUpdater: { preventManualExecution: false }, + ping: { preventManualExecution: false }, + smartHomeEntityState: { preventManualExecution: false }, + mediaServer: { preventManualExecution: false }, + mediaOrganizer: { preventManualExecution: false }, + downloads: { preventManualExecution: false }, + dnsHole: { preventManualExecution: false }, + mediaRequestStats: { preventManualExecution: false }, + mediaRequestList: { preventManualExecution: false }, + rssFeeds: { preventManualExecution: false }, + indexerManager: { preventManualExecution: false }, + healthMonitoring: { preventManualExecution: false }, + sessionCleanup: { preventManualExecution: false }, + updateChecker: { preventManualExecution: false }, + mediaTranscoding: { preventManualExecution: false }, + minecraftServerStatus: { preventManualExecution: false }, +} satisfies Record; /** * Triggers a cron job to run immediately. @@ -21,7 +31,12 @@ export const registerCronJobRunner = () => { * @param jobName name of the job to be triggered */ export const triggerCronJobAsync = async (jobName: JobGroupKeys) => { + if (cronJobs[jobName].preventManualExecution) { + throw new Error(`The job "${jobName}" can not be executed manually`); + } await cronJobRunnerChannel.publishAsync(jobName); }; -export const jobNameSchema = zodEnumFromArray(jobGroup.getKeys()); +export const cronJobNames = objectKeys(cronJobs); + +export const jobNameSchema = zodEnumFromArray(cronJobNames); diff --git a/packages/cron-job-runner/src/register.ts b/packages/cron-job-runner/src/register.ts new file mode 100644 index 000000000..a58eb72a0 --- /dev/null +++ b/packages/cron-job-runner/src/register.ts @@ -0,0 +1,12 @@ +import { jobGroup } from "@homarr/cron-jobs"; + +import { cronJobRunnerChannel } from "."; + +/** + * Registers the cron job runner to listen to the Redis PubSub channel. + */ +export const registerCronJobRunner = () => { + cronJobRunnerChannel.subscribe((jobName) => { + jobGroup.runManually(jobName); + }); +}; diff --git a/packages/cron-job-status/package.json b/packages/cron-job-status/package.json index 87e6a80ad..95ff056e3 100644 --- a/packages/cron-job-status/package.json +++ b/packages/cron-job-status/package.json @@ -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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/cron-jobs-core/package.json b/packages/cron-jobs-core/package.json index 9bb87eb63..bf0e99913 100644 --- a/packages/cron-jobs-core/package.json +++ b/packages/cron-jobs-core/package.json @@ -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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/cron-jobs/package.json b/packages/cron-jobs/package.json index 0e9abe18f..aa596906d 100644 --- a/packages/cron-jobs/package.json +++ b/packages/cron-jobs/package.json @@ -44,7 +44,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/db/migrations/seed.ts b/packages/db/migrations/seed.ts index fa86e77a3..cb4fac094 100644 --- a/packages/db/migrations/seed.ts +++ b/packages/db/migrations/seed.ts @@ -1,18 +1,22 @@ -import SuperJSON from "superjson"; - import { objectKeys } from "@homarr/common"; -import { everyoneGroup } from "@homarr/definitions"; +import { createDocumentationLink, everyoneGroup } from "@homarr/definitions"; import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings"; -import { createId, eq } from ".."; import type { Database } from ".."; -import { onboarding, serverSettings } from "../schema"; +import { createId, eq } from ".."; +import { + getServerSettingByKeyAsync, + insertServerSettingByKeyAsync, + updateServerSettingByKeyAsync, +} from "../queries/server-setting"; +import { onboarding, searchEngines } from "../schema"; import { groups } from "../schema/mysql"; export const seedDataAsync = async (db: Database) => { await seedEveryoneGroupAsync(db); await seedOnboardingAsync(db); await seedServerSettingsAsync(db); + await seedDefaultSearchEnginesAsync(db); }; const seedEveryoneGroupAsync = async (db: Database) => { @@ -48,21 +52,73 @@ const seedOnboardingAsync = async (db: Database) => { console.log("Created onboarding step through seed"); }; +const seedDefaultSearchEnginesAsync = async (db: Database) => { + const existingSearchEngines = await db.$count(searchEngines); + + if (existingSearchEngines > 0) { + console.log("Skipping seeding of default search engines as some already exists"); + return; + } + + const homarrId = createId(); + const defaultSearchEngines = [ + { + id: createId(), + name: "Google", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/google.svg", + short: "g", + description: "Search the web with Google", + urlTemplate: "https://www.google.com/search?q=%s", + type: "generic" as const, + integrationId: null, + }, + { + id: createId(), + name: "YouTube", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/youtube.svg", + short: "yt", + description: "Search for videos on YouTube", + urlTemplate: "https://www.youtube.com/results?search_query=%s", + type: "generic" as const, + integrationId: null, + }, + { + id: homarrId, + name: "Homarr Docs", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg", + short: "docs", + description: "Search the Homarr documentation", + urlTemplate: createDocumentationLink("/search", undefined, { q: "%s" }), + type: "generic" as const, + integrationId: null, + }, + ]; + + await db.insert(searchEngines).values(defaultSearchEngines); + console.log(`Created ${defaultSearchEngines.length} default search engines through seeding process`); + + // Set Homarr docs as the default search engine in server settings + const searchSettings = await getServerSettingByKeyAsync(db, "search"); + + await updateServerSettingByKeyAsync(db, "search", { + ...searchSettings, + defaultSearchEngineId: homarrId, + }); + console.log("Set Homarr docs as the default search engine"); +}; + const seedServerSettingsAsync = async (db: Database) => { const serverSettingsData = await db.query.serverSettings.findMany(); for (const settingsKey of defaultServerSettingsKeys) { const currentDbEntry = serverSettingsData.find((setting) => setting.settingKey === settingsKey); if (!currentDbEntry) { - await db.insert(serverSettings).values({ - settingKey: settingsKey, - value: SuperJSON.stringify(defaultServerSettings[settingsKey]), - }); + await insertServerSettingByKeyAsync(db, settingsKey, defaultServerSettings[settingsKey]); console.log(`Created serverSetting through seed key=${settingsKey}`); continue; } - const currentSettings = SuperJSON.parse>(currentDbEntry.value); + const currentSettings = await getServerSettingByKeyAsync(db, settingsKey); const defaultSettings = defaultServerSettings[settingsKey]; const missingKeys = objectKeys(defaultSettings).filter((key) => !(key in currentSettings)); @@ -71,12 +127,7 @@ const seedServerSettingsAsync = async (db: Database) => { continue; } - await db - .update(serverSettings) - .set({ - value: SuperJSON.stringify({ ...defaultSettings, ...currentSettings }), // Add missing keys - }) - .where(eq(serverSettings.settingKey, settingsKey)); + await updateServerSettingByKeyAsync(db, settingsKey, { ...defaultSettings, ...currentSettings }); console.log(`Updated serverSetting through seed key=${settingsKey}`); } }; diff --git a/packages/db/package.json b/packages/db/package.json index 1539f407e..fb068d37c 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -44,14 +44,14 @@ "@homarr/env": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", - "@mantine/core": "^7.17.2", + "@mantine/core": "^7.17.3", "@paralleldrive/cuid2": "^2.2.2", - "@testcontainers/mysql": "^10.21.0", + "@testcontainers/mysql": "^10.23.0", "better-sqlite3": "^11.9.1", "dotenv": "^16.4.7", - "drizzle-kit": "^0.30.5", + "drizzle-kit": "^0.30.6", "drizzle-orm": "^0.41.0", - "drizzle-zod": "^0.7.0", + "drizzle-zod": "^0.7.1", "mysql2": "3.14.0" }, "devDependencies": { @@ -60,7 +60,7 @@ "@homarr/tsconfig": "workspace:^0.1.0", "@types/better-sqlite3": "7.6.12", "dotenv-cli": "^8.0.0", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "prettier": "^3.5.3", "tsx": "4.19.3", "typescript": "^5.8.2" diff --git a/packages/db/queries/server-setting.ts b/packages/db/queries/server-setting.ts index 73f5156bc..371b18f29 100644 --- a/packages/db/queries/server-setting.ts +++ b/packages/db/queries/server-setting.ts @@ -50,3 +50,14 @@ export const updateServerSettingByKeyAsync = async ( + db: Database, + key: TKey, + value: ServerSettings[TKey], +) => { + await db.insert(serverSettings).values({ + settingKey: key, + value: SuperJSON.stringify(value), + }); +}; diff --git a/packages/definitions/package.json b/packages/definitions/package.json index eff08c7fa..69e6a99ec 100644 --- a/packages/definitions/package.json +++ b/packages/definitions/package.json @@ -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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts index ad106780b..0bc3c8fe0 100644 --- a/packages/definitions/src/docs/homarr-docs-sitemap.ts +++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts @@ -207,6 +207,178 @@ export type HomarrDocumentationPath = | "/docs/1.10.0/widgets/rss" | "/docs/1.10.0/widgets/video" | "/docs/1.10.0/widgets/weather" + | "/docs/1.11.0/tags" + | "/docs/1.11.0/tags/active-directory" + | "/docs/1.11.0/tags/ad-guard" + | "/docs/1.11.0/tags/ad-guard-home" + | "/docs/1.11.0/tags/administration" + | "/docs/1.11.0/tags/advanced" + | "/docs/1.11.0/tags/analytics" + | "/docs/1.11.0/tags/api" + | "/docs/1.11.0/tags/apps" + | "/docs/1.11.0/tags/banner" + | "/docs/1.11.0/tags/blocking" + | "/docs/1.11.0/tags/boards" + | "/docs/1.11.0/tags/bookmark" + | "/docs/1.11.0/tags/bookmarks" + | "/docs/1.11.0/tags/caddy" + | "/docs/1.11.0/tags/certificates" + | "/docs/1.11.0/tags/checklist" + | "/docs/1.11.0/tags/code" + | "/docs/1.11.0/tags/community" + | "/docs/1.11.0/tags/configuration" + | "/docs/1.11.0/tags/connections" + | "/docs/1.11.0/tags/customization" + | "/docs/1.11.0/tags/data-sources" + | "/docs/1.11.0/tags/database" + | "/docs/1.11.0/tags/developer" + | "/docs/1.11.0/tags/development" + | "/docs/1.11.0/tags/dns" + | "/docs/1.11.0/tags/docker" + | "/docs/1.11.0/tags/donation" + | "/docs/1.11.0/tags/edit-mode" + | "/docs/1.11.0/tags/env" + | "/docs/1.11.0/tags/environment-variables" + | "/docs/1.11.0/tags/feeds" + | "/docs/1.11.0/tags/getting-started" + | "/docs/1.11.0/tags/google" + | "/docs/1.11.0/tags/grafana" + | "/docs/1.11.0/tags/groups" + | "/docs/1.11.0/tags/hardware" + | "/docs/1.11.0/tags/health" + | "/docs/1.11.0/tags/help" + | "/docs/1.11.0/tags/icon-picker" + | "/docs/1.11.0/tags/icon-repositories" + | "/docs/1.11.0/tags/icons" + | "/docs/1.11.0/tags/iframe" + | "/docs/1.11.0/tags/images" + | "/docs/1.11.0/tags/installation" + | "/docs/1.11.0/tags/integrade" + | "/docs/1.11.0/tags/integration" + | "/docs/1.11.0/tags/integrations" + | "/docs/1.11.0/tags/interface" + | "/docs/1.11.0/tags/jellyserr" + | "/docs/1.11.0/tags/layout" + | "/docs/1.11.0/tags/ldap" + | "/docs/1.11.0/tags/links" + | "/docs/1.11.0/tags/lists" + | "/docs/1.11.0/tags/management" + | "/docs/1.11.0/tags/media" + | "/docs/1.11.0/tags/minecraft" + | "/docs/1.11.0/tags/monitoring" + | "/docs/1.11.0/tags/news" + | "/docs/1.11.0/tags/notebook" + | "/docs/1.11.0/tags/notes" + | "/docs/1.11.0/tags/oidc" + | "/docs/1.11.0/tags/open-collective" + | "/docs/1.11.0/tags/open-media-vault" + | "/docs/1.11.0/tags/overseerr" + | "/docs/1.11.0/tags/permissions" + | "/docs/1.11.0/tags/pgid" + | "/docs/1.11.0/tags/pi-hole" + | "/docs/1.11.0/tags/ping" + | "/docs/1.11.0/tags/programming" + | "/docs/1.11.0/tags/proxmox" + | "/docs/1.11.0/tags/proxy" + | "/docs/1.11.0/tags/puid" + | "/docs/1.11.0/tags/responsive" + | "/docs/1.11.0/tags/roles" + | "/docs/1.11.0/tags/rss" + | "/docs/1.11.0/tags/search" + | "/docs/1.11.0/tags/search-engines" + | "/docs/1.11.0/tags/security" + | "/docs/1.11.0/tags/self-signed" + | "/docs/1.11.0/tags/seo" + | "/docs/1.11.0/tags/server" + | "/docs/1.11.0/tags/settings" + | "/docs/1.11.0/tags/sinkhole" + | "/docs/1.11.0/tags/sso" + | "/docs/1.11.0/tags/system" + | "/docs/1.11.0/tags/table" + | "/docs/1.11.0/tags/technical-documentation" + | "/docs/1.11.0/tags/text" + | "/docs/1.11.0/tags/torrent" + | "/docs/1.11.0/tags/traefik" + | "/docs/1.11.0/tags/translations" + | "/docs/1.11.0/tags/unraid" + | "/docs/1.11.0/tags/uploads" + | "/docs/1.11.0/tags/usenet" + | "/docs/1.11.0/tags/users" + | "/docs/1.11.0/tags/variables" + | "/docs/1.11.0/tags/widgets" + | "/docs/1.11.0/advanced/command-line" + | "/docs/1.11.0/advanced/command-line/fix-usernames" + | "/docs/1.11.0/advanced/command-line/password-recovery" + | "/docs/1.11.0/advanced/development/getting-started" + | "/docs/1.11.0/advanced/development/kubernetes" + | "/docs/1.11.0/advanced/environment-variables" + | "/docs/1.11.0/advanced/icons" + | "/docs/1.11.0/advanced/keyboard-shortcuts" + | "/docs/1.11.0/advanced/proxy" + | "/docs/1.11.0/advanced/running-as-different-user" + | "/docs/1.11.0/advanced/single-sign-on" + | "/docs/1.11.0/category/advanced" + | "/docs/1.11.0/category/community" + | "/docs/1.11.0/category/developer-guides" + | "/docs/1.11.0/category/getting-started" + | "/docs/1.11.0/category/installation" + | "/docs/1.11.0/category/installation-1" + | "/docs/1.11.0/category/integrations" + | "/docs/1.11.0/category/management" + | "/docs/1.11.0/category/widgets" + | "/docs/1.11.0/community/donate" + | "/docs/1.11.0/community/faq" + | "/docs/1.11.0/community/get-in-touch" + | "/docs/1.11.0/community/license" + | "/docs/1.11.0/community/translations" + | "/docs/1.11.0/getting-started" + | "/docs/1.11.0/getting-started/after-the-installation" + | "/docs/1.11.0/getting-started/glossary" + | "/docs/1.11.0/getting-started/installation/docker" + | "/docs/1.11.0/getting-started/installation/easy-panel" + | "/docs/1.11.0/getting-started/installation/helm" + | "/docs/1.11.0/getting-started/installation/home-assistant" + | "/docs/1.11.0/getting-started/installation/portainer" + | "/docs/1.11.0/getting-started/installation/qnap" + | "/docs/1.11.0/getting-started/installation/railway" + | "/docs/1.11.0/getting-started/installation/saltbox" + | "/docs/1.11.0/getting-started/installation/source" + | "/docs/1.11.0/getting-started/installation/synology" + | "/docs/1.11.0/getting-started/installation/unraid" + | "/docs/1.11.0/integrations/containers" + | "/docs/1.11.0/integrations/dns" + | "/docs/1.11.0/integrations/hardware" + | "/docs/1.11.0/integrations/kubernetes" + | "/docs/1.11.0/integrations/media-requester" + | "/docs/1.11.0/integrations/media-server" + | "/docs/1.11.0/integrations/servarr" + | "/docs/1.11.0/integrations/torrent" + | "/docs/1.11.0/integrations/usenet" + | "/docs/1.11.0/management/api" + | "/docs/1.11.0/management/apps" + | "/docs/1.11.0/management/boards" + | "/docs/1.11.0/management/certificates" + | "/docs/1.11.0/management/integrations" + | "/docs/1.11.0/management/media" + | "/docs/1.11.0/management/search-engines" + | "/docs/1.11.0/management/settings" + | "/docs/1.11.0/management/users" + | "/docs/1.11.0/widgets/bookmarks" + | "/docs/1.11.0/widgets/calendar" + | "/docs/1.11.0/widgets/clock" + | "/docs/1.11.0/widgets/dns-hole" + | "/docs/1.11.0/widgets/downloads" + | "/docs/1.11.0/widgets/health-monitoring" + | "/docs/1.11.0/widgets/home-assistant" + | "/docs/1.11.0/widgets/iframe" + | "/docs/1.11.0/widgets/indexer-manager" + | "/docs/1.11.0/widgets/media-requests" + | "/docs/1.11.0/widgets/media-server" + | "/docs/1.11.0/widgets/minecraft-server-status" + | "/docs/1.11.0/widgets/notebook" + | "/docs/1.11.0/widgets/rss" + | "/docs/1.11.0/widgets/video" + | "/docs/1.11.0/widgets/weather" | "/docs/next/tags" | "/docs/next/tags/active-directory" | "/docs/next/tags/ad-guard" @@ -240,6 +412,7 @@ export type HomarrDocumentationPath = | "/docs/next/tags/env" | "/docs/next/tags/environment-variables" | "/docs/next/tags/feeds" + | "/docs/next/tags/finance" | "/docs/next/tags/getting-started" | "/docs/next/tags/google" | "/docs/next/tags/grafana" @@ -263,6 +436,7 @@ export type HomarrDocumentationPath = | "/docs/next/tags/links" | "/docs/next/tags/lists" | "/docs/next/tags/management" + | "/docs/next/tags/market" | "/docs/next/tags/media" | "/docs/next/tags/minecraft" | "/docs/next/tags/monitoring" @@ -293,6 +467,7 @@ export type HomarrDocumentationPath = | "/docs/next/tags/settings" | "/docs/next/tags/sinkhole" | "/docs/next/tags/sso" + | "/docs/next/tags/stocks" | "/docs/next/tags/system" | "/docs/next/tags/table" | "/docs/next/tags/technical-documentation" @@ -317,6 +492,7 @@ export type HomarrDocumentationPath = | "/docs/next/advanced/proxy" | "/docs/next/advanced/running-as-different-user" | "/docs/next/advanced/single-sign-on" + | "/docs/next/advanced/styling" | "/docs/next/category/advanced" | "/docs/next/category/community" | "/docs/next/category/developer-guides" @@ -346,6 +522,7 @@ export type HomarrDocumentationPath = | "/docs/next/getting-started/installation/source" | "/docs/next/getting-started/installation/synology" | "/docs/next/getting-started/installation/unraid" + | "/docs/next/integrations/cloud" | "/docs/next/integrations/containers" | "/docs/next/integrations/dns" | "/docs/next/integrations/hardware" @@ -378,6 +555,7 @@ export type HomarrDocumentationPath = | "/docs/next/widgets/minecraft-server-status" | "/docs/next/widgets/notebook" | "/docs/next/widgets/rss" + | "/docs/next/widgets/stocks" | "/docs/next/widgets/video" | "/docs/next/widgets/weather" | "/docs/tags" @@ -413,6 +591,7 @@ export type HomarrDocumentationPath = | "/docs/tags/env" | "/docs/tags/environment-variables" | "/docs/tags/feeds" + | "/docs/tags/finance" | "/docs/tags/getting-started" | "/docs/tags/google" | "/docs/tags/grafana" @@ -436,6 +615,7 @@ export type HomarrDocumentationPath = | "/docs/tags/links" | "/docs/tags/lists" | "/docs/tags/management" + | "/docs/tags/market" | "/docs/tags/media" | "/docs/tags/minecraft" | "/docs/tags/monitoring" @@ -466,6 +646,7 @@ export type HomarrDocumentationPath = | "/docs/tags/settings" | "/docs/tags/sinkhole" | "/docs/tags/sso" + | "/docs/tags/stocks" | "/docs/tags/system" | "/docs/tags/table" | "/docs/tags/technical-documentation" @@ -512,6 +693,7 @@ export type HomarrDocumentationPath = | "/docs/getting-started/installation/helm" | "/docs/getting-started/installation/home-assistant" | "/docs/getting-started/installation/portainer" + | "/docs/getting-started/installation/proxmox" | "/docs/getting-started/installation/qnap" | "/docs/getting-started/installation/railway" | "/docs/getting-started/installation/saltbox" @@ -550,6 +732,7 @@ export type HomarrDocumentationPath = | "/docs/widgets/minecraft-server-status" | "/docs/widgets/notebook" | "/docs/widgets/rss" + | "/docs/widgets/stocks" | "/docs/widgets/video" | "/docs/widgets/weather" | "" diff --git a/packages/definitions/src/docs/index.ts b/packages/definitions/src/docs/index.ts index 5d3c11f5c..af6731239 100644 --- a/packages/definitions/src/docs/index.ts +++ b/packages/definitions/src/docs/index.ts @@ -3,5 +3,12 @@ import type { HomarrDocumentationPath } from "./homarr-docs-sitemap"; const documentationBaseUrl = "https://homarr.dev"; // Please use the method so the path can be checked! -export const createDocumentationLink = (path: HomarrDocumentationPath, hashTag?: `#${string}`) => - `${documentationBaseUrl}${path}${hashTag ?? ""}`; +export const createDocumentationLink = ( + path: HomarrDocumentationPath, + hashTag?: `#${string}`, + queryParams?: Record, +) => { + const url = `${documentationBaseUrl}${path}`; + const params = queryParams ? `?${new URLSearchParams(queryParams)}` : ""; + return `${url}${params}${hashTag ?? ""}`; +}; diff --git a/packages/definitions/src/test/docs.spec.ts b/packages/definitions/src/test/docs.spec.ts new file mode 100644 index 000000000..d7690f22e --- /dev/null +++ b/packages/definitions/src/test/docs.spec.ts @@ -0,0 +1,47 @@ +/* eslint-disable no-restricted-syntax */ +import { describe, expect, test } from "vitest"; + +import { createDocumentationLink } from "../docs"; +import type { HomarrDocumentationPath } from "../docs/homarr-docs-sitemap"; + +describe("createDocumentationLink should generate correct URLs", () => { + test.each([ + ["/docs/getting-started", undefined, undefined, "https://homarr.dev/docs/getting-started"], + ["/blog", undefined, undefined, "https://homarr.dev/blog"], + ["/docs/widgets/weather", "#configuration", undefined, "https://homarr.dev/docs/widgets/weather#configuration"], + [ + "/docs/advanced/environment-variables", + undefined, + { lang: "en" }, + "https://homarr.dev/docs/advanced/environment-variables?lang=en", + ], + [ + "/docs/widgets/bookmarks", + "#sorting", + { lang: "fr", theme: "dark" }, + "https://homarr.dev/docs/widgets/bookmarks?lang=fr&theme=dark#sorting", + ], + ] satisfies [HomarrDocumentationPath, `#${string}` | undefined, Record | undefined, string][])( + "should create correct URL for path %s with hash %s and params %o", + (path, hashTag, queryParams, expected) => { + expect(createDocumentationLink(path, hashTag, queryParams)).toBe(expected); + }, + ); +}); + +describe("createDocumentationLink parameter validation", () => { + test("should work with only path parameter", () => { + const result = createDocumentationLink("/docs/getting-started"); + expect(result).toBe("https://homarr.dev/docs/getting-started"); + }); + + test("should work with path and hashtag", () => { + const result = createDocumentationLink("/docs/getting-started", "#installation"); + expect(result).toBe("https://homarr.dev/docs/getting-started#installation"); + }); + + test("should work with path and query params", () => { + const result = createDocumentationLink("/docs/getting-started", undefined, { version: "1.0" }); + expect(result).toBe("https://homarr.dev/docs/getting-started?version=1.0"); + }); +}); diff --git a/packages/docker/package.json b/packages/docker/package.json index b3b6b2456..cb8de282c 100644 --- a/packages/docker/package.json +++ b/packages/docker/package.json @@ -31,8 +31,8 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "@types/dockerode": "^3.3.35", - "eslint": "^9.22.0", + "@types/dockerode": "^3.3.36", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/env/package.json b/packages/env/package.json index a78aefcef..f13b22ae9 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/form/package.json b/packages/form/package.json index 7a1e68d0e..eb8d9d604 100644 --- a/packages/form/package.json +++ b/packages/form/package.json @@ -26,14 +26,14 @@ "@homarr/common": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/form": "^7.17.2", + "@mantine/form": "^7.17.3", "zod": "^3.24.2" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/forms-collection/package.json b/packages/forms-collection/package.json index 22c6aa7ff..baea01b3e 100644 --- a/packages/forms-collection/package.json +++ b/packages/forms-collection/package.json @@ -29,7 +29,7 @@ "@homarr/notifications": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/core": "^7.17.2", + "@mantine/core": "^7.17.3", "react": "19.0.0", "zod": "^3.24.2" }, @@ -37,7 +37,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/forms-collection/src/icon-picker/icon-picker.tsx b/packages/forms-collection/src/icon-picker/icon-picker.tsx index 9dbfb55f7..a571abbe2 100644 --- a/packages/forms-collection/src/icon-picker/icon-picker.tsx +++ b/packages/forms-collection/src/icon-picker/icon-picker.tsx @@ -33,9 +33,17 @@ interface IconPickerProps { error?: string | null; onFocus?: FocusEventHandler; onBlur?: FocusEventHandler; + withAsterisk?: boolean; } -export const IconPicker = ({ value: propsValue, onChange, error, onFocus, onBlur }: IconPickerProps) => { +export const IconPicker = ({ + value: propsValue, + onChange, + error, + onFocus, + onBlur, + withAsterisk = true, +}: IconPickerProps) => { const [value, setValue] = useUncontrolled({ value: propsValue, onChange, @@ -145,7 +153,7 @@ export const IconPicker = ({ value: propsValue, onChange, error, onFocus, onBlur setSearch(value || ""); }} rightSectionPointerEvents="none" - withAsterisk + withAsterisk={withAsterisk} error={error} label={tCommon("iconPicker.label")} placeholder={tCommon("iconPicker.header", { countIcons: String(data?.countIcons ?? 0) })} diff --git a/packages/forms-collection/src/new-app/_form.tsx b/packages/forms-collection/src/new-app/_form.tsx index b47869f3d..146ed5151 100644 --- a/packages/forms-collection/src/new-app/_form.tsx +++ b/packages/forms-collection/src/new-app/_form.tsx @@ -1,17 +1,19 @@ "use client"; import type { ChangeEventHandler } from "react"; -import { useRef } from "react"; +import { useEffect, useRef } from "react"; import Link from "next/link"; import { Button, Checkbox, Collapse, Group, Stack, Textarea, TextInput } from "@mantine/core"; -import { useDisclosure } from "@mantine/hooks"; +import { useDebouncedValue, useDisclosure } from "@mantine/hooks"; import type { z } from "zod"; +import { clientApi } from "@homarr/api/client"; import { useZodForm } from "@homarr/form"; import { useI18n } from "@homarr/translation/client"; import { validation } from "@homarr/validation"; import { IconPicker } from "../icon-picker/icon-picker"; +import { findBestIconMatch } from "./icon-matcher"; type FormType = z.infer; @@ -45,6 +47,9 @@ export const AppForm = ({ }, }); + // Debounce the name value with 200ms delay + const [debouncedName] = useDebouncedValue(form.values.name, 200); + const shouldCreateAnother = useRef(false); const handleSubmit = (values: FormType) => { const redirect = !shouldCreateAnother.current; @@ -68,6 +73,25 @@ export const AppForm = ({ } }; + // Auto-select icon based on app name with debounced search + const { data: iconsData } = clientApi.icon.findIcons.useQuery( + { + searchText: debouncedName, + }, + { + enabled: debouncedName.length > 3, + }, + ); + + useEffect(() => { + if (debouncedName && !form.values.iconUrl && iconsData?.icons) { + const bestMatch = findBestIconMatch(debouncedName, iconsData.icons); + if (bestMatch) { + form.setFieldValue("iconUrl", bestMatch); + } + } + }, [debouncedName, iconsData]); + return (
diff --git a/packages/forms-collection/src/new-app/icon-matcher.ts b/packages/forms-collection/src/new-app/icon-matcher.ts new file mode 100644 index 000000000..bbd611977 --- /dev/null +++ b/packages/forms-collection/src/new-app/icon-matcher.ts @@ -0,0 +1,30 @@ +import type { inferRouterOutputs } from "@trpc/server"; + +import type { AppRouter } from "@homarr/api"; + +type RouterOutput = inferRouterOutputs; +type IconGroupsOutput = RouterOutput["icon"]["findIcons"]["icons"]; + +export const findBestIconMatch = (searchTerm: string, iconGroups: IconGroupsOutput): string | null => { + const nameLower = searchTerm.toLowerCase(); + const allIcons = iconGroups.flatMap((group) => group.icons); + + const getIconPriority = (iconUrl: string) => { + const fileName = iconUrl.toLowerCase().split("/").pop()?.split(".")[0]; + if (!fileName) return -1; + + const isSvg = iconUrl.endsWith(".svg"); + const isExactMatch = fileName === nameLower; + + if (isExactMatch) return isSvg ? 0 : 1; + if (fileName.includes(nameLower)) return isSvg ? 2 : 3; + return -1; + }; + + for (let priority = 0; priority <= 3; priority++) { + const match = allIcons.find((icon) => getIconPriority(icon.url) === priority); + if (match) return match.url; + } + + return null; +}; diff --git a/packages/icons/package.json b/packages/icons/package.json index 7d4a3179c..86b387af9 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 88fcc468f..4643a35be 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -39,7 +39,7 @@ "node-ical": "^0.20.1", "proxmox-api": "1.1.1", "tsdav": "^2.1.3", - "undici": "7.5.0", + "undici": "7.6.0", "xml2js": "^0.6.2", "zod": "^3.24.2" }, @@ -48,7 +48,7 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/xml2js": "^0.4.14", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/integrations/src/overseerr/overseerr-integration.ts b/packages/integrations/src/overseerr/overseerr-integration.ts index 55e08d084..c3feddb6f 100644 --- a/packages/integrations/src/overseerr/overseerr-integration.ts +++ b/packages/integrations/src/overseerr/overseerr-integration.ts @@ -280,6 +280,7 @@ const mediaInformationSchema = z.union([ seasons: z.array( z.object({ id: z.number(), + seasonNumber: z.number(), name: z.string().min(0), episodeCount: z.number().min(0), }), diff --git a/packages/log/package.json b/packages/log/package.json index 0c00104cf..d41775252 100644 --- a/packages/log/package.json +++ b/packages/log/package.json @@ -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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/log/src/error.ts b/packages/log/src/error.ts new file mode 100644 index 000000000..d8c0682dd --- /dev/null +++ b/packages/log/src/error.ts @@ -0,0 +1,50 @@ +import { formatMetadata } from "./metadata"; + +/** + * Formats the cause of an error in the format + * @example caused by Error: {message} + * {stack-trace} + * @param cause next cause in the chain + * @param iteration current iteration of the function + * @returns formatted and stacked causes + */ +export const formatErrorCause = (cause: unknown, iteration = 0): string => { + // Prevent infinite recursion + if (iteration > 5) { + return ""; + } + + if (cause instanceof Error) { + if (!cause.cause) { + return `\ncaused by ${formatErrorTitle(cause)}\n${formatErrorStack(cause.stack)}`; + } + + return `\ncaused by ${formatErrorTitle(cause)}\n${formatErrorStack(cause.stack)}${formatErrorCause(cause.cause, iteration + 1)}`; + } + + return `\ncaused by ${cause as string}`; +}; + +const ignoredErrorProperties = ["stack", "message", "name", "cause"]; + +/** + * Formats the title of an error + * @example {name}: {message} {metadata} + * @param error error to format title from + * @returns formatted error title + */ +export const formatErrorTitle = (error: Error) => { + const title = error.message.length === 0 ? error.name : `${error.name}: ${error.message}`; + const metadata = formatMetadata(error, ignoredErrorProperties); + + return `${title} ${metadata}`; +}; + +/** + * Formats the stack trance of an error + * We remove the first line as it contains the error name and message + * @param stack stack trace + * @returns formatted stack trace + */ +export const formatErrorStack = (stack: string | undefined) => (stack ? removeFirstLine(stack) : ""); +const removeFirstLine = (stack: string) => stack.split("\n").slice(1).join("\n"); diff --git a/packages/log/src/index.ts b/packages/log/src/index.ts index 2da0b175e..5a8e7af19 100644 --- a/packages/log/src/index.ts +++ b/packages/log/src/index.ts @@ -2,43 +2,22 @@ import type { transport as Transport } from "winston"; import winston, { format, transports } from "winston"; import { env } from "./env"; +import { formatErrorCause, formatErrorStack } from "./error"; +import { formatMetadata } from "./metadata"; import { RedisTransport } from "./redis-transport"; -/** - * Formats the cause of an error in the format - * @example caused by Error: {message} - * {stack-trace} - * @param cause next cause in the chain - * @param iteration current iteration of the function - * @returns formatted and stacked causes - */ -const formatCause = (cause: unknown, iteration = 0): string => { - // Prevent infinite recursion - if (iteration > 5) { - return ""; - } - - if (cause instanceof Error) { - if (!cause.cause) { - return `\ncaused by ${cause.stack}`; - } - - return `\ncaused by ${cause.stack}${formatCause(cause.cause, iteration + 1)}`; - } - - return `\ncaused by ${cause as string}`; -}; - -const logMessageFormat = format.printf(({ level, message, timestamp, cause, stack }) => { +const logMessageFormat = format.printf(({ level, message, timestamp, cause, stack, ...metadata }) => { if (!cause && !stack) { return `${timestamp as string} ${level}: ${message as string}`; } + const formatedStack = formatErrorStack(stack as string | undefined); + if (!cause) { - return `${timestamp as string} ${level}: ${message as string}\n${stack as string}`; + return `${timestamp as string} ${level}: ${message as string} ${formatMetadata(metadata)}\n${formatedStack}`; } - return `${timestamp as string} ${level}: ${message as string}\n${stack as string}${formatCause(cause)}`; + return `${timestamp as string} ${level}: ${message as string} ${formatMetadata(metadata)}\n${formatedStack}${formatErrorCause(cause)}`; }); const logTransports: Transport[] = [new transports.Console()]; diff --git a/packages/log/src/metadata.ts b/packages/log/src/metadata.ts new file mode 100644 index 000000000..089a14caa --- /dev/null +++ b/packages/log/src/metadata.ts @@ -0,0 +1,8 @@ +export const formatMetadata = (metadata: Record | Error, ignoreKeys?: string[]) => { + const filteredMetadata = Object.keys(metadata) + .filter((key) => !ignoreKeys?.includes(key)) + .map((key) => ({ key, value: metadata[key as keyof typeof metadata] })) + .filter(({ value }) => typeof value !== "object" && typeof value !== "function"); + + return filteredMetadata.map(({ key, value }) => `${key}="${value as string}"`).join(" "); +}; diff --git a/packages/log/src/redis-transport.ts b/packages/log/src/redis-transport.ts index d56bb8302..2647a392d 100644 --- a/packages/log/src/redis-transport.ts +++ b/packages/log/src/redis-transport.ts @@ -17,10 +17,8 @@ export class RedisTransport extends Transport { this.emit("logged", info); }); - if (!this.redis) { - // Is only initialized here because it did not work when initialized in the constructor or outside the class - this.redis = new Redis(); - } + // Is only initialized here because it did not work when initialized in the constructor or outside the class + this.redis ??= new Redis(); this.redis .publish( diff --git a/packages/modals-collection/package.json b/packages/modals-collection/package.json index 5296152c1..f8f0d87c4 100644 --- a/packages/modals-collection/package.json +++ b/packages/modals-collection/package.json @@ -33,10 +33,10 @@ "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/core": "^7.17.2", + "@mantine/core": "^7.17.3", "@tabler/icons-react": "^3.31.0", "dayjs": "^1.11.13", - "next": "15.1.7", + "next": "15.2.4", "react": "19.0.0", "react-dom": "19.0.0", "zod": "^3.24.2" @@ -45,7 +45,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/modals-collection/src/search-engines/request-media-modal.tsx b/packages/modals-collection/src/search-engines/request-media-modal.tsx index 85678e44c..efb92cf7a 100644 --- a/packages/modals-collection/src/search-engines/request-media-modal.tsx +++ b/packages/modals-collection/src/search-engines/request-media-modal.tsx @@ -72,7 +72,7 @@ export const RequestMediaModal = createModal(({ actions, const anySelected = Object.keys(table.getState().rowSelection).length > 0; const handleMutate = () => { - const selectedSeasons = table.getSelectedRowModel().rows.flatMap((row) => row.original.id); + const selectedSeasons = table.getSelectedRowModel().rows.flatMap((row) => row.original.seasonNumber); mutate({ integrationId: innerProps.integrationId, mediaId: innerProps.mediaId, @@ -114,6 +114,7 @@ export const RequestMediaModal = createModal(({ actions, interface Season { id: number; + seasonNumber: number; name: string; episodeCount: number; } diff --git a/packages/modals/package.json b/packages/modals/package.json index b7b7f0f9c..99ffe93a5 100644 --- a/packages/modals/package.json +++ b/packages/modals/package.json @@ -24,15 +24,15 @@ "dependencies": { "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", - "@mantine/core": "^7.17.2", - "@mantine/hooks": "^7.17.2", + "@mantine/core": "^7.17.3", + "@mantine/hooks": "^7.17.3", "react": "19.0.0" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/notifications/package.json b/packages/notifications/package.json index 6fab04615..2e5bd0d7e 100644 --- a/packages/notifications/package.json +++ b/packages/notifications/package.json @@ -24,14 +24,14 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/ui": "workspace:^0.1.0", - "@mantine/notifications": "^7.17.2", + "@mantine/notifications": "^7.17.3", "@tabler/icons-react": "^3.31.0" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/old-import/package.json b/packages/old-import/package.json index f0d284dc1..704e6b209 100644 --- a/packages/old-import/package.json +++ b/packages/old-import/package.json @@ -26,6 +26,7 @@ }, "prettier": "@homarr/prettier-config", "dependencies": { + "@homarr/auth": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", @@ -37,10 +38,10 @@ "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/core": "^7.17.2", - "@mantine/hooks": "^7.17.2", + "@mantine/core": "^7.17.3", + "@mantine/hooks": "^7.17.3", "adm-zip": "0.5.16", - "next": "15.1.7", + "next": "15.2.4", "react": "19.0.0", "react-dom": "19.0.0", "superjson": "2.2.2", @@ -52,7 +53,7 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/adm-zip": "0.5.7", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/old-import/src/import-error.ts b/packages/old-import/src/import-error.ts index 8485aa3b9..15efdecfb 100644 --- a/packages/old-import/src/import-error.ts +++ b/packages/old-import/src/import-error.ts @@ -1,4 +1,4 @@ -import type { BoardSize, OldmarrConfig } from "@homarr/old-schema"; +import type { OldmarrConfig } from "@homarr/old-schema"; export class OldHomarrImportError extends Error { constructor(oldConfig: OldmarrConfig, cause: unknown) { @@ -7,9 +7,3 @@ export class OldHomarrImportError extends Error { }); } } - -export class OldHomarrScreenSizeError extends Error { - constructor(type: "app" | "widget", id: string, screenSize: BoardSize) { - super(`Screen size not found for type=${type} id=${id} screenSize=${screenSize}`); - } -} diff --git a/packages/old-import/src/import/collections/board-collection.ts b/packages/old-import/src/import/collections/board-collection.ts index 18475ea53..2fd2a5142 100644 --- a/packages/old-import/src/import/collections/board-collection.ts +++ b/packages/old-import/src/import/collections/board-collection.ts @@ -1,10 +1,14 @@ +import type { Session } from "@homarr/auth"; +import { isWidgetRestricted } from "@homarr/auth/shared"; import { createId } from "@homarr/db"; import { createDbInsertCollectionForTransaction } from "@homarr/db/collection"; import { logger } from "@homarr/log"; -import type { BoardSize } from "@homarr/old-schema"; +import type { BoardSize, OldmarrConfig } from "@homarr/old-schema"; import { boardSizes, getBoardSizeName } from "@homarr/old-schema"; +import { widgetImports } from "../../../../widgets/src"; import { fixSectionIssues } from "../../fix-section-issues"; +import { OldHomarrImportError } from "../../import-error"; import { mapBoard } from "../../mappers/map-board"; import { mapBreakpoint } from "../../mappers/map-breakpoint"; import { mapColumnCount } from "../../mappers/map-column-count"; @@ -17,6 +21,7 @@ import type { InitialOldmarrImportSettings } from "../../settings"; export const createBoardInsertCollection = ( { preparedApps, preparedBoards }: Omit, "preparedIntegrations">, settings: InitialOldmarrImportSettings, + session: Session | null, ) => { const insertCollection = createDbInsertCollectionForTransaction([ "apps", @@ -57,6 +62,13 @@ export const createBoardInsertCollection = ( logger.debug(`Added apps to board insert collection count=${insertCollection.apps.length}`); preparedBoards.forEach((board) => { + if (!hasEnoughItemShapes(board.config)) { + throw new OldHomarrImportError( + board.config, + new Error("Your config contains items without shapes for all board sizes."), + ); + } + const { wrappers, categories, wrapperIdsToMerge } = fixSectionIssues(board.config); const { apps, widgets } = moveWidgetsAndAppsIfMerge(board.config, wrapperIdsToMerge, { ...settings, @@ -105,10 +117,18 @@ export const createBoardInsertCollection = ( layoutMapping, mappedBoard.id, ); - preparedItems.forEach(({ layouts, ...item }) => { - insertCollection.items.push(item); - insertCollection.itemLayouts.push(...layouts); - }); + preparedItems + .filter((item) => { + return !isWidgetRestricted({ + definition: widgetImports[item.kind].definition, + user: session?.user ?? null, + check: (level) => level !== "none", + }); + }) + .forEach(({ layouts, ...item }) => { + insertCollection.items.push(item); + insertCollection.itemLayouts.push(...layouts); + }); logger.debug(`Added items to board insert collection count=${insertCollection.items.length}`); }); @@ -118,3 +138,26 @@ export const createBoardInsertCollection = ( return insertCollection; }; + +export const hasEnoughItemShapes = (config: { + apps: Pick[]; + widgets: Pick[]; +}) => { + const invalidSizes: BoardSize[] = []; + + for (const size of boardSizes) { + if (invalidSizes.includes(size)) continue; + + if (config.apps.some((app) => app.shape[size] === undefined)) { + invalidSizes.push(size); + } + + if (invalidSizes.includes(size)) continue; + + if (config.widgets.some((widget) => widget.shape[size] === undefined)) { + invalidSizes.push(size); + } + } + + return invalidSizes.length <= 2; +}; diff --git a/packages/old-import/src/import/import-initial-oldmarr.ts b/packages/old-import/src/import/import-initial-oldmarr.ts index fdf1a79da..de35a993b 100644 --- a/packages/old-import/src/import/import-initial-oldmarr.ts +++ b/packages/old-import/src/import/import-initial-oldmarr.ts @@ -1,5 +1,6 @@ import type { z } from "zod"; +import type { Session } from "@homarr/auth"; import { Stopwatch } from "@homarr/common"; import { handleTransactionsAsync } from "@homarr/db"; import type { Database } from "@homarr/db"; @@ -16,6 +17,7 @@ import { ensureValidTokenOrThrow } from "./validate-token"; export const importInitialOldmarrAsync = async ( db: Database, input: z.infer, + session: Session | null, ) => { const stopwatch = new Stopwatch(); const { checksum, configs, users: importUsers } = await analyseOldmarrImportAsync(input.file); @@ -29,7 +31,7 @@ export const importInitialOldmarrAsync = async ( logger.info("Preparing import data in insert collections for database"); - const boardInsertCollection = createBoardInsertCollection({ preparedApps, preparedBoards }, input.settings); + const boardInsertCollection = createBoardInsertCollection({ preparedApps, preparedBoards }, input.settings, session); const userInsertCollection = createUserInsertCollection(importUsers, input.token); const integrationInsertCollection = createIntegrationInsertCollection(preparedIntegrations, input.token); diff --git a/packages/old-import/src/import/import-single-oldmarr.ts b/packages/old-import/src/import/import-single-oldmarr.ts index 2ca6ed4c1..2ca42870f 100644 --- a/packages/old-import/src/import/import-single-oldmarr.ts +++ b/packages/old-import/src/import/import-single-oldmarr.ts @@ -1,3 +1,4 @@ +import type { Session } from "@homarr/auth"; import { handleTransactionsAsync, inArray } from "@homarr/db"; import type { Database } from "@homarr/db"; import { apps } from "@homarr/db/schema"; @@ -12,6 +13,7 @@ export const importSingleOldmarrConfigAsync = async ( db: Database, config: OldmarrConfig, settings: OldmarrImportConfiguration, + session: Session | null, ) => { const { preparedApps, preparedBoards } = prepareSingleImport(config, settings); const existingApps = await db.query.apps.findMany({ @@ -29,7 +31,7 @@ export const importSingleOldmarrConfigAsync = async ( return app; }); - const boardInsertCollection = createBoardInsertCollection({ preparedApps, preparedBoards }, settings); + const boardInsertCollection = createBoardInsertCollection({ preparedApps, preparedBoards }, settings, session); await handleTransactionsAsync(db, { async handleAsync(db) { diff --git a/packages/old-import/src/import/test/board-collection.spec.ts b/packages/old-import/src/import/test/board-collection.spec.ts new file mode 100644 index 000000000..0933b0de7 --- /dev/null +++ b/packages/old-import/src/import/test/board-collection.spec.ts @@ -0,0 +1,53 @@ +import { describe, expect, test } from "vitest"; + +import type { BoardSize } from "@homarr/old-schema"; + +import { hasEnoughItemShapes } from "../collections/board-collection"; + +const defaultShape = { + location: { + x: 0, + y: 0, + }, + size: { + width: 1, + height: 1, + }, +}; + +describe("hasEnoughItemShapes should check if there are more than one shape available for automatic reconstruction", () => { + test.each([ + [true, [], []], // no items, so nothing to check + [true, [{ lg: true }], []], // lg always exists + [true, [], [{ md: true }]], // md always exists + [true, [{ md: true, sm: true }], [{ md: true, lg: true }]], // md always exists + [true, [{ md: true }], [{ md: true }]], // md always exists + [false, [{ md: true }, { md: true }], [{ lg: true }]], // md is missing for widgets + [false, [{ md: true }], [{ lg: true }]], // md is missing for widgets + [false, [{ md: true }], [{ md: true, lg: true }, { lg: true }]], // md is missing for 2. widget + ] as [boolean, Shape[], Shape[]][])( + "should return %s if there are more than one shape available", + (returnValue, appShapes, widgetShapes) => { + const result = hasEnoughItemShapes({ + apps: appShapes.map((shapes) => ({ + shape: { + sm: shapes.sm ? defaultShape : undefined, + md: shapes.md ? defaultShape : undefined, + lg: shapes.lg ? defaultShape : undefined, + }, + })), + widgets: widgetShapes.map((shapes) => ({ + shape: { + sm: shapes.sm ? defaultShape : undefined, + md: shapes.md ? defaultShape : undefined, + lg: shapes.lg ? defaultShape : undefined, + }, + })), + }); + + expect(result).toBe(returnValue); + }, + ); +}); + +type Shape = Partial>; diff --git a/packages/old-import/src/index.ts b/packages/old-import/src/index.ts index d3153a189..c36d9e70b 100644 --- a/packages/old-import/src/index.ts +++ b/packages/old-import/src/index.ts @@ -1,3 +1,4 @@ +import type { Session } from "@homarr/auth"; import type { Database } from "@homarr/db"; import type { OldmarrConfig } from "@homarr/old-schema"; @@ -8,6 +9,7 @@ export const importOldmarrAsync = async ( db: Database, old: OldmarrConfig, configuration: OldmarrImportConfiguration, + session: Session | null, ) => { - await importSingleOldmarrConfigAsync(db, old, configuration); + await importSingleOldmarrConfigAsync(db, old, configuration, session); }; diff --git a/packages/old-import/src/move-widgets-and-apps-merge.ts b/packages/old-import/src/move-widgets-and-apps-merge.ts index ac3989172..5b77fa925 100644 --- a/packages/old-import/src/move-widgets-and-apps-merge.ts +++ b/packages/old-import/src/move-widgets-and-apps-merge.ts @@ -3,7 +3,6 @@ import { logger } from "@homarr/log"; import type { BoardSize, OldmarrApp, OldmarrConfig, OldmarrWidget } from "@homarr/old-schema"; import { boardSizes } from "@homarr/old-schema"; -import { OldHomarrScreenSizeError } from "./import-error"; import { mapColumnCount } from "./mappers/map-column-count"; import type { OldmarrImportConfiguration } from "./settings"; @@ -60,7 +59,7 @@ export const moveWidgetsAndAppsIfMerge = ( for (const screenSize of boardSizes) { const screenSizeShape = app.shape[screenSize]; if (!screenSizeShape) { - throw new OldHomarrScreenSizeError("app", app.id, screenSize); + continue; } // Find the highest widget in the wrapper to increase the offset accordingly @@ -81,7 +80,7 @@ export const moveWidgetsAndAppsIfMerge = ( for (const screenSize of boardSizes) { const screenSizeShape = widget.shape[screenSize]; if (!screenSizeShape) { - throw new OldHomarrScreenSizeError("widget", widget.id, screenSize); + continue; } // Find the highest widget in the wrapper to increase the offset accordingly @@ -145,15 +144,6 @@ const moveWidgetsAndAppsInLeftSidebar = ( item.area.properties.location === "left" && (columnCount >= 2 || item.shape[screenSize]?.location.x === 0), update: (item) => { - const screenSizeShape = item.shape[screenSize]; - if (!screenSizeShape) { - throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize); - } - // Reduce width to one if column count is one - if (screenSizeShape.size.width > columnCount) { - screenSizeShape.size.width = columnCount; - } - item.area = { type: "wrapper", properties: { @@ -161,6 +151,14 @@ const moveWidgetsAndAppsInLeftSidebar = ( }, }; + const screenSizeShape = item.shape[screenSize]; + if (!screenSizeShape) return; + + // Reduce width to one if column count is one + if (screenSizeShape.size.width > columnCount) { + screenSizeShape.size.width = columnCount; + } + screenSizeShape.location.y += offset; }, }); @@ -184,11 +182,6 @@ const moveWidgetsAndAppsInLeftSidebar = ( item.area.properties.location === "left" && item.shape[screenSize]?.location.x === 1, update: (item) => { - const screenSizeShape = item.shape[screenSize]; - if (!screenSizeShape) { - throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize); - } - item.area = { type: "wrapper", properties: { @@ -196,6 +189,9 @@ const moveWidgetsAndAppsInLeftSidebar = ( }, }; + const screenSizeShape = item.shape[screenSize]; + if (!screenSizeShape) return; + screenSizeShape.location.x = 0; screenSizeShape.location.y += offset; }, @@ -222,16 +218,6 @@ const moveWidgetsAndAppsInRightSidebar = ( item.area.properties.location === "right" && (columnCount >= 2 || item.shape[screenSize]?.location.x === 0), update: (item) => { - const screenSizeShape = item.shape[screenSize]; - if (!screenSizeShape) { - throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize); - } - - // Reduce width to one if column count is one - if (screenSizeShape.size.width > columnCount) { - screenSizeShape.size.width = columnCount; - } - item.area = { type: "wrapper", properties: { @@ -239,6 +225,14 @@ const moveWidgetsAndAppsInRightSidebar = ( }, }; + const screenSizeShape = item.shape[screenSize]; + if (!screenSizeShape) return; + + // Reduce width to one if column count is one + if (screenSizeShape.size.width > columnCount) { + screenSizeShape.size.width = columnCount; + } + screenSizeShape.location.y += offset; screenSizeShape.location.x += xOffsetDelta; }, @@ -260,11 +254,6 @@ const moveWidgetsAndAppsInRightSidebar = ( item.area.properties.location === "left" && item.shape[screenSize]?.location.x === 1, update: (item) => { - const screenSizeShape = item.shape[screenSize]; - if (!screenSizeShape) { - throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, screenSize); - } - item.area = { type: "wrapper", properties: { @@ -272,6 +261,9 @@ const moveWidgetsAndAppsInRightSidebar = ( }, }; + const screenSizeShape = item.shape[screenSize]; + if (!screenSizeShape) return; + screenSizeShape.location.x = 0; screenSizeShape.location.y += offset; }, @@ -312,16 +304,15 @@ const updateItems = (options: { for (const item of items) { const before = createItemSnapshot(item, options.screenSize); + options.update(item); + const screenSizeShape = item.shape[options.screenSize]; - if (!screenSizeShape) { - throw new OldHomarrScreenSizeError("kind" in item ? "widget" : "app", item.id, options.screenSize); - } + if (!screenSizeShape) return requiredHeight; if (screenSizeShape.location.y + screenSizeShape.size.height > requiredHeight) { requiredHeight = screenSizeShape.location.y + screenSizeShape.size.height; } - options.update(item); const after = createItemSnapshot(item, options.screenSize); logger.debug( diff --git a/packages/old-schema/package.json b/packages/old-schema/package.json index af380ad2d..23ccfe8d3 100644 --- a/packages/old-schema/package.json +++ b/packages/old-schema/package.json @@ -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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/ping/package.json b/packages/ping/package.json index 18444e58c..8caba6d05 100644 --- a/packages/ping/package.json +++ b/packages/ping/package.json @@ -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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/ping/src/index.ts b/packages/ping/src/index.ts index d96d1a51a..b8f024c60 100644 --- a/packages/ping/src/index.ts +++ b/packages/ping/src/index.ts @@ -1,16 +1,16 @@ -import { formatError } from "pretty-print-error"; import type { fetch } from "undici"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { extractErrorMessage } from "@homarr/common"; import { logger } from "@homarr/log"; export const sendPingRequestAsync = async (url: string) => { try { return await fetchWithTimeoutAndCertificates(url).then((response) => ({ statusCode: response.status })); } catch (error) { - logger.error("packages/ping/src/index.ts:", formatError(error)); + logger.error(new Error(`Failed to send ping request to "${url}"`, { cause: error })); return { - error: formatError(error), + error: extractErrorMessage(error), }; } }; diff --git a/packages/redis/package.json b/packages/redis/package.json index 957a497b8..5dc362c90 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/request-handler/package.json b/packages/request-handler/package.json index 56d96d47b..5483e469c 100644 --- a/packages/request-handler/package.json +++ b/packages/request-handler/package.json @@ -38,7 +38,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/request-handler/src/lib/cached-request-integration-job-handler.ts b/packages/request-handler/src/lib/cached-request-integration-job-handler.ts index 1c306f300..2c9bc39b8 100644 --- a/packages/request-handler/src/lib/cached-request-integration-job-handler.ts +++ b/packages/request-handler/src/lib/cached-request-integration-job-handler.ts @@ -1,4 +1,3 @@ -import { formatError } from "pretty-print-error"; import SuperJSON from "superjson"; import { hashObjectBase64, Stopwatch } from "@homarr/common"; @@ -107,7 +106,9 @@ export const createRequestIntegrationJobHandler = < ); } catch (error) { logger.error( - `Failed to run integration job integration=${integrationId} inputHash='${inputHash}' error=${formatError(error)}`, + new Error(`Failed to run integration job integration=${integrationId} inputHash='${inputHash}'`, { + cause: error, + }), ); } } diff --git a/packages/server-settings/package.json b/packages/server-settings/package.json index ecab641a5..1aa5f0b0f 100644 --- a/packages/server-settings/package.json +++ b/packages/server-settings/package.json @@ -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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/settings/package.json b/packages/settings/package.json index be055b7ff..5815f82a5 100644 --- a/packages/settings/package.json +++ b/packages/settings/package.json @@ -25,8 +25,8 @@ "@homarr/api": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", - "@mantine/dates": "^7.17.2", - "next": "15.1.7", + "@mantine/dates": "^7.17.3", + "next": "15.2.4", "react": "19.0.0", "react-dom": "19.0.0" }, @@ -34,7 +34,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/spotlight/package.json b/packages/spotlight/package.json index d58a4b751..a1ab57f95 100644 --- a/packages/spotlight/package.json +++ b/packages/spotlight/package.json @@ -33,12 +33,12 @@ "@homarr/settings": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", - "@mantine/core": "^7.17.2", - "@mantine/hooks": "^7.17.2", - "@mantine/spotlight": "^7.17.2", + "@mantine/core": "^7.17.3", + "@mantine/hooks": "^7.17.3", + "@mantine/spotlight": "^7.17.3", "@tabler/icons-react": "^3.31.0", "jotai": "^2.12.2", - "next": "15.1.7", + "next": "15.2.4", "react": "19.0.0", "react-dom": "19.0.0", "use-deep-compare-effect": "^1.8.1" @@ -47,7 +47,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/translation/package.json b/packages/translation/package.json index 550ffc5cf..cd2944d93 100644 --- a/packages/translation/package.json +++ b/packages/translation/package.json @@ -32,7 +32,7 @@ "dayjs": "^1.11.13", "deepmerge": "4.3.1", "mantine-react-table": "2.0.0-beta.9", - "next": "15.1.7", + "next": "15.2.4", "next-intl": "4.0.2", "react": "19.0.0", "react-dom": "19.0.0" @@ -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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/translation/src/config.ts b/packages/translation/src/config.ts index e30789ae9..e1bc750c2 100644 --- a/packages/translation/src/config.ts +++ b/packages/translation/src/config.ts @@ -60,9 +60,31 @@ export const localeConfigurations = { return import("dayjs/locale/de").then((module) => module.default); }, }, + "de-CH": { + name: "Deutsch (Schweiz)", + translatedName: "German (Swiss)", + flagIcon: "ch", + importMrtLocalization() { + return import("mantine-react-table/locales/de/index.esm.mjs").then((module) => module.MRT_Localization_DE); + }, + importDayJsLocale() { + return import("dayjs/locale/de-ch").then((module) => module.default); + }, + }, + "en-gb": { + name: "English (UK)", + translatedName: "English (UK)", + flagIcon: "gb", + importMrtLocalization() { + return import("mantine-react-table/locales/en/index.esm.mjs").then((module) => module.MRT_Localization_EN); + }, + importDayJsLocale() { + return import("dayjs/locale/en-gb").then((module) => module.default); + }, + }, en: { - name: "English", - translatedName: "English", + name: "English (US)", + translatedName: "English (US)", flagIcon: "us", importMrtLocalization() { return import("mantine-react-table/locales/en/index.esm.mjs").then((module) => module.MRT_Localization_EN); diff --git a/packages/translation/src/lang/ca.json b/packages/translation/src/lang/ca.json index ce74c4b2b..f159de6c3 100644 --- a/packages/translation/src/lang/ca.json +++ b/packages/translation/src/lang/ca.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "", diff --git a/packages/translation/src/lang/cn.json b/packages/translation/src/lang/cn.json index 5db899dfa..f5b5362b8 100644 --- a/packages/translation/src/lang/cn.json +++ b/packages/translation/src/lang/cn.json @@ -980,6 +980,9 @@ "remove": "删除动态部分" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "边界颜色" } @@ -1721,7 +1724,11 @@ "noIntegration": "未选择集成", "noData": "没有可用的集成数据" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "视频流", diff --git a/packages/translation/src/lang/cs.json b/packages/translation/src/lang/cs.json index 5e8ed22cf..a60b56854 100644 --- a/packages/translation/src/lang/cs.json +++ b/packages/translation/src/lang/cs.json @@ -980,6 +980,9 @@ "remove": "Odstranit dynamickou sekci" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "Nebyla vybrána žádná integrace", "noData": "Nejsou k dispozici žádná data o integraci" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Streamování videa", diff --git a/packages/translation/src/lang/da.json b/packages/translation/src/lang/da.json index 86c5a634d..9b0f19865 100644 --- a/packages/translation/src/lang/da.json +++ b/packages/translation/src/lang/da.json @@ -980,6 +980,9 @@ "remove": "Fjern dynamisk sektion" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "Kantfarve" } @@ -1425,76 +1428,76 @@ } }, "stockPrice": { - "name": "", - "description": "", + "name": "Aktiepris", + "description": "Viser den aktuelle aktiepris for en virksomhed", "option": { "stock": { - "label": "" + "label": "Aktiesymbol" }, "timeRange": { - "label": "", + "label": "Tidsinterval", "option": { "1d": { - "label": "" + "label": "1 dag" }, "5d": { - "label": "" + "label": "5 Dage" }, "1mo": { - "label": "" + "label": "1 Måned" }, "3mo": { - "label": "" + "label": "3 Måneder" }, "6mo": { - "label": "" + "label": "6 Måneder" }, "ytd": { - "label": "" + "label": "Året indtil nu" }, "1y": { - "label": "" + "label": "1 År" }, "2y": { - "label": "" + "label": "2 År" }, "5y": { - "label": "" + "label": "5 År" }, "10y": { - "label": "" + "label": "10 År" }, "max": { - "label": "" + "label": "Maks" } } }, "timeInterval": { - "label": "", + "label": "Tidsinterval", "option": { "5m": { - "label": "" + "label": "5 Minutter" }, "15m": { - "label": "" + "label": "15 Minutter" }, "30m": { - "label": "" + "label": "30 Minutter" }, "1h": { - "label": "" + "label": "1 Time" }, "1d": { - "label": "" + "label": "1 Dag" }, "5d": { - "label": "" + "label": "5 Dage" }, "1wk": { - "label": "" + "label": "1 Uge" }, "1mo": { - "label": "" + "label": "1 Måned" } } } @@ -1721,7 +1724,11 @@ "noIntegration": "Ingen integration valgt", "noData": "Ingen tilgængelige integrationsdata" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Video Stream", diff --git a/packages/translation/src/lang/de-CH.json b/packages/translation/src/lang/de-CH.json new file mode 100644 index 000000000..01e5382f9 --- /dev/null +++ b/packages/translation/src/lang/de-CH.json @@ -0,0 +1,3759 @@ +{ + "init": { + "step": { + "start": { + "title": "Willkomme zu Homarr", + "subtitle": "Lass uns mit der Einrichtung deiner Homarr-Instanz beginnen.", + "description": "Um loszulegen, wähle bitte wie du deine Homarr Instanz einrichten möchtest.", + "action": { + "scratch": "Vo vorne starte", + "importOldmarr": "Importiärä us Homarr vor 1.0" + } + }, + "import": { + "title": "Date importiärä", + "subtitle": "Du casch date us ere existierende Homarr Instanz importiere.", + "dropzone": { + "title": "Zieh e ZIP-Datei hiä ane oder klick druf um z durchsueche", + "description": "Die uechegladeni ZIP Datei wird verarbeitet und du wirsch chönnä uswähle, was importiere wotsch" + }, + "fileInfo": { + "action": { + "change": "Datei ändere" + } + }, + "importSettings": { + "title": "Istellige importiere", + "description": "Import verhalte konfiguriere" + }, + "boardSelection": { + "title": "{count} boards gfunge", + "description": "Wähl alli boards, mit dr grössi wo de wotsch importiere, us", + "action": { + "selectAll": "Alli uswähle", + "unselectAll": "Gesamte Auswahl aufheben" + } + }, + "summary": { + "title": "Import zämäfassig", + "description": "Ir zämäfassig unge gsehsch, was importier wird", + "action": { + "import": "Bestätige und witer" + }, + "entities": { + "apps": "Apps", + "boards": "Boads", + "integrations": "Integratione", + "credentialUsers": "Benutzer mit Zugangsdaten" + } + }, + "tokenModal": { + "title": "Import token igäh", + "field": { + "token": { + "label": "Token", + "description": "Gib z import token vo dire vorherige Homarr Instanz ah" + } + }, + "notification": { + "error": { + "title": "Invalids token", + "message": "Z token wo de hesch agäh isch invalid" + } + } + } + }, + "user": { + "title": "Administrator benutzer", + "subtitle": "Gib z Zugangsdate für di Administrator benutzer ah.", + "notification": { + "success": { + "title": "Benutzer ersteut", + "message": "Dr Benutzer isch erfougrich ersteut worde" + }, + "error": { + "title": "Benutzerersteuig isch fehlgschlagge" + } + } + }, + "group": { + "title": "Externi Gruppe", + "subtitle": "Gib d gruppe ah wo für externi Benutzer verwendet werde söu", + "form": { + "name": { + "label": "Guppename", + "description": "Name muss mit der Admin-Gruppe des externen Anbieters übereinstimmen" + } + } + }, + "settings": { + "title": "Istellige", + "subtitle": "Serveristellige apasse." + }, + "finish": { + "title": "Irichtig abschliesse", + "subtitle": "Du bisch startklar!", + "description": "Du hast die Einrichtung erfolgreich abgeschlossen. Du kannst jetzt mit der Nutzung von Homarr beginnen. Wähle deine nächste Aktion:", + "action": { + "goToBoard": "Gehe zu {name} Board", + "createBoard": "Erstelle dein erstes Board", + "inviteUser": "Andere Benutzer einladen", + "docs": "Lese die Dokumentation" + } + } + }, + "backToStart": "Zurück zum Anfang" + }, + "user": { + "title": "Benutzer", + "name": "Benutzer", + "page": { + "login": { + "title": "Melde dich bei deinem Konto an", + "subtitle": "Willkommen zurück! Bitte gib deine Zugangsdaten ein" + }, + "invite": { + "title": "Homarr beitreten", + "subtitle": "Willkommen bei Homarr! Bitte erstelle dein Konto", + "description": "Du wurdest von {username} eingeladen" + }, + "init": { + "title": "Neue Homarr Installation", + "subtitle": "Bitte erstelle den initialen Administrator" + } + }, + "field": { + "email": { + "label": "", + "verified": "Verifiziert" + }, + "username": { + "label": "Benutzername" + }, + "password": { + "label": "Passwort", + "requirement": { + "length": "Enthält mindestens 8 Zeichen", + "lowercase": "Enthält Kleinbuchstaben", + "uppercase": "Enthält Großbuchstaben", + "number": "Enthält Ziffern", + "special": "Enthält Sonderzeichen" + } + }, + "passwordConfirm": { + "label": "Passwort bestätigen" + }, + "previousPassword": { + "label": "Vorheriges Passwort" + }, + "homeBoard": { + "label": "Home Board" + }, + "pingIconsEnabled": { + "label": "Symbole für Pings verwenden" + }, + "defaultSearchEngine": { + "label": "Standard Suchmaschine" + }, + "openSearchInNewTab": { + "label": "Suchergebnisse in neuem Tab öffnen" + } + }, + "error": { + "usernameTaken": "Benutzername schon vergeben" + }, + "action": { + "login": { + "label": "Anmelden", + "labelWith": "Mit {provider} anmelden", + "notification": { + "success": { + "title": "Anmeldung erfolgreich", + "message": "Du bist jetzt angemeldet" + }, + "error": { + "title": "Anmeldung fehlgeschlagen", + "message": "Deine Anmeldung ist fehlgeschlagen" + } + }, + "forgotPassword": { + "label": "Hast du dein Passwort vergessen?", + "description": "Ein Administrator kann den folgenden Befehl verwenden, um dein Passwort zurückzusetzen:" + } + }, + "register": { + "label": "Account erstellen", + "notification": { + "success": { + "title": "Account erstellt", + "message": "Bitte melde dich an um fortzufahren" + }, + "error": { + "title": "Kontoerstellung fehlgeschlagen", + "message": "Dein Benutzerkonto konnte nicht erstellt werden" + } + } + }, + "create": "Benutzer erstellen", + "changePassword": { + "label": "Passwort ändern", + "notification": { + "success": { + "message": "Passwort erfolgreich geändert" + }, + "error": { + "message": "Password het nid chöne gänderet werde" + } + } + }, + "changeHomeBoard": { + "notification": { + "success": { + "message": "Hauptboard erfougrich gwächselt" + }, + "error": { + "message": "Homeboard het nid chönne gwächselt werde" + } + } + }, + "changeSearchPreferences": { + "notification": { + "success": { + "message": "Suechistellige erfolgrich gwechselt" + }, + "error": { + "message": "D Suechistellige hei nid chönne gwechselt werde" + } + } + }, + "changeFirstDayOfWeek": { + "notification": { + "success": { + "message": "Erst Tag vor Wuche erfougrich gändert" + }, + "error": { + "message": "Konnte den ersten Wochentag nicht ändern" + } + } + }, + "changePingIconsEnabled": { + "notification": { + "success": { + "message": "Ping Icons erfolgreich umgeschaltet" + }, + "error": { + "message": "Konnte Ping-Icons nicht umschalten" + } + } + }, + "manageAvatar": { + "changeImage": { + "label": "Biud wächsle", + "notification": { + "success": { + "message": "Buid erfougrich gwächselt" + }, + "error": { + "message": "Bild konnte nicht geändert werden" + }, + "toLarge": { + "title": "Biud isch zu gross", + "message": "Maximali biud grössi isch {size}" + } + } + }, + "removeImage": { + "label": "Biud entferne", + "confirm": "Bisch sicher, dass das buid wotsch entferne?", + "notification": { + "success": { + "message": "Biud isch erfougrich entfernt worde" + }, + "error": { + "message": "Biud het nid chönne entfernt wärde" + } + } + } + }, + "editProfile": { + "notification": { + "success": { + "message": "Profiu erfougrich aktualisiert" + }, + "error": { + "message": "Profiu het nid chönne aktualisiert werde" + } + } + }, + "delete": { + "label": "Benutzer dauerhaft löschen", + "description": "Löscht diesen Benutzer einschließlich seiner Einstellungen. Wird keine Boards löschen. Benutzer wird nicht benachrichtigt.", + "confirm": "Sind Sie sicher, dass Sie den Benutzer {username} mit seinen Einstellungen löschen möchten?" + }, + "select": { + "label": "Benutzer uswähle", + "notFound": "Ke Benutzer gfunge" + }, + "transfer": { + "label": "Nöie Bsitzer uswähle" + } + } + }, + "group": { + "title": "Gruppe", + "name": "Gruppe", + "search": "Gruppe sueche", + "field": { + "name": "Name", + "members": "Mitglider", + "homeBoard": { + "label": "Hauptboard", + "description": "Nur Boards welche für die Gruppe zugänglich sind, können ausgewählt werden" + }, + "mobileBoard": { + "label": "Mobiles Board", + "description": "Nur Boards welche für die Gruppe zugänglich sind, können ausgewählt werden" + } + }, + "permission": { + "admin": { + "title": "", + "item": { + "admin": { + "label": "", + "description": "Mitglieder mit dieser Berechtigung haben vollen Zugriff auf alle Funktionen und Einstellungen" + } + } + }, + "app": { + "title": "", + "item": { + "create": { + "label": "App erstäue", + "description": "Mitgliedern das Erstellen von Apps erlauben" + }, + "use-all": { + "label": "Alle Apps verwenden", + "description": "Mitgliedern erlauben, Apps zu ihren Boards hinzuzufügen" + }, + "modify-all": { + "label": "Alle Apps ändern", + "description": "Mitgliedern das Erstellen von Apps erlauben" + }, + "full-all": { + "label": "Voller App Zugriff", + "description": "Mitgliedern das Benutzen, Verwalten und Löschen von Apps erlauben" + } + } + }, + "board": { + "title": "", + "item": { + "create": { + "label": "Boards erstellen", + "description": "Mitgliedern das Erstellen von Boards erlauben" + }, + "view-all": { + "label": "Alle Boards anzeigen", + "description": "Mitgliedern das Ansehen von Boards erlauben" + }, + "modify-all": { + "label": "Alle Boards ändern", + "description": "Erlaube Mitgliedern alle Boards zu ändern (beinhaltet keine Zugangskontrolle und Gefahrenbereich)" + }, + "full-all": { + "label": "Voller Board Zugriff", + "description": "Erlaube Mitgliedern alle Boards (einschließlich Zugriffskontrolle und Gefahrenbereich) anzuzeigen, zu ändern und zu löschen" + } + } + }, + "integration": { + "title": "Integrationen", + "item": { + "create": { + "label": "Integrationen erstellen", + "description": "Mitgliedern erlauben, Integrationen zu erstellen" + }, + "use-all": { + "label": "Alle Integrationen nutzen", + "description": "Erlaubt Mitgliedern Integrationen zu ihren Boards hinzuzufügen" + }, + "interact-all": { + "label": "Mit jeder Integration interagieren", + "description": "Mitgliedern erlauben, mit jeder Integration zu interagieren" + }, + "full-all": { + "label": "Voller Zugriff auf Integrationen", + "description": "Erlaube Mitgliedern jede Integration zu verwalten, zu nutzen und zu interagieren" + } + } + }, + "media": { + "title": "Medien", + "item": { + "upload": { + "label": "Medien Hochladen", + "description": "Erlaube Mitgliedern Medien hochzuladen" + }, + "view-all": { + "label": "Alle Medien anzeigen", + "description": "Erlaubt Mitgliedern alle Medien anzusehen" + }, + "full-all": { + "label": "Vollständiger Medienzugriff", + "description": "Mitgliedern das Verwalten und Löschen von Medien erlauben" + } + } + }, + "other": { + "title": "Andere", + "item": { + "view-logs": { + "label": "Logs anzeigen", + "description": "Erlaubt Mitgliedern Logs anzusehen" + } + } + }, + "search-engine": { + "title": "Suchmaschinen", + "item": { + "create": { + "label": "Suchmaschinen anlegen", + "description": "Erlaubt es Mitgliedern Suchmaschinen anzulegen" + }, + "modify-all": { + "label": "Alle Suchmaschinen ändern", + "description": "Mitgliedern erlauben alle Suchmaschinen zu ändern" + }, + "full-all": { + "label": "Voller Zugriff auf Suchmaschinen", + "description": "Mitgliedern das Verwalten und Löschen von Suchmaschinen erlauben" + } + } + } + }, + "memberNotice": { + "mixed": "Einige Mitglieder haben externe Anmeldeoptionen genutzt und können von hier aus nicht verwaltet werden", + "external": "Alle Mitglieder haben externe Anmeldeoptionen genutzt und können von hier aus nicht verwaltet werden" + }, + "reservedNotice": { + "message": "Diese Gruppe ist für die Systemnutzung reserviert und beschränkt einige Aktionen. " + }, + "action": { + "create": { + "label": "Neue Gruppe", + "notification": { + "success": { + "message": "Die Gruppe wurde erfolgreich erstellt" + }, + "error": { + "message": "Die Gruppe konnte nicht erstellt werden" + } + } + }, + "transfer": { + "label": "Eigentum übertragen", + "description": "Übertrage den Eigentümer dieser Gruppe an einen anderen Benutzer.", + "confirm": "Bist du sicher, dass du das Eigentum für die Gruppe {name} an {username} übertragen möchtest?", + "notification": { + "success": { + "message": "Gruppe {group} erfolgreich an {user} übertragen" + }, + "error": { + "message": "Übertragung des Eigentums nicht möglich" + } + } + }, + "addMember": { + "label": "Mitglied hinzufügen" + }, + "removeMember": { + "label": "Mitglied entfernen", + "confirm": "Bist du sicher, dass du {user} aus dieser Gruppe entfernen möchtest?" + }, + "delete": { + "label": "Gruppe löschen", + "description": "Sobald Sie einen Arbeitsbereich löschen, gibt es keinen Weg sie wiederherzustellen. Bitte seien Sie sich dessen bewusst.", + "confirm": "Sind Sie sicher, dass Sie die Gruppe {name} löschen möchten?", + "notification": { + "success": { + "message": "Gruppe {name} erfolgreich gelöscht" + }, + "error": { + "message": "Gruppe {name} konnte nicht gelöscht werden" + } + } + }, + "changePermissions": { + "notification": { + "success": { + "title": "Berechtigungen gespeichert", + "message": "Berechtigungen wurden erfolgreich gespeichert" + }, + "error": { + "title": "Berechtigungen nicht gespeichert", + "message": "Berechtigungen wurden nicht gespeichert" + } + } + }, + "update": { + "notification": { + "success": { + "message": "Die Gruppe {name} wurde erfolgreich gespeichert" + }, + "error": { + "message": "Gruppe {name} konnte nicht gespeichert werden" + } + } + }, + "select": { + "label": "Gruppe auswählen", + "notFound": "Keine Gruppe gefunden" + }, + "settings": { + "board": { + "notification": { + "success": { + "title": "Einstellungen gespeichert", + "message": "Board Einstellungen erfolgreich gespeichert" + }, + "error": { + "title": "Einstellungen konnten nicht gespeichert werden", + "message": "Board Einstellungen konnten nicht gespeichert werden" + } + } + } + }, + "changePosition": { + "notification": { + "success": { + "message": "Position erfolgreich geändert" + }, + "error": { + "message": "Position konnte nicht geändert werden" + } + } + } + }, + "defaultGroup": { + "name": "Standardgruppe", + "description": "{name} - Alle angemeldeten Benutzer" + } + }, + "app": { + "search": "App suchen", + "page": { + "list": { + "title": "", + "noResults": { + "title": "Noch keine Apps vorhanden", + "action": "Erstelle deine erste App" + } + }, + "create": { + "title": "Neue App", + "notification": { + "success": { + "title": "Erstellung erfolgreich", + "message": "Die App wurde erfolgreich erstellt" + }, + "error": { + "title": "Erstellung fehlgeschlagen", + "message": "Die App konnte nicht angelegt werden" + } + } + }, + "edit": { + "title": "App bearbeiten", + "notification": { + "success": { + "title": "Änderungen erfolgreich angewendet", + "message": "Die App wurde erfolgreich gespeichert" + }, + "error": { + "title": "Änderungen konnten nicht angewendet werden", + "message": "Die App konnte nicht gespeichert werden" + } + } + }, + "delete": { + "title": "App löschen", + "message": "Sind Sie sicher, dass Sie die App {name} löschen möchten?", + "notification": { + "success": { + "title": "Löschen erfolgreich", + "message": "Die App wurde erfolgreich gelöscht" + }, + "error": { + "title": "Löschen fehlgeschlagen", + "message": "App konnte nicht gelöscht werden" + } + } + } + }, + "field": { + "name": { + "label": "" + }, + "description": { + "label": "Beschreibung" + }, + "url": { + "label": "URL" + }, + "useDifferentUrlForPing": { + "checkbox": { + "label": "Verwend de anders URL für Ping", + "description": "Nützlich, wenn Homarr direkt über en internen Hostname oder Netzwerk zugreifen cha, um Bandbreite vom ISP z'spare" + } + } + }, + "action": { + "select": { + "label": "App wähle", + "notFound": "Keni app gfunge" + } + } + }, + "integration": { + "page": { + "list": { + "title": "Integratione", + "search": "Integratione sueche", + "noResults": { + "title": "Es git no keni Integratione" + } + }, + "create": { + "title": "Nöi integration {name}", + "notification": { + "success": { + "title": "Ersteuig erfougrich", + "message": "D integration isch erfougrich erstellt worde" + }, + "error": { + "title": "Erstellig isch fählgschlage", + "message": "D Integration het nid chönne erstäut werde" + } + } + }, + "edit": { + "title": "Integration {name} bearbeite", + "notification": { + "success": { + "title": "Änderige erfougrich agwendet", + "message": "D Integration isch erfougrich gspichert worde" + }, + "error": { + "title": "Änderige chöi nöd aawendet werde", + "message": "D Integration het nöd chönne gspichert werde" + } + } + }, + "delete": { + "title": "Integration lösche", + "message": "Bisch sicher, dass d Integration {name} wotsch lösche?", + "notification": { + "success": { + "title": "Löschig erfougrich", + "message": "D Integration isch erfougrich glöscht worde" + }, + "error": { + "title": "Löschig isch fehlgschlage", + "message": "Integration het nid chönnä glöscht wärde" + } + } + } + }, + "field": { + "name": { + "label": "Name" + }, + "url": { + "label": "Url" + }, + "attemptSearchEngineCreation": { + "label": "Suechmaschine erstelle", + "description": "Integration '{kind}' cha als Suechmaschine verwendet werde. Wähl das us, um se outomatisch als Suechmaschine z konfiguriere." + }, + "createApp": { + "label": "App ersteue", + "description": "Mach en App mit de gleiche Name und dem gleiche Icon wie d'Integration. Lass d'Eingabefeld unger im Fall leer, um d'App mit em Integrations-URL z'schaffe." + }, + "appHref": { + "placeholder": "Benutzerdefinierti App URL" + } + }, + "action": { + "create": "Nöi Integration" + }, + "testConnection": { + "action": { + "create": "Verbindig teste und erstäue", + "edit": "Verbinding teste und spichere" + }, + "alertNotice": "Dr Spicherknopf isch aktiv sobald d Verbindig het chönne hergstellt werde", + "notification": { + "success": { + "title": "Verbindig erfougrich", + "message": "D Verbindig het erfougrich chönne härgstellt werde" + }, + "invalidUrl": { + "title": "Ungültige URL", + "message": "Die Adresse ist ungültig" + }, + "secretNotDefined": { + "title": "Fehlende Anmeldedaten", + "message": "Nicht alle Zugangsdaten wurden angegeben" + }, + "invalidCredentials": { + "title": "Ungültige Zugangsdaten", + "message": "Die Zugangsdaten sind ungültig" + }, + "commonError": { + "title": "Verbindungsaufbau fehlgeschlagen", + "message": "Die Verbindung konnte nicht hergestellt werden" + }, + "badRequest": { + "title": "Fehlerhafte Anfrage", + "message": "Die Anfrage war fehlerhaft" + }, + "unauthorized": { + "title": "Nicht autorisiert", + "message": "Wahrscheinlich falsche Anmeldeinformationen" + }, + "forbidden": { + "title": "Verweigert", + "message": "Vermutlich fehlende Berechtigungen" + }, + "notFound": { + "title": "Nicht gefunden", + "message": "Wahrscheinlich falsche URL oder Pfad" + }, + "internalServerError": { + "title": "Interner Serverfehler", + "message": "Auf dem Server ist ein Fehler aufgetreten" + }, + "serviceUnavailable": { + "title": "Dienst nicht verfügbar", + "message": "Der Server ist derzeit nicht erreichbar" + }, + "connectionAborted": { + "title": "Verbindung abgebrochen", + "message": "Die Verbindung wurde abgebrochen" + }, + "domainNotFound": { + "title": "Domain nicht gefunden", + "message": "Die Domain konnte nicht gefunden werden" + }, + "connectionRefused": { + "title": "Verbindung verweigert", + "message": "Die Verbindung wurde abgelehnt" + }, + "invalidJson": { + "title": "Ungültiges JSON", + "message": "Die Antwort war ungültig JSON" + }, + "wrongPath": { + "title": "Falscher Pfad", + "message": "Der Pfad ist vermutlich nicht korrekt" + } + } + }, + "secrets": { + "title": "", + "lastUpdated": "Zuletzt aktualisiert am {date}", + "notSet": { + "label": "Kein Wert festgelegt", + "tooltip": "Das erforderliche Secret wurde noch nicht festgelegt" + }, + "secureNotice": "Das Secret kann nicht nach der Erstellung abgerufen werden", + "reset": { + "title": "Secret zurücksetzen", + "message": "Bisch sicher, dass du das Gheimnis wotsch lösche?" + }, + "noSecretsRequired": { + "segmentTitle": "Keni Gheimnis", + "text": "Für die Integration bruchts keni Gheimniss" + }, + "kind": { + "username": { + "label": "Benutzername", + "newLabel": "Nöie Benutzername" + }, + "apiKey": { + "label": "API Schlüssu", + "newLabel": "Nöie API Schlüssu" + }, + "password": { + "label": "Passwort", + "newLabel": "Nöis Passwort" + }, + "tokenId": { + "label": "Token ID", + "newLabel": "Nöie Token ID" + }, + "realm": { + "label": "Bereich", + "newLabel": "Neuer Bereich" + } + } + }, + "permission": { + "use": "Integrationen in Elementen auswählen", + "interact": "Mit Integrationen interagieren", + "full": "Voller Zugriff auf Integrationen" + } + }, + "media": { + "plural": "Medien", + "search": "Medien suchen", + "field": { + "name": "", + "size": "Größe", + "creator": "Ersteller" + }, + "action": { + "upload": { + "label": "Medien Hochladen", + "file": "Datei auswählen", + "notification": { + "success": { + "message": "Das Medium wurde erfolgreich hochgeladen" + }, + "error": { + "message": "Das Medium konnte nicht hochgeladen werden" + } + } + }, + "delete": { + "label": "Medium löschen", + "description": "Sind Sie sicher, dass Sie die Medien löschen möchten?", + "notification": { + "success": { + "message": "Das Medium wurde erfolgreich gelöscht" + }, + "error": { + "message": "Die Medien konnten nicht gelöscht werden" + } + } + }, + "copy": { + "label": "URL in Zwischenablage kopieren" + }, + "open": { + "label": "Medium öffnen" + } + } + }, + "common": { + "beta": "", + "error": "Fehler", + "action": { + "add": "Hinzufügen", + "apply": "Übernehmen", + "backToOverview": "Zurück zur Übersicht", + "create": "Erstellen", + "createAnother": "Erstellen und neu starten", + "edit": "Bearbeiten", + "import": "", + "insert": "Einfügen", + "remove": "Entfernen", + "save": "Speichern", + "saveChanges": "Änderungen speichern", + "cancel": "Abbrechen", + "delete": "Löschen", + "discard": "Verwerfen", + "confirm": "Bestätigen", + "continue": "Fortfahren", + "previous": "Zurück", + "next": "Weiter", + "checkoutDocs": "Die Dokumentation ansehen", + "checkLogs": "Logs für weitere Details prüfen", + "tryAgain": "Erneut versuchen", + "loading": "Wird geladen" + }, + "here": "Hier", + "iconPicker": { + "label": "", + "header": "Geben Sie Namen oder Objekte ein, um nach Symbolen zu filtern... Homarr sucht nach {countIcons} Icons für Sie." + }, + "colorScheme": { + "options": { + "light": "Hell", + "dark": "Dunkel" + } + }, + "information": { + "min": "", + "max": "", + "days": "Tage", + "hours": "Stunden", + "minutes": "Minuten" + }, + "notification": { + "create": { + "success": "Erstellung erfolgreich", + "error": "Erstellung fehlgeschlagen" + }, + "delete": { + "success": "Löschen erfolgreich", + "error": "Löschen fehlgeschlagen" + }, + "update": { + "success": "Änderungen erfolgreich angewendet", + "error": "Änderungen konnten nicht angewendet werden" + }, + "transfer": { + "success": "Übertragung erfolgreich", + "error": "Übertragung ist fehlgeschlagen" + } + }, + "multiSelect": { + "placeholder": "Wählen Sie einen oder mehrere Werte" + }, + "multiText": { + "placeholder": "Weitere Werte hinzufügen", + "addLabel": "{value} hinzufügen" + }, + "select": { + "placeholder": "Wert auswählen", + "badge": { + "recommended": "Empfohlen" + } + }, + "userAvatar": { + "menu": { + "switchToDarkMode": "Zum dunklen Design wechseln", + "switchToLightMode": "Zum hellen Design wechseln", + "management": "Verwaltung", + "preferences": "Ihre Einstellungen", + "logout": "Abmelden", + "login": "Anmelden", + "homeBoard": "Ihr Home Board", + "loggedOut": "Abgemeldet", + "updateAvailable": "{countUpdates} Updates verfügbar: {tag}" + } + }, + "dangerZone": "Gefahrenbereich", + "noResults": "Die Suche ergab keine Treffer", + "unsavedChanges": "Sie haben ungespeicherte Änderungen!", + "preview": { + "show": "Vorschau ansehen", + "hide": "Vorschau ausblenden" + }, + "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", + "invalidEmail": "Dieses Feld beinhaltet keine gültige E-Mail-Adresse" + }, + "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" + }, + "custom": { + "passwordsDoNotMatch": "Passwörter stimmen nicht überein", + "passwordRequirements": "Passwort erfüllt nicht alle Anforderungen", + "boardAlreadyExists": "Ein Board mit diesem Namen existiert bereits", + "invalidFileType": "Ungültiger Dateityp, erwartete {expected}", + "invalidFileName": "Ungültiger Dateiname", + "fileTooLarge": "Datei ist zu groß, maximale Größe ist {maxSize}", + "invalidConfiguration": "Ungültige Konfiguration", + "groupNameTaken": "Gruppenname bereits vergeben" + } + } + } + }, + "section": { + "dynamic": { + "action": { + "create": "Neuer dynamischer Abschnitt", + "remove": "Dynamischen Abschnitt entfernen" + }, + "option": { + "title": { + "label": "" + }, + "borderColor": { + "label": "Rahmenfarbe" + } + }, + "remove": { + "title": "Dynamischen Abschnitt entfernen", + "message": "Möchten Sie diesen dynamischen Abschnitt wirklich entfernen? Die Elemente werden an die gleiche Position im übergeordneten Abschnitt verschoben." + } + }, + "category": { + "field": { + "name": { + "label": "" + } + }, + "action": { + "create": "Neue Kategorie", + "edit": "Kategorie umbenennen", + "remove": "Kategorie entfernen", + "moveUp": "Nach oben bewegen", + "moveDown": "Nach unten bewegen", + "createAbove": "Neue Kategorie oben", + "createBelow": "Neue Kategorie unten", + "openAllInNewTabs": "Alle in Tabs öffnen" + }, + "create": { + "title": "Neue Kategorie", + "submit": "Kategorie hinzufügen" + }, + "remove": { + "title": "Kategorie entfernen", + "message": "Möchten Sie die Kategorie {name} wirklich entfernen?" + }, + "edit": { + "title": "Kategorie umbenennen", + "submit": "Kategorie umbenennen" + }, + "menu": { + "label": { + "create": "Neue Kategorie", + "changePosition": "Position wechseln" + } + }, + "openAllInNewTabs": { + "title": "Alle in tabs öffnen", + "text": "Einige Browser können aus Sicherheitsgründen das Öffnen von mehreren Tabs aufeinmal blockieren. Homarr konnte nicht alle Fenster öffnen, weil Ihr Browser diese Aktion blockiert hat. Bitte erlauben Sie \"Pop-up Fenster öffnen\" und versuchen Sie es erneut." + } + } + }, + "item": { + "action": { + "create": "Neues Element", + "import": "Element importieren", + "edit": "Element bearbeiten", + "moveResize": "Element verschieben/Größe ändern", + "duplicate": "Element duplizieren", + "remove": "Element löschen" + }, + "menu": { + "label": { + "settings": "Einstellungen" + } + }, + "create": { + "title": "Wählen Sie das hinzuzufügende Element aus", + "search": "Gegenstände filtern", + "addToBoard": "Zum Board hinzufügen" + }, + "moveResize": { + "title": "Element verschieben/Größe ändern", + "field": { + "width": { + "label": "Breite" + }, + "height": { + "label": "Höhe" + }, + "xOffset": { + "label": "X Versatz" + }, + "yOffset": { + "label": "Y Versatz" + } + } + }, + "edit": { + "title": "Element bearbeiten", + "advancedOptions": { + "label": "Erweiterte Einstellungen", + "title": "Erweiterte Einstellungen Optionen" + }, + "field": { + "integrations": { + "label": "Integrationen" + }, + "customCssClasses": { + "label": "Benutzerdefinierte CSS Klassen" + }, + "borderColor": { + "label": "Rahmenfarbe" + } + } + }, + "remove": { + "title": "Element entfernen", + "message": "Soll dieses Element wirklich entfernt werden?" + } + }, + "widget": { + "app": { + "name": "", + "description": "Bettet eine App in das Board ein.", + "option": { + "appId": { + "label": "App auswählen" + }, + "openInNewTab": { + "label": "In neuem Tab öffnen" + }, + "showTitle": { + "label": "App Namen anzeigen" + }, + "showDescriptionTooltip": { + "label": "Beschreibungs des Tooltips anzeigen" + }, + "pingEnabled": { + "label": "Statusüberprüfung aktivieren" + } + }, + "error": { + "notFound": { + "label": "Keine App", + "tooltip": "Sie haben keine gültige App ausgewählt" + } + } + }, + "bookmarks": { + "name": "Lesezeichen", + "description": "Mehrere App Links anzeigen", + "option": { + "title": { + "label": "Titel" + }, + "layout": { + "label": "Ansicht", + "option": { + "row": { + "label": "" + }, + "column": { + "label": "Vertikal" + }, + "grid": { + "label": "Raster" + }, + "gridHorizontal": { + "label": "Gitter horizontal" + } + } + }, + "hideTitle": { + "label": "Titel ausblenden" + }, + "hideIcon": { + "label": "Symbole verbergen" + }, + "hideHostname": { + "label": "Hostnamen ausblenden" + }, + "openNewTab": { + "label": "In neuem Tab öffnen" + }, + "items": { + "label": "Lesezeichen", + "add": "Lesezeichen hinzufügen" + } + } + }, + "dnsHoleSummary": { + "name": "DNS Hole Zusammenfassung", + "description": "Zeigt die Zusammenfassung deines DNS Holes an", + "option": { + "layout": { + "label": "Ansicht", + "option": { + "row": { + "label": "" + }, + "column": { + "label": "Vertikal" + }, + "grid": { + "label": "Raster" + } + } + }, + "usePiHoleColors": { + "label": "Pi Hole Farben verwenden" + } + }, + "error": { + "internalServerError": "Fehler beim Abrufen der DNS Hole Zusammenfassung", + "integrationsDisconnected": "Keine Daten verfügbar, alle Integrationen getrennt" + }, + "data": { + "adsBlockedToday": "Heute blockiert", + "adsBlockedTodayPercentage": "Heute blockiert", + "dnsQueriesToday": "Abfragen heute", + "domainsBeingBlocked": "Domänen auf der Sperrliste" + }, + "domainsTooltip": "Aufgrund mehrerer Integrationen kann Homarr die genaue Anzahl der blockierten Domains nicht berechnen" + }, + "dnsHoleControls": { + "name": "DNS Hole Steuerung", + "description": "Steuern Sie PiHole oder AdGuard von Ihrem Dashboard aus", + "option": { + "layout": { + "label": "Ansicht", + "option": { + "row": { + "label": "" + }, + "column": { + "label": "Vertikal" + }, + "grid": { + "label": "Raster" + } + } + }, + "showToggleAllButtons": { + "label": "Alle Schaltflächen anzeigen" + } + }, + "error": { + "internalServerError": "Fehler bei der Kontrolle des DNS Holes" + }, + "controls": { + "enableAll": "Alle aktivieren", + "disableAll": "Alle deaktivieren", + "setTimer": "Timer setzen", + "set": "Übernehmen", + "enabled": "Aktiviert", + "disabled": "Deaktiviert", + "processing": "Wird verarbeitet", + "disconnected": "Verbindung getrennt", + "hours": "Stunden", + "minutes": "Minuten", + "unlimited": "Freilassen für unbegrenzt" + } + }, + "clock": { + "name": "Datum und Uhrzeit", + "description": "Zeigt das aktuelle Datum und die Uhrzeit an.", + "option": { + "customTitleToggle": { + "label": "Eigene Titel/Stadt Anzeige", + "description": "Zeige einen benutzerdefinierten Titel oder den Namen der Stadt oder des Landes oben auf der Uhr." + }, + "customTitle": { + "label": "Titel" + }, + "is24HourFormat": { + "label": "24-Stunden Format", + "description": "24-Stunden Format anstelle von 12-Stunden Format verwenden" + }, + "showSeconds": { + "label": "Sekunden anzeigen" + }, + "useCustomTimezone": { + "label": "Eine feste Zeitzone verwenden" + }, + "timezone": { + "label": "Zeitzone", + "description": "Wählen Sie die Zeitzone nach dem IANA-Standard" + }, + "showDate": { + "label": "Datum anzeigen" + }, + "dateFormat": { + "label": "Datumsformat", + "description": "Wie das Datum aussehen sollte" + }, + "customTimeFormat": { + "label": "Benutzerdefiniertes Zeitformat", + "description": "ISO 8601 Standard benutzen um die Zeit zu formatieren (dies wird andere Optionen überschreiben)" + }, + "customDateFormat": { + "label": "Benutzerdefiniertes Datumsformat", + "description": "ISO 8601 Standard benutzen um das Datum zu formatieren (dies wird andere Optionen überschreiben)" + } + } + }, + "minecraftServerStatus": { + "name": "Status des Minecraft Servers", + "description": "Status des Minecraft Servers anzeigen", + "option": { + "title": { + "label": "Titel" + }, + "domain": { + "label": "Server Adresse" + }, + "isBedrockServer": { + "label": "Bedrock Server" + } + }, + "status": { + "online": "", + "offline": "" + } + }, + "notebook": { + "name": "Notizbuch", + "description": "Ein einfaches Notizbuch Widget, das Markdown unterstützt", + "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": "", + "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": "Verknüpfung", + "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": "", + "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": { + "noUrl": "Keine iFrame URL angegeben", + "unsupportedProtocol": "Die angegebene URL verwendet ein nicht unterstütztes Protokoll. Bitte verwenden Sie eines von ({supportedProtocols})", + "noBrowerSupport": "Ihr Browser unterstützt keine iframes. Bitte aktualisieren Sie Ihren Browser." + } + }, + "smartHome-entityState": { + "name": "Zustand der Entität", + "description": "Zeigt den Status einer Entität an und schaltet ihn optional ein", + "option": { + "entityId": { + "label": "Eintrag-ID" + }, + "displayName": { + "label": "Anzeigename" + }, + "entityUnit": { + "label": "Einheit der Entität" + }, + "clickable": { + "label": "Anklickbar" + } + } + }, + "smartHome-executeAutomation": { + "name": "Automatisierung ausführen", + "description": "Automatisierung mit einem Klick auslösen", + "option": { + "displayName": { + "label": "Anzeigename" + }, + "automationId": { + "label": "Automatisierungs-ID" + } + }, + "spotlightAction": { + "run": "{name} ausführen" + } + }, + "stockPrice": { + "name": "", + "description": "", + "option": { + "stock": { + "label": "" + }, + "timeRange": { + "label": "", + "option": { + "1d": { + "label": "" + }, + "5d": { + "label": "" + }, + "1mo": { + "label": "" + }, + "3mo": { + "label": "" + }, + "6mo": { + "label": "" + }, + "ytd": { + "label": "" + }, + "1y": { + "label": "" + }, + "2y": { + "label": "" + }, + "5y": { + "label": "" + }, + "10y": { + "label": "" + }, + "max": { + "label": "" + } + } + }, + "timeInterval": { + "label": "", + "option": { + "5m": { + "label": "" + }, + "15m": { + "label": "" + }, + "30m": { + "label": "" + }, + "1h": { + "label": "" + }, + "1d": { + "label": "" + }, + "5d": { + "label": "" + }, + "1wk": { + "label": "" + }, + "1mo": { + "label": "" + } + } + } + } + }, + "calendar": { + "name": "Kalender", + "description": "Zeigt Ereignisse aus Ihren Integrationen in einer Kalenderansicht innerhalb eines bestimmten Zeitraums an", + "option": { + "releaseType": { + "label": "Radarr Veröffentlichungs Typ", + "options": { + "inCinemas": "In Kinos", + "digitalRelease": "Digitale Veröffentlichung", + "physicalRelease": "Physische Veröffentlichung" + } + }, + "filterPastMonths": { + "label": "Start von" + }, + "filterFutureMonths": { + "label": "Endet am" + }, + "showUnmonitored": { + "label": "Nicht überwachte anzeigen" + } + } + }, + "weather": { + "name": "Wetter", + "description": "Zeigt die aktuellen Wetterinformationen für einen bestimmten Ort an.", + "option": { + "isFormatFahrenheit": { + "label": "Temperatur in Fahrenheit" + }, + "disableTemperatureDecimals": { + "label": "Deaktivieren der Temperatur Dezimalstellen" + }, + "showCurrentWindSpeed": { + "label": "Aktuelle Windgeschwindigkeit anzeigen", + "description": "Nur bei aktuellem Wetter" + }, + "location": { + "label": "Wetterstandort" + }, + "showCity": { + "label": "Stadt anzeigen" + }, + "hasForecast": { + "label": "Prognose anzeigen" + }, + "forecastDayCount": { + "label": "Anzahl der Prognose Tage", + "description": "Wenn das Widget nicht breit genug ist, werden weniger Tage angezeigt" + }, + "dateFormat": { + "label": "Datumsformat", + "description": "Wie das Datum aussehen sollte" + } + }, + "currentWindSpeed": "", + "dailyForecast": { + "sunrise": "Sonnenaufgang", + "sunset": "Sonnenuntergang", + "maxWindSpeed": "Maximale Windgeschwindigkeit: {maxWindSpeed} km/h", + "maxWindGusts": "Maximale Windböen: {maxWindGusts} km/h" + }, + "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 Index Managers", + "description": "Status des Indexer", + "option": { + "openIndexerSiteInNewTab": { + "label": "Indexer Seite in neuem Tab öffnen" + } + }, + "title": "Index Manager", + "testAll": "Alle testen", + "error": { + "internalServerError": "Fehler beim Abrufen des Indexer Status" + } + }, + "healthMonitoring": { + "name": "Überwachung des Systemzustands", + "description": "Zeigt Informationen zum Zustand und Status Ihres/Ihrer Systeme(s) an.", + "tab": { + "system": "", + "cluster": "Gruppe" + }, + "option": { + "fahrenheit": { + "label": "CPU-Temperatur in Fahrenheit" + }, + "cpu": { + "label": "CPU-Info anzeigen" + }, + "memory": { + "label": "Speicher-Info anzeigen" + }, + "fileSystem": { + "label": "Dateisystem Info anzeigen" + }, + "defaultTab": { + "label": "Standard Tab" + }, + "sectionIndicatorRequirement": { + "label": "Anforderung der Sektionsindikatoren" + } + }, + "popover": { + "information": "Informationen", + "processor": "Prozessor: {cpuModelName}", + "memory": "Speicher: {memory}GiB", + "memoryAvailable": "Verfügbar: {memoryAvailable} GiB ({percent}%)", + "version": "", + "uptime": "Laufzeit: {months} Monate, {days} Tage, {hours} Stunden, {minutes} Minuten", + "loadAverage": "Durchschnittliche Last:", + "minute": "1 Minute", + "minutes": "{count} Minuten", + "used": "Belegt", + "available": "Verfügbar", + "lastSeen": "Letzte Statusaktualisierung: {lastSeen}" + }, + "memory": {}, + "error": { + "internalServerError": "Fehler beim Abrufen des Gesundheitsstatus" + }, + "cluster": { + "summary": { + "cpu": "", + "memory": "" + }, + "resource": { + "node": { + "name": "" + }, + "qemu": { + "name": "" + }, + "lxc": { + "name": "" + }, + "storage": { + "name": "Speicher" + } + }, + "popover": { + "rightSection": { + "node": "", + "vmId": "", + "plugin": "" + }, + "detail": { + "cpu": "Kerne", + "memory": "Speicher", + "storage": "Speicher", + "uptime": "Laufzeit", + "haState": "HA Status", + "storageType": { + "local": "Lokaler Speicher", + "shared": "Geteilter Speicher" + } + } + }, + "table": { + "header": { + "name": "", + "cpu": "", + "memory": "", + "node": "" + } + } + } + }, + "common": { + "location": { + "query": "Stadt / Postleitzahl", + "latitude": "Breitengrad", + "longitude": "Längengrad", + "disabledTooltip": "Gib eine Stadt oder Postleitzahl an", + "unknownLocation": "Unbekannter Ort", + "search": "Suche", + "table": { + "header": { + "city": "Stadt", + "country": "Land", + "coordinates": "Koordinaten", + "population": "Bevölkerung" + }, + "action": { + "select": "Wähle {city}, {countryCode}" + }, + "population": { + "fallback": "Unbekannt" + } + } + }, + "integration": { + "noData": "Keine Integration gefunden", + "description": "Klicken Sie um eine neue Integration zu erstellen" + }, + "app": { + "noData": "Keine App gefunden", + "description": "Klicken Sie um eine neue App zu erstellen", + "quickCreate": "Jetzt eine App erstellen" + }, + "error": { + "noIntegration": "Keine Integration ausgewählt", + "noData": "Keine Integrationsdaten verfügbar" + }, + "option": {}, + "restricted": { + "title": "", + "description": "" + } + }, + "video": { + "name": "Videostream", + "description": "Einbetten eines Videostreams oder eines Videos von einer Kamera oder einer Website", + "option": { + "feedUrl": { + "label": "" + }, + "hasAutoPlay": { + "label": "Automatische Wiedergabe", + "description": "Autoplay funktioniert nur bei Stummschaltung aufgrund von Browser Einschränkungen" + }, + "isMuted": { + "label": "Stumm geschaltet" + }, + "hasControls": { + "label": "Steuerelemente anzeigen" + } + }, + "error": { + "noUrl": "Keine Video URL angegeben", + "forYoutubeUseIframe": "Für YouTube Videos verwenden Sie die iframe Option" + } + }, + "mediaServer": { + "name": "Aktuelle Media Server Streams", + "description": "Zeige die aktuellen Streams auf deinen Medienservern an", + "option": {}, + "items": { + "currentlyPlaying": "Aktuelle Wiedergabe", + "user": "Benutzer", + "name": "", + "id": "" + } + }, + "downloads": { + "name": "", + "description": "Ermöglicht es Ihnen, Ihre Downloads sowohl von Torrent- als auch von Usenet-Clients anzusehen und zu verwalten.", + "option": { + "columns": { + "label": "Anzuzeigende Spalten" + }, + "enableRowSorting": { + "label": "Sortierung von Elementen aktivieren" + }, + "defaultSort": { + "label": "Standardmäßig verwendete Spalte zur Sortierung" + }, + "descendingDefaultSort": { + "label": "Sortierung umkehren" + }, + "showCompletedUsenet": { + "label": "Usenet Einträge anzeigen die abgeschlossen wurden" + }, + "showCompletedTorrent": { + "label": "Torrent Einträge anzeigen die abgeschlossen wurden" + }, + "activeTorrentThreshold": { + "label": "Abgeschlossene Torrents unter diesem Schwellenwert ausblenden (in kiB/s)" + }, + "categoryFilter": { + "label": "Kategorien/Labels zum Filtern" + }, + "filterIsWhitelist": { + "label": "Als Whitelist filtern" + }, + "applyFilterToRatio": { + "label": "Filter zur Berechnung des Verhältnisses verwenden" + } + }, + "errors": { + "noColumns": "Spalten in Elementen auswählen", + "noCommunications": "Kann keine Daten von der Integration laden" + }, + "items": { + "actions": { + "columnTitle": "Steuerung" + }, + "added": { + "columnTitle": "Hinzugefügt", + "detailsTitle": "Hinzugefügt am" + }, + "category": { + "columnTitle": "", + "detailsTitle": "Kategorien (Oder zusätzliche Informationen)" + }, + "downSpeed": { + "columnTitle": "Download", + "detailsTitle": "Download Geschwindigkeit" + }, + "index": { + "columnTitle": "", + "detailsTitle": "Aktueller Index im Client" + }, + "id": { + "columnTitle": "ID" + }, + "integration": { + "columnTitle": "" + }, + "name": { + "columnTitle": "Job Name" + }, + "progress": { + "columnTitle": "Fortschritt", + "detailsTitle": "Download Fortschritt" + }, + "ratio": { + "columnTitle": "Verhältnis", + "detailsTitle": "Torrent Verhältnis (Empfangen/Gesendet)" + }, + "received": { + "columnTitle": "Download gesamt", + "detailsTitle": "Insgesamt heruntergeladen" + }, + "sent": { + "columnTitle": "Upload gesamt", + "detailsTitle": "Insgesamt hochgeladen" + }, + "size": { + "columnTitle": "Dateigröße", + "detailsTitle": "Gesamtgröße der Auswahl/Dateien" + }, + "state": { + "columnTitle": "Staat", + "detailsTitle": "Jobstatus" + }, + "time": { + "columnTitle": "Endzeit", + "detailsTitle": "Zeit danach/zur Fertigstellung" + }, + "type": { + "columnTitle": "Typ", + "detailsTitle": "Download Client" + }, + "upSpeed": { + "columnTitle": "Upload", + "detailsTitle": "Upload Geschwindigkeit" + } + }, + "states": { + "downloading": "Wird heruntergeladen", + "queued": "In der Warteschlange", + "paused": "Pausiert", + "completed": "Abgeschlossen", + "failed": "Fehlgeschlagen", + "processing": "Wird verarbeitet", + "leeching": "", + "stalled": "Angehalten", + "unknown": "Unbekannt", + "seeding": "Seede" + }, + "actions": { + "clients": { + "modalTitle": "Liste der Download Clients", + "pause": "Alle Clients/Elemente pausieren", + "resume": "Alle Clients/Elemente fortsetzen" + }, + "client": { + "pause": "Client pausieren", + "resume": "Client fortsetzen" + }, + "item": { + "pause": "Element pausieren", + "resume": "Element fortsetzen", + "delete": { + "title": "Element löschen", + "modalTitle": "Sind Sie sicher, dass Sie diesen Auftrag löschen möchten?", + "entry": "Eintrag löschen", + "entryAndFiles": "Eintrag und Datei(en) löschen" + } + } + }, + "globalRatio": "Globales Verhältnis" + }, + "mediaRequests-requestList": { + "name": "Liste der Medienanfragen", + "description": "Sehen Sie eine Liste aller Medienanfragen von Ihrer Overseerr- oder Jellyseerr-Instanz", + "option": { + "linksTargetNewTab": { + "label": "Links in neuem Tab öffnen" + } + }, + "pending": { + "approve": "Anfrage annehmen", + "approving": "Anfrage wird bestätigt...", + "decline": "Antrag ablehnen" + }, + "availability": { + "unknown": "Unbekannt", + "pending": "Ausstehend", + "processing": "In Bearbeitung", + "partiallyAvailable": "Teilweise", + "available": "Verfügbar" + }, + "status": { + "pending": "Ausstehend", + "approved": "Bestätigt", + "declined": "Abgelehnt", + "failed": "Fehlgeschlagen" + }, + "toBeDetermined": "Noch Festzulegen" + }, + "mediaRequests-requestStats": { + "name": "Statistik der Medienanfragen", + "description": "Statistiken über Ihre Medienanfragen", + "option": {}, + "titles": { + "stats": { + "main": "Medien Statistiken", + "approved": "Bereits genehmigt", + "pending": "Ausstehende Freigaben", + "processing": "Wird verarbeitet", + "declined": "Bereits abgelehnt", + "available": "Bereits verfügbar", + "tv": "TV Anfragen", + "movie": "Film Anfragen", + "total": "Gesamt" + }, + "users": { + "main": "Top Nutzer", + "requests": "Anfragen" + } + } + }, + "mediaTranscoding": { + "name": "Medien Transkodierung", + "description": "Statistiken, aktuelle Warteschlange und Status Ihrer Medien Transkordierung", + "option": { + "defaultView": { + "label": "Standardansicht" + }, + "queuePageSize": { + "label": "Größe der Warteschlange" + } + }, + "tab": { + "workers": "Arbeiter", + "queue": "Warteschlange", + "statistics": "Statistiken" + }, + "currentIndex": "{start}-{end} von {total}", + "healthCheck": { + "title": "Gesundheitscheck", + "queued": "In der Warteschlange", + "status": { + "healthy": "Gesund", + "unhealthy": "Fehlerhaft" + } + }, + "panel": { + "statistics": { + "empty": "Leer", + "transcodes": "Transkodieren", + "transcodesCount": "Transkodierungen", + "healthChecksCount": "Health Check", + "filesCount": "Dateien", + "savedSpace": "Belegter Speicherplatz", + "healthChecks": "Gesundheitscheck", + "videoCodecs": "", + "videoContainers": "Container", + "videoResolutions": "Auflösungen" + }, + "workers": { + "empty": "Leer", + "table": { + "file": "Datei", + "eta": "Voraussichtlicher Abschluss", + "progress": "Fortschritt", + "transcode": "Transkodieren", + "healthCheck": "Gesundheitscheck" + } + }, + "queue": { + "empty": "Leer", + "table": { + "file": "Datei", + "size": "Größe", + "transcode": "Transkodieren", + "healthCheck": "Gesundheitscheck" + } + } + } + }, + "rssFeed": { + "name": "RSS Feeds", + "description": "Einen oder mehrere generische RSS, ATOM oder JSON-Feeds überwachen und anzeigen", + "option": { + "feedUrls": { + "label": "" + }, + "enableRtl": { + "label": "RTL aktivieren" + }, + "textLinesClamp": { + "label": "Beschreibung line clamp" + }, + "maximumAmountPosts": { + "label": "Limit für Beiträge" + } + } + } + }, + "widgetPreview": { + "toggle": { + "enabled": "Bearbeitungsmodus aktiviert", + "disabled": "Bearbeitungsmodus deaktiviert" + }, + "dimensions": { + "title": "Dimensionen ändern" + } + }, + "board": { + "action": { + "duplicate": { + "title": "Board duplizieren", + "message": "Dies wird das Board {name} mit all seinen Inhalten duplizieren. Wenn Widgets Integrationen, hinterlegt sind die Sie nicht verwenden dürfen, werden diese entfernt.", + "notification": { + "success": { + "title": "Board dupliziert", + "message": "Das Board wurde erfolgreich dupliziert" + }, + "error": { + "title": "Board konnte nicht dupliziert werden", + "message": "Das Board konnte nicht dupliziert werden" + } + } + }, + "edit": { + "notification": { + "success": { + "title": "Änderungen erfolgreich angewendet", + "message": "Das Board wurde erfolgreich dupliziert" + }, + "error": { + "title": "Änderungen konnten nicht angewendet werden", + "message": "Das Board konnte nicht gespeichert werden" + } + }, + "confirmLeave": { + "title": "Nicht gespeicherte Änderungen", + "message": "Sie haben ungespeicherte Änderungen, sind sie sicher dass sie abbrechen möchten?" + } + }, + "oldImport": { + "label": "Import von Homarr vor Version 1.0.0", + "notification": { + "success": { + "title": "Import erfolgreich", + "message": "Das Board wurde erfolgreich importiert" + }, + "error": { + "title": "Import fehlgeschlagen", + "message": "Das Board konnte nicht importiert werden, überprüfen Sie die Protokolle für weitere Details" + } + }, + "form": { + "file": { + "label": "Wähle eine JSON-Datei", + "invalidError": "Ungültige Konfigurationsdatei" + }, + "apps": { + "label": "", + "avoidDuplicates": { + "label": "Duplikate vermeiden", + "description": "Ignoriert Apps, bei denen eine App mit dem gleichen href bereits existiert" + }, + "onlyImportApps": { + "label": "Nur Apps importieren", + "description": "Fügt nur die Apps hinzu, das Board muss manuell neu erstellt werden" + } + }, + "name": { + "label": "Board Name" + }, + "screenSize": { + "label": "Bildschirmgröße", + "description": "In Versionen vor 1.0 gab es drei verschiedene Modi, so dass Sie die Anzahl der Spalten für jede Bildschirmgröße festlegen konnten.", + "option": { + "sm": "Klein", + "md": "Mittel", + "lg": "Groß" + } + }, + "sidebarBehavior": { + "label": "Verhalten der Seitenleiste", + "description": "Seitenleisten wurden in 1.0 entfernt, Sie können wählen, was mit den Elementen in ihnen passieren soll.", + "option": { + "lastSection": { + "label": "Letzter Abschnitt", + "description": "Seitenleiste wird unterhalb des letzten Abschnitts angezeigt" + }, + "removeItems": { + "label": "Elemente entfernen", + "description": "Elemente in der Seitenleiste werden entfernt" + } + } + } + } + }, + "quickCreateApp": { + "modal": { + "title": "Jetzt eine App erstellen", + "createAndUse": "Erstellen und verwenden" + } + } + }, + "field": { + "pageTitle": { + "label": "Seitentitel" + }, + "metaTitle": { + "label": "Metatitel" + }, + "logoImageUrl": { + "label": "URL des Logos" + }, + "faviconImageUrl": { + "label": "URL des Favicons" + }, + "backgroundImageUrl": { + "label": "URL des Hintergrundbildes" + }, + "backgroundImageAttachment": { + "label": "Anhang des Hintergrundbildes", + "option": { + "fixed": { + "label": "Fixiert", + "description": "Hintergrund bleibt in der gleichen Position." + }, + "scroll": { + "label": "Scrollen", + "description": "Hintergrund scrollt mit der Maus mit." + } + } + }, + "backgroundImageRepeat": { + "label": "Hintergrundbild wiederholen", + "option": { + "repeat": { + "label": "Wiederholen", + "description": "Das Bild wird so oft wiederholt, wie nötig, um den gesamten Bildbereich des Hintergrundbildes abzudecken." + }, + "no-repeat": { + "label": "Nicht wiederholen", + "description": "Das Bild wird nicht wiederholt und kann nicht den gesamten Raum füllen." + }, + "repeat-x": { + "label": "X Wiederholen", + "description": "Wie 'Wiederholen', nur auf der horizontalen Achse." + }, + "repeat-y": { + "label": "Y Wiederholen", + "description": "Wie 'Wiederholen', nur auf der horizontalen Achse." + } + } + }, + "backgroundImageSize": { + "label": "Hintergrundbild-Größe", + "option": { + "cover": { + "label": "", + "description": "Skaliert das Bild so klein wie möglich, um das gesamte Fenster zu bedecken, indem der überschüssige Bereich zugeschnitten wird." + }, + "contain": { + "label": "Enthält", + "description": "Skaliert das Bild so groß wie möglich innerhalb seines Behälters, ohne das Bild zu schneiden oder zu dehnen." + } + } + }, + "primaryColor": { + "label": "Primärfarbe" + }, + "secondaryColor": { + "label": "Sekundärfarbe" + }, + "opacity": { + "label": "Transparenz" + }, + "iconColor": { + "label": "Symbolfarbe" + }, + "customCss": { + "label": "Benutzerdefinierte css für dieses Board", + "description": "Außerdem können Sie Ihr Dashboard mittels CSS anpassen, dies wird nur für erfahrene Benutzer empfohlen", + "customClassesAlert": { + "title": "Benutzerdefinierte Klassen", + "description": "Sie können in den erweiterten Optionen jedes Elements eigene Klassen zu Ihren Board-Elementen hinzufügen und diese in dem benutzerdefinierten CSS oben verwenden." + } + }, + "disableStatus": { + "label": "App Status deaktivieren", + "description": "Deaktiviert die Statusüberprüfung für alle Apps auf diesem Board" + }, + "columnCount": { + "label": "Anzahl der Spalten" + }, + "itemRadius": { + "label": "Elementradius", + "description": "Ändert den Grad der Rundung ihrer Kacheln auf dem Board", + "option": { + "xs": "Sehr klein", + "sm": "Klein", + "md": "Mittel", + "lg": "Groß", + "xl": "Sehr groß" + } + }, + "name": { + "label": "" + }, + "isPublic": { + "label": "Öffentlich", + "description": "Öffentliche Boards sind für jedermann zugänglich, auch ohne Benutzerkonto." + } + }, + "content": { + "metaTitle": "{boardName} Board" + }, + "setting": { + "title": "Einstellungen für {boardName} Board", + "section": { + "general": { + "title": "Allgemein", + "unrecognizedLink": "Der angegebene Link wird nicht erkannt und wird nicht in der Vorschau angezeigt, er könnte jedoch trotzdem funktionieren." + }, + "layout": { + "title": "Ansicht", + "responsive": { + "title": "Reaktive Layouts", + "action": { + "add": "Layout hinzufügen" + } + } + }, + "background": { + "title": "Hintergrund" + }, + "appearance": { + "title": "Aussehen" + }, + "customCss": { + "title": "Benutzerdefiniertes CSS" + }, + "behavior": { + "title": "Verhalten" + }, + "access": { + "title": "Zugriffskontrolle", + "permission": { + "item": { + "view": { + "label": "Board anzeigen" + }, + "modify": { + "label": "Board bearbeiten" + }, + "full": { + "label": "Voller Zugriff" + } + } + } + }, + "dangerZone": { + "title": "Gefahrenbereich", + "action": { + "rename": { + "label": "Board umbenennen", + "description": "Das Ändern des Namens wird alle Links zu diesem Board aufheben.", + "button": "Namen ändern", + "modal": { + "title": "Board umbenennen" + } + }, + "visibility": { + "label": "Sichtbarkeit des Boards ändern", + "description": { + "public": "Dieses Board ist derzeit öffentlich.", + "private": "Dieses Board ist derzeit privat." + }, + "button": { + "public": "Als privat festlegen", + "private": "Als öffentlich festlegen" + }, + "confirm": { + "public": { + "title": "Board auf privat schalten", + "description": "Sind Sie sicher, dass Sie dieses Board privat machen möchten? Dies wird das Board vor der Öffentlichkeit ausblenden. Links für Gastbenutzer werden unbrauchbar." + }, + "private": { + "title": "Board auf öffentlich schalten", + "description": "Sind Sie sicher, dass Sie dieses Board öffentlich machen möchten? Dies wird das Board für alle zugänglich machen." + } + } + }, + "delete": { + "label": "Board löschen", + "description": "Sobald Sie ein Board löschen, gibt es kein zurück mehr. Bitte seien sie sich dessen bewusst.", + "button": "Board löschen", + "confirm": { + "title": "Board löschen", + "description": "Sind Sie sicher, dass Sie dieses Board löschen möchten? Dies wird das Board und seinen gesamten Inhalt permanent löschen." + } + } + } + } + } + }, + "error": { + "noBoard": { + "title": "Willkommen bei Homarr", + "description": "Ein schlankens, modernes Dashboard, das alle Ihre Apps und Dienste bereit stellt.", + "link": "Erstellen Sie Ihr erstes Board", + "notice": "Um diese Seite verschwinden zu lassen, erstellen Sie ein Board und setzen es als Home Board" + }, + "notFound": { + "title": "Board nicht gefunden", + "description": "Das angegebene Board wurde entweder nicht gefunden oder Sie haben keinen Zugriff darauf.", + "link": "Alle Boards anzeigen", + "notice": "Überprüfen Sie den Link oder wenden Sie sich an einen Administrator, wenn Sie glauben, dass er zugänglich sein sollte" + }, + "homeBoard": { + "title": "Kein Home Board", + "admin": { + "description": "Es wurde noch kein Home Board für den Server festgelegt.", + "link": "Serverweites Home Board konfigurieren", + "notice": "Um diese Seite für alle Benutzer verschwinden zu lassen, legen sie ein Home-Board für den Server fest" + }, + "user": { + "description": "Es wurde noch kein Home Board festgelegt.", + "link": "Home Board konfigurieren", + "notice": "Um diese Seite verschwinden zu lassen, geben Sie das Home Board in Ihren Einstellungen ein" + }, + "anonymous": { + "description": "Der Server-Administrator hat noch kein Home Board eingerichtet.", + "link": "Öffentliche Boards anzeigen", + "notice": "Um diese Seite verschwinden zu lassen, bitten sie den Server Administrator, ein Home Board für den Server festzulegen" + } + } + } + }, + "layout": { + "field": { + "name": { + "label": "" + }, + "columnCount": { + "label": "Anzahl der Spalten" + }, + "breakpoint": { + "label": "", + "description": "Das Layout wird auf allen Bildschirmen, die größer als dieser breakpoint sind, bis zum nächsten größeren breakpoint verwendet." + } + } + }, + "management": { + "metaTitle": "Verwaltung", + "title": { + "morning": "Guten Morgen, {username}", + "afternoon": "Guten Nachmittag, {username}", + "evening": "Guten Abend, {username}" + }, + "notFound": { + "title": "Nicht gefunden", + "text": "Konnte die angeforderte Ressource nicht finden" + }, + "navbar": { + "items": { + "home": "Startseite", + "boards": "", + "apps": "", + "integrations": "Integrationen", + "searchEngies": "Suchmaschinen", + "medias": "Medien", + "users": { + "label": "Benutzer", + "items": { + "manage": "Verwalten", + "invites": "Einladungen", + "groups": "Gruppen" + } + }, + "tools": { + "label": "Werkzeuge", + "items": { + "docker": "", + "kubernetes": "", + "logs": "", + "api": "", + "certificates": "Zertifikate", + "tasks": "Aufgaben" + } + }, + "settings": "Einstellungen", + "help": { + "label": "Hilfe", + "items": { + "documentation": "Dokumentation", + "submitIssue": "Ein Problem melden", + "discord": "", + "sourceCode": "Quellcode" + } + }, + "about": "Über" + } + }, + "page": { + "home": { + "statistic": { + "board": "", + "user": "Benutzer", + "invite": "Einladungen", + "integration": "Integrationen", + "app": "", + "group": "Gruppen" + }, + "statisticLabel": { + "boards": "", + "resources": "Ressourcen", + "authentication": "Authentifizierung", + "authorization": "Autorisierung" + } + }, + "board": { + "title": "Deine Boards", + "action": { + "new": { + "label": "Neues Board" + }, + "open": { + "label": "Board öffnen" + }, + "settings": { + "label": "Einstellungen" + }, + "setHomeBoard": { + "label": "Als Home Board festlegen", + "badge": { + "label": "Startseite", + "tooltip": "Dieses Board wird als Startboard angezeigt" + } + }, + "setMobileHomeBoard": { + "label": "Als Mobiles Board festlegen", + "badge": { + "label": "Mobil", + "tooltip": "Dieses Board wird als Ihr mobiles Board angezeigt" + } + }, + "duplicate": { + "label": "Board duplizieren" + }, + "delete": { + "label": "Dauerhaft löschen", + "confirm": { + "title": "Board löschen", + "description": "Sind Sie sicher, dass Sie das Board {name} löschen möchten?" + } + } + }, + "visibility": { + "public": "Dieses Board ist öffentlich", + "private": "Dieses Board ist privat" + }, + "modal": { + "createBoard": { + "field": { + "name": { + "label": "" + } + } + } + } + }, + "media": { + "includeFromAllUsers": "Medien von allen Benutzern einbeziehen" + }, + "user": { + "back": "Zurück zu den Benutzern", + "fieldsDisabledExternalProvider": "Bestimmte Felder sind deaktiviert, da sie von einem externen Authentifizierungsanbieter verwaltet werden.", + "setting": { + "general": { + "title": "Allgemein", + "item": { + "language": "Sprache und Region", + "board": { + "title": "Home Board", + "type": { + "general": "Allgemein", + "mobile": "Mobil" + } + }, + "search": "Suchen", + "firstDayOfWeek": "Erster Tag der Woche", + "accessibility": "Barrierefreiheit" + } + }, + "security": { + "title": "Sicherheit" + }, + "board": { + "title": "" + } + }, + "list": { + "metaTitle": "Verwaltung von Benutzern", + "title": "Benutzer" + }, + "edit": { + "metaTitle": "Benutzer bearbeiten: {username}" + }, + "create": { + "metaTitle": "Benutzer erstellen", + "title": "Neuen Benutzer erstellen", + "step": { + "personalInformation": { + "label": "Persönliche Information" + }, + "security": { + "label": "Sicherheit" + }, + "groups": { + "label": "Gruppen", + "title": "Wählen Sie alle Gruppen aus, wessen Benutzer Mitglieder sein sollen", + "description": "Die {everyoneGroup} Gruppe ist allen Benutzern zugewiesen und kann nicht entfernt werden." + }, + "review": { + "label": "Überprüfen" + }, + "completed": { + "title": "Benutzer erstellt" + }, + "error": { + "title": "Erstellung des Benutzers fehlgeschlagen" + } + }, + "action": { + "createAnother": "Weiteren Benutzer erstellen", + "back": "Zurück zur Benutzerliste" + } + }, + "invite": { + "title": "Verwalten von Benutzereinladungen", + "action": { + "new": { + "title": "Neue Einladung", + "description": "Nach Ablauf der Frist ist die Einladung nicht mehr gültig und der Empfänger der Einladung kann kein Konto mehr erstellen." + }, + "copy": { + "title": "Einladung kopieren", + "description": "Ihre Einladung wurde angelegt. Nach dem dieses Dialogfenster geschlossen wurde ist es nicht mehr möglich diesen Link zu kopieren. Falls Sie nicht mehr wünschen die entsprechende Person einzuladen können sie diese Einladung jederzeit löschen.", + "link": "Link zur Einladung", + "button": "Kopieren und schließen" + }, + "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": "" + }, + "creator": { + "label": "Ersteller" + }, + "expirationDate": { + "label": "Ablaufdatum" + }, + "token": { + "label": "" + } + } + } + }, + "group": { + "back": "Zurück zu den Gruppen", + "setting": { + "general": { + "title": "Allgemein", + "owner": "Eigentümer", + "ownerOfGroup": "Eigentümer dieser Gruppe", + "ownerOfGroupDeleted": "Der Eigentümer dieser Gruppe wurde gelöscht. Derzeit hat sie keinen Besitzer." + }, + "setting": { + "title": "Einstellungen", + "alert": "Gruppeneinstellungen werden nach der Reihenfolge der Gruppen in der Liste priorisiert. Die oberen Einstellungen überschreiben die unteren Einstellungen.", + "board": { + "title": "" + } + }, + "members": { + "title": "Mitglieder", + "search": "Mitglied suchen", + "notFound": "Keine Mitglieder gefunden" + }, + "permissions": { + "title": "Berechtigungen", + "form": { + "unsavedChanges": "Du hast noch ungespeicherte Änderungen!" + } + } + } + }, + "settings": { + "title": "Einstellungen", + "notification": { + "success": { + "message": "Einstellungen gespeichert" + }, + "error": { + "message": "Einstellungen konnten nicht gespeichert werden" + } + }, + "section": { + "analytics": { + "title": "Nutzungsdaten", + "general": { + "title": "Anonymisierte Nutzungsdaten senden", + "text": "Homarr schickt anonymisierte Nutzungsdaten mit der Open-Source-Software Umami. Es sammelt zu keinem Zeitpunkt personenbezogene Daten und ist daher vollständig DPR & CCPA konform. Wir empfehlen diese Nutzungsdaten zu aktivieren, da es unserem Open-Source-Team hilft Probleme zu identifizieren und unserem Backlog ermöglicht Priorität einzuräumen." + }, + "widgetData": { + "title": "Widget Daten", + "text": "Senden Sie die Widgets (und deren Anzahl) die Sie konfiguriert haben. Enthält keine URLs, Namen oder andere Daten." + }, + "integrationData": { + "title": "Integrationsdaten", + "text": "Senden Sie die Intregrationen (und deren Anzahl) die Sie konfiguriert haben. Enthält keine URLs, Namen oder andere Daten." + }, + "usersData": { + "title": "Benutzerdaten", + "text": "Senden Sie die Anzahl der Benutzer und ob Sie SSO aktiviert haben" + } + }, + "crawlingAndIndexing": { + "title": "Suche und Indexiere", + "warning": "Das Aktivieren oder Deaktivieren von Einstellungen hier wird schwerwiegende Auswirkungen haben, wie Suchmaschinen Ihre Seite suchen und indizieren. Jede Einstellung startet eine Anforderung und es ist die Aufgabe des Crawlers, diese Einstellungen zu übernehmen. Änderungen können mehrere Tage oder Wochen dauern. Einige Einstellungen können Suchmaschinenspezifisch sein.", + "noIndex": { + "title": "Kein Index", + "text": "Die Webseite nicht in Suchmaschinen indizieren und in keinem Suchergebnis anzeigen" + }, + "noFollow": { + "title": "Keine Folgen", + "text": "Folgen Sie keine Links während der Indexierung. Deaktivieren kann dazu führen, dass Crawler versuchen, allen Links auf Homarr zu folgen." + }, + "noTranslate": { + "title": "Nicht Übersetzen", + "text": "Wenn die Sprache der Seite wahrscheinlich nicht zu lesen ist wird der Benutzer höchstwahrscheinlich wollen das Google einen Übersetzungslink in den Suchergebnissen zeigt" + }, + "noSiteLinksSearchBox": { + "title": "Kein Suchfeld für Seitenlinks", + "text": "Google baut ein Suchfeld mit den gecrawlten Links zusammen mit anderen direkten Links auf. Durch das Aktivieren dieser Option wird Google aufgefordert, dieses Feld zu deaktivieren." + } + }, + "board": { + "title": "", + "homeBoard": { + "label": "Globales Home Board", + "mobileLabel": "Globales Mobil Board", + "description": "Nur öffentliche Boards stehen zur Auswahl" + }, + "status": { + "title": "App Status", + "enableStatusByDefault": { + "label": "Status standardmäßig aktivieren", + "description": "Beim Hinzufügen eines App-Elements wird der Status standardmäßig aktiviert" + }, + "forceDisableStatus": { + "label": "Status deaktivieren erzwingen", + "description": "Status für Apps wird für alle Benutzer deaktiviert und kann nicht aktiviert werden" + } + } + }, + "search": { + "title": "Suche", + "defaultSearchEngine": { + "label": "Standard Suchmaschine", + "description": "Integrations Suchmaschinen können hier nicht ausgewählt werden" + } + }, + "appearance": { + "title": "Aussehen", + "defaultColorScheme": { + "label": "Standard Farbschema", + "options": { + "light": "Hell", + "dark": "Dunkel" + } + } + }, + "culture": { + "title": "Kultur", + "defaultLocale": { + "label": "Standardsprache" + } + } + } + }, + "tool": { + "tasks": { + "title": "Aufgaben", + "status": { + "idle": "Inaktiv", + "running": "Wird ausgeführt", + "error": "Fehler" + }, + "job": { + "minecraftServerStatus": { + "label": "Status des Minecraft Servers" + }, + "iconsUpdater": { + "label": "Symbol Updater" + }, + "analytics": { + "label": "Nutzungsdaten" + }, + "smartHomeEntityState": { + "label": "Status der Smart Home Entitäten" + }, + "ping": { + "label": "" + }, + "mediaServer": { + "label": "Medien Server" + }, + "mediaOrganizer": { + "label": "Medienorganisatoren" + }, + "downloads": { + "label": "" + }, + "mediaRequestStats": { + "label": "Statistik der Medienanfragen" + }, + "mediaRequestList": { + "label": "Liste der Medienanfragen" + }, + "rssFeeds": { + "label": "RSS Feeds" + }, + "indexerManager": { + "label": "Index Manager" + }, + "healthMonitoring": { + "label": "Überwachung des Gesundheit" + }, + "dnsHole": { + "label": "DNS Hole Daten" + }, + "sessionCleanup": { + "label": "Sitzung bereinigen" + }, + "updateChecker": { + "label": "Updateprüfer" + }, + "mediaTranscoding": { + "label": "Medien Transkodierung" + } + } + }, + "api": { + "title": "", + "modal": { + "createApiToken": { + "title": "API Token erstellt", + "description": "API Token wurde erstellt. Seien Sie vorsichtig, dieser Token wird in der Datenbank verschlüsselt und nie wieder an Sie übertragen. Wenn Sie diesen Token verlegen, können Sie ihn nicht mehr abrufen.", + "button": "Kopieren und schließen" + } + }, + "tab": { + "documentation": { + "label": "Dokumentation" + }, + "apiKey": { + "label": "Authentifizierung", + "title": "API Schlüssel", + "button": { + "createApiToken": "API Token erstellen" + }, + "modal": { + "delete": { + "title": "API Token löschen", + "text": "Dies wird den API Token permanent löschen. API-Clients mit diesem Token können sich nicht mehr authentifizieren und API Anfragen durchführen. Diese Aktion kann nicht rückgängig gemacht werden." + } + }, + "table": { + "header": { + "id": "", + "createdBy": "Erstellt von", + "actions": "Aktionen" + } + } + } + } + } + }, + "about": { + "version": "", + "text": "Homarr ist ein gemeinschaftsorientiertes Open-Source Projekt, das von Freiwilligen betreut wird. Dank dieser Menschen ist Homarr seit 2021 ein wachsendes Projekt. Unser Team, sitzhaft mit ihren Migliedern in vielen verschiedenen Ländern, arbeitet in ihrer Freizeit ohne Vergütung.", + "accordion": { + "contributors": { + "title": "Mitwirkende", + "subtitle": "{count} Wartung von Code & Homarr" + }, + "translators": { + "title": "Übersetzer", + "subtitle": "{count} tragen Übersetzungen in viele Sprachen bei" + }, + "libraries": { + "title": "Bibliotheken", + "subtitle": "{count} im Code von Homarr verwendet" + } + } + } + } + }, + "docker": { + "title": "Container", + "table": { + "updated": "Aktualisiert {when}", + "search": "{count} Container durchsuchen", + "selected": "{selectCount} von {totalCount} ausgewählten Containern" + }, + "field": { + "name": { + "label": "" + }, + "state": { + "label": "Staat", + "option": { + "created": "Erstellt", + "running": "Aktiv", + "paused": "Pausiert", + "restarting": "Startet neu", + "exited": "Beendet", + "removing": "Wird entfernt", + "dead": "Tot" + } + }, + "containerImage": { + "label": "" + }, + "ports": { + "label": "" + } + }, + "action": { + "start": { + "label": "Starten", + "notification": { + "success": { + "title": "Container gestartet", + "message": "Die Container wurden erfolgreich gestartet" + }, + "error": { + "title": "Container nicht gestartet", + "message": "Die Container konnten nicht gestartet werden" + } + } + }, + "stop": { + "label": "Stopp", + "notification": { + "success": { + "title": "Container gestoppt", + "message": "Die Container wurden erfolgreich gestoppt" + }, + "error": { + "title": "Container nicht gestoppt", + "message": "Die Container konnten nicht gestoppt werden" + } + } + }, + "restart": { + "label": "Neustarten", + "notification": { + "success": { + "title": "Container neu gestartet", + "message": "Die Container wurden erfolgreich neu gestartet" + }, + "error": { + "title": "Container nicht neu gestartet", + "message": "Die Container konnten nicht neu gestartet werden" + } + } + }, + "remove": { + "label": "Entfernen", + "notification": { + "success": { + "title": "Container entfernt", + "message": "Die Container wurden erfolgreich entfernt" + }, + "error": { + "title": "Container nicht entfernt", + "message": "Die Behälter konnten nicht entfernt werden" + } + } + }, + "refresh": { + "label": "Aktualisieren", + "notification": { + "success": { + "title": "Container aktualisiert", + "message": "Sie sehen jetzt die neuesten Daten" + }, + "error": { + "title": "Container nicht aktualisiert", + "message": "Beim Aktualisieren der Container ist ein Fehler aufgetreten" + } + } + }, + "addToHomarr": { + "label": "Zu Homarr hinzufügen", + "notification": { + "success": { + "title": "Zu Homarr hinzufügen", + "message": "Ausgewählte Apps wurden zu Homarr hinzugefügt" + }, + "error": { + "title": "Konnte nicht zu Homarr hinzugefügt werden", + "message": "Ausgewählte Apps konnten nicht zu Homarr hinzugefügt werden" + } + }, + "modal": { + "title": "Docker Container zu Homarr hinzufügen" + } + } + }, + "error": { + "internalServerError": "Fehler beim Abrufen der Docker Container" + } + }, + "kubernetes": { + "cluster": { + "title": "", + "label": "", + "providers": "Anbieter", + "version": "", + "architecture": "Architektur", + "capacity": { + "title": "Kapazität", + "resource": { + "reserved": "Reserviert", + "used": "Benutzt" + } + }, + "resources": { + "title": "Ressourcen", + "nodes": "", + "namespaces": "Namensräume", + "ingresses": "Ingresse", + "services": "Dienste", + "pods": "", + "configmaps": "", + "secrets": "", + "volumes": "Laufwerke" + } + }, + "nodes": { + "label": "", + "field": { + "name": { + "label": "" + }, + "state": { + "label": "Zustand", + "option": { + "ready": "Bereit", + "NotReady": "Nicht Bereit" + } + }, + "cpu": { + "label": "" + }, + "memory": { + "label": "" + }, + "pods": { + "label": "" + }, + "operatingSystem": { + "label": "" + }, + "architecture": { + "label": "Architektur" + }, + "kubernetesVersion": { + "label": "Kubernetes Version" + }, + "creationTimestamp": { + "label": "Erstellt" + } + }, + "table": { + "search": "{count} Nodes durchsuchen" + } + }, + "namespaces": { + "label": "Namensräume", + "field": { + "name": { + "label": "" + }, + "state": { + "label": "Zustand", + "option": { + "active": "Aktiv", + "terminating": "Beendet" + } + }, + "creationTimestamp": { + "label": "Erstellt" + } + }, + "table": { + "search": "{count} Namensräume durchsuchen" + } + }, + "ingresses": { + "label": "Ingresse", + "field": { + "name": { + "label": "" + }, + "namespace": { + "label": "Namensraum" + }, + "className": { + "label": "Klassenname" + }, + "rulesAndPaths": { + "label": "Regeln & Pfade" + }, + "creationTimestamp": { + "label": "Erstellt" + } + }, + "table": { + "search": "{count} Ingresse durchsuchen" + } + }, + "services": { + "label": "Dienste", + "field": { + "name": { + "label": "" + }, + "namespace": { + "label": "Namensraum" + }, + "type": { + "label": "Typ" + }, + "ports": { + "label": "" + }, + "targetPorts": { + "label": "Zielports" + }, + "clusterIP": { + "label": "" + }, + "creationTimestamp": { + "label": "Erstellt" + } + }, + "table": { + "search": "{count} Services durchsuchen" + } + }, + "pods": { + "label": "", + "field": { + "name": { + "label": "" + }, + "namespace": { + "label": "Namensraum" + }, + "image": { + "label": "" + }, + "applicationType": { + "label": "Typ der Anwendung" + }, + "status": { + "label": "" + }, + "creationTimestamp": { + "label": "Erstellt" + } + }, + "table": { + "search": "{count} Pods durchsuchen" + } + }, + "secrets": { + "label": "", + "field": { + "name": { + "label": "" + }, + "namespace": { + "label": "namensraum" + }, + "type": { + "label": "typ" + }, + "creationTimestamp": { + "label": "Erstellt" + } + }, + "table": { + "search": "{count} Secrets durchsuchen" + } + }, + "configmaps": { + "label": "", + "field": { + "name": { + "label": "" + }, + "namespace": { + "label": "namensraum" + }, + "creationTimestamp": { + "label": "Erstellt" + } + }, + "table": { + "search": "{count} configMaps durchsuchen" + } + }, + "volumes": { + "label": "Laufwerke", + "field": { + "name": { + "label": "" + }, + "namespace": { + "label": "Namensraum" + }, + "accessModes": { + "label": "Zugriffsmodi" + }, + "storage": { + "label": "Speicher" + }, + "storageClassName": { + "label": "Name der Speicherklasse" + }, + "volumeMode": { + "label": "Laufwerksmodus" + }, + "volumeName": { + "label": "Laufwerkname" + }, + "status": { + "label": "" + }, + "creationTimestamp": { + "label": "Erstellt" + } + }, + "table": { + "search": "{count} Laufwerke durchsuchen" + } + }, + "error": { + "internalServerError": "Fehler beim Abrufen der Kubernetes Daten" + } + }, + "permission": { + "title": "Berechtigungen", + "userSelect": { + "title": "Benutzerberechtigung hinzufügen" + }, + "groupSelect": { + "title": "Gruppenberechtigung hinzufügen" + }, + "tab": { + "user": "Benutzer", + "group": "Gruppen", + "inherited": "Vererbte Gruppen" + }, + "field": { + "user": { + "label": "Benutzer" + }, + "group": { + "label": "Gruppe" + }, + "permission": { + "label": "Berechtigung" + } + }, + "action": { + "saveUser": "Benutzerberechtigung hinzufügen", + "saveGroup": "Gruppenberechtigung hinzufügen" + } + }, + "navigationStructure": { + "manage": { + "label": "Verwalten", + "boards": { + "label": "" + }, + "integrations": { + "label": "Integrationen", + "edit": { + "label": "Bearbeiten" + }, + "new": { + "label": "Neu" + } + }, + "search-engines": { + "label": "Suchmaschinen", + "new": { + "label": "Neu" + }, + "edit": { + "label": "Bearbeiten" + } + }, + "medias": { + "label": "Medien" + }, + "apps": { + "label": "", + "new": { + "label": "Neu" + }, + "edit": { + "label": "Bearbeiten" + } + }, + "users": { + "label": "Benutzer", + "create": { + "label": "Erstellen" + }, + "general": "Allgemein", + "security": "Sicherheit", + "board": "", + "groups": { + "label": "Gruppen" + }, + "invites": { + "label": "Einladungen" + } + }, + "tools": { + "label": "Werkzeuge", + "docker": { + "label": "" + }, + "kubernetes": { + "label": "", + "nodes": { + "label": "" + }, + "namespaces": { + "label": "Namensräume" + }, + "ingresses": { + "label": "Ingresse" + }, + "services": { + "label": "Dienste" + }, + "pods": { + "label": "" + }, + "configmaps": { + "label": "" + }, + "secrets": { + "label": "" + }, + "volumes": { + "label": "Laufwerke" + } + }, + "logs": { + "label": "" + }, + "certificates": { + "label": "Zertifikate" + } + }, + "settings": { + "label": "Einstellungen" + }, + "about": { + "label": "Über" + } + } + }, + "search": { + "placeholder": "Nach allem suchen", + "nothingFound": "Keine Treffer", + "error": { + "fetch": "Beim Abrufen der Daten ist ein Fehler aufgetreten" + }, + "mode": { + "appIntegrationBoard": { + "help": "Nach Apps, Integrationen oder Boards suchen", + "group": { + "app": { + "title": "", + "children": { + "action": { + "open": { + "label": "App URL öffnen" + }, + "edit": { + "label": "App bearbeiten" + } + }, + "detail": { + "title": "Wählen Sie eine Aktion für die App" + } + } + }, + "board": { + "title": "", + "children": { + "action": { + "open": { + "label": "Board öffnen" + }, + "homeBoard": { + "label": "Als Home Board festlegen" + }, + "mobileBoard": { + "label": "Als mobiles Board festlegen" + }, + "settings": { + "label": "Einstellungen öffnen" + } + }, + "detail": { + "title": "Wählen Sie eine Aktion für die App" + } + } + }, + "integration": { + "title": "Integrationen" + } + } + }, + "command": { + "help": "Befehlsmodus aktivieren", + "group": { + "localCommand": { + "title": "Lokale Befehle" + }, + "globalCommand": { + "title": "Globale Befehle", + "option": { + "colorScheme": { + "light": "Zum hellen Design wechseln", + "dark": "Zum dunklen Design wechseln" + }, + "language": { + "label": "Sprache ändern", + "children": { + "detail": { + "title": "Bitte wählen Sie Ihre Sprache aus" + } + } + }, + "newBoard": { + "label": "Erstellen Sie ein neues Board" + }, + "importBoard": { + "label": "Ein Board importieren" + }, + "newApp": { + "label": "Neue App erstellen" + }, + "newIntegration": { + "label": "Neue Integration erstellen", + "children": { + "detail": { + "title": "Wählen Sie den Integrationstyp aus, den Sie erstellen möchten" + } + } + }, + "newUser": { + "label": "Neuen Benutzer erstellen" + }, + "newInvite": { + "label": "Neue Einladung erstellen" + }, + "newGroup": { + "label": "Neue Gruppe erstellen" + } + } + } + } + }, + "media": { + "requestMovie": "Einen Film anfordern", + "requestSeries": "Eine Serie anfordern", + "openIn": "In {kind} öffnen" + }, + "external": { + "help": "Externe Suchmaschine verwenden", + "group": { + "searchEngine": { + "title": "Suchmaschinen", + "children": { + "action": { + "search": { + "label": "Suche mit {name}" + } + }, + "detail": { + "title": "Eine Aktion für die Suchmaschine auswählen" + }, + "searchResults": { + "title": "Wählen Sie ein Suchergebnis für weitere Aktionen" + } + }, + "option": { + "google": { + "name": "", + "description": "Das Web mit Google durchsuchen" + }, + "bing": { + "name": "", + "description": "Das Web mit Bing durchsuchen" + }, + "duckduckgo": { + "name": "", + "description": "Das Web mit DuckDuckGo durchsuchen" + }, + "torrent": { + "name": "", + "description": "Suche nach Torrents auf torrentdownloads.pro" + }, + "youTube": { + "name": "", + "description": "Suche nach Videos auf YouTube" + } + } + } + } + }, + "help": { + "group": { + "mode": { + "title": "Modi" + }, + "help": { + "title": "Hilfe", + "option": { + "documentation": { + "label": "Dokumentation" + }, + "submitIssue": { + "label": "Ein Problem melden" + }, + "discord": { + "label": "" + } + } + } + } + }, + "home": { + "group": { + "search": { + "title": "Suchen", + "option": { + "other": { + "label": "Mit einer anderen Suchmaschine suchen" + }, + "no-default": { + "label": "Keine Standard Suchmaschine festgelegt", + "description": "Eine Standard Suchmaschine in den Einstellungen festlegen" + }, + "search": { + "label": "Suche nach \"{query}\" mit {name}" + }, + "from-integration": { + "description": "Tippen um zu Suchen" + } + } + }, + "local": { + "title": "Lokale Ergebnisse" + } + } + }, + "page": { + "help": "Nach Seiten suchen", + "group": { + "page": { + "title": "Seiten", + "option": { + "manageHome": { + "label": "Home Board verwalten" + }, + "manageBoard": { + "label": "Boards verwalten" + }, + "manageApp": { + "label": "Apps verwalten" + }, + "manageIntegration": { + "label": "Integrationen verwalten" + }, + "manageSearchEngine": { + "label": "Suchmaschinen verwalten" + }, + "manageMedia": { + "label": "Medien verwalten" + }, + "manageUser": { + "label": "Verwaltung von Benutzern" + }, + "manageInvite": { + "label": "Einladungen verwalten" + }, + "manageGroup": { + "label": "Gruppen verwalten" + }, + "manageDocker": { + "label": "Docker verwalten" + }, + "manageApi": { + "label": "" + }, + "manageLog": { + "label": "Logs anzeigen" + }, + "manageTask": { + "label": "Aufgaben verwalten" + }, + "manageSettings": { + "label": "Allgemeine Einstellungen" + }, + "about": { + "label": "Über" + }, + "homeBoard": { + "label": "Home Board" + }, + "preferences": { + "label": "Ihre Einstellungen" + } + } + } + } + }, + "userGroup": { + "help": "Nach Benutzer oder Gruppe suchen", + "group": { + "user": { + "title": "Benutzer", + "children": { + "action": { + "detail": { + "label": "Benutzerdetails anzeigen" + } + }, + "detail": { + "title": "Eine Aktion für den Benutzer auswählen" + } + } + }, + "group": { + "title": "Gruppen", + "children": { + "action": { + "detail": { + "label": "Gruppendetails anzeigen" + }, + "manageMember": { + "label": "Mitglieder verwalten" + }, + "managePermission": { + "label": "Berechtigungen verwalten" + } + }, + "detail": { + "title": "Wählen Sie eine Aktion für diese Gruppe" + } + } + } + } + } + }, + "engine": { + "search": "Nach einer Suchmaschine suchen", + "field": { + "name": { + "label": "" + }, + "short": { + "label": "Kurz" + }, + "urlTemplate": { + "label": "URL Suchvorlage" + }, + "description": { + "label": "Beschreibung" + } + }, + "page": { + "list": { + "title": "Suchmaschinen", + "noResults": { + "title": "Noch keine Suchmaschinen vorhanden", + "action": "Erstelle deine erste Suchmaschine" + }, + "interactive": "Interaktiv, verwendet eine Integration" + }, + "create": { + "title": "Neue Suchmaschine", + "notification": { + "success": { + "title": "Suchmaschine angelegt", + "message": "Die Suchmaschine wurde erfolgreich angelegt" + }, + "error": { + "title": "Die Suchmaschine wurde nicht angelegt", + "message": "Die Suchmaschine konnte nicht erstellt werden" + } + } + }, + "edit": { + "title": "Suchmaschine bearbeiten", + "notification": { + "success": { + "title": "Änderungen erfolgreich angewendet", + "message": "Suchmaschine wurde erfolgreich gespeichert" + }, + "error": { + "title": "Änderungen konnten nicht angewendet werden", + "message": "Die Suchmaschine konnte nicht gespeichert werden" + } + }, + "configControl": "Konfiguration", + "searchEngineType": { + "generic": "Allgemein", + "fromIntegration": "Aus den Integration" + } + }, + "delete": { + "title": "Suchmaschine löschen", + "message": "Sind Sie sicher, dass Sie die Suchmaschine {name} löschen möchten?", + "notification": { + "success": { + "title": "Suchmaschine gelöscht", + "message": "Die Suchmaschine wurde erfolgreich gelöscht" + }, + "error": { + "title": "Die Suchmaschine wurde nicht gelöscht", + "message": "Die Suchmaschine konnte nicht gelöscht werden" + } + } + } + }, + "media": { + "request": { + "modal": { + "title": "\"{name} \" anfordern", + "table": { + "header": { + "season": "Staffel", + "episodes": "Episoden" + } + }, + "button": { + "send": "Anfrage senden" + } + } + } + } + } + }, + "certificate": { + "page": { + "list": { + "title": "Vertrauenswürdige Zertifikate", + "description": "Wird von Homarr verwendet, um Daten von Integrationen anzufordern.", + "noResults": { + "title": "Es gibt noch keine Zertifikate" + }, + "expires": "Gültig bis {when}" + } + }, + "action": { + "create": { + "label": "Zertifikat hinzufügen", + "notification": { + "success": { + "title": "Zertifikat hinzugefügt", + "message": "Das Zertifikat wurde erfolgreich hinzugefügt" + }, + "error": { + "title": "Ein Fehler ist beim hinzufügen des Zertifikats aufgetreten", + "message": "Das Zertifikat konnte nicht hinzugefügt werden" + } + } + }, + "remove": { + "label": "Zertifikat entfernen", + "confirm": "Sind Sie sicher, dass Sie das Zertifikat entfernen möchten?", + "notification": { + "success": { + "title": "Zertifikat entfernt", + "message": "Das Zertifikat wurde erfolgreich entfern" + }, + "error": { + "title": "Zertifikat nicht entfernt", + "message": "Das Zertifikat konnte nicht entfernt werden" + } + } + } + } + } +} diff --git a/packages/translation/src/lang/de.json b/packages/translation/src/lang/de.json index 17f11a112..69db83331 100644 --- a/packages/translation/src/lang/de.json +++ b/packages/translation/src/lang/de.json @@ -980,6 +980,9 @@ "remove": "Dynamischen Abschnitt entfernen" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "Rahmenfarbe" } @@ -1721,7 +1724,11 @@ "noIntegration": "Keine Integration ausgewählt", "noData": "Keine Integrationsdaten verfügbar" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Videostream", diff --git a/packages/translation/src/lang/el.json b/packages/translation/src/lang/el.json index c7b67ab5f..57b304b3a 100644 --- a/packages/translation/src/lang/el.json +++ b/packages/translation/src/lang/el.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Ροή Βίντεο", diff --git a/packages/translation/src/lang/en-gb.json b/packages/translation/src/lang/en-gb.json new file mode 100644 index 000000000..4e6679e6b --- /dev/null +++ b/packages/translation/src/lang/en-gb.json @@ -0,0 +1,3759 @@ +{ + "init": { + "step": { + "start": { + "title": "", + "subtitle": "", + "description": "", + "action": { + "scratch": "", + "importOldmarr": "" + } + }, + "import": { + "title": "", + "subtitle": "", + "dropzone": { + "title": "", + "description": "" + }, + "fileInfo": { + "action": { + "change": "" + } + }, + "importSettings": { + "title": "", + "description": "" + }, + "boardSelection": { + "title": "", + "description": "", + "action": { + "selectAll": "", + "unselectAll": "" + } + }, + "summary": { + "title": "", + "description": "", + "action": { + "import": "" + }, + "entities": { + "apps": "", + "boards": "", + "integrations": "", + "credentialUsers": "" + } + }, + "tokenModal": { + "title": "", + "field": { + "token": { + "label": "", + "description": "" + } + }, + "notification": { + "error": { + "title": "", + "message": "" + } + } + } + }, + "user": { + "title": "", + "subtitle": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "" + } + } + }, + "group": { + "title": "", + "subtitle": "", + "form": { + "name": { + "label": "", + "description": "" + } + } + }, + "settings": { + "title": "", + "subtitle": "" + }, + "finish": { + "title": "", + "subtitle": "", + "description": "", + "action": { + "goToBoard": "", + "createBoard": "", + "inviteUser": "", + "docs": "" + } + } + }, + "backToStart": "" + }, + "user": { + "title": "", + "name": "", + "page": { + "login": { + "title": "", + "subtitle": "" + }, + "invite": { + "title": "", + "subtitle": "", + "description": "" + }, + "init": { + "title": "", + "subtitle": "" + } + }, + "field": { + "email": { + "label": "", + "verified": "" + }, + "username": { + "label": "" + }, + "password": { + "label": "", + "requirement": { + "length": "", + "lowercase": "", + "uppercase": "", + "number": "", + "special": "" + } + }, + "passwordConfirm": { + "label": "" + }, + "previousPassword": { + "label": "" + }, + "homeBoard": { + "label": "" + }, + "pingIconsEnabled": { + "label": "" + }, + "defaultSearchEngine": { + "label": "" + }, + "openSearchInNewTab": { + "label": "" + } + }, + "error": { + "usernameTaken": "" + }, + "action": { + "login": { + "label": "", + "labelWith": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + }, + "forgotPassword": { + "label": "", + "description": "" + } + }, + "register": { + "label": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + }, + "create": "", + "changePassword": { + "label": "", + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + } + } + }, + "changeHomeBoard": { + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + } + } + }, + "changeSearchPreferences": { + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + } + } + }, + "changeFirstDayOfWeek": { + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + } + } + }, + "changePingIconsEnabled": { + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + } + } + }, + "manageAvatar": { + "changeImage": { + "label": "", + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + }, + "toLarge": { + "title": "", + "message": "" + } + } + }, + "removeImage": { + "label": "", + "confirm": "", + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + } + } + } + }, + "editProfile": { + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + } + } + }, + "delete": { + "label": "", + "description": "", + "confirm": "" + }, + "select": { + "label": "", + "notFound": "" + }, + "transfer": { + "label": "" + } + } + }, + "group": { + "title": "", + "name": "", + "search": "", + "field": { + "name": "", + "members": "", + "homeBoard": { + "label": "", + "description": "" + }, + "mobileBoard": { + "label": "", + "description": "" + } + }, + "permission": { + "admin": { + "title": "", + "item": { + "admin": { + "label": "", + "description": "" + } + } + }, + "app": { + "title": "", + "item": { + "create": { + "label": "", + "description": "" + }, + "use-all": { + "label": "", + "description": "" + }, + "modify-all": { + "label": "", + "description": "" + }, + "full-all": { + "label": "", + "description": "" + } + } + }, + "board": { + "title": "", + "item": { + "create": { + "label": "", + "description": "" + }, + "view-all": { + "label": "", + "description": "" + }, + "modify-all": { + "label": "", + "description": "" + }, + "full-all": { + "label": "", + "description": "" + } + } + }, + "integration": { + "title": "", + "item": { + "create": { + "label": "", + "description": "" + }, + "use-all": { + "label": "", + "description": "" + }, + "interact-all": { + "label": "", + "description": "" + }, + "full-all": { + "label": "", + "description": "" + } + } + }, + "media": { + "title": "", + "item": { + "upload": { + "label": "", + "description": "" + }, + "view-all": { + "label": "", + "description": "" + }, + "full-all": { + "label": "", + "description": "" + } + } + }, + "other": { + "title": "", + "item": { + "view-logs": { + "label": "", + "description": "" + } + } + }, + "search-engine": { + "title": "", + "item": { + "create": { + "label": "", + "description": "" + }, + "modify-all": { + "label": "", + "description": "" + }, + "full-all": { + "label": "", + "description": "" + } + } + } + }, + "memberNotice": { + "mixed": "", + "external": "" + }, + "reservedNotice": { + "message": "" + }, + "action": { + "create": { + "label": "", + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + } + } + }, + "transfer": { + "label": "", + "description": "", + "confirm": "", + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + } + } + }, + "addMember": { + "label": "" + }, + "removeMember": { + "label": "", + "confirm": "" + }, + "delete": { + "label": "", + "description": "", + "confirm": "", + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + } + } + }, + "changePermissions": { + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + }, + "update": { + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + } + } + }, + "select": { + "label": "", + "notFound": "" + }, + "settings": { + "board": { + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + } + }, + "changePosition": { + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + } + } + } + }, + "defaultGroup": { + "name": "", + "description": "" + } + }, + "app": { + "search": "", + "page": { + "list": { + "title": "", + "noResults": { + "title": "", + "action": "" + } + }, + "create": { + "title": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + }, + "edit": { + "title": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + }, + "delete": { + "title": "", + "message": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + } + }, + "field": { + "name": { + "label": "" + }, + "description": { + "label": "" + }, + "url": { + "label": "" + }, + "useDifferentUrlForPing": { + "checkbox": { + "label": "", + "description": "" + } + } + }, + "action": { + "select": { + "label": "", + "notFound": "" + } + } + }, + "integration": { + "page": { + "list": { + "title": "", + "search": "", + "noResults": { + "title": "" + } + }, + "create": { + "title": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + }, + "edit": { + "title": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + }, + "delete": { + "title": "", + "message": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + } + }, + "field": { + "name": { + "label": "" + }, + "url": { + "label": "" + }, + "attemptSearchEngineCreation": { + "label": "", + "description": "" + }, + "createApp": { + "label": "", + "description": "" + }, + "appHref": { + "placeholder": "" + } + }, + "action": { + "create": "" + }, + "testConnection": { + "action": { + "create": "", + "edit": "" + }, + "alertNotice": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "invalidUrl": { + "title": "", + "message": "" + }, + "secretNotDefined": { + "title": "", + "message": "" + }, + "invalidCredentials": { + "title": "", + "message": "" + }, + "commonError": { + "title": "", + "message": "" + }, + "badRequest": { + "title": "", + "message": "" + }, + "unauthorized": { + "title": "", + "message": "" + }, + "forbidden": { + "title": "", + "message": "" + }, + "notFound": { + "title": "", + "message": "" + }, + "internalServerError": { + "title": "", + "message": "" + }, + "serviceUnavailable": { + "title": "", + "message": "" + }, + "connectionAborted": { + "title": "", + "message": "" + }, + "domainNotFound": { + "title": "", + "message": "" + }, + "connectionRefused": { + "title": "", + "message": "" + }, + "invalidJson": { + "title": "", + "message": "" + }, + "wrongPath": { + "title": "", + "message": "" + } + } + }, + "secrets": { + "title": "", + "lastUpdated": "", + "notSet": { + "label": "", + "tooltip": "" + }, + "secureNotice": "", + "reset": { + "title": "", + "message": "" + }, + "noSecretsRequired": { + "segmentTitle": "", + "text": "" + }, + "kind": { + "username": { + "label": "", + "newLabel": "" + }, + "apiKey": { + "label": "", + "newLabel": "" + }, + "password": { + "label": "", + "newLabel": "" + }, + "tokenId": { + "label": "", + "newLabel": "" + }, + "realm": { + "label": "", + "newLabel": "" + } + } + }, + "permission": { + "use": "", + "interact": "", + "full": "" + } + }, + "media": { + "plural": "", + "search": "", + "field": { + "name": "", + "size": "", + "creator": "" + }, + "action": { + "upload": { + "label": "", + "file": "", + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + } + } + }, + "delete": { + "label": "", + "description": "", + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + } + } + }, + "copy": { + "label": "" + }, + "open": { + "label": "" + } + } + }, + "common": { + "beta": "", + "error": "", + "action": { + "add": "", + "apply": "", + "backToOverview": "", + "create": "", + "createAnother": "", + "edit": "", + "import": "", + "insert": "", + "remove": "", + "save": "", + "saveChanges": "", + "cancel": "", + "delete": "", + "discard": "", + "confirm": "", + "continue": "", + "previous": "", + "next": "", + "checkoutDocs": "", + "checkLogs": "", + "tryAgain": "", + "loading": "" + }, + "here": "", + "iconPicker": { + "label": "", + "header": "" + }, + "colorScheme": { + "options": { + "light": "", + "dark": "" + } + }, + "information": { + "min": "", + "max": "", + "days": "", + "hours": "", + "minutes": "" + }, + "notification": { + "create": { + "success": "", + "error": "" + }, + "delete": { + "success": "", + "error": "" + }, + "update": { + "success": "", + "error": "" + }, + "transfer": { + "success": "", + "error": "" + } + }, + "multiSelect": { + "placeholder": "" + }, + "multiText": { + "placeholder": "", + "addLabel": "" + }, + "select": { + "placeholder": "", + "badge": { + "recommended": "" + } + }, + "userAvatar": { + "menu": { + "switchToDarkMode": "", + "switchToLightMode": "", + "management": "", + "preferences": "", + "logout": "", + "login": "", + "homeBoard": "", + "loggedOut": "", + "updateAvailable": "" + } + }, + "dangerZone": "", + "noResults": "", + "unsavedChanges": "", + "preview": { + "show": "", + "hide": "" + }, + "zod": { + "errors": { + "default": "", + "required": "", + "string": { + "startsWith": "", + "endsWith": "", + "includes": "", + "invalidEmail": "" + }, + "tooSmall": { + "string": "", + "number": "" + }, + "tooBig": { + "string": "", + "number": "" + }, + "custom": { + "passwordsDoNotMatch": "", + "passwordRequirements": "", + "boardAlreadyExists": "", + "invalidFileType": "", + "invalidFileName": "", + "fileTooLarge": "", + "invalidConfiguration": "", + "groupNameTaken": "" + } + } + } + }, + "section": { + "dynamic": { + "action": { + "create": "", + "remove": "" + }, + "option": { + "title": { + "label": "" + }, + "borderColor": { + "label": "" + } + }, + "remove": { + "title": "", + "message": "" + } + }, + "category": { + "field": { + "name": { + "label": "" + } + }, + "action": { + "create": "", + "edit": "", + "remove": "", + "moveUp": "", + "moveDown": "", + "createAbove": "", + "createBelow": "", + "openAllInNewTabs": "" + }, + "create": { + "title": "", + "submit": "" + }, + "remove": { + "title": "", + "message": "" + }, + "edit": { + "title": "", + "submit": "" + }, + "menu": { + "label": { + "create": "", + "changePosition": "" + } + }, + "openAllInNewTabs": { + "title": "", + "text": "" + } + } + }, + "item": { + "action": { + "create": "", + "import": "", + "edit": "", + "moveResize": "", + "duplicate": "", + "remove": "" + }, + "menu": { + "label": { + "settings": "" + } + }, + "create": { + "title": "", + "search": "", + "addToBoard": "" + }, + "moveResize": { + "title": "", + "field": { + "width": { + "label": "" + }, + "height": { + "label": "" + }, + "xOffset": { + "label": "" + }, + "yOffset": { + "label": "" + } + } + }, + "edit": { + "title": "", + "advancedOptions": { + "label": "", + "title": "" + }, + "field": { + "integrations": { + "label": "" + }, + "customCssClasses": { + "label": "" + }, + "borderColor": { + "label": "" + } + } + }, + "remove": { + "title": "", + "message": "" + } + }, + "widget": { + "app": { + "name": "", + "description": "", + "option": { + "appId": { + "label": "" + }, + "openInNewTab": { + "label": "" + }, + "showTitle": { + "label": "" + }, + "showDescriptionTooltip": { + "label": "" + }, + "pingEnabled": { + "label": "" + } + }, + "error": { + "notFound": { + "label": "", + "tooltip": "" + } + } + }, + "bookmarks": { + "name": "", + "description": "", + "option": { + "title": { + "label": "" + }, + "layout": { + "label": "", + "option": { + "row": { + "label": "" + }, + "column": { + "label": "" + }, + "grid": { + "label": "" + }, + "gridHorizontal": { + "label": "" + } + } + }, + "hideTitle": { + "label": "" + }, + "hideIcon": { + "label": "" + }, + "hideHostname": { + "label": "" + }, + "openNewTab": { + "label": "" + }, + "items": { + "label": "", + "add": "" + } + } + }, + "dnsHoleSummary": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "row": { + "label": "" + }, + "column": { + "label": "" + }, + "grid": { + "label": "" + } + } + }, + "usePiHoleColors": { + "label": "" + } + }, + "error": { + "internalServerError": "", + "integrationsDisconnected": "" + }, + "data": { + "adsBlockedToday": "", + "adsBlockedTodayPercentage": "", + "dnsQueriesToday": "", + "domainsBeingBlocked": "" + }, + "domainsTooltip": "" + }, + "dnsHoleControls": { + "name": "", + "description": "", + "option": { + "layout": { + "label": "", + "option": { + "row": { + "label": "" + }, + "column": { + "label": "" + }, + "grid": { + "label": "" + } + } + }, + "showToggleAllButtons": { + "label": "" + } + }, + "error": { + "internalServerError": "" + }, + "controls": { + "enableAll": "", + "disableAll": "", + "setTimer": "", + "set": "", + "enabled": "", + "disabled": "", + "processing": "", + "disconnected": "", + "hours": "", + "minutes": "", + "unlimited": "" + } + }, + "clock": { + "name": "", + "description": "", + "option": { + "customTitleToggle": { + "label": "", + "description": "" + }, + "customTitle": { + "label": "" + }, + "is24HourFormat": { + "label": "", + "description": "" + }, + "showSeconds": { + "label": "" + }, + "useCustomTimezone": { + "label": "" + }, + "timezone": { + "label": "", + "description": "" + }, + "showDate": { + "label": "" + }, + "dateFormat": { + "label": "", + "description": "" + }, + "customTimeFormat": { + "label": "", + "description": "" + }, + "customDateFormat": { + "label": "", + "description": "" + } + } + }, + "minecraftServerStatus": { + "name": "", + "description": "", + "option": { + "title": { + "label": "" + }, + "domain": { + "label": "" + }, + "isBedrockServer": { + "label": "" + } + }, + "status": { + "online": "", + "offline": "" + } + }, + "notebook": { + "name": "", + "description": "", + "option": { + "showToolbar": { + "label": "" + }, + "allowReadOnlyCheck": { + "label": "" + }, + "content": { + "label": "" + } + }, + "controls": { + "bold": "", + "italic": "", + "strikethrough": "", + "underline": "", + "colorText": "", + "colorHighlight": "", + "code": "", + "clear": "", + "heading": "", + "align": "", + "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": "", + "description": "", + "option": { + "embedUrl": { + "label": "" + }, + "allowFullScreen": { + "label": "" + }, + "allowTransparency": { + "label": "" + }, + "allowScrolling": { + "label": "" + }, + "allowPayment": { + "label": "" + }, + "allowAutoPlay": { + "label": "" + }, + "allowMicrophone": { + "label": "" + }, + "allowCamera": { + "label": "" + }, + "allowGeolocation": { + "label": "" + } + }, + "error": { + "noUrl": "", + "unsupportedProtocol": "", + "noBrowerSupport": "" + } + }, + "smartHome-entityState": { + "name": "", + "description": "", + "option": { + "entityId": { + "label": "" + }, + "displayName": { + "label": "" + }, + "entityUnit": { + "label": "" + }, + "clickable": { + "label": "" + } + } + }, + "smartHome-executeAutomation": { + "name": "", + "description": "", + "option": { + "displayName": { + "label": "" + }, + "automationId": { + "label": "" + } + }, + "spotlightAction": { + "run": "" + } + }, + "stockPrice": { + "name": "", + "description": "", + "option": { + "stock": { + "label": "" + }, + "timeRange": { + "label": "", + "option": { + "1d": { + "label": "" + }, + "5d": { + "label": "" + }, + "1mo": { + "label": "" + }, + "3mo": { + "label": "" + }, + "6mo": { + "label": "" + }, + "ytd": { + "label": "" + }, + "1y": { + "label": "" + }, + "2y": { + "label": "" + }, + "5y": { + "label": "" + }, + "10y": { + "label": "" + }, + "max": { + "label": "" + } + } + }, + "timeInterval": { + "label": "", + "option": { + "5m": { + "label": "" + }, + "15m": { + "label": "" + }, + "30m": { + "label": "" + }, + "1h": { + "label": "" + }, + "1d": { + "label": "" + }, + "5d": { + "label": "" + }, + "1wk": { + "label": "" + }, + "1mo": { + "label": "" + } + } + } + } + }, + "calendar": { + "name": "", + "description": "", + "option": { + "releaseType": { + "label": "", + "options": { + "inCinemas": "", + "digitalRelease": "", + "physicalRelease": "" + } + }, + "filterPastMonths": { + "label": "" + }, + "filterFutureMonths": { + "label": "" + }, + "showUnmonitored": { + "label": "" + } + } + }, + "weather": { + "name": "", + "description": "", + "option": { + "isFormatFahrenheit": { + "label": "" + }, + "disableTemperatureDecimals": { + "label": "" + }, + "showCurrentWindSpeed": { + "label": "", + "description": "" + }, + "location": { + "label": "" + }, + "showCity": { + "label": "" + }, + "hasForecast": { + "label": "" + }, + "forecastDayCount": { + "label": "", + "description": "" + }, + "dateFormat": { + "label": "", + "description": "" + } + }, + "currentWindSpeed": "", + "dailyForecast": { + "sunrise": "", + "sunset": "", + "maxWindSpeed": "", + "maxWindGusts": "" + }, + "kind": { + "clear": "", + "mainlyClear": "", + "fog": "", + "drizzle": "", + "freezingDrizzle": "", + "rain": "", + "freezingRain": "", + "snowFall": "", + "snowGrains": "", + "rainShowers": "", + "snowShowers": "", + "thunderstorm": "", + "thunderstormWithHail": "", + "unknown": "" + } + }, + "indexerManager": { + "name": "", + "description": "", + "option": { + "openIndexerSiteInNewTab": { + "label": "" + } + }, + "title": "", + "testAll": "", + "error": { + "internalServerError": "" + } + }, + "healthMonitoring": { + "name": "", + "description": "", + "tab": { + "system": "", + "cluster": "" + }, + "option": { + "fahrenheit": { + "label": "" + }, + "cpu": { + "label": "" + }, + "memory": { + "label": "" + }, + "fileSystem": { + "label": "" + }, + "defaultTab": { + "label": "" + }, + "sectionIndicatorRequirement": { + "label": "" + } + }, + "popover": { + "information": "", + "processor": "", + "memory": "", + "memoryAvailable": "", + "version": "", + "uptime": "", + "loadAverage": "", + "minute": "", + "minutes": "", + "used": "", + "available": "", + "lastSeen": "" + }, + "memory": {}, + "error": { + "internalServerError": "" + }, + "cluster": { + "summary": { + "cpu": "", + "memory": "" + }, + "resource": { + "node": { + "name": "" + }, + "qemu": { + "name": "" + }, + "lxc": { + "name": "" + }, + "storage": { + "name": "" + } + }, + "popover": { + "rightSection": { + "node": "", + "vmId": "", + "plugin": "" + }, + "detail": { + "cpu": "", + "memory": "", + "storage": "", + "uptime": "", + "haState": "", + "storageType": { + "local": "", + "shared": "" + } + } + }, + "table": { + "header": { + "name": "", + "cpu": "", + "memory": "", + "node": "" + } + } + } + }, + "common": { + "location": { + "query": "", + "latitude": "", + "longitude": "", + "disabledTooltip": "", + "unknownLocation": "", + "search": "", + "table": { + "header": { + "city": "", + "country": "", + "coordinates": "", + "population": "" + }, + "action": { + "select": "" + }, + "population": { + "fallback": "" + } + } + }, + "integration": { + "noData": "", + "description": "" + }, + "app": { + "noData": "", + "description": "", + "quickCreate": "" + }, + "error": { + "noIntegration": "", + "noData": "" + }, + "option": {}, + "restricted": { + "title": "", + "description": "" + } + }, + "video": { + "name": "", + "description": "", + "option": { + "feedUrl": { + "label": "" + }, + "hasAutoPlay": { + "label": "", + "description": "" + }, + "isMuted": { + "label": "" + }, + "hasControls": { + "label": "" + } + }, + "error": { + "noUrl": "", + "forYoutubeUseIframe": "" + } + }, + "mediaServer": { + "name": "", + "description": "", + "option": {}, + "items": { + "currentlyPlaying": "", + "user": "", + "name": "", + "id": "" + } + }, + "downloads": { + "name": "", + "description": "", + "option": { + "columns": { + "label": "" + }, + "enableRowSorting": { + "label": "" + }, + "defaultSort": { + "label": "" + }, + "descendingDefaultSort": { + "label": "" + }, + "showCompletedUsenet": { + "label": "" + }, + "showCompletedTorrent": { + "label": "" + }, + "activeTorrentThreshold": { + "label": "" + }, + "categoryFilter": { + "label": "" + }, + "filterIsWhitelist": { + "label": "" + }, + "applyFilterToRatio": { + "label": "" + } + }, + "errors": { + "noColumns": "", + "noCommunications": "" + }, + "items": { + "actions": { + "columnTitle": "" + }, + "added": { + "columnTitle": "", + "detailsTitle": "" + }, + "category": { + "columnTitle": "", + "detailsTitle": "" + }, + "downSpeed": { + "columnTitle": "", + "detailsTitle": "" + }, + "index": { + "columnTitle": "", + "detailsTitle": "" + }, + "id": { + "columnTitle": "" + }, + "integration": { + "columnTitle": "" + }, + "name": { + "columnTitle": "" + }, + "progress": { + "columnTitle": "", + "detailsTitle": "" + }, + "ratio": { + "columnTitle": "", + "detailsTitle": "" + }, + "received": { + "columnTitle": "", + "detailsTitle": "" + }, + "sent": { + "columnTitle": "", + "detailsTitle": "" + }, + "size": { + "columnTitle": "", + "detailsTitle": "" + }, + "state": { + "columnTitle": "", + "detailsTitle": "" + }, + "time": { + "columnTitle": "", + "detailsTitle": "" + }, + "type": { + "columnTitle": "", + "detailsTitle": "" + }, + "upSpeed": { + "columnTitle": "", + "detailsTitle": "" + } + }, + "states": { + "downloading": "", + "queued": "", + "paused": "", + "completed": "", + "failed": "", + "processing": "", + "leeching": "", + "stalled": "", + "unknown": "", + "seeding": "" + }, + "actions": { + "clients": { + "modalTitle": "", + "pause": "", + "resume": "" + }, + "client": { + "pause": "", + "resume": "" + }, + "item": { + "pause": "", + "resume": "", + "delete": { + "title": "", + "modalTitle": "", + "entry": "", + "entryAndFiles": "" + } + } + }, + "globalRatio": "" + }, + "mediaRequests-requestList": { + "name": "", + "description": "", + "option": { + "linksTargetNewTab": { + "label": "" + } + }, + "pending": { + "approve": "", + "approving": "", + "decline": "" + }, + "availability": { + "unknown": "", + "pending": "", + "processing": "", + "partiallyAvailable": "", + "available": "" + }, + "status": { + "pending": "", + "approved": "", + "declined": "", + "failed": "" + }, + "toBeDetermined": "" + }, + "mediaRequests-requestStats": { + "name": "", + "description": "", + "option": {}, + "titles": { + "stats": { + "main": "", + "approved": "", + "pending": "", + "processing": "", + "declined": "", + "available": "", + "tv": "", + "movie": "", + "total": "" + }, + "users": { + "main": "", + "requests": "" + } + } + }, + "mediaTranscoding": { + "name": "", + "description": "", + "option": { + "defaultView": { + "label": "" + }, + "queuePageSize": { + "label": "" + } + }, + "tab": { + "workers": "", + "queue": "", + "statistics": "" + }, + "currentIndex": "", + "healthCheck": { + "title": "", + "queued": "", + "status": { + "healthy": "", + "unhealthy": "" + } + }, + "panel": { + "statistics": { + "empty": "", + "transcodes": "", + "transcodesCount": "", + "healthChecksCount": "", + "filesCount": "", + "savedSpace": "", + "healthChecks": "", + "videoCodecs": "", + "videoContainers": "", + "videoResolutions": "" + }, + "workers": { + "empty": "", + "table": { + "file": "", + "eta": "", + "progress": "", + "transcode": "", + "healthCheck": "" + } + }, + "queue": { + "empty": "", + "table": { + "file": "", + "size": "", + "transcode": "", + "healthCheck": "" + } + } + } + }, + "rssFeed": { + "name": "", + "description": "", + "option": { + "feedUrls": { + "label": "" + }, + "enableRtl": { + "label": "" + }, + "textLinesClamp": { + "label": "" + }, + "maximumAmountPosts": { + "label": "" + } + } + } + }, + "widgetPreview": { + "toggle": { + "enabled": "", + "disabled": "" + }, + "dimensions": { + "title": "" + } + }, + "board": { + "action": { + "duplicate": { + "title": "", + "message": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + }, + "edit": { + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + }, + "confirmLeave": { + "title": "", + "message": "" + } + }, + "oldImport": { + "label": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + }, + "form": { + "file": { + "label": "", + "invalidError": "" + }, + "apps": { + "label": "", + "avoidDuplicates": { + "label": "", + "description": "" + }, + "onlyImportApps": { + "label": "", + "description": "" + } + }, + "name": { + "label": "" + }, + "screenSize": { + "label": "", + "description": "", + "option": { + "sm": "", + "md": "", + "lg": "" + } + }, + "sidebarBehavior": { + "label": "", + "description": "", + "option": { + "lastSection": { + "label": "", + "description": "" + }, + "removeItems": { + "label": "", + "description": "" + } + } + } + } + }, + "quickCreateApp": { + "modal": { + "title": "", + "createAndUse": "" + } + } + }, + "field": { + "pageTitle": { + "label": "" + }, + "metaTitle": { + "label": "" + }, + "logoImageUrl": { + "label": "" + }, + "faviconImageUrl": { + "label": "" + }, + "backgroundImageUrl": { + "label": "" + }, + "backgroundImageAttachment": { + "label": "", + "option": { + "fixed": { + "label": "", + "description": "" + }, + "scroll": { + "label": "", + "description": "" + } + } + }, + "backgroundImageRepeat": { + "label": "", + "option": { + "repeat": { + "label": "", + "description": "" + }, + "no-repeat": { + "label": "", + "description": "" + }, + "repeat-x": { + "label": "", + "description": "" + }, + "repeat-y": { + "label": "", + "description": "" + } + } + }, + "backgroundImageSize": { + "label": "", + "option": { + "cover": { + "label": "", + "description": "" + }, + "contain": { + "label": "", + "description": "" + } + } + }, + "primaryColor": { + "label": "" + }, + "secondaryColor": { + "label": "" + }, + "opacity": { + "label": "" + }, + "iconColor": { + "label": "" + }, + "customCss": { + "label": "", + "description": "", + "customClassesAlert": { + "title": "", + "description": "" + } + }, + "disableStatus": { + "label": "", + "description": "" + }, + "columnCount": { + "label": "" + }, + "itemRadius": { + "label": "", + "description": "", + "option": { + "xs": "", + "sm": "", + "md": "", + "lg": "", + "xl": "" + } + }, + "name": { + "label": "" + }, + "isPublic": { + "label": "", + "description": "" + } + }, + "content": { + "metaTitle": "" + }, + "setting": { + "title": "", + "section": { + "general": { + "title": "", + "unrecognizedLink": "" + }, + "layout": { + "title": "", + "responsive": { + "title": "", + "action": { + "add": "" + } + } + }, + "background": { + "title": "" + }, + "appearance": { + "title": "" + }, + "customCss": { + "title": "" + }, + "behavior": { + "title": "" + }, + "access": { + "title": "", + "permission": { + "item": { + "view": { + "label": "" + }, + "modify": { + "label": "" + }, + "full": { + "label": "" + } + } + } + }, + "dangerZone": { + "title": "", + "action": { + "rename": { + "label": "", + "description": "", + "button": "", + "modal": { + "title": "" + } + }, + "visibility": { + "label": "", + "description": { + "public": "", + "private": "" + }, + "button": { + "public": "", + "private": "" + }, + "confirm": { + "public": { + "title": "", + "description": "" + }, + "private": { + "title": "", + "description": "" + } + } + }, + "delete": { + "label": "", + "description": "", + "button": "", + "confirm": { + "title": "", + "description": "" + } + } + } + } + } + }, + "error": { + "noBoard": { + "title": "", + "description": "", + "link": "", + "notice": "" + }, + "notFound": { + "title": "", + "description": "", + "link": "", + "notice": "" + }, + "homeBoard": { + "title": "", + "admin": { + "description": "", + "link": "", + "notice": "" + }, + "user": { + "description": "", + "link": "", + "notice": "" + }, + "anonymous": { + "description": "", + "link": "", + "notice": "" + } + } + } + }, + "layout": { + "field": { + "name": { + "label": "" + }, + "columnCount": { + "label": "" + }, + "breakpoint": { + "label": "", + "description": "" + } + } + }, + "management": { + "metaTitle": "", + "title": { + "morning": "", + "afternoon": "", + "evening": "" + }, + "notFound": { + "title": "", + "text": "" + }, + "navbar": { + "items": { + "home": "", + "boards": "", + "apps": "", + "integrations": "", + "searchEngies": "", + "medias": "", + "users": { + "label": "", + "items": { + "manage": "", + "invites": "", + "groups": "" + } + }, + "tools": { + "label": "", + "items": { + "docker": "", + "kubernetes": "", + "logs": "", + "api": "", + "certificates": "", + "tasks": "" + } + }, + "settings": "", + "help": { + "label": "", + "items": { + "documentation": "", + "submitIssue": "", + "discord": "", + "sourceCode": "" + } + }, + "about": "" + } + }, + "page": { + "home": { + "statistic": { + "board": "", + "user": "", + "invite": "", + "integration": "", + "app": "", + "group": "" + }, + "statisticLabel": { + "boards": "", + "resources": "", + "authentication": "", + "authorization": "" + } + }, + "board": { + "title": "", + "action": { + "new": { + "label": "" + }, + "open": { + "label": "" + }, + "settings": { + "label": "" + }, + "setHomeBoard": { + "label": "", + "badge": { + "label": "", + "tooltip": "" + } + }, + "setMobileHomeBoard": { + "label": "", + "badge": { + "label": "", + "tooltip": "" + } + }, + "duplicate": { + "label": "" + }, + "delete": { + "label": "", + "confirm": { + "title": "", + "description": "" + } + } + }, + "visibility": { + "public": "", + "private": "" + }, + "modal": { + "createBoard": { + "field": { + "name": { + "label": "" + } + } + } + } + }, + "media": { + "includeFromAllUsers": "" + }, + "user": { + "back": "", + "fieldsDisabledExternalProvider": "", + "setting": { + "general": { + "title": "", + "item": { + "language": "", + "board": { + "title": "", + "type": { + "general": "", + "mobile": "" + } + }, + "search": "", + "firstDayOfWeek": "", + "accessibility": "" + } + }, + "security": { + "title": "" + }, + "board": { + "title": "" + } + }, + "list": { + "metaTitle": "", + "title": "" + }, + "edit": { + "metaTitle": "" + }, + "create": { + "metaTitle": "", + "title": "", + "step": { + "personalInformation": { + "label": "" + }, + "security": { + "label": "" + }, + "groups": { + "label": "", + "title": "", + "description": "" + }, + "review": { + "label": "" + }, + "completed": { + "title": "" + }, + "error": { + "title": "" + } + }, + "action": { + "createAnother": "", + "back": "" + } + }, + "invite": { + "title": "", + "action": { + "new": { + "title": "", + "description": "" + }, + "copy": { + "title": "", + "description": "", + "link": "", + "button": "" + }, + "delete": { + "title": "", + "description": "" + } + }, + "field": { + "id": { + "label": "" + }, + "creator": { + "label": "" + }, + "expirationDate": { + "label": "" + }, + "token": { + "label": "" + } + } + } + }, + "group": { + "back": "", + "setting": { + "general": { + "title": "", + "owner": "", + "ownerOfGroup": "", + "ownerOfGroupDeleted": "" + }, + "setting": { + "title": "", + "alert": "", + "board": { + "title": "" + } + }, + "members": { + "title": "", + "search": "", + "notFound": "" + }, + "permissions": { + "title": "", + "form": { + "unsavedChanges": "" + } + } + } + }, + "settings": { + "title": "", + "notification": { + "success": { + "message": "" + }, + "error": { + "message": "" + } + }, + "section": { + "analytics": { + "title": "", + "general": { + "title": "", + "text": "" + }, + "widgetData": { + "title": "", + "text": "" + }, + "integrationData": { + "title": "", + "text": "" + }, + "usersData": { + "title": "", + "text": "" + } + }, + "crawlingAndIndexing": { + "title": "", + "warning": "", + "noIndex": { + "title": "", + "text": "" + }, + "noFollow": { + "title": "", + "text": "" + }, + "noTranslate": { + "title": "", + "text": "" + }, + "noSiteLinksSearchBox": { + "title": "", + "text": "" + } + }, + "board": { + "title": "", + "homeBoard": { + "label": "", + "mobileLabel": "", + "description": "" + }, + "status": { + "title": "", + "enableStatusByDefault": { + "label": "", + "description": "" + }, + "forceDisableStatus": { + "label": "", + "description": "" + } + } + }, + "search": { + "title": "", + "defaultSearchEngine": { + "label": "", + "description": "" + } + }, + "appearance": { + "title": "", + "defaultColorScheme": { + "label": "", + "options": { + "light": "", + "dark": "" + } + } + }, + "culture": { + "title": "", + "defaultLocale": { + "label": "" + } + } + } + }, + "tool": { + "tasks": { + "title": "", + "status": { + "idle": "", + "running": "", + "error": "" + }, + "job": { + "minecraftServerStatus": { + "label": "" + }, + "iconsUpdater": { + "label": "" + }, + "analytics": { + "label": "" + }, + "smartHomeEntityState": { + "label": "" + }, + "ping": { + "label": "" + }, + "mediaServer": { + "label": "" + }, + "mediaOrganizer": { + "label": "" + }, + "downloads": { + "label": "" + }, + "mediaRequestStats": { + "label": "" + }, + "mediaRequestList": { + "label": "" + }, + "rssFeeds": { + "label": "" + }, + "indexerManager": { + "label": "" + }, + "healthMonitoring": { + "label": "" + }, + "dnsHole": { + "label": "" + }, + "sessionCleanup": { + "label": "" + }, + "updateChecker": { + "label": "" + }, + "mediaTranscoding": { + "label": "" + } + } + }, + "api": { + "title": "", + "modal": { + "createApiToken": { + "title": "", + "description": "", + "button": "" + } + }, + "tab": { + "documentation": { + "label": "" + }, + "apiKey": { + "label": "", + "title": "", + "button": { + "createApiToken": "" + }, + "modal": { + "delete": { + "title": "", + "text": "" + } + }, + "table": { + "header": { + "id": "", + "createdBy": "", + "actions": "" + } + } + } + } + } + }, + "about": { + "version": "", + "text": "", + "accordion": { + "contributors": { + "title": "", + "subtitle": "" + }, + "translators": { + "title": "", + "subtitle": "" + }, + "libraries": { + "title": "", + "subtitle": "" + } + } + } + } + }, + "docker": { + "title": "", + "table": { + "updated": "", + "search": "", + "selected": "" + }, + "field": { + "name": { + "label": "" + }, + "state": { + "label": "", + "option": { + "created": "", + "running": "", + "paused": "", + "restarting": "", + "exited": "", + "removing": "", + "dead": "" + } + }, + "containerImage": { + "label": "" + }, + "ports": { + "label": "" + } + }, + "action": { + "start": { + "label": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + }, + "stop": { + "label": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + }, + "restart": { + "label": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + }, + "remove": { + "label": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + }, + "refresh": { + "label": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + }, + "addToHomarr": { + "label": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + }, + "modal": { + "title": "" + } + } + }, + "error": { + "internalServerError": "" + } + }, + "kubernetes": { + "cluster": { + "title": "", + "label": "", + "providers": "", + "version": "", + "architecture": "", + "capacity": { + "title": "", + "resource": { + "reserved": "", + "used": "" + } + }, + "resources": { + "title": "", + "nodes": "", + "namespaces": "", + "ingresses": "", + "services": "", + "pods": "", + "configmaps": "", + "secrets": "", + "volumes": "" + } + }, + "nodes": { + "label": "", + "field": { + "name": { + "label": "" + }, + "state": { + "label": "", + "option": { + "ready": "", + "NotReady": "" + } + }, + "cpu": { + "label": "" + }, + "memory": { + "label": "" + }, + "pods": { + "label": "" + }, + "operatingSystem": { + "label": "" + }, + "architecture": { + "label": "" + }, + "kubernetesVersion": { + "label": "" + }, + "creationTimestamp": { + "label": "" + } + }, + "table": { + "search": "" + } + }, + "namespaces": { + "label": "", + "field": { + "name": { + "label": "" + }, + "state": { + "label": "", + "option": { + "active": "", + "terminating": "" + } + }, + "creationTimestamp": { + "label": "" + } + }, + "table": { + "search": "" + } + }, + "ingresses": { + "label": "", + "field": { + "name": { + "label": "" + }, + "namespace": { + "label": "" + }, + "className": { + "label": "" + }, + "rulesAndPaths": { + "label": "" + }, + "creationTimestamp": { + "label": "" + } + }, + "table": { + "search": "" + } + }, + "services": { + "label": "", + "field": { + "name": { + "label": "" + }, + "namespace": { + "label": "" + }, + "type": { + "label": "" + }, + "ports": { + "label": "" + }, + "targetPorts": { + "label": "" + }, + "clusterIP": { + "label": "" + }, + "creationTimestamp": { + "label": "" + } + }, + "table": { + "search": "" + } + }, + "pods": { + "label": "", + "field": { + "name": { + "label": "" + }, + "namespace": { + "label": "" + }, + "image": { + "label": "" + }, + "applicationType": { + "label": "" + }, + "status": { + "label": "" + }, + "creationTimestamp": { + "label": "" + } + }, + "table": { + "search": "" + } + }, + "secrets": { + "label": "", + "field": { + "name": { + "label": "" + }, + "namespace": { + "label": "" + }, + "type": { + "label": "" + }, + "creationTimestamp": { + "label": "" + } + }, + "table": { + "search": "" + } + }, + "configmaps": { + "label": "", + "field": { + "name": { + "label": "" + }, + "namespace": { + "label": "" + }, + "creationTimestamp": { + "label": "" + } + }, + "table": { + "search": "" + } + }, + "volumes": { + "label": "", + "field": { + "name": { + "label": "" + }, + "namespace": { + "label": "" + }, + "accessModes": { + "label": "" + }, + "storage": { + "label": "" + }, + "storageClassName": { + "label": "" + }, + "volumeMode": { + "label": "" + }, + "volumeName": { + "label": "" + }, + "status": { + "label": "" + }, + "creationTimestamp": { + "label": "" + } + }, + "table": { + "search": "" + } + }, + "error": { + "internalServerError": "" + } + }, + "permission": { + "title": "", + "userSelect": { + "title": "" + }, + "groupSelect": { + "title": "" + }, + "tab": { + "user": "", + "group": "", + "inherited": "" + }, + "field": { + "user": { + "label": "" + }, + "group": { + "label": "" + }, + "permission": { + "label": "" + } + }, + "action": { + "saveUser": "", + "saveGroup": "" + } + }, + "navigationStructure": { + "manage": { + "label": "", + "boards": { + "label": "" + }, + "integrations": { + "label": "", + "edit": { + "label": "" + }, + "new": { + "label": "" + } + }, + "search-engines": { + "label": "", + "new": { + "label": "" + }, + "edit": { + "label": "" + } + }, + "medias": { + "label": "" + }, + "apps": { + "label": "", + "new": { + "label": "" + }, + "edit": { + "label": "" + } + }, + "users": { + "label": "", + "create": { + "label": "" + }, + "general": "", + "security": "", + "board": "", + "groups": { + "label": "" + }, + "invites": { + "label": "" + } + }, + "tools": { + "label": "", + "docker": { + "label": "" + }, + "kubernetes": { + "label": "", + "nodes": { + "label": "" + }, + "namespaces": { + "label": "" + }, + "ingresses": { + "label": "" + }, + "services": { + "label": "" + }, + "pods": { + "label": "" + }, + "configmaps": { + "label": "" + }, + "secrets": { + "label": "" + }, + "volumes": { + "label": "" + } + }, + "logs": { + "label": "" + }, + "certificates": { + "label": "" + } + }, + "settings": { + "label": "" + }, + "about": { + "label": "" + } + } + }, + "search": { + "placeholder": "", + "nothingFound": "", + "error": { + "fetch": "" + }, + "mode": { + "appIntegrationBoard": { + "help": "", + "group": { + "app": { + "title": "", + "children": { + "action": { + "open": { + "label": "" + }, + "edit": { + "label": "" + } + }, + "detail": { + "title": "" + } + } + }, + "board": { + "title": "", + "children": { + "action": { + "open": { + "label": "" + }, + "homeBoard": { + "label": "" + }, + "mobileBoard": { + "label": "" + }, + "settings": { + "label": "" + } + }, + "detail": { + "title": "" + } + } + }, + "integration": { + "title": "" + } + } + }, + "command": { + "help": "", + "group": { + "localCommand": { + "title": "" + }, + "globalCommand": { + "title": "", + "option": { + "colorScheme": { + "light": "", + "dark": "" + }, + "language": { + "label": "", + "children": { + "detail": { + "title": "" + } + } + }, + "newBoard": { + "label": "" + }, + "importBoard": { + "label": "" + }, + "newApp": { + "label": "" + }, + "newIntegration": { + "label": "", + "children": { + "detail": { + "title": "" + } + } + }, + "newUser": { + "label": "" + }, + "newInvite": { + "label": "" + }, + "newGroup": { + "label": "" + } + } + } + } + }, + "media": { + "requestMovie": "", + "requestSeries": "", + "openIn": "" + }, + "external": { + "help": "", + "group": { + "searchEngine": { + "title": "", + "children": { + "action": { + "search": { + "label": "" + } + }, + "detail": { + "title": "" + }, + "searchResults": { + "title": "" + } + }, + "option": { + "google": { + "name": "", + "description": "" + }, + "bing": { + "name": "", + "description": "" + }, + "duckduckgo": { + "name": "", + "description": "" + }, + "torrent": { + "name": "", + "description": "" + }, + "youTube": { + "name": "", + "description": "" + } + } + } + } + }, + "help": { + "group": { + "mode": { + "title": "" + }, + "help": { + "title": "", + "option": { + "documentation": { + "label": "" + }, + "submitIssue": { + "label": "" + }, + "discord": { + "label": "" + } + } + } + } + }, + "home": { + "group": { + "search": { + "title": "", + "option": { + "other": { + "label": "" + }, + "no-default": { + "label": "", + "description": "" + }, + "search": { + "label": "" + }, + "from-integration": { + "description": "" + } + } + }, + "local": { + "title": "" + } + } + }, + "page": { + "help": "", + "group": { + "page": { + "title": "", + "option": { + "manageHome": { + "label": "" + }, + "manageBoard": { + "label": "" + }, + "manageApp": { + "label": "" + }, + "manageIntegration": { + "label": "" + }, + "manageSearchEngine": { + "label": "" + }, + "manageMedia": { + "label": "" + }, + "manageUser": { + "label": "" + }, + "manageInvite": { + "label": "" + }, + "manageGroup": { + "label": "" + }, + "manageDocker": { + "label": "" + }, + "manageApi": { + "label": "" + }, + "manageLog": { + "label": "" + }, + "manageTask": { + "label": "" + }, + "manageSettings": { + "label": "" + }, + "about": { + "label": "" + }, + "homeBoard": { + "label": "" + }, + "preferences": { + "label": "" + } + } + } + } + }, + "userGroup": { + "help": "", + "group": { + "user": { + "title": "", + "children": { + "action": { + "detail": { + "label": "" + } + }, + "detail": { + "title": "" + } + } + }, + "group": { + "title": "", + "children": { + "action": { + "detail": { + "label": "" + }, + "manageMember": { + "label": "" + }, + "managePermission": { + "label": "" + } + }, + "detail": { + "title": "" + } + } + } + } + } + }, + "engine": { + "search": "", + "field": { + "name": { + "label": "" + }, + "short": { + "label": "" + }, + "urlTemplate": { + "label": "" + }, + "description": { + "label": "" + } + }, + "page": { + "list": { + "title": "", + "noResults": { + "title": "", + "action": "" + }, + "interactive": "" + }, + "create": { + "title": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + }, + "edit": { + "title": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + }, + "configControl": "", + "searchEngineType": { + "generic": "", + "fromIntegration": "" + } + }, + "delete": { + "title": "", + "message": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + } + }, + "media": { + "request": { + "modal": { + "title": "", + "table": { + "header": { + "season": "", + "episodes": "" + } + }, + "button": { + "send": "" + } + } + } + } + } + }, + "certificate": { + "page": { + "list": { + "title": "", + "description": "", + "noResults": { + "title": "" + }, + "expires": "" + } + }, + "action": { + "create": { + "label": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + }, + "remove": { + "label": "", + "confirm": "", + "notification": { + "success": { + "title": "", + "message": "" + }, + "error": { + "title": "", + "message": "" + } + } + } + } + } +} diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 54307c005..b7bb3a9f9 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -980,6 +980,9 @@ "remove": "Remove dynamic section" }, "option": { + "title": { + "label": "Title" + }, "borderColor": { "label": "Border color" } @@ -1721,7 +1724,11 @@ "noIntegration": "No integration selected", "noData": "No integration data available" }, - "option": {} + "option": {}, + "restricted": { + "title": "Restricted", + "description": "You don't have access to the {name} widget." + } }, "video": { "name": "Video Stream", diff --git a/packages/translation/src/lang/es.json b/packages/translation/src/lang/es.json index 2015228bd..cc4c852ed 100644 --- a/packages/translation/src/lang/es.json +++ b/packages/translation/src/lang/es.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Video en directo", diff --git a/packages/translation/src/lang/et.json b/packages/translation/src/lang/et.json index aac0fb4d8..c7c3e884c 100644 --- a/packages/translation/src/lang/et.json +++ b/packages/translation/src/lang/et.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "", diff --git a/packages/translation/src/lang/fr.json b/packages/translation/src/lang/fr.json index ad55d9fb9..cbd292355 100644 --- a/packages/translation/src/lang/fr.json +++ b/packages/translation/src/lang/fr.json @@ -980,6 +980,9 @@ "remove": "Supprimer la section dynamique" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "Couleur de la bordure" } @@ -1721,7 +1724,11 @@ "noIntegration": "Aucune intégration sélectionnée", "noData": "Aucune donnée d’interaction disponible" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Flux vidéo", diff --git a/packages/translation/src/lang/he.json b/packages/translation/src/lang/he.json index 31381665f..2b7c70fa0 100644 --- a/packages/translation/src/lang/he.json +++ b/packages/translation/src/lang/he.json @@ -980,6 +980,9 @@ "remove": "הסר קטע דינמי" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "צבע מסגרת" } @@ -1721,7 +1724,11 @@ "noIntegration": "לא נבחרה אינטגרציה", "noData": "אין נתוני אינטגרציה זמינים" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "זרם וידאו", diff --git a/packages/translation/src/lang/hr.json b/packages/translation/src/lang/hr.json index efc83ef79..ae450be35 100644 --- a/packages/translation/src/lang/hr.json +++ b/packages/translation/src/lang/hr.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "", diff --git a/packages/translation/src/lang/hu.json b/packages/translation/src/lang/hu.json index f34e578c6..f564df03e 100644 --- a/packages/translation/src/lang/hu.json +++ b/packages/translation/src/lang/hu.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Videófolyam", diff --git a/packages/translation/src/lang/it.json b/packages/translation/src/lang/it.json index 53e680a95..e1eba41d0 100644 --- a/packages/translation/src/lang/it.json +++ b/packages/translation/src/lang/it.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Stream Video", diff --git a/packages/translation/src/lang/ja.json b/packages/translation/src/lang/ja.json index 80f5e570d..f18a405c7 100644 --- a/packages/translation/src/lang/ja.json +++ b/packages/translation/src/lang/ja.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "ビデオストリーム", diff --git a/packages/translation/src/lang/ko.json b/packages/translation/src/lang/ko.json index 4106cee8e..e107f54ec 100644 --- a/packages/translation/src/lang/ko.json +++ b/packages/translation/src/lang/ko.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "비디오 스트림", diff --git a/packages/translation/src/lang/lt.json b/packages/translation/src/lang/lt.json index 8bd9fdb64..4a5285aa8 100644 --- a/packages/translation/src/lang/lt.json +++ b/packages/translation/src/lang/lt.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "", diff --git a/packages/translation/src/lang/lv.json b/packages/translation/src/lang/lv.json index 76e5a7c1c..cef77f103 100644 --- a/packages/translation/src/lang/lv.json +++ b/packages/translation/src/lang/lv.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Videostraume", diff --git a/packages/translation/src/lang/nl.json b/packages/translation/src/lang/nl.json index 36034cae6..5ad46adda 100644 --- a/packages/translation/src/lang/nl.json +++ b/packages/translation/src/lang/nl.json @@ -376,7 +376,7 @@ }, "use-all": { "label": "Alle integraties gebruiken", - "description": "Geeft leden de mogelijkheid om integraties toe te voegen aan hun borden" + "description": "Laat leden integraties toevoegen aan hun borden" }, "interact-all": { "label": "Interactie met elke integratie", @@ -677,11 +677,11 @@ "description": "Integratie \"{kind}\" kan worden gebruikt met de zoekmachines. Controleer dit om automatisch de zoekmachine te configureren." }, "createApp": { - "label": "", - "description": "" + "label": "App aanmaken", + "description": "Maak een app n met dezelfde naam en icoon als de integratie. Laat het invoerveld hieronder leeg om de app te maken met de integratie-URL." }, "appHref": { - "placeholder": "" + "placeholder": "Aangepaste app-URL" } }, "action": { @@ -980,8 +980,11 @@ "remove": "Dynamische sectie verwijderen" }, "option": { - "borderColor": { + "title": { "label": "" + }, + "borderColor": { + "label": "Randkleur" } }, "remove": { @@ -1079,7 +1082,7 @@ "label": "Aangepaste CSS-classes" }, "borderColor": { - "label": "" + "label": "Randkleur" } } }, @@ -1136,12 +1139,12 @@ "label": "Raster" }, "gridHorizontal": { - "label": "" + "label": "Raster horizontaal" } } }, "hideTitle": { - "label": "" + "label": "Titel verbergen" }, "hideIcon": { "label": "Iconen verbergen" @@ -1425,76 +1428,76 @@ } }, "stockPrice": { - "name": "", - "description": "", + "name": "Aandelen prijs", + "description": "Toont de huidige aandelenprijs van een bedrijf", "option": { "stock": { - "label": "" + "label": "Aandelen symbool" }, "timeRange": { - "label": "", + "label": "Tijdsbereik", "option": { "1d": { - "label": "" + "label": "1 dag" }, "5d": { - "label": "" + "label": "5 dagen" }, "1mo": { - "label": "" + "label": "1 maand" }, "3mo": { - "label": "" + "label": "3 maanden" }, "6mo": { - "label": "" + "label": "6 maanden" }, "ytd": { - "label": "" + "label": "Jaar tot nu toe" }, "1y": { - "label": "" + "label": "1 jaar" }, "2y": { - "label": "" + "label": "2 jaar" }, "5y": { - "label": "" + "label": "5 jaar" }, "10y": { - "label": "" + "label": "10 jaar" }, "max": { - "label": "" + "label": "Max." } } }, "timeInterval": { - "label": "", + "label": "Tijdsinterval", "option": { "5m": { - "label": "" + "label": "5 minuten" }, "15m": { - "label": "" + "label": "15 minuten" }, "30m": { - "label": "" + "label": "30 minuten" }, "1h": { - "label": "" + "label": "1 uur" }, "1d": { - "label": "" + "label": "1 dag" }, "5d": { - "label": "" + "label": "5 dagen" }, "1wk": { - "label": "" + "label": "1 week" }, "1mo": { - "label": "" + "label": "1 maand" } } } @@ -1513,13 +1516,13 @@ } }, "filterPastMonths": { - "label": "Start vanaf" + "label": "Starten vanaf" }, "filterFutureMonths": { "label": "Eindig bij" }, "showUnmonitored": { - "label": "" + "label": "Niet-bewaakte items weergeven" } } }, @@ -1555,7 +1558,7 @@ "description": "Hoe de datum eruit moet zien" } }, - "currentWindSpeed": "", + "currentWindSpeed": "{currentWindSpeed} km/u", "dailyForecast": { "sunrise": "Zonsopgang", "sunset": "Zonsondergang", @@ -1598,7 +1601,7 @@ "description": "Toont informatie over de gezondheid en status van je systeem(en).", "tab": { "system": "Systeem", - "cluster": "" + "cluster": "Cluster" }, "option": { "fahrenheit": { @@ -1721,7 +1724,11 @@ "noIntegration": "Geen integratie geselecteerd", "noData": "Geen integratiegegevens beschikbaar" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Video stream", @@ -1751,7 +1758,7 @@ "description": "De huidige streams op je mediaservers weergeven", "option": {}, "items": { - "currentlyPlaying": "", + "currentlyPlaying": "Momenteel aan het afspelen", "user": "Gebruiker", "name": "Naam", "id": "Id" @@ -1918,10 +1925,10 @@ "available": "Beschikbaar" }, "status": { - "pending": "", - "approved": "", - "declined": "", - "failed": "" + "pending": "In afwachting", + "approved": "Goedgekeurd", + "declined": "Afgewezen", + "failed": "Mislukt" }, "toBeDetermined": "TBD" }, @@ -2220,7 +2227,7 @@ "label": "Kolom aantal" }, "itemRadius": { - "label": "", + "label": "Item radius", "description": "Verandert de rondheid van tegels op je bord", "option": { "xs": "Zeer klein", @@ -2372,7 +2379,7 @@ "label": "Kolom aantal" }, "breakpoint": { - "label": "", + "label": "Breakpoint", "description": "Lay-out wordt gebruikt op alle schermen die groter zijn dan deze breakpoint tot de volgende grotere breakpoint." } } @@ -2408,7 +2415,7 @@ "label": "Gereedschappen", "items": { "docker": "Docker", - "kubernetes": "", + "kubernetes": "Kubernetes", "logs": "Logs", "api": "API", "certificates": "Certificaten", @@ -2684,7 +2691,7 @@ "description": "Alleen openbare borden zijn beschikbaar voor selectie" }, "status": { - "title": "", + "title": "App status", "enableStatusByDefault": { "label": "Status standaard inschakelen", "description": "Bij het toevoegen van een app-item wordt de status standaard ingeschakeld" @@ -2841,7 +2848,7 @@ "docker": { "title": "Containers", "table": { - "updated": "Bijgewerkt {when}", + "updated": "{when} bijgewerkt", "search": "Zoek {count} containers", "selected": "{selectCount} van {totalCount} containers geselecteerd" }, @@ -2856,13 +2863,13 @@ "running": "Actief", "paused": "Gepauzeerd", "restarting": "Herstarten", - "exited": "Verlaten", + "exited": "Beëindigd", "removing": "Verwijderen", "dead": "Dood" } }, "containerImage": { - "label": "Afbeelding" + "label": "Image" }, "ports": { "label": "Poorten" @@ -2870,7 +2877,7 @@ }, "action": { "start": { - "label": "Start", + "label": "Starten", "notification": { "success": { "title": "Containers gestart", @@ -2957,242 +2964,242 @@ }, "kubernetes": { "cluster": { - "title": "", - "label": "", - "providers": "", - "version": "", - "architecture": "", + "title": "Cluster dashboard", + "label": "Cluster", + "providers": "Aanbieders", + "version": "Versie", + "architecture": "Architectuur", "capacity": { - "title": "", + "title": "Capaciteit", "resource": { - "reserved": "", - "used": "" + "reserved": "Gereserveerd", + "used": "Gebruikt" } }, "resources": { - "title": "", - "nodes": "", - "namespaces": "", - "ingresses": "", - "services": "", - "pods": "", - "configmaps": "", - "secrets": "", - "volumes": "" + "title": "Bronnen", + "nodes": "Nodes", + "namespaces": "Namespaces", + "ingresses": "Ingresses", + "services": "Diensten", + "pods": "Pods", + "configmaps": "ConfigMaps", + "secrets": "Geheimen", + "volumes": "Volumes" } }, "nodes": { - "label": "", + "label": "Nodes", "field": { "name": { - "label": "" + "label": "Naam" }, "state": { - "label": "", + "label": "Status", "option": { - "ready": "", - "NotReady": "" + "ready": "Gereed", + "NotReady": "Niet gereed" } }, "cpu": { - "label": "" + "label": "CPU" }, "memory": { - "label": "" + "label": "RAM" }, "pods": { - "label": "" + "label": "Pods" }, "operatingSystem": { - "label": "" + "label": "OS" }, "architecture": { - "label": "" + "label": "Architectuur" }, "kubernetesVersion": { - "label": "" + "label": "Kubernetes versie" }, "creationTimestamp": { - "label": "" + "label": "Aangemaakt" } }, "table": { - "search": "" + "search": "{count} nodes zoeken" } }, "namespaces": { - "label": "", + "label": "Namespaces", "field": { "name": { - "label": "" + "label": "Naam" }, "state": { - "label": "", + "label": "Status", "option": { - "active": "", - "terminating": "" + "active": "Actief", + "terminating": "Beëindigen" } }, "creationTimestamp": { - "label": "" + "label": "Aangemaakt" } }, "table": { - "search": "" + "search": "{count} namespaces zoeken" } }, "ingresses": { - "label": "", + "label": "Ingresses", "field": { "name": { - "label": "" + "label": "Naam" }, "namespace": { - "label": "" + "label": "Namespace" }, "className": { - "label": "" + "label": "Class naam" }, "rulesAndPaths": { - "label": "" + "label": "Regels & paden" }, "creationTimestamp": { - "label": "" + "label": "Aangemaakt" } }, "table": { - "search": "" + "search": "{count} ingresses zoeken" } }, "services": { - "label": "", + "label": "Diensten", "field": { "name": { - "label": "" + "label": "Naam" }, "namespace": { - "label": "" + "label": "Namespace" }, "type": { - "label": "" + "label": "Type" }, "ports": { - "label": "" + "label": "Poorten" }, "targetPorts": { - "label": "" + "label": "Doel poorten" }, "clusterIP": { - "label": "" + "label": "Cluster IP" }, "creationTimestamp": { - "label": "" + "label": "Aangemaakt" } }, "table": { - "search": "" + "search": "{count} diensten zoeken" } }, "pods": { - "label": "", + "label": "Pods", "field": { "name": { - "label": "" + "label": "Naam" }, "namespace": { - "label": "" + "label": "Namespace" }, "image": { - "label": "" + "label": "Afbeelding" }, "applicationType": { - "label": "" + "label": "Applicatietype" }, "status": { - "label": "" + "label": "Status" }, "creationTimestamp": { - "label": "" + "label": "Aangemaakt" } }, "table": { - "search": "" + "search": "{count} pods zoeken" } }, "secrets": { - "label": "", + "label": "Geheimen", "field": { "name": { - "label": "" + "label": "Naam" }, "namespace": { - "label": "" + "label": "namespace" }, "type": { - "label": "" + "label": "type" }, "creationTimestamp": { - "label": "" + "label": "Aangemaakt" } }, "table": { - "search": "" + "search": "{count} geheimen zoeken" } }, "configmaps": { - "label": "", + "label": "ConfigMaps", "field": { "name": { - "label": "" + "label": "Naam" }, "namespace": { - "label": "" + "label": "namespace" }, "creationTimestamp": { - "label": "" + "label": "Aangemaakt" } }, "table": { - "search": "" + "search": "{count} configMaps zoeken" } }, "volumes": { - "label": "", + "label": "Volumes", "field": { "name": { - "label": "" + "label": "Naam" }, "namespace": { - "label": "" + "label": "Namespace" }, "accessModes": { - "label": "" + "label": "Toegangsmodi" }, "storage": { - "label": "" + "label": "Opslag" }, "storageClassName": { - "label": "" + "label": "Opslag class naam" }, "volumeMode": { - "label": "" + "label": "Volume modus" }, "volumeName": { - "label": "" + "label": "Volume naam" }, "status": { - "label": "" + "label": "Status" }, "creationTimestamp": { - "label": "" + "label": "Aangemaakt" } }, "table": { - "search": "" + "search": "{count} volumes zoeken" } }, "error": { - "internalServerError": "" + "internalServerError": "Kubernetes gegevens ophalen mislukt" } }, "permission": { @@ -3281,30 +3288,30 @@ "label": "Docker" }, "kubernetes": { - "label": "", + "label": "Kubernetes", "nodes": { - "label": "" + "label": "Nodes" }, "namespaces": { - "label": "" + "label": "Namespaces" }, "ingresses": { - "label": "" + "label": "Ingresses" }, "services": { - "label": "" + "label": "Diensten" }, "pods": { - "label": "" + "label": "pods" }, "configmaps": { - "label": "" + "label": "ConfigMaps" }, "secrets": { - "label": "" + "label": "Geheimen" }, "volumes": { - "label": "" + "label": "Volumes" } }, "logs": { diff --git a/packages/translation/src/lang/no.json b/packages/translation/src/lang/no.json index 96e06cc6a..6d14ecdb2 100644 --- a/packages/translation/src/lang/no.json +++ b/packages/translation/src/lang/no.json @@ -980,6 +980,9 @@ "remove": "Fjern dynamisk seksjon" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "Grensefarge" } @@ -1721,7 +1724,11 @@ "noIntegration": "Ingen integrasjon valgt", "noData": "Ingen integrasjonsdata er tilgjengelig" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Videostrømming", diff --git a/packages/translation/src/lang/pl.json b/packages/translation/src/lang/pl.json index 6b9c9374d..8024c0fbf 100644 --- a/packages/translation/src/lang/pl.json +++ b/packages/translation/src/lang/pl.json @@ -980,6 +980,9 @@ "remove": "Usuń sekcję dynamiczną" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "Nie wybrano integracji", "noData": "Brak danych dotyczących integracji" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Strumień wideo", diff --git a/packages/translation/src/lang/pt.json b/packages/translation/src/lang/pt.json index 7ea59e6fd..c5b40b8ad 100644 --- a/packages/translation/src/lang/pt.json +++ b/packages/translation/src/lang/pt.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Transmissão de vídeo", diff --git a/packages/translation/src/lang/ro.json b/packages/translation/src/lang/ro.json index 5096589c8..64582719d 100644 --- a/packages/translation/src/lang/ro.json +++ b/packages/translation/src/lang/ro.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Stream video", diff --git a/packages/translation/src/lang/ru.json b/packages/translation/src/lang/ru.json index 07b91ff7a..56ed8b361 100644 --- a/packages/translation/src/lang/ru.json +++ b/packages/translation/src/lang/ru.json @@ -980,6 +980,9 @@ "remove": "Удалить динамический элемент" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "Цвет границы" } @@ -1721,7 +1724,11 @@ "noIntegration": "Интеграция не выбрана", "noData": "Данные интеграции недоступны" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Видеотрансляция", diff --git a/packages/translation/src/lang/sk.json b/packages/translation/src/lang/sk.json index 20475cbb5..d7a15d322 100644 --- a/packages/translation/src/lang/sk.json +++ b/packages/translation/src/lang/sk.json @@ -980,6 +980,9 @@ "remove": "Nová dynamická sekcia" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "Nie je vybraná žiadna integrácia", "noData": "Nie sú k dispozícii žiadne údaje o integrácii" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Video stream", diff --git a/packages/translation/src/lang/sl.json b/packages/translation/src/lang/sl.json index 6d03d058f..95873ff9c 100644 --- a/packages/translation/src/lang/sl.json +++ b/packages/translation/src/lang/sl.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Video tok", diff --git a/packages/translation/src/lang/sv.json b/packages/translation/src/lang/sv.json index af1cb19ac..9c6546da2 100644 --- a/packages/translation/src/lang/sv.json +++ b/packages/translation/src/lang/sv.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Videoström", diff --git a/packages/translation/src/lang/tr.json b/packages/translation/src/lang/tr.json index 2235d7c14..706108b3d 100644 --- a/packages/translation/src/lang/tr.json +++ b/packages/translation/src/lang/tr.json @@ -980,6 +980,9 @@ "remove": "Dinamik bölümü kaldır" }, "option": { + "title": { + "label": "Başlık" + }, "borderColor": { "label": "Kenarlık rengi" } @@ -1425,76 +1428,76 @@ } }, "stockPrice": { - "name": "", - "description": "", + "name": "Hisse Senedi Fiyatı", + "description": "Bir şirketin güncel hisse senedi fiyatını görüntüler", "option": { "stock": { - "label": "" + "label": "Hisse senedi sembolü" }, "timeRange": { - "label": "", + "label": "Zaman Aralığı", "option": { "1d": { - "label": "" + "label": "1 Gün" }, "5d": { - "label": "" + "label": "5 Gün" }, "1mo": { - "label": "" + "label": "1 Ay" }, "3mo": { - "label": "" + "label": "3 Ay" }, "6mo": { - "label": "" + "label": "6 Ay" }, "ytd": { - "label": "" + "label": "Yıl Başından Bugüne" }, "1y": { - "label": "" + "label": "1 Yıl" }, "2y": { - "label": "" + "label": "2 Yıl" }, "5y": { - "label": "" + "label": "5 Yıl" }, "10y": { - "label": "" + "label": "10 Yıl" }, "max": { - "label": "" + "label": "Maksimum" } } }, "timeInterval": { - "label": "", + "label": "Zaman Aralığı", "option": { "5m": { - "label": "" + "label": "5 Dakika" }, "15m": { - "label": "" + "label": "15 Dakika" }, "30m": { - "label": "" + "label": "30 Dakika" }, "1h": { - "label": "" + "label": "1 Saat" }, "1d": { - "label": "" + "label": "1 Gün" }, "5d": { - "label": "" + "label": "5 Gün" }, "1wk": { - "label": "" + "label": "1 Hafta" }, "1mo": { - "label": "" + "label": "1 Ay" } } } @@ -1721,7 +1724,11 @@ "noIntegration": "Hiçbir entegrasyon seçilmedi", "noData": "Entegrasyon verisi mevcut değil" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Video Akışı", diff --git a/packages/translation/src/lang/uk.json b/packages/translation/src/lang/uk.json index 9549359d1..6f1acbe2e 100644 --- a/packages/translation/src/lang/uk.json +++ b/packages/translation/src/lang/uk.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "Інтеграція не вибрана", "noData": "Немає даних про інтеграцію" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Потокова трансляція відео", diff --git a/packages/translation/src/lang/vi.json b/packages/translation/src/lang/vi.json index 2afa3a73e..f7d9e6755 100644 --- a/packages/translation/src/lang/vi.json +++ b/packages/translation/src/lang/vi.json @@ -980,6 +980,9 @@ "remove": "" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "" } @@ -1721,7 +1724,11 @@ "noIntegration": "", "noData": "" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "Luồng video", diff --git a/packages/translation/src/lang/zh.json b/packages/translation/src/lang/zh.json index 9007ecaf5..2d5d721a6 100644 --- a/packages/translation/src/lang/zh.json +++ b/packages/translation/src/lang/zh.json @@ -980,6 +980,9 @@ "remove": "移除動態區段" }, "option": { + "title": { + "label": "" + }, "borderColor": { "label": "邊框顏色" } @@ -1721,7 +1724,11 @@ "noIntegration": "未選擇集成", "noData": "無可用的集成數據" }, - "option": {} + "option": {}, + "restricted": { + "title": "", + "description": "" + } }, "video": { "name": "串流影音", diff --git a/packages/ui/package.json b/packages/ui/package.json index d2008bdca..00e67c5ad 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -29,12 +29,12 @@ "@homarr/log": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/core": "^7.17.2", - "@mantine/dates": "^7.17.2", - "@mantine/hooks": "^7.17.2", + "@mantine/core": "^7.17.3", + "@mantine/dates": "^7.17.3", + "@mantine/hooks": "^7.17.3", "@tabler/icons-react": "^3.31.0", "mantine-react-table": "2.0.0-beta.9", - "next": "15.1.7", + "next": "15.2.4", "react": "19.0.0", "react-dom": "19.0.0" }, @@ -43,7 +43,7 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/css-modules": "^1.0.5", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/validation/package.json b/packages/validation/package.json index 5cf9b4134..c0aa3f3c9 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -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.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/validation/src/shared.ts b/packages/validation/src/shared.ts index 733d3edef..ddf7c30ea 100644 --- a/packages/validation/src/shared.ts +++ b/packages/validation/src/shared.ts @@ -60,6 +60,7 @@ const emptySectionSchema = z.object({ }); export const dynamicSectionOptionsSchema = z.object({ + title: z.string().max(20).default(""), borderColor: z.string().default(""), }); diff --git a/packages/widgets/package.json b/packages/widgets/package.json index 7e9b57a8f..494beef17 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -44,29 +44,29 @@ "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0", - "@mantine/charts": "^7.17.2", - "@mantine/core": "^7.17.2", - "@mantine/hooks": "^7.17.2", + "@mantine/charts": "^7.17.3", + "@mantine/core": "^7.17.3", + "@mantine/hooks": "^7.17.3", "@tabler/icons-react": "^3.31.0", - "@tiptap/extension-color": "2.11.5", - "@tiptap/extension-highlight": "2.11.5", - "@tiptap/extension-image": "2.11.5", - "@tiptap/extension-link": "^2.11.5", - "@tiptap/extension-table": "2.11.5", - "@tiptap/extension-table-cell": "2.11.5", - "@tiptap/extension-table-header": "2.11.5", - "@tiptap/extension-table-row": "2.11.5", - "@tiptap/extension-task-item": "2.11.5", - "@tiptap/extension-task-list": "2.11.5", - "@tiptap/extension-text-align": "2.11.5", - "@tiptap/extension-text-style": "2.11.5", - "@tiptap/extension-underline": "2.11.5", - "@tiptap/react": "^2.11.5", - "@tiptap/starter-kit": "^2.11.5", + "@tiptap/extension-color": "2.11.6", + "@tiptap/extension-highlight": "2.11.6", + "@tiptap/extension-image": "2.11.6", + "@tiptap/extension-link": "^2.11.6", + "@tiptap/extension-table": "2.11.6", + "@tiptap/extension-table-cell": "2.11.6", + "@tiptap/extension-table-header": "2.11.6", + "@tiptap/extension-table-row": "2.11.6", + "@tiptap/extension-task-item": "2.11.6", + "@tiptap/extension-task-list": "2.11.6", + "@tiptap/extension-text-align": "2.11.6", + "@tiptap/extension-text-style": "2.11.6", + "@tiptap/extension-underline": "2.11.6", + "@tiptap/react": "^2.11.6", + "@tiptap/starter-kit": "^2.11.6", "clsx": "^2.1.1", "dayjs": "^1.11.13", "mantine-react-table": "2.0.0-beta.9", - "next": "15.1.7", + "next": "15.2.4", "react": "19.0.0", "react-dom": "19.0.0", "recharts": "^2.15.1", @@ -78,7 +78,7 @@ "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/video.js": "^7.3.58", - "eslint": "^9.22.0", + "eslint": "^9.23.0", "typescript": "^5.8.2" } } diff --git a/packages/widgets/src/_inputs/widget-app-input.tsx b/packages/widgets/src/_inputs/widget-app-input.tsx index ca7bfcf79..c2015c6d2 100644 --- a/packages/widgets/src/_inputs/widget-app-input.tsx +++ b/packages/widgets/src/_inputs/widget-app-input.tsx @@ -34,7 +34,6 @@ export const WidgetAppInput = ({ property, kind }: CommonWidgetInputProps<"app">