refactor: consolidate English translations (#6640)
This commit is contained in:
1 parent
81aa704537
commit
9e14a07e64
449 files changed
+3515
-3922
No files matched your search
@@ -3,7 +3,7 @@ import { Group, Stack, Tabs } from "@mantine/core";
|
||||
import { IconUser, IconUserDown, IconUsersGroup } from "@tabler/icons-react";
|
||||
|
||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
import { CountBadge } from "@homarr/ui";
|
||||
|
||||
@@ -178,7 +178,7 @@ interface TabItemProps {
|
||||
}
|
||||
|
||||
const TabItem = ({ value, icon: Icon, count }: TabItemProps) => {
|
||||
const t = useScopedI18n("permission");
|
||||
const t = useI18n("permission");
|
||||
|
||||
return (
|
||||
<Tabs.Tab value={value} leftSection={<Icon stroke={1.5} size={16} />}>
|
||||
|
||||
@@ -18,7 +18,7 @@ interface AccessSelectRowProps {
|
||||
}
|
||||
|
||||
export const AccessSelectRow = ({ itemContent, permission, index, handleCountChange }: AccessSelectRowProps) => {
|
||||
const tRoot = useI18n();
|
||||
const tCommon = useI18n("common");
|
||||
const { icons, getSelectData } = useAccessContext();
|
||||
const form = useFormContext();
|
||||
|
||||
@@ -48,7 +48,7 @@ export const AccessSelectRow = ({ itemContent, permission, index, handleCountCha
|
||||
/>
|
||||
|
||||
<Button size="xs" variant="subtle" onClick={handleRemove}>
|
||||
{tRoot("common.action.remove")}
|
||||
{tCommon("action.remove")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</TableTd>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Anchor, Button, Group, Stack, Table, TableTbody, TableTh, TableThead, T
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import type { AccessQueryData } from "./access-settings";
|
||||
@@ -25,8 +25,8 @@ export const GroupAccessForm = <TPermission extends string>({
|
||||
new Map(accessQueryData.groups.map(({ group }) => [group.id, group])),
|
||||
);
|
||||
const { openModal } = useModalAction(GroupSelectModal);
|
||||
const t = useI18n();
|
||||
const tPermissions = useScopedI18n("permission");
|
||||
const tCommon = useI18n("common");
|
||||
const tPermissions = useI18n("permission");
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
items: accessQueryData.groups.map(({ group, permission }) => ({
|
||||
@@ -80,10 +80,10 @@ export const GroupAccessForm = <TPermission extends string>({
|
||||
|
||||
<Group justify="space-between">
|
||||
<Button rightSection={<IconPlus size="1rem" />} variant="light" onClick={handleAddUser}>
|
||||
{t("common.action.add")}
|
||||
{tCommon("action.add")}
|
||||
</Button>
|
||||
<Button type="submit" loading={isPending}>
|
||||
{t("permission.action.saveGroup")}
|
||||
{tPermissions("action.saveGroup")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -19,7 +19,8 @@ interface GroupSelectFormType {
|
||||
}
|
||||
|
||||
export const GroupSelectModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const tGroup = useI18n("group.action.select");
|
||||
const tCommon = useI18n("common");
|
||||
const { data: groups, isPending } = clientApi.group.selectable.useQuery({
|
||||
withPermissions: innerProps.withPermissions,
|
||||
});
|
||||
@@ -39,18 +40,18 @@ export const GroupSelectModal = createModal<InnerProps>(({ actions, innerProps }
|
||||
actions.closeModal();
|
||||
};
|
||||
|
||||
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
|
||||
const confirmLabel = innerProps.confirmLabel ?? tCommon("action.add");
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
|
||||
<Stack>
|
||||
<Select
|
||||
{...form.getInputProps("groupId")}
|
||||
label={t("group.action.select.label")}
|
||||
label={tGroup("label")}
|
||||
clearable
|
||||
searchable
|
||||
leftSection={isPending ? <Loader size="xs" /> : undefined}
|
||||
nothingFoundMessage={t("group.action.select.notFound")}
|
||||
nothingFoundMessage={tGroup("notFound")}
|
||||
limit={5}
|
||||
data={groups
|
||||
?.filter((group) => !innerProps.presentGroupIds.includes(group.id))
|
||||
@@ -58,7 +59,7 @@ export const GroupSelectModal = createModal<InnerProps>(({ actions, innerProps }
|
||||
/>
|
||||
<Group justify="end">
|
||||
<Button variant="default" onClick={actions.closeModal}>
|
||||
{t("common.action.cancel")}
|
||||
{tCommon("action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={loading}>
|
||||
{confirmLabel}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Stack, Table, TableTbody, TableTh, TableThead, TableTr } from "@mantine
|
||||
|
||||
import type { GroupPermissionKey } from "@homarr/definitions";
|
||||
import { getPermissionsWithChildren } from "@homarr/definitions";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { AccessQueryData } from "./access-settings";
|
||||
import { AccessDisplayRow } from "./access-table-rows";
|
||||
@@ -19,7 +19,7 @@ export const InheritAccessTable = <TPermission extends string>({
|
||||
mapPermissions,
|
||||
fullAccessGroupPermission,
|
||||
}: InheritTableProps<TPermission>) => {
|
||||
const tPermissions = useScopedI18n("permission");
|
||||
const tPermissions = useI18n("permission");
|
||||
return (
|
||||
<Stack pt="sm">
|
||||
<Table>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Anchor, Box, Button, Group, Stack, Table, TableTbody, TableTh, TableThe
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Link, UserAvatar } from "@homarr/ui";
|
||||
|
||||
import type { AccessQueryData } from "./access-settings";
|
||||
@@ -42,8 +42,8 @@ export const UsersAccessForm = <TPermission extends string>({
|
||||
new Map(accessQueryData.users.map(({ user }) => [user.id, user])),
|
||||
);
|
||||
const { openModal } = useModalAction(UserSelectModal);
|
||||
const t = useI18n();
|
||||
const tPermissions = useScopedI18n("permission");
|
||||
const tCommon = useI18n("common");
|
||||
const tPermissions = useI18n("permission");
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
items: accessQueryData.users.map(({ user, permission }) => ({
|
||||
@@ -102,10 +102,10 @@ export const UsersAccessForm = <TPermission extends string>({
|
||||
|
||||
<Group justify="space-between">
|
||||
<Button rightSection={<IconPlus size="1rem" />} variant="light" onClick={handleAddUser}>
|
||||
{t("common.action.add")}
|
||||
{tCommon("action.add")}
|
||||
</Button>
|
||||
<Button type="submit" loading={isPending}>
|
||||
{t("permission.action.saveUser")}
|
||||
{tPermissions("action.saveUser")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -23,7 +23,8 @@ interface UserSelectFormType {
|
||||
}
|
||||
|
||||
export const UserSelectModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const tUser = useI18n("user.action.select");
|
||||
const tCommon = useI18n("common");
|
||||
const { data: users, isPending } = clientApi.user.selectable.useQuery({
|
||||
providers: innerProps.allowedProviders,
|
||||
});
|
||||
@@ -44,7 +45,7 @@ export const UserSelectModal = createModal<InnerProps>(({ actions, innerProps })
|
||||
actions.closeModal();
|
||||
};
|
||||
|
||||
const confirmLabel = innerProps.confirmLabel ?? t("common.action.add");
|
||||
const confirmLabel = innerProps.confirmLabel ?? tCommon("action.add");
|
||||
const currentUser = users?.find((user) => user.id === form.values.userId);
|
||||
|
||||
return (
|
||||
@@ -52,13 +53,13 @@ export const UserSelectModal = createModal<InnerProps>(({ actions, innerProps })
|
||||
<Stack>
|
||||
<Select
|
||||
{...form.getInputProps("userId")}
|
||||
label={t("user.action.select.label")}
|
||||
label={tUser("label")}
|
||||
searchable
|
||||
clearable
|
||||
leftSection={
|
||||
isPending ? <Loader size="xs" /> : currentUser ? <UserAvatar user={currentUser} size="xs" /> : undefined
|
||||
}
|
||||
nothingFoundMessage={t("user.action.select.notFound")}
|
||||
nothingFoundMessage={tUser("notFound")}
|
||||
renderOption={createRenderOption(users ?? [])}
|
||||
limit={5}
|
||||
data={users
|
||||
@@ -67,7 +68,7 @@ export const UserSelectModal = createModal<InnerProps>(({ actions, innerProps })
|
||||
/>
|
||||
<Group justify="end">
|
||||
<Button variant="default" onClick={actions.closeModal}>
|
||||
{t("common.action.cancel")}
|
||||
{tCommon("action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={loading}>
|
||||
{confirmLabel}
|
||||
|
||||
@@ -40,7 +40,7 @@ import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { backgroundImageAttachments, backgroundImageRepeats, backgroundImageSizes } from "@homarr/definitions";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { boardSavePartialSettingsSchema } from "@homarr/validation/board";
|
||||
|
||||
import {
|
||||
@@ -97,7 +97,8 @@ export const AssistantConfigureBoardSettingsTool = ({
|
||||
status,
|
||||
toolCallId,
|
||||
}: ToolCallMessagePartProps<ConfigureBoardSettingsArgs, ConfigureBoardSettingsResult>) => {
|
||||
const t = useScopedI18n("common.assistant.configureBoardSettings");
|
||||
const t = useI18n("assistant.configureBoardSettings");
|
||||
const actionT = useI18n("common.action");
|
||||
const hasCompleteArguments = hasCompleteAssistantToolArguments(status);
|
||||
const boardId = args?.boardId ?? "";
|
||||
const settings = clientApi.board.getBoardSettings.useQuery(
|
||||
@@ -150,7 +151,7 @@ export const AssistantConfigureBoardSettingsTool = ({
|
||||
<Stack gap="sm">
|
||||
<Text size="sm">{t("loadErrorDescription")}</Text>
|
||||
<Button variant="light" color="red" size="compact-sm" w="fit-content" onClick={() => settings.refetch()}>
|
||||
{t("retry")}
|
||||
{actionT("tryAgain")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Alert>
|
||||
@@ -199,7 +200,8 @@ const BoardSettingsForm = ({
|
||||
onSubmit: (changes: AssistantBoardSettingsChanges) => void;
|
||||
onCancel: () => void;
|
||||
}) => {
|
||||
const t = useScopedI18n("common.assistant.configureBoardSettings");
|
||||
const t = useI18n("assistant.configureBoardSettings");
|
||||
const actionT = useI18n("common.action");
|
||||
const boardT = useI18n();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const currentValues = useMemo(() => toCurrentValues(current), [current]);
|
||||
@@ -297,7 +299,7 @@ const BoardSettingsForm = ({
|
||||
leftSection={copied ? <IconCheck size={14} /> : <IconCopy size={14} />}
|
||||
onClick={copy}
|
||||
>
|
||||
{copied ? t("copied") : t("copy")}
|
||||
{copied ? t("copied") : actionT("copy")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { PropsWithChildren } from "react";
|
||||
import { useMemo } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { AssistantWidgetRendererProvider } from "@homarr/widgets/assistant/context";
|
||||
|
||||
import { AssistantContext } from "./assistant-context";
|
||||
@@ -73,7 +73,7 @@ const unavailableMessageKeys = {
|
||||
} as const;
|
||||
|
||||
export const AssistantGate = ({ availability, children }: AssistantGateProps) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
|
||||
if (availability === "enabled") {
|
||||
return <EnabledAssistantRoot>{children}</EnabledAssistantRoot>;
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
import { AppForm } from "@homarr/forms-collection";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { appManageSchema } from "@homarr/validation/app";
|
||||
|
||||
import { getAssistantAskUserOptionKind } from "./assistant-ask-user";
|
||||
@@ -54,7 +54,7 @@ export const getAssistantAppFormValues = (
|
||||
) => (hasCompleteAssistantToolArguments(status) ? toAssistantAppValues(args) : null);
|
||||
|
||||
export const AssistantHumanToolError = () => {
|
||||
const t = useScopedI18n("common.assistant.toolPreparationError");
|
||||
const t = useI18n("assistant.toolPreparationError");
|
||||
return (
|
||||
<Alert color="red" variant="light" title={t("title")} icon={<IconAlertTriangle size={18} />}>
|
||||
{t("description")}
|
||||
@@ -68,7 +68,7 @@ export const AssistantAskUserTool = ({
|
||||
addResult,
|
||||
status,
|
||||
}: ToolCallMessagePartProps<AskUserArgs, AskUserResult>) => {
|
||||
const t = useScopedI18n("common.assistant.askUser");
|
||||
const t = useI18n("assistant.askUser");
|
||||
const [showOther, setShowOther] = useState(false);
|
||||
const [other, setOther] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
@@ -272,7 +272,7 @@ export const AssistantConfigureAppTool = ({
|
||||
status,
|
||||
toolCallId,
|
||||
}: ToolCallMessagePartProps<ConfigureAppArgs, AppValues>) => {
|
||||
const t = useScopedI18n("common.assistant.configureApp");
|
||||
const t = useI18n("assistant.configureApp");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const initialValues = getAssistantAppFormValues(args, status);
|
||||
const autoConfirming = useAssistantAutomaticAction({
|
||||
|
||||
@@ -131,7 +131,7 @@ import { assistantProviderIds, assistantProviderPresets, assistantReasoningModes
|
||||
import type { AssistantProvider } from "@homarr/definitions";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useConfirmModal } from "@homarr/modals";
|
||||
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useCurrentIntlLocale, useI18n } from "@homarr/translation/client";
|
||||
|
||||
import classes from "./assistant-panel.module.css";
|
||||
import { getAssistantActivityState } from "./assistant-activity-state";
|
||||
@@ -218,7 +218,7 @@ const ContextDirectiveChip = ({
|
||||
label,
|
||||
iconUrl,
|
||||
}: DirectiveChipProps & { iconUrl?: string }) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const Icon = contextIcons[directiveType as keyof typeof contextIcons] ?? IconAt;
|
||||
const safeIconUrl = getSafeAssistantMarkdownImageSource(iconUrl);
|
||||
const translationKey = getAssistantDirectiveTranslationKey(directiveType);
|
||||
@@ -291,7 +291,7 @@ const MarkdownTable = ({ children, ...props }: ComponentPropsWithoutRef<"table">
|
||||
);
|
||||
|
||||
const MarkdownImage = ({ src, alt = "", ...props }: ComponentPropsWithoutRef<"img">) => {
|
||||
const t = useScopedI18n("common.assistant.image");
|
||||
const t = useI18n("assistant.image");
|
||||
const safeSource = getSafeAssistantMarkdownImageSource(src);
|
||||
if (!safeSource) return null;
|
||||
|
||||
@@ -343,7 +343,7 @@ const AssistantSyntaxHighlighter = ({ code, components, language }: SyntaxHighli
|
||||
};
|
||||
|
||||
const MermaidDiagram = ({ code, components }: SyntaxHighlighterProps) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const colorScheme = useComputedColorScheme("light");
|
||||
const diagramId = `assistant-mermaid-${useId().replaceAll(/[^A-Za-z0-9_-]/gu, "")}`;
|
||||
const [rendered, setRendered] = useState<{ code: string; svg?: string; failed?: boolean }>({ code });
|
||||
@@ -541,7 +541,7 @@ const AttachmentPreview = () => {
|
||||
};
|
||||
|
||||
const Attachment = ({ removable = false }: { removable?: boolean }) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const attachment = useAuiState((state) => state.attachment);
|
||||
return (
|
||||
<AttachmentPrimitive.Root className={classes.attachment}>
|
||||
@@ -583,7 +583,7 @@ const ReasoningPart = (_props: ReasoningMessagePartProps) => (
|
||||
);
|
||||
|
||||
const AssistantMessagePending = ({ status }: EmptyMessagePartProps) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
if (status.type !== "running") return null;
|
||||
|
||||
return (
|
||||
@@ -622,7 +622,7 @@ const SourcePart = (source: SourceMessagePartProps) => {
|
||||
};
|
||||
|
||||
const FilePart = ({ data, filename, mimeType }: FileMessagePartProps) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const downloadable = data.startsWith("data:");
|
||||
const displayName = filename ?? mimeType;
|
||||
return (
|
||||
@@ -649,7 +649,7 @@ const FilePart = ({ data, filename, mimeType }: FileMessagePartProps) => {
|
||||
};
|
||||
|
||||
const ImagePart = ({ image, filename }: ImageMessagePartProps) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const source = getSafeAssistantAttachmentImageSource(
|
||||
image,
|
||||
typeof window === "undefined" ? undefined : window.location.origin,
|
||||
@@ -814,7 +814,7 @@ const getToolResultNavigation = (result: unknown) => {
|
||||
};
|
||||
|
||||
const ToolResultNavigation = ({ result }: { result: unknown }) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const router = useRouter();
|
||||
const links = getToolResultNavigation(result);
|
||||
|
||||
@@ -870,7 +870,7 @@ const ToolPart = ({
|
||||
respondToApproval,
|
||||
timing,
|
||||
}: ToolCallMessagePartProps) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const compact = useContext(AgentTraceToolContext);
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [approvalResponse, setApprovalResponse] = useState<"approve" | "deny" | null>(null);
|
||||
@@ -1048,7 +1048,7 @@ const ChainOfThoughtLayout = ({ children }: { children?: ReactNode }) => (
|
||||
);
|
||||
|
||||
const AssistantChainOfThought = ({ children }: { children?: ReactNode }) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const contentId = useId();
|
||||
const { collapsed: preferredCollapsed, setCollapsed: setPreferredCollapsed } = useContext(ReasoningVisibilityContext);
|
||||
const { chainStatus, collapsed } = useAssistantReasoningState(preferredCollapsed);
|
||||
@@ -1083,7 +1083,7 @@ const AssistantChainOfThought = ({ children }: { children?: ReactNode }) => {
|
||||
};
|
||||
|
||||
const BranchPicker = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
return (
|
||||
<BranchPickerPrimitive.Root hideWhenSingleBranch className={classes.branchPicker}>
|
||||
<BranchPickerPrimitive.Previous asChild>
|
||||
@@ -1116,7 +1116,7 @@ const downloadAssistantMarkdown = (markdown: string, filename: string) => {
|
||||
};
|
||||
|
||||
const AssistantMessageActions = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const aui = useAui();
|
||||
const exportMessage = () => {
|
||||
const message = aui.message().getState();
|
||||
@@ -1129,7 +1129,6 @@ const AssistantMessageActions = () => {
|
||||
});
|
||||
downloadAssistantMarkdown(markdown, `assistant-message-${message.id.slice(0, 8)}.md`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={classes.messageActions}>
|
||||
<ActionBarPrimitive.Root
|
||||
@@ -1211,7 +1210,7 @@ const AssistantMessageActions = () => {
|
||||
};
|
||||
|
||||
const UserMessageActions = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
return (
|
||||
<ActionBarPrimitive.Root hideWhenRunning autohide="not-last" className={classes.userActions}>
|
||||
<Group gap={2}>
|
||||
@@ -1235,14 +1234,15 @@ const UserMessageActions = () => {
|
||||
};
|
||||
|
||||
const EditComposer = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const actionT = useI18n("common.action");
|
||||
return (
|
||||
<ComposerPrimitive.Root className={classes.editComposer}>
|
||||
<ComposerPrimitive.Input className={classes.editComposerInput} rows={2} aria-label={t("editMessage")} />
|
||||
<Group gap="xs" justify="flex-end">
|
||||
<ComposerPrimitive.Cancel asChild>
|
||||
<Button size="compact-sm" variant="default">
|
||||
{t("cancelEdit")}
|
||||
{actionT("cancel")}
|
||||
</Button>
|
||||
</ComposerPrimitive.Cancel>
|
||||
<ComposerPrimitive.Send asChild>
|
||||
@@ -1361,7 +1361,7 @@ const ConversationTurn = ({
|
||||
threadId?: string;
|
||||
turn: AssistantConversationTurnUsage;
|
||||
}) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const detailsId = useId();
|
||||
const [opened, setOpened] = useState(false);
|
||||
const generationQuery = clientApi.assistant.getGenerationTelemetry.useQuery(
|
||||
@@ -1657,7 +1657,7 @@ const ConversationTurn = ({
|
||||
};
|
||||
|
||||
const ConversationContextBreakdown = ({ breakdown }: { breakdown: AssistantContextBreakdown }) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const hasContext = breakdown.percentage !== undefined;
|
||||
const categories = [
|
||||
{ label: t("usage.input"), value: breakdown.inputTokens },
|
||||
@@ -1715,7 +1715,7 @@ const ConversationContextBreakdown = ({ breakdown }: { breakdown: AssistantConte
|
||||
};
|
||||
|
||||
const ConversationContext = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const [opened, setOpened] = useState(false);
|
||||
const messages = useAuiState((state) => state.thread.messages);
|
||||
const threadId = useAuiState((state) => state.threadListItem.remoteId);
|
||||
@@ -1847,7 +1847,8 @@ const ConversationContext = () => {
|
||||
);
|
||||
};
|
||||
const RuntimeError = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const actionT = useI18n("common.action");
|
||||
return (
|
||||
<MessagePrimitive.Error>
|
||||
<ErrorPrimitive.Root className={classes.messageError}>
|
||||
@@ -1868,7 +1869,7 @@ const RuntimeError = () => {
|
||||
w="fit-content"
|
||||
leftSection={<IconRefresh size={14} />}
|
||||
>
|
||||
{t("responseError.retry")}
|
||||
{actionT("tryAgain")}
|
||||
</Button>
|
||||
</ActionBarPrimitive.Reload>
|
||||
</Stack>
|
||||
@@ -1879,7 +1880,7 @@ const RuntimeError = () => {
|
||||
};
|
||||
|
||||
const WebSearchActivity = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const metadata = useAuiState((state) => state.message.metadata);
|
||||
const telemetry = getAssistantTelemetry(metadata);
|
||||
if (!telemetry) return null;
|
||||
@@ -1997,7 +1998,8 @@ const assistantThreadMessageComponents = { UserMessage, AssistantMessage };
|
||||
const HistorySelectContext = createContext<() => void>(() => undefined);
|
||||
|
||||
const ThreadListItem = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const actionT = useI18n("common.action");
|
||||
const aui = useAui();
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const onSelect = useContext(HistorySelectContext);
|
||||
@@ -2057,7 +2059,7 @@ const ThreadListItem = () => {
|
||||
title: t("deleteConversation.title"),
|
||||
children: t("deleteConversation.description", { title: title ?? t("newConversation") }),
|
||||
confirmProps: { color: "red" },
|
||||
labels: { confirm: t("delete"), cancel: t("cancel") },
|
||||
labels: { confirm: actionT("delete"), cancel: actionT("cancel") },
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await aui.threadListItem().delete();
|
||||
@@ -2103,7 +2105,7 @@ const ThreadListItem = () => {
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="sm"
|
||||
aria-label={t("cancel")}
|
||||
aria-label={actionT("cancel")}
|
||||
disabled={renaming}
|
||||
onClick={() => setRenameOpened(false)}
|
||||
>
|
||||
@@ -2154,8 +2156,8 @@ const ThreadListItem = () => {
|
||||
variant="subtle"
|
||||
color="red"
|
||||
size="sm"
|
||||
title={t("delete")}
|
||||
aria-label={t("delete")}
|
||||
title={actionT("delete")}
|
||||
aria-label={actionT("delete")}
|
||||
onClick={deleteConversation}
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
@@ -2179,7 +2181,7 @@ const ThreadListItem = () => {
|
||||
};
|
||||
|
||||
const ThreadHistory = ({ onSelect }: { onSelect: () => void }) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const [query, setQuery] = useState("");
|
||||
const normalizedQuery = query.trim().toLocaleLowerCase();
|
||||
const threadItems = useAuiState((state) => state.threads.threadItems);
|
||||
@@ -2233,7 +2235,7 @@ const ThreadHistory = ({ onSelect }: { onSelect: () => void }) => {
|
||||
};
|
||||
|
||||
const ConversationHistory = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const [opened, setOpened] = useState(false);
|
||||
return (
|
||||
<Popover
|
||||
@@ -2272,7 +2274,7 @@ const ConversationHistory = () => {
|
||||
};
|
||||
|
||||
const AutoApprovalControl = () => {
|
||||
const t = useScopedI18n("common.assistant.autoApproval");
|
||||
const t = useI18n("assistant.autoApproval");
|
||||
const [opened, setOpened] = useState(false);
|
||||
const { enabled, setEnabled } = useAssistantAutoApproval();
|
||||
|
||||
@@ -2331,7 +2333,7 @@ const ViewRefreshAction = ({
|
||||
isRefreshing,
|
||||
onRefresh,
|
||||
}: Pick<AssistantConversationControls, "isRefreshing" | "onRefresh">) => {
|
||||
const t = useScopedI18n("common.assistant.refresh");
|
||||
const t = useI18n("assistant.refresh");
|
||||
|
||||
const refresh = async () => {
|
||||
try {
|
||||
@@ -2360,7 +2362,7 @@ const ViewRefreshAction = ({
|
||||
};
|
||||
|
||||
const EmptyThread = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
return (
|
||||
<ThreadPrimitive.Empty>
|
||||
<Box className={classes.empty}>
|
||||
@@ -2461,7 +2463,7 @@ const TriggerItem = ({ item, index }: { item: Unstable_TriggerItem; index: numbe
|
||||
};
|
||||
|
||||
const ComposerTriggers = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const aui = useAui();
|
||||
const mentionViewportRef = useRef<HTMLDivElement>(null);
|
||||
const slashViewportRef = useRef<HTMLDivElement>(null);
|
||||
@@ -2604,7 +2606,7 @@ const RuntimeControls = ({
|
||||
onModelChange,
|
||||
onReasoningChange,
|
||||
}: ComposerProps) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const [modelSearch, setModelSearch] = useState("");
|
||||
const selectedModel = models.find((model) => model.id === modelId);
|
||||
const normalizedModelSearch = modelSearch.trim().toLocaleLowerCase();
|
||||
@@ -2759,7 +2761,7 @@ const providerQuotaColors = {
|
||||
} as const;
|
||||
|
||||
const HomarrProviderQuota = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const locale = useCurrentIntlLocale();
|
||||
const preferences = useAssistantPreferences();
|
||||
const quota = preferences.quota;
|
||||
@@ -2882,7 +2884,7 @@ const HomarrProviderQuota = () => {
|
||||
};
|
||||
|
||||
const Composer = (props: ComposerProps) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const preferences = useAssistantPreferences();
|
||||
const running = useAuiState((state) => state.thread.isRunning);
|
||||
const hasPendingAction = props.pendingAction !== undefined;
|
||||
@@ -2987,7 +2989,7 @@ const Composer = (props: ComposerProps) => {
|
||||
};
|
||||
|
||||
const usePendingActionCopy = (action: AssistantPendingAction | undefined) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
if (!action) return undefined;
|
||||
if (action.kind === "question") {
|
||||
return { title: t("pendingAction.answerTitle"), detail: action.detail ?? t("pendingAction.answerFallback") };
|
||||
@@ -3004,7 +3006,7 @@ const usePendingActionCopy = (action: AssistantPendingAction | undefined) => {
|
||||
};
|
||||
|
||||
const PendingActionBanner = ({ pendingAction }: { pendingAction: AssistantPendingAction | undefined }) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const copy = usePendingActionCopy(pendingAction);
|
||||
if (!copy || pendingAction?.kind === "question") return null;
|
||||
|
||||
@@ -3039,7 +3041,7 @@ const PendingQuestionDock = ({
|
||||
pendingAction: AssistantPendingAction | undefined;
|
||||
setTarget: (target: HTMLDivElement | null) => void;
|
||||
}) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
if (pendingAction?.kind !== "question") return null;
|
||||
|
||||
return (
|
||||
@@ -3073,7 +3075,7 @@ const AssistantActivityBar = ({
|
||||
| "onDismissActivity"
|
||||
| "activityDismissed"
|
||||
>) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const pendingCopy = usePendingActionCopy(pendingAction);
|
||||
const needsApproval = pendingAction !== undefined || latestStatus?.type === "requires-action";
|
||||
const failed = latestStatus?.type === "incomplete" && latestStatus.reason !== "cancelled";
|
||||
@@ -3161,7 +3163,7 @@ interface AssistantConversationSurfaceProps extends AssistantConversationControl
|
||||
}
|
||||
|
||||
const AssistantPanelIdentity = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const threadTitle = useAuiState((state) => state.threadListItem.title);
|
||||
|
||||
return (
|
||||
@@ -3197,7 +3199,7 @@ export const AssistantConversationSurface = ({
|
||||
onMinimize,
|
||||
onDismiss,
|
||||
}: AssistantConversationSurfaceProps) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const reducedMotion = useReducedMotion();
|
||||
const [questionPortalTarget, setQuestionPortalTarget] = useState<HTMLDivElement | null>(null);
|
||||
const [reasoningCollapsed, setReasoningCollapsed] = useState(false);
|
||||
@@ -3343,7 +3345,7 @@ export const AssistantPanel = ({
|
||||
onModelChange,
|
||||
onReasoningChange,
|
||||
}: AssistantPanelProps) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const aui = useAui();
|
||||
const previousFocusRef = useRef<HTMLElement | null>(null);
|
||||
const panelRef = useRef<HTMLDialogElement>(null);
|
||||
|
||||
@@ -30,7 +30,7 @@ import { createId } from "@homarr/common";
|
||||
import { assistantHomarrProviderTokenHeader, hotkeys } from "@homarr/definitions";
|
||||
import { showErrorNotification, showWarningNotification } from "@homarr/notifications";
|
||||
import { closeSpotlight } from "@homarr/spotlight/store";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { openMediaRequestSearch, openSpotlight, useRegisterSpotlightContextResults } from "@homarr/spotlight";
|
||||
import { AssistantWidgetRendererProvider } from "@homarr/widgets";
|
||||
import { createWorkshopClient } from "~/components/workshop/workshop-client";
|
||||
@@ -135,7 +135,7 @@ const createAssistantAttachmentAdapter = (allowImages: boolean): AttachmentAdapt
|
||||
};
|
||||
|
||||
const AssistantPreferencesProvider = ({ children }: PropsWithChildren) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const { data, isLoading } = clientApi.assistant.getRuntimeOptions.useQuery(undefined, {
|
||||
staleTime: 10 * 60_000,
|
||||
});
|
||||
@@ -454,7 +454,7 @@ const createHistoryAdapter = (threadId: string | undefined): ThreadHistoryAdapte
|
||||
});
|
||||
|
||||
const AssistantThreadRuntime = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const aui = useAui();
|
||||
const localThreadId = useAuiState((state) => state.threadListItem.id);
|
||||
const threadId = useAuiState((state) => state.threadListItem.remoteId);
|
||||
@@ -652,7 +652,7 @@ const AssistantPreferenceSync = () => {
|
||||
};
|
||||
|
||||
const AssistantRuntimeEvents = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
useAuiEvent("composer.attachmentAddError", ({ message }) => {
|
||||
showErrorNotification({
|
||||
title: t("attachments.errorTitle"),
|
||||
@@ -679,7 +679,7 @@ const getNotificationKey = (message: ThreadMessage | undefined) => {
|
||||
};
|
||||
|
||||
const EnabledAssistantProvider = ({ children }: PropsWithChildren) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [activityDismissed, setActivityDismissed] = useState(true);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
} from "./assistant-runtime-provider";
|
||||
|
||||
vi.mock("@homarr/notifications", () => ({ showErrorNotification: vi.fn() }));
|
||||
vi.mock("@homarr/translation/client", () => ({ useScopedI18n: () => (key: string) => key }));
|
||||
vi.mock("@homarr/translation/client", () => ({ useI18n: () => (key: string) => key }));
|
||||
|
||||
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { AssistantRuntime, AttachmentAdapter, ThreadListRuntime, Toolkit }
|
||||
import { AssistantRuntimeProvider, INTERNAL, Tools, useAui, useAuiEvent } from "@assistant-ui/react";
|
||||
|
||||
import { showErrorNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface AssistantRuntimeProviderWithToolsProps extends PropsWithChildren {
|
||||
runtime: AssistantRuntime;
|
||||
@@ -437,7 +437,7 @@ const getClipboardAttachmentName = (file: File, index: number) => {
|
||||
};
|
||||
|
||||
const AssistantComposerSurfaceEvents = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
useAuiEvent("composer.attachmentAddError", ({ message }) => {
|
||||
showErrorNotification({ title: t("attachments.errorTitle"), message });
|
||||
});
|
||||
@@ -445,7 +445,7 @@ const AssistantComposerSurfaceEvents = () => {
|
||||
};
|
||||
|
||||
export const AssistantComposerSurfaceBoundary = ({ children, surfaceId }: PropsWithChildren<{ surfaceId: string }>) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const aui = useAui();
|
||||
const instanceId = useId();
|
||||
const resolvedSurfaceId = `${surfaceId}:${instanceId}`;
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useRegisterSpotlightContextResults } from "@homarr/spotlight";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
/**
|
||||
* Keeps the assistant discoverable in the spotlight when it is unavailable, without loading the
|
||||
* assistant runtime. Lives apart from `assistant-provider` so the disabled path stays cheap.
|
||||
*/
|
||||
export const useRegisterAssistantSpotlightPlaceholder = (description: string) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
const spotlightItem = useMemo(
|
||||
() => ({
|
||||
id: "homarr-assistant",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Group, Kbd, Stack, Text, ThemeIcon } from "@mantine/core";
|
||||
import { IconArrowUp, IconRobot } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface AssistantPromptInteractionOptions {
|
||||
sendPrompt: (prompt: string) => boolean;
|
||||
@@ -13,7 +13,7 @@ interface AssistantPromptInteractionOptions {
|
||||
}
|
||||
|
||||
const AssistantPromptDetail = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
return (
|
||||
<Group px="md" py="sm" wrap="nowrap">
|
||||
<ThemeIcon color="red" variant="light" radius="xl">
|
||||
@@ -30,7 +30,7 @@ const AssistantPromptDetail = () => {
|
||||
};
|
||||
|
||||
const AssistantPromptAction = ({ hasPrompt }: { hasPrompt: boolean }) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const t = useI18n("assistant");
|
||||
return (
|
||||
<Group w="100%" px="md" py="xs" justify="space-between" wrap="nowrap" opacity={hasPrompt ? 1 : 0.6}>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
|
||||
@@ -7,11 +7,12 @@ import { IconAlertTriangle, IconCheck, IconSettings } from "@tabler/icons-react"
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { getWidgetName } from "@homarr/definitions";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import type { SettingsContextProps } from "@homarr/settings/creator";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
|
||||
import type { IntegrationSelectOption } from "@homarr/widgets/widget-integration-select";
|
||||
import { WidgetEditModal } from "@homarr/widgets/modals";
|
||||
@@ -66,7 +67,8 @@ export const AssistantConfigureWidgetTool = ({
|
||||
status,
|
||||
toolCallId,
|
||||
}: ToolCallMessagePartProps<ConfigureWidgetArgs, ConfigureWidgetResult>) => {
|
||||
const t = useScopedI18n("common.assistant.configureWidget");
|
||||
const t = useI18n("assistant.configureWidget");
|
||||
const actionT = useI18n("common.action");
|
||||
const fullT = useI18n();
|
||||
const { data: session } = useSession();
|
||||
const settings = useSettings();
|
||||
@@ -116,7 +118,7 @@ export const AssistantConfigureWidgetTool = ({
|
||||
{cancelled ? t("cancelled") : t("ready")}
|
||||
</Text>
|
||||
<Text size="sm" fw={600} truncate>
|
||||
{args?.kind ? fullT(`widget.${args.kind}.name`) : t("title")}
|
||||
{args?.kind ? getWidgetName(args.kind, fullT) : t("title")}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -149,7 +151,7 @@ export const AssistantConfigureWidgetTool = ({
|
||||
<Stack gap="sm">
|
||||
<Text size="sm">{t("loadErrorDescription")}</Text>
|
||||
<Button variant="light" color="red" size="compact-sm" w="fit-content" onClick={() => integrations.refetch()}>
|
||||
{t("retry")}
|
||||
{actionT("tryAgain")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Alert>
|
||||
@@ -174,7 +176,7 @@ export const AssistantConfigureWidgetTool = ({
|
||||
addResult({ boardId: args.boardId, kind: args.kind, options, integrationIds }),
|
||||
},
|
||||
{
|
||||
title: (titleT) => `${titleT("item.edit.title")} - ${titleT(`widget.${args.kind}.name`)}`,
|
||||
title: (titleT) => `${titleT("item.edit.title")} - ${getWidgetName(args.kind, titleT)}`,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -246,7 +248,7 @@ export const AssistantConfigureWidgetTool = ({
|
||||
<WidgetIcon size={20} />
|
||||
</ThemeIcon>
|
||||
<Box miw={0} flex={1}>
|
||||
<Text fw={700}>{fullT(`widget.${args.kind}.name`)}</Text>
|
||||
<Text fw={700}>{getWidgetName(args.kind, fullT)}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{args.summary}
|
||||
</Text>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Badge, Group, Paper, Stack, Table, Text, ThemeIcon, Title } from "@mantine/core";
|
||||
import { IconCheck, IconDatabase, IconGitBranch } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { BackupAnalysis } from "./types";
|
||||
import { PREVIEW_TABLE_KEYS } from "./types";
|
||||
@@ -13,10 +13,25 @@ interface BackupPreviewPanelProps {
|
||||
}
|
||||
|
||||
export const BackupPreviewPanel = ({ analysis }: BackupPreviewPanelProps) => {
|
||||
const t = useScopedI18n("management.page.tool.backup.restore.preview");
|
||||
const t = useI18n("management.page.tool.backup.restore.preview");
|
||||
const tEntities = useI18n("common.entity");
|
||||
const exportDate = new Date(analysis.metadata.exportedAt).toLocaleString();
|
||||
const pendingCount = analysis.migrations.pending.length;
|
||||
const hasPendingMigrations = pendingCount > 0;
|
||||
const entityKeys = {
|
||||
app: "apps",
|
||||
board: "boards",
|
||||
integration: "integrations",
|
||||
item: undefined,
|
||||
media: "media",
|
||||
search_engine: "searchEngines",
|
||||
user: "users",
|
||||
} as const;
|
||||
const getEntityLabel = (key: (typeof PREVIEW_TABLE_KEYS)[number]) => {
|
||||
const entityKey = entityKeys[key];
|
||||
if (entityKey) return tEntities(entityKey);
|
||||
return t(`entities.${key}` as never);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
@@ -47,7 +62,7 @@ export const BackupPreviewPanel = ({ analysis }: BackupPreviewPanelProps) => {
|
||||
<Table.Tbody>
|
||||
{PREVIEW_TABLE_KEYS.map((key) => (
|
||||
<Table.Tr key={key}>
|
||||
<Table.Td>{t(`entities.${key}` as never)}</Table.Td>
|
||||
<Table.Td>{getEntityLabel(key)}</Table.Td>
|
||||
<Table.Td ta="right">
|
||||
<Group gap={4} justify="flex-end">
|
||||
{(analysis.counts[key] ?? 0) > 0 && <IconCheck size={14} color="var(--mantine-color-green-6)" />}
|
||||
@@ -64,7 +79,7 @@ export const BackupPreviewPanel = ({ analysis }: BackupPreviewPanelProps) => {
|
||||
{analysis.boardNames.length > 0 && (
|
||||
<Group gap="xs" wrap="wrap">
|
||||
<Text size="sm" fw={500}>
|
||||
{t("boards")}:
|
||||
{tEntities("boards")}:
|
||||
</Text>
|
||||
{analysis.boardNames.map((name) => (
|
||||
<Badge key={name} variant="light" size="sm">
|
||||
|
||||
@@ -8,7 +8,7 @@ import { IconAlertTriangle, IconArrowRight, IconFileZip, IconUpload, IconX } fro
|
||||
|
||||
import "@mantine/dropzone/styles.css"; // oxlint-disable-line import/no-unassigned-import
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { RestoreStep } from "./types";
|
||||
import { BackupPreviewPanel } from "./backup-preview-panel";
|
||||
@@ -103,7 +103,8 @@ export const waitForServerReadinessAsync = async ({
|
||||
};
|
||||
|
||||
export const DatabaseRestoreFlow = ({ variant = "card", onRestoreComplete }: DatabaseRestoreFlowProps) => {
|
||||
const t = useScopedI18n("management.page.tool.backup.restore");
|
||||
const t = useI18n("management.page.tool.backup.restore");
|
||||
const tCommon = useI18n("common");
|
||||
const [file, setFile] = useState<FileWithPath | null>(null);
|
||||
const [step, setStep] = useState<RestoreStep>("upload");
|
||||
const [importError, setImportError] = useState<string | null>(null);
|
||||
@@ -330,7 +331,7 @@ export const DatabaseRestoreFlow = ({ variant = "card", onRestoreComplete }: Dat
|
||||
</Alert>
|
||||
<Group>
|
||||
<Button variant="subtle" onClick={handleClear}>
|
||||
{t("tryAgain")}
|
||||
{tCommon("action.tryAgain")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Box, Group, Loader, Paper, Stack, Text, ThemeIcon, Timeline } from "@mantine/core";
|
||||
import { IconCheck, IconDatabase, IconLoader } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { MigrationProgress } from "./use-backup-analysis";
|
||||
|
||||
@@ -12,7 +12,7 @@ interface MigrationProgressPanelProps {
|
||||
}
|
||||
|
||||
export const MigrationProgressPanel = ({ progress }: MigrationProgressPanelProps) => {
|
||||
const t = useScopedI18n("management.page.tool.backup.restore.migration");
|
||||
const t = useI18n("management.page.tool.backup.restore.migration");
|
||||
|
||||
return (
|
||||
<Paper p="md" radius="md" bg="var(--mantine-color-dark-7)">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { IconDatabaseImport } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { DangerousActionConfirmation } from "./dangerous-action-confirmation";
|
||||
|
||||
@@ -13,17 +13,19 @@ interface RestoreConfirmationProps {
|
||||
}
|
||||
|
||||
export const RestoreConfirmation = ({ onConfirm, onCancel, disabled }: RestoreConfirmationProps) => {
|
||||
const t = useScopedI18n("management.page.tool.backup.restore.confirm");
|
||||
const t = useI18n("management.page.tool.backup.restore.confirm");
|
||||
const tBackup = useI18n("management.page.tool.backup");
|
||||
const tCommon = useI18n("common");
|
||||
|
||||
return (
|
||||
<DangerousActionConfirmation
|
||||
title={t("title")}
|
||||
warningTitle={t("warningTitle")}
|
||||
warningBody={t("warningBody")}
|
||||
typePrompt={t("typePrompt", { phrase: "I understand" })}
|
||||
typePrompt={tBackup("confirm.typePrompt", { phrase: "I understand" })}
|
||||
submitLabel={t("submit")}
|
||||
submitIcon={<IconDatabaseImport size={16} />}
|
||||
cancelLabel={t("cancel")}
|
||||
cancelLabel={tCommon("action.cancel")}
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -7,16 +7,18 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@homarr/translation/client", () => {
|
||||
const translations: Record<string, string> = {
|
||||
"progress.title": "Restoring database...",
|
||||
"progress.restarting": "Restarting server",
|
||||
"timeout.title": "Server did not come back online",
|
||||
"timeout.message": "Retry the connection check or refresh this page manually.",
|
||||
tryAgain: "Try again",
|
||||
"docker.action.refresh.label": "Refresh",
|
||||
"management.page.tool.backup.restore.progress.title": "Restoring database...",
|
||||
"management.page.tool.backup.restore.progress.restarting": "Restarting server",
|
||||
"management.page.tool.backup.restore.timeout.title": "Server did not come back online",
|
||||
"management.page.tool.backup.restore.timeout.message": "Retry the connection check or refresh this page manually.",
|
||||
"common.action.tryAgain": "Try again",
|
||||
"common.action.refresh": "Refresh",
|
||||
};
|
||||
return {
|
||||
useScopedI18n: () => (key: string) => translations[key] ?? key,
|
||||
useI18n: () => (key: string) => translations[key] ?? key,
|
||||
useI18n: (namespace?: string) => (key: string) => {
|
||||
const fullKey = namespace ? `${namespace}.${key}` : key;
|
||||
return translations[fullKey] ?? key;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button, Group, Loader, Paper, Stack, Text, ThemeIcon } from "@mantine/c
|
||||
import { useReducedMotion } from "@mantine/hooks";
|
||||
import { IconAlertTriangle, IconCheck, IconDatabaseImport, IconRefresh } from "@tabler/icons-react";
|
||||
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
export type RestoreProgressStatus = "restoring" | "restarting" | "timedOut";
|
||||
|
||||
@@ -16,8 +16,8 @@ interface RestoreProgressPanelProps {
|
||||
}
|
||||
|
||||
export const RestoreProgressPanel = ({ active, status, onRetry, onReload }: RestoreProgressPanelProps) => {
|
||||
const t = useScopedI18n("management.page.tool.backup.restore");
|
||||
const tRoot = useI18n();
|
||||
const t = useI18n("management.page.tool.backup.restore");
|
||||
const tCommon = useI18n("common");
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
if (!active) return null;
|
||||
@@ -66,10 +66,10 @@ export const RestoreProgressPanel = ({ active, status, onRetry, onReload }: Rest
|
||||
{isTimedOut ? (
|
||||
<Group>
|
||||
<Button variant="light" onClick={onRetry} leftSection={<IconRefresh size={16} />}>
|
||||
{t("tryAgain")}
|
||||
{tCommon("action.tryAgain")}
|
||||
</Button>
|
||||
<Button variant="default" onClick={onReload}>
|
||||
{tRoot("docker.action.refresh.label")}
|
||||
{tCommon("action.refresh")}
|
||||
</Button>
|
||||
</Group>
|
||||
) : null}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useTiks } from "@rexa-developer/tiks/react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { BackupAnalysis, MigrationFile, MigrationStatus } from "./types";
|
||||
import { PREVIEW_TABLE_KEYS } from "./types";
|
||||
@@ -60,7 +60,7 @@ export interface MigrationProgress {
|
||||
}
|
||||
|
||||
export const useBackupAnalysis = () => {
|
||||
const t = useScopedI18n("management.page.tool.backup.restore");
|
||||
const t = useI18n("management.page.tool.backup.restore");
|
||||
const sounds = useTiks({ theme: "soft", volume: 0.12, respectReducedMotion: true });
|
||||
const [analysis, setAnalysis] = useState<BackupAnalysis | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IconBox, IconLayoutDashboard, IconPlug, IconResize } from "@tabler/icon
|
||||
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { useBoardAddActions } from "./use-board-add-actions";
|
||||
import classes from "./board-empty-state.module.css";
|
||||
@@ -14,7 +14,7 @@ export const BoardEmptyState = () => {
|
||||
const board = useRequiredBoard();
|
||||
const [isEditMode] = useEditMode();
|
||||
const { addWidget, addApp, connectService, canConnectService } = useBoardAddActions();
|
||||
const t = useScopedI18n("board.emptyState");
|
||||
const t = useI18n("board.emptyState");
|
||||
const hasBoardContent = board.items.length > 0 || board.sections.some((section) => section.kind !== "empty");
|
||||
|
||||
if (!isEditMode || hasBoardContent) return null;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IconCheck, IconChevronDown, IconChecklist } from "@tabler/icons-react";
|
||||
import { useIntegrationsWithUseAccess } from "@homarr/auth/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { useSetupAnalytics } from "~/components/create/setup-analytics";
|
||||
import { getBoardSetupProgress } from "./board-setup-progress";
|
||||
@@ -23,7 +23,8 @@ export const BoardSetupChecklist = () => {
|
||||
key: `homarr-board-setup-checklist-${board.id}`,
|
||||
defaultValue: false,
|
||||
});
|
||||
const t = useScopedI18n("board.setupChecklist");
|
||||
const t = useI18n("board.setupChecklist");
|
||||
const tCommon = useI18n("common.action");
|
||||
const trackSetup = useSetupAnalytics();
|
||||
const progress = getBoardSetupProgress({
|
||||
itemKinds: board.items.map((item) => item.kind),
|
||||
@@ -74,14 +75,14 @@ export const BoardSetupChecklist = () => {
|
||||
complete={progress.steps.content}
|
||||
label={t("step.content.label")}
|
||||
description={t("step.content.description")}
|
||||
actionLabel={t("step.content.action")}
|
||||
actionLabel={tCommon("add")}
|
||||
onAction={addWidget}
|
||||
/>
|
||||
<ChecklistStep
|
||||
complete={progress.steps.app}
|
||||
label={t("step.app.label")}
|
||||
description={t("step.app.description")}
|
||||
actionLabel={t("step.app.action")}
|
||||
actionLabel={tCommon("add")}
|
||||
onAction={addApp}
|
||||
/>
|
||||
<ChecklistStep
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ErrorBoundary } from "react-error-boundary";
|
||||
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { getWidgetName } from "@homarr/definitions";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { WidgetError } from "@homarr/widgets/errors";
|
||||
@@ -178,7 +179,7 @@ const LoadedBoardItemContent = ({
|
||||
const { active, viewportSize, open, close, dismiss, hover, leave } = useAdvancedFocus();
|
||||
const { width: viewportWidth, height: viewportHeight } = viewportSize;
|
||||
const supportsAdvancedFocus = definitionSupportsAdvancedFocus(definition);
|
||||
const widgetName = t(`widget.${item.kind}.name`);
|
||||
const widgetName = getWidgetName(item.kind, t);
|
||||
const advancedViewLabel = t("item.advancedFocus.label", { widget: widgetName });
|
||||
const advancedViewId = `advanced-focus-${item.id}`;
|
||||
const activeFocus = supportsAdvancedFocus && active?.itemId === item.id ? active : null;
|
||||
|
||||
@@ -4,9 +4,10 @@ import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil } from "@table
|
||||
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { getWidgetName } from "@homarr/definitions";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { WidgetDefinition } from "@homarr/widgets/definition";
|
||||
|
||||
import type { SectionItem } from "~/app/[locale]/boards/_types";
|
||||
@@ -34,7 +35,7 @@ const BoardItemMenuInner = ({ item, definition, resetErrorBoundary }: BoardItemM
|
||||
const { data: session } = useSession();
|
||||
const canDuplicate = item.kind !== "customApi" || (session?.user.permissions.includes("admin") ?? false);
|
||||
const refResetErrorBoundaryOnNextRender = useRef(false);
|
||||
const tItem = useScopedI18n("item");
|
||||
const tItem = useI18n("item");
|
||||
const t = useI18n();
|
||||
const { openModal } = useModalAction(LazyWidgetEditModal);
|
||||
const openMoveModal = useOpenItemMoveModal();
|
||||
@@ -43,7 +44,7 @@ const BoardItemMenuInner = ({ item, definition, resetErrorBoundary }: BoardItemM
|
||||
useItemActions();
|
||||
const { integrations: integrationData, section } = useSectionContext();
|
||||
const settings = useSettings();
|
||||
const label = item.advancedOptions.title?.trim() || t(`widget.${item.kind}.name`);
|
||||
const label = item.advancedOptions.title?.trim() || getWidgetName(item.kind, t);
|
||||
|
||||
// Reset error boundary on next render if item has been edited
|
||||
useEffect(() => {
|
||||
@@ -92,7 +93,7 @@ const BoardItemMenuInner = ({ item, definition, resetErrorBoundary }: BoardItemM
|
||||
},
|
||||
{
|
||||
title(translate) {
|
||||
return `${translate("item.edit.title")} - ${translate(`widget.${item.kind}.name`)}`;
|
||||
return `${translate("item.edit.title")} - ${getWidgetName(item.kind, translate)}`;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
|
||||
import { getRootSectionLane } from "@homarr/definitions";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { createModal, ModalFormFooter, modalSizeForm, useModalAction } from "@homarr/modals";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { Board, ContainerSectionItem, SectionItem } from "~/app/[locale]/boards/_types";
|
||||
import { getLayoutRowCount } from "../layout";
|
||||
@@ -53,9 +53,9 @@ type MoveTargetCandidate = Omit<MoveTarget, "label"> & {
|
||||
};
|
||||
|
||||
export const ItemMoveModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
const tLandmark = useScopedI18n("board.landmark");
|
||||
const tContainer = useScopedI18n("section.container");
|
||||
const tMoveResize = useScopedI18n("item.moveResize");
|
||||
const tLandmark = useI18n("board.landmark");
|
||||
const tContainer = useI18n("section.container");
|
||||
const tMoveResize = useI18n("item.moveResize");
|
||||
const { board, commitSectionGrids, currentLayoutId, entry, sourceSectionId } = innerProps;
|
||||
const minimumSize = useMemo(
|
||||
() => getEntryMinimumSize(board, currentLayoutId, entry),
|
||||
|
||||
@@ -24,7 +24,6 @@ vi.mock("@homarr/translation/client", () => ({
|
||||
};
|
||||
return translations[key] ?? key;
|
||||
},
|
||||
useScopedI18n: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/definitions", async () => {
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
getBoardLaneColumnCount,
|
||||
getIconUrl,
|
||||
getIntegrationName,
|
||||
getWidgetName,
|
||||
getRootSectionLane,
|
||||
widgetIntegrationSupport,
|
||||
widgetKinds,
|
||||
@@ -144,14 +145,24 @@ const ItemSelectModalContent = ({
|
||||
widgetKinds
|
||||
.filter((kind) => kind !== "customApi")
|
||||
.map((kind) => {
|
||||
let description: string;
|
||||
if (kind === "mediaMissing") {
|
||||
description = t("widget.mediaMissing.description", {
|
||||
radarr: getIntegrationName("radarr"),
|
||||
sonarr: getIntegrationName("sonarr"),
|
||||
});
|
||||
} else {
|
||||
description = t(`widget.${kind}.description`);
|
||||
}
|
||||
|
||||
return {
|
||||
kind,
|
||||
supportedIntegrations: (widgetIntegrationSupport[kind] ?? []).filter(
|
||||
(integration) => integration !== "mock",
|
||||
),
|
||||
icon: widgetCatalogIcons[kind],
|
||||
name: t(`widget.${kind}.name`),
|
||||
description: t(`widget.${kind}.description`),
|
||||
name: getWidgetName(kind, t),
|
||||
description,
|
||||
};
|
||||
})
|
||||
.sort((itemA, itemB) => {
|
||||
@@ -324,7 +335,7 @@ const ItemSelectModalContent = ({
|
||||
targetSectionId: effectiveSectionId ?? undefined,
|
||||
advancedOptions,
|
||||
});
|
||||
if (!notifyCreated(updatedBoard, itemId, t(`widget.${kind}.name`))) return;
|
||||
if (!notifyCreated(updatedBoard, itemId, getWidgetName(kind, t))) return;
|
||||
trackSetup("widget-completed", {
|
||||
entryPoint: "board",
|
||||
intent: kind,
|
||||
@@ -337,7 +348,7 @@ const ItemSelectModalContent = ({
|
||||
settings,
|
||||
},
|
||||
{
|
||||
title: (titleT) => `${titleT("item.edit.title")} - ${titleT(`widget.${kind}.name`)}`,
|
||||
title: (titleT) => `${titleT("item.edit.title")} - ${getWidgetName(kind, titleT)}`,
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -458,7 +469,7 @@ const ItemSelectModalContent = ({
|
||||
<Group gap="xs">
|
||||
<IconApi size={14} />
|
||||
<Text size="xs" fw={600}>
|
||||
{t("customWidget.page.list.title")}
|
||||
{t("common.entity.customWidgets")}
|
||||
</Text>
|
||||
</Group>
|
||||
}
|
||||
@@ -643,7 +654,7 @@ const ItemSelectModalFrame = ({
|
||||
canCreateIntegration={canCreateIntegration}
|
||||
initialSearch={
|
||||
innerProps.initialWidgetKind
|
||||
? t(`widget.${innerProps.initialWidgetKind}.name`)
|
||||
? getWidgetName(innerProps.initialWidgetKind, t)
|
||||
: innerProps.initialIntegrationKind
|
||||
? getIntegrationName(innerProps.initialIntegrationKind)
|
||||
: undefined
|
||||
|
||||
@@ -16,12 +16,14 @@ import { useTimeAgo } from "@homarr/common";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { showErrorNotification } from "@homarr/notifications";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
import { getWidgetName } from "@homarr/definitions";
|
||||
import { translateIfNecessary } from "@homarr/translation";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { WidgetDefinition, WidgetRuntimeRef } from "@homarr/widgets/definition";
|
||||
import { getWidgetQueryKeys, getWidgetRuntimeQueries, supportsAdvancedFocus } from "@homarr/widgets/definition";
|
||||
import { reduceWidgetOptionsWithDefinition } from "@homarr/widgets/manifest";
|
||||
import { getWidgetOptionTranslationNamespace } from "@homarr/widgets/option-translation";
|
||||
|
||||
import type { SectionItem } from "~/app/[locale]/boards/_types";
|
||||
import { useAdvancedFocus } from "../advanced-focus/context";
|
||||
@@ -51,8 +53,9 @@ export const WidgetContextMenu = ({
|
||||
const board = useRequiredBoard();
|
||||
const { hasChangeAccess } = useBoardPermissions(board);
|
||||
const { updateAndPersistBoard } = usePersistBoard(board);
|
||||
const tMenu = useScopedI18n("item.menu.label");
|
||||
const t = useI18n();
|
||||
const tMenu = useI18n("item.menu.label");
|
||||
const tCommon = useI18n("common.action");
|
||||
const settings = useSettings();
|
||||
const { openModal } = useModalAction(LazyWidgetEditModal);
|
||||
const { updateItemOptions, updateItemAdvancedOptions, updateItemIntegrations } = useItemActions();
|
||||
@@ -189,7 +192,9 @@ export const WidgetContextMenu = ({
|
||||
appId: item.kind === "app" ? (item.options.appId as string | undefined) : undefined,
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
{ title: (fn: any) => `${fn("item.edit.title")} - ${fn(`widget.${item.kind}.name`)}` },
|
||||
{
|
||||
title: (fn: any) => `${fn("item.edit.title")} - ${getWidgetName(item.kind, fn)}`,
|
||||
},
|
||||
);
|
||||
}, [
|
||||
definition,
|
||||
@@ -250,8 +255,7 @@ export const WidgetContextMenu = ({
|
||||
<Menu.Label>{tMenu("options")}</Menu.Label>
|
||||
{toggleOptions.map(([key]) => (
|
||||
<Menu.CheckboxItem key={key} checked={Boolean(options[key])} onChange={handleToggle(key)}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||
{String(translateIfNecessary(t, ((fn: any) => fn(`widget.${item.kind}.option.${key}.label`)) as any))}
|
||||
{t(`${getWidgetOptionTranslationNamespace(item.kind, key)}.label` as never)}
|
||||
</Menu.CheckboxItem>
|
||||
))}
|
||||
</>
|
||||
@@ -286,7 +290,7 @@ export const WidgetContextMenu = ({
|
||||
disabled={isWidgetFetching}
|
||||
>
|
||||
<Group justify="space-between" wrap="nowrap" gap="sm">
|
||||
{tMenu("refresh")}
|
||||
{tCommon("refresh")}
|
||||
<WidgetQueryStatus
|
||||
queryClient={queryClient}
|
||||
matchesQuery={matchesWidgetQuery}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Avatar, Text, Tooltip } from "@mantine/core";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { getIconUrl } from "@homarr/definitions";
|
||||
import { getIconUrl, getWidgetName } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { loadWidgetDefinition } from "@homarr/widgets/manifest";
|
||||
|
||||
@@ -34,7 +34,7 @@ const LoadedWidgetHoverOverlay = ({ item, integrations }: WidgetHoverOverlayProp
|
||||
const definition = use(loadWidgetDefinition(item.kind));
|
||||
const WidgetIcon = definition.icon;
|
||||
|
||||
const displayName = item.advancedOptions.title?.trim() || t(`widget.${item.kind}.name`);
|
||||
const displayName = item.advancedOptions.title?.trim() || getWidgetName(item.kind, t);
|
||||
|
||||
const connectedIntegrations = (integrations ?? []).filter((integration) =>
|
||||
item.integrationIds.includes(integration.id),
|
||||
|
||||
@@ -17,7 +17,7 @@ interface InnerProps {
|
||||
|
||||
export const BoardRenameModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
const utils = clientApi.useUtils();
|
||||
const t = useI18n();
|
||||
const tCommon = useI18n("common");
|
||||
const { mutate, isPending } = clientApi.board.renameBoard.useMutation({
|
||||
onSettled() {
|
||||
void utils.board.getBoardByName.invalidate({
|
||||
@@ -50,20 +50,20 @@ export const BoardRenameModal = createModal<InnerProps>(({ actions, innerProps }
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput label={t("board.field.name.label")} {...form.getInputProps("name")} data-autofocus />
|
||||
<TextInput label={tCommon("field.name")} {...form.getInputProps("name")} data-autofocus />
|
||||
<Group justify="end">
|
||||
<Button variant="subtle" color="gray" onClick={actions.closeModal}>
|
||||
{t("common.action.cancel")}
|
||||
{tCommon("action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={isPending}>
|
||||
{t("common.action.confirm")}
|
||||
{tCommon("action.confirm")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}).withOptions({
|
||||
defaultTitle: (t) => t("board.setting.section.dangerZone.action.rename.modal.title"),
|
||||
defaultTitle: (t) => t("board.setting.section.dangerZone.action.rename.label"),
|
||||
});
|
||||
|
||||
type FormType = Omit<z.infer<typeof boardRenameSchema>, "id">;
|
||||
@@ -5,7 +5,7 @@ import combineClasses from "clsx";
|
||||
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { ContainerSectionItem } from "~/app/[locale]/boards/_types";
|
||||
import { SectionGrid } from "./grid/section-grid";
|
||||
@@ -25,8 +25,8 @@ interface Props {
|
||||
export const BoardContainerSection = ({ section }: Props) => {
|
||||
const board = useRequiredBoard();
|
||||
const [isEditMode] = useEditMode();
|
||||
const t = useScopedI18n("section.container");
|
||||
const tAll = useI18n();
|
||||
const t = useI18n("section.container");
|
||||
const tSection = useI18n("section");
|
||||
const options = section.options;
|
||||
const { open: openAllInNewTabs, isLoading: areAppsLoading } = useOpenSectionApps(
|
||||
section.id,
|
||||
@@ -137,7 +137,7 @@ export const BoardContainerSection = ({ section }: Props) => {
|
||||
radius="sm"
|
||||
loading={areAppsLoading}
|
||||
onClick={openAllInNewTabs}
|
||||
aria-label={tAll("section.action.openAllInNewTabsFor", { name: label })}
|
||||
aria-label={tSection("action.openAllInNewTabsFor", { name: label })}
|
||||
>
|
||||
<IconExternalLink size={16} />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -15,7 +15,7 @@ interface ModalProps {
|
||||
}
|
||||
|
||||
export const ContainerEditModal = createModal<ModalProps>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const tSection = useI18n("section");
|
||||
const theme = useMantineTheme();
|
||||
const form = useZodForm(containerSectionOptionsSchema.unwrap(), {
|
||||
mode: "controlled",
|
||||
@@ -30,28 +30,28 @@ export const ContainerEditModal = createModal<ModalProps>(({ actions, innerProps
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput label={t("section.container.option.title.label")} data-autofocus {...form.getInputProps("title")} />
|
||||
<TextInput label={tSection("container.option.title.label")} data-autofocus {...form.getInputProps("title")} />
|
||||
<Switch
|
||||
label={t("section.option.showLabel.label")}
|
||||
description={t("section.option.showLabel.description")}
|
||||
label={tSection("option.showLabel.label")}
|
||||
description={tSection("option.showLabel.description")}
|
||||
{...form.getInputProps("showLabel", { type: "checkbox" })}
|
||||
/>
|
||||
<Switch
|
||||
label={t("section.option.collapsible.label")}
|
||||
description={t("section.option.collapsible.description")}
|
||||
label={tSection("option.collapsible.label")}
|
||||
description={tSection("option.collapsible.description")}
|
||||
{...form.getInputProps("collapsible", { type: "checkbox" })}
|
||||
/>
|
||||
<Switch
|
||||
label={t("section.option.showOpenAll.label")}
|
||||
description={t("section.option.showOpenAll.description")}
|
||||
label={tSection("option.showOpenAll.label")}
|
||||
description={tSection("option.showOpenAll.description")}
|
||||
{...form.getInputProps("showOpenAll", { type: "checkbox" })}
|
||||
/>
|
||||
<TextMultiSelect
|
||||
label={t("section.container.option.customCssClasses.label")}
|
||||
label={tSection("container.option.customCssClasses.label")}
|
||||
{...form.getInputProps("customCssClasses")}
|
||||
/>
|
||||
<ColorInput
|
||||
label={t("section.container.option.borderColor.label")}
|
||||
label={tSection("container.option.borderColor.label")}
|
||||
format="hex"
|
||||
swatches={Object.values(theme.colors).map((color) => color[6])}
|
||||
rightSection={
|
||||
|
||||
@@ -3,7 +3,7 @@ import { IconArrowsMove, IconLayoutKanban, IconPencil } from "@tabler/icons-reac
|
||||
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { ContainerSectionItem } from "~/app/[locale]/boards/_types";
|
||||
import { BoardRemoveConfirmationMenuItem } from "../../remove-confirmation-menu-item";
|
||||
@@ -13,8 +13,8 @@ import { useContainerActions } from "./container-actions";
|
||||
import { ContainerEditModal } from "./container-edit-modal";
|
||||
|
||||
export const BoardContainerMenu = ({ section }: { section: ContainerSectionItem }) => {
|
||||
const tContainer = useScopedI18n("section.container");
|
||||
const tItem = useScopedI18n("item");
|
||||
const tContainer = useI18n("section.container");
|
||||
const tItem = useI18n("item");
|
||||
const { openModal } = useModalAction(ContainerEditModal);
|
||||
const openMoveModal = useOpenItemMoveModal();
|
||||
const { updateContainer, removeContainer } = useContainerActions();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { EmptySection } from "~/app/[locale]/boards/_types";
|
||||
import { SectionGrid } from "./grid/section-grid";
|
||||
@@ -16,7 +16,7 @@ export const BoardEmptySection = ({ section, columnCount, requestedRowCount, rai
|
||||
const { items, innerSections } = useSectionItems(section.id);
|
||||
const totalLength = items.length + innerSections.length;
|
||||
const [isEditMode] = useEditMode();
|
||||
const t = useScopedI18n("board.landmark");
|
||||
const t = useI18n("board.landmark");
|
||||
|
||||
if (totalLength === 0 && !isEditMode && requestedRowCount === 0 && railPlacement !== "main") return null;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import combineClasses from "clsx";
|
||||
|
||||
import { useCurrentLayout } from "@homarr/boards/context";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { getWidgetName } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { ContainerSectionItem, SectionItem } from "~/app/[locale]/boards/_types";
|
||||
@@ -76,7 +77,7 @@ export const FixedGridItem = ({
|
||||
});
|
||||
const displayName =
|
||||
item.type === "item"
|
||||
? item.advancedOptions.title?.trim() || t(`widget.${item.kind}.name`)
|
||||
? item.advancedOptions.title?.trim() || getWidgetName(item.kind, t)
|
||||
: item.options.title.trim() || t("section.container.untitled");
|
||||
const accessibleLabel = t("item.moveResize.entryLabel", {
|
||||
name: displayName,
|
||||
|
||||
@@ -36,6 +36,7 @@ import { DragDropProvider, DragOverlay, useDragDropManager, useDraggable, useDro
|
||||
import { IconGripHorizontal, IconGripVertical } from "@tabler/icons-react";
|
||||
|
||||
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
|
||||
import { getWidgetName } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { getLogicalTrackSize, LOGICAL_GRID_PITCH } from "~/components/board/layout";
|
||||
@@ -743,7 +744,7 @@ export default function GridEditor({
|
||||
[...items, ...innerSections].map((entry) => [
|
||||
entry.id,
|
||||
entry.type === "item"
|
||||
? entry.advancedOptions.title?.trim() || t(`widget.${entry.kind}.name`)
|
||||
? entry.advancedOptions.title?.trim() || getWidgetName(entry.kind, t)
|
||||
: entry.options.title.trim() || t("section.container.untitled"),
|
||||
]),
|
||||
),
|
||||
|
||||
@@ -80,7 +80,7 @@ export const useOpenSectionApps = (sectionId: string, enabled: boolean) => {
|
||||
const currentLayoutId = useCurrentLayout();
|
||||
const settings = useSettings();
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const t = useI18n();
|
||||
const t = useI18n("section");
|
||||
const appDefinition = use(loadWidgetDefinition("app"));
|
||||
const appIds = useMemo(() => {
|
||||
const items: Item[] = getSectionItemsForLayout(board, sectionId, currentLayoutId);
|
||||
@@ -115,8 +115,8 @@ export const useOpenSectionApps = (sectionId: string, enabled: boolean) => {
|
||||
if (openedWindow) continue;
|
||||
|
||||
openConfirmModal({
|
||||
title: t("section.openAllInNewTabs.title"),
|
||||
children: t("section.openAllInNewTabs.text"),
|
||||
title: t("openAllInNewTabs.title"),
|
||||
children: t("openAllInNewTabs.text"),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { usePersistBoard } from "@homarr/boards/updater";
|
||||
import { createId } from "@homarr/common";
|
||||
import { getBoardLaneColumnCount, getRootSectionLane } from "@homarr/definitions";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { ContainerSection, ItemLayout } from "~/app/[locale]/boards/_types";
|
||||
import { createItemCallback } from "~/components/board/items/actions/create-item";
|
||||
@@ -42,8 +42,8 @@ export const BoardSelectionProvider = ({ children }: PropsWithChildren) => {
|
||||
const board = useRequiredBoard();
|
||||
const currentLayoutId = useCurrentLayout();
|
||||
const { updateAndPersistBoard } = usePersistBoard(board);
|
||||
const t = useI18n();
|
||||
const tSelection = useScopedI18n("item.selection");
|
||||
const tCommon = useI18n("common");
|
||||
const tSelection = useI18n("item.selection");
|
||||
|
||||
const persistBoard = useCallback(
|
||||
(updater: (previous: typeof board) => typeof board) =>
|
||||
@@ -205,7 +205,7 @@ export const BoardSelectionProvider = ({ children }: PropsWithChildren) => {
|
||||
const notificationId = `batch-remove-${Date.now()}`;
|
||||
showSuccessNotification({
|
||||
id: notificationId,
|
||||
title: t("common.success"),
|
||||
title: tCommon("success"),
|
||||
message: (
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Text size="sm">{tSelection("removed", { count })}</Text>
|
||||
@@ -220,13 +220,13 @@ export const BoardSelectionProvider = ({ children }: PropsWithChildren) => {
|
||||
notifications.hide(notificationId);
|
||||
}}
|
||||
>
|
||||
{t("common.action.undo")}
|
||||
{tCommon("action.undo")}
|
||||
</Button>
|
||||
</Group>
|
||||
),
|
||||
autoClose: 10_000,
|
||||
});
|
||||
}, [board.items, clearSelection, persistBoard, selectedItemIds, t, tSelection]);
|
||||
}, [board.items, clearSelection, persistBoard, selectedItemIds, tCommon, tSelection]);
|
||||
|
||||
const moveSelectedItemsToSection = useCallback(
|
||||
(targetSectionId: string) => {
|
||||
|
||||
@@ -20,8 +20,8 @@ import { IconCheck, IconClipboard, IconCopy, IconFolderShare, IconTrash, IconX }
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { getBoardLaneColumnCount, getRootSectionLane } from "@homarr/definitions";
|
||||
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import { getBoardLaneColumnCount, getRootSectionLane, getWidgetName } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { widgetCatalogIcons } from "@homarr/widgets/catalog";
|
||||
|
||||
import type { EmptySection } from "~/app/[locale]/boards/_types";
|
||||
@@ -43,7 +43,7 @@ export const BoardSelectionToolbar = () => {
|
||||
const currentLayoutId = useCurrentLayout();
|
||||
const currentLayout = board.layouts.find((layout) => layout.id === currentLayoutId);
|
||||
const t = useI18n();
|
||||
const tSelection = useScopedI18n("item.selection");
|
||||
const tSelection = useI18n("item.selection");
|
||||
|
||||
const selectedItems = board.items.filter((item) => selectedItemIds.has(item.id));
|
||||
const hasAppWidgets = selectedItems.some((item) => item.kind === "app");
|
||||
@@ -71,7 +71,7 @@ export const BoardSelectionToolbar = () => {
|
||||
const appId = (item.options as { appId?: string })?.appId;
|
||||
const app = appId ? appMap.get(appId) : undefined;
|
||||
if (!displayName) {
|
||||
displayName = app?.name || t("widget.app.name");
|
||||
displayName = app?.name || getWidgetName("app", t);
|
||||
}
|
||||
if (app?.iconUrl) {
|
||||
iconUrl = app.iconUrl;
|
||||
@@ -79,7 +79,7 @@ export const BoardSelectionToolbar = () => {
|
||||
}
|
||||
|
||||
if (!displayName) {
|
||||
displayName = t(`widget.${item.kind}.name`);
|
||||
displayName = getWidgetName(item.kind, t);
|
||||
}
|
||||
|
||||
return { displayName, iconUrl, IconComponent, kind: item.kind };
|
||||
|
||||
@@ -16,7 +16,6 @@ import { BoardSelectionProvider, useBoardSelection } from "./board-selection-con
|
||||
|
||||
vi.mock("@homarr/translation/client", () => ({
|
||||
useI18n: () => (key: string) => key,
|
||||
useScopedI18n: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock("../audio/board-sounds", () => ({
|
||||
|
||||
@@ -32,12 +32,12 @@ export interface SetupMetricProperties {
|
||||
* Never pass record IDs, names, URLs, search text, credentials, or provider responses here.
|
||||
*/
|
||||
export const useSetupAnalytics = () => {
|
||||
const t = useI18n();
|
||||
const tCommon = useI18n("common");
|
||||
const { mutate } = clientApi.analytics.trackFeature.useMutation({
|
||||
onError() {
|
||||
showErrorNotification({
|
||||
title: t("common.error"),
|
||||
message: t("common.analyticsError"),
|
||||
title: tCommon("error"),
|
||||
message: tCommon("analyticsError"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ModalProps } from "@mantine/core";
|
||||
|
||||
import type { HomarrCustomWidgetV2 } from "@homarr/custom-widgets/core";
|
||||
import { CustomWidgetSourceSetupPanel, ImportReviewDialog } from "@homarr/custom-widgets/workbench";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { useCustomWidgetImport } from "./use-custom-widget-import";
|
||||
|
||||
@@ -36,7 +36,7 @@ export function CustomWidgetImportDialog({
|
||||
onClose,
|
||||
onImported,
|
||||
}: CustomWidgetImportDialogProps) {
|
||||
const t = useScopedI18n("customWidget");
|
||||
const t = useI18n("customWidget");
|
||||
const importer = useCustomWidgetImport({
|
||||
widget,
|
||||
legacyId,
|
||||
|
||||
@@ -13,12 +13,9 @@ import {
|
||||
CustomWidgetSourceSetupPanel,
|
||||
isCustomWidgetSourceSetupReady,
|
||||
} from "@homarr/custom-widgets/workbench";
|
||||
import type {
|
||||
CustomWidgetSourceSetupMessages,
|
||||
CustomWidgetSourceSetupValue,
|
||||
} from "@homarr/custom-widgets/workbench";
|
||||
import type { CustomWidgetSourceSetupMessages, CustomWidgetSourceSetupValue } from "@homarr/custom-widgets/workbench";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface CustomWidgetSourceSetupDialogProps {
|
||||
definitionId: string;
|
||||
@@ -26,12 +23,9 @@ interface CustomWidgetSourceSetupDialogProps {
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
export function CustomWidgetSourceSetupDialog({
|
||||
definitionId,
|
||||
opened,
|
||||
onClose,
|
||||
}: CustomWidgetSourceSetupDialogProps) {
|
||||
const t = useScopedI18n("customWidget");
|
||||
export function CustomWidgetSourceSetupDialog({ definitionId, opened, onClose }: CustomWidgetSourceSetupDialogProps) {
|
||||
const t = useI18n("customWidget");
|
||||
const tCommon = useI18n("common");
|
||||
const router = useRouter();
|
||||
const utils = clientApi.useUtils();
|
||||
const definitionQuery = clientApi.customWidget.get.useQuery({ id: definitionId }, { enabled: opened });
|
||||
@@ -60,7 +54,7 @@ export function CustomWidgetSourceSetupDialog({
|
||||
credentialsMissing: t("importReview.sourceSetup.credentialsMissing"),
|
||||
credentialsOptional: t("sourceSetupDialog.credentialsRequired"),
|
||||
configured: t("workbench.sources.configured"),
|
||||
secret: (kind) => t(`importReview.credentials.field.${kind}`),
|
||||
secret: (kind) => t(`secret.${kind}`),
|
||||
urlError: (issue) => t(`workbench.sources.baseUrlError.${issue}`),
|
||||
};
|
||||
|
||||
@@ -123,7 +117,7 @@ export function CustomWidgetSourceSetupDialog({
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={onClose} disabled={saving}>
|
||||
{t("sourceSetupDialog.cancel")}
|
||||
{tCommon("action.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => void save()}
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
ImportReviewContentMessages,
|
||||
} from "@homarr/custom-widgets/workbench";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface UseCustomWidgetImportOptions {
|
||||
widget: HomarrCustomWidgetV2 | null;
|
||||
@@ -33,7 +33,8 @@ interface UseCustomWidgetImportOptions {
|
||||
* means the Workshop page and the file/clipboard dialog can never drift apart.
|
||||
*/
|
||||
export function useCustomWidgetImport({ widget, legacyId, onImported }: UseCustomWidgetImportOptions) {
|
||||
const t = useScopedI18n("customWidget");
|
||||
const t = useI18n("customWidget");
|
||||
const tCommon = useI18n("common");
|
||||
const router = useRouter();
|
||||
const utils = clientApi.useUtils();
|
||||
|
||||
@@ -75,7 +76,7 @@ export function useCustomWidgetImport({ widget, legacyId, onImported }: UseCusto
|
||||
const onSuccess = (result: { id: string }) => {
|
||||
setSucceeded(true);
|
||||
showSuccessNotification({
|
||||
title: legacyId ? t("action.migrate") : t("action.import"),
|
||||
title: legacyId ? t("action.migrate") : tCommon("action.import"),
|
||||
message: legacyId ? t("notification.migrated") : t("notification.imported"),
|
||||
});
|
||||
void utils.customWidget.list.invalidate();
|
||||
@@ -88,7 +89,7 @@ export function useCustomWidgetImport({ widget, legacyId, onImported }: UseCusto
|
||||
};
|
||||
const onError = (error: { message?: string }) => {
|
||||
showErrorNotification({
|
||||
title: legacyId ? t("action.migrate") : t("action.import"),
|
||||
title: legacyId ? t("action.migrate") : tCommon("action.import"),
|
||||
message: error.message || (legacyId ? t("notification.migrationError") : t("notification.importError")),
|
||||
});
|
||||
};
|
||||
@@ -146,7 +147,7 @@ export function useCustomWidgetImport({ widget, legacyId, onImported }: UseCusto
|
||||
credentialsMissing: t("importReview.sourceSetup.credentialsMissing"),
|
||||
credentialsOptional: t("importReview.sourceSetup.credentialsOptional"),
|
||||
configured: t("workbench.sources.configured"),
|
||||
secret: (kind) => t(`importReview.credentials.field.${kind}`),
|
||||
secret: (kind) => t(`secret.${kind}`),
|
||||
urlError: (issue) => t(`workbench.sources.baseUrlError.${issue}`),
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ export const EmbeddedIntegrationEditForm = ({
|
||||
handleRef,
|
||||
onSuccess,
|
||||
}: EmbeddedIntegrationEditFormProps) => {
|
||||
const t = useI18n();
|
||||
const tItem = useI18n("item.edit.integration");
|
||||
const tCommon = useI18n("common");
|
||||
const integrationFormRef = useRef<EditIntegrationFormHandle>(null);
|
||||
const {
|
||||
data: integration,
|
||||
@@ -47,7 +48,7 @@ export const EmbeddedIntegrationEditForm = ({
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<Stack gap="sm" aria-busy="true" aria-label={t("item.edit.integration.loading")}>
|
||||
<Stack gap="sm" aria-busy="true" aria-label={tItem("loading")}>
|
||||
<Skeleton height={36} width="60%" />
|
||||
<Skeleton height={60} />
|
||||
<Skeleton height={120} />
|
||||
@@ -57,11 +58,11 @@ export const EmbeddedIntegrationEditForm = ({
|
||||
|
||||
if (isError || !integration) {
|
||||
return (
|
||||
<Alert color="red" title={t("common.error")}>
|
||||
<Alert color="red" title={tCommon("error")}>
|
||||
<Stack gap="sm">
|
||||
<Text size="sm">{t("item.edit.integration.loadError")}</Text>
|
||||
<Text size="sm">{tItem("loadError")}</Text>
|
||||
<Button variant="light" color="red" onClick={() => void refetch()}>
|
||||
{t("common.action.tryAgain")}
|
||||
{tCommon("action.tryAgain")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Alert>
|
||||
@@ -82,7 +83,7 @@ export const EmbeddedIntegrationEditForm = ({
|
||||
</Stack>
|
||||
</Group>
|
||||
<Alert icon={<IconInfoCircle size={16} />} color="blue" variant="light">
|
||||
{t("item.edit.integration.propagationNotice")}
|
||||
{tItem("propagationNotice")}
|
||||
</Alert>
|
||||
<EditIntegrationForm integration={integration} hideButtons formRef={integrationFormRef} onSuccess={onSuccess} />
|
||||
</Stack>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useSession } from "@homarr/auth/client";
|
||||
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||
import { useOptionalBoard } from "@homarr/boards/context";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
import { getIntegrationName, getWidgetKindsForIntegration } from "@homarr/definitions";
|
||||
import { getIntegrationName, getWidgetKindsForIntegration, getWidgetName } from "@homarr/definitions";
|
||||
import { createModal, modalSizeForm, useModalAction } from "@homarr/modals";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
@@ -27,6 +27,8 @@ interface IntegrationCompletionModalProps {
|
||||
}
|
||||
|
||||
export const IntegrationCompletionModal = createModal<IntegrationCompletionModalProps>(({ actions, innerProps }) => {
|
||||
const tIntegration = useI18n("integration");
|
||||
const tCommon = useI18n("common");
|
||||
const t = useI18n();
|
||||
const board = useOptionalBoard();
|
||||
const { data: session } = useSession();
|
||||
@@ -88,10 +90,10 @@ export const IntegrationCompletionModal = createModal<IntegrationCompletionModal
|
||||
</ThemeIcon>
|
||||
<Stack gap={2}>
|
||||
<Title order={3} size="h4">
|
||||
{t("integration.completion.title", { name: innerProps.result.integration.name })}
|
||||
{tIntegration("completion.title", { name: innerProps.result.integration.name })}
|
||||
</Title>
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("integration.completion.description")}
|
||||
{tIntegration("completion.description")}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
@@ -107,14 +109,14 @@ export const IntegrationCompletionModal = createModal<IntegrationCompletionModal
|
||||
<List.Item>
|
||||
<Group gap="xs">
|
||||
<IconPlugConnected size={17} />
|
||||
<Text size="sm">{t("integration.completion.connectionReady")}</Text>
|
||||
<Text size="sm">{tIntegration("completion.connectionReady")}</Text>
|
||||
</Group>
|
||||
</List.Item>
|
||||
{innerProps.result.appId && (
|
||||
<List.Item>
|
||||
<Group gap="xs">
|
||||
<IconAppWindow size={17} />
|
||||
<Text size="sm">{t("integration.completion.appReady")}</Text>
|
||||
<Text size="sm">{tIntegration("completion.appReady")}</Text>
|
||||
</Group>
|
||||
</List.Item>
|
||||
)}
|
||||
@@ -122,7 +124,7 @@ export const IntegrationCompletionModal = createModal<IntegrationCompletionModal
|
||||
<Group gap="xs">
|
||||
<IconLayoutDashboard size={17} />
|
||||
<Text size="sm">
|
||||
{t("integration.completion.compatibleWidgets", { count: String(compatibleWidgets.length) })}
|
||||
{tIntegration("completion.compatibleWidgets", { count: String(compatibleWidgets.length) })}
|
||||
</Text>
|
||||
</Group>
|
||||
</List.Item>
|
||||
@@ -134,17 +136,17 @@ export const IntegrationCompletionModal = createModal<IntegrationCompletionModal
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<div>
|
||||
<Text fw={600} size="sm">
|
||||
{t("integration.completion.recipes.title")}
|
||||
{tIntegration("completion.recipes.title")}
|
||||
</Text>
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("integration.completion.recipes.description")}
|
||||
{tIntegration("completion.recipes.description")}
|
||||
</Text>
|
||||
</div>
|
||||
<Tooltip label={t("integration.completion.recipes.dismiss")}>
|
||||
<Tooltip label={tIntegration("completion.recipes.dismiss")}>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={t("integration.completion.recipes.dismiss")}
|
||||
aria-label={tIntegration("completion.recipes.dismiss")}
|
||||
onClick={() => setRecommendationsDismissed(true)}
|
||||
>
|
||||
<IconX size={16} />
|
||||
@@ -155,13 +157,13 @@ export const IntegrationCompletionModal = createModal<IntegrationCompletionModal
|
||||
<Group key={recommendation.widgetKind} justify="space-between" wrap="nowrap" gap="sm">
|
||||
<Stack gap={0} style={{ minWidth: 0 }}>
|
||||
<Text size="sm" fw={500} truncate>
|
||||
{t(`widget.${recommendation.widgetKind}.name`)}
|
||||
{getWidgetName(recommendation.widgetKind, t)}
|
||||
</Text>
|
||||
<Text c="dimmed" size="xs" truncate>
|
||||
{t(
|
||||
{tIntegration(
|
||||
recommendation.isNewlyAvailable
|
||||
? "integration.completion.recipes.newConnectionReason"
|
||||
: "integration.completion.recipes.existingConnectionReason",
|
||||
? "completion.recipes.newConnectionReason"
|
||||
: "completion.recipes.existingConnectionReason",
|
||||
{
|
||||
integration: recommendation.isNewlyAvailable
|
||||
? innerProps.result.integration.name
|
||||
@@ -171,7 +173,7 @@ export const IntegrationCompletionModal = createModal<IntegrationCompletionModal
|
||||
</Text>
|
||||
</Stack>
|
||||
<Button variant="light" size="compact-xs" onClick={() => addRecommendedWidget(recommendation.widgetKind)}>
|
||||
{t("integration.completion.recipes.add")}
|
||||
{tCommon("action.add")}
|
||||
</Button>
|
||||
</Group>
|
||||
))}
|
||||
@@ -180,10 +182,10 @@ export const IntegrationCompletionModal = createModal<IntegrationCompletionModal
|
||||
|
||||
<Group justify="end">
|
||||
<Button variant="default" onClick={actions.closeModal}>
|
||||
{t("common.action.close")}
|
||||
{tCommon("action.close")}
|
||||
</Button>
|
||||
{innerProps.boardId && compatibleWidgets.length > 0 && (
|
||||
<Button onClick={addWidget}>{t("integration.completion.chooseWidget")}</Button>
|
||||
<Button onClick={addWidget}>{tIntegration("completion.chooseWidget")}</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -27,7 +27,8 @@ interface IntegrationSelectModalProps {
|
||||
}
|
||||
|
||||
export const IntegrationSelectModal = createModal<IntegrationSelectModalProps>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const tCommon = useI18n("common");
|
||||
const tIntegration = useI18n("integration");
|
||||
const directKind =
|
||||
innerProps.initialKind ?? (innerProps.allowedKinds?.length === 1 ? innerProps.allowedKinds[0] : null);
|
||||
const isDirectForm = Boolean(directKind);
|
||||
@@ -72,8 +73,12 @@ export const IntegrationSelectModal = createModal<IntegrationSelectModalProps>((
|
||||
};
|
||||
|
||||
if (step === "form" && selectedKind) {
|
||||
const HeaderIcon = isDirectForm ? IconX : IconArrowLeft;
|
||||
const headerLabel = isDirectForm ? t("common.action.close") : t("common.action.previous");
|
||||
let HeaderIcon = IconArrowLeft;
|
||||
let headerLabel = tCommon("action.previous");
|
||||
if (isDirectForm) {
|
||||
HeaderIcon = IconX;
|
||||
headerLabel = tCommon("action.close");
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea.Autosize mah="80vh">
|
||||
@@ -97,11 +102,11 @@ export const IntegrationSelectModal = createModal<IntegrationSelectModalProps>((
|
||||
|
||||
if (isIntegrationDataError) {
|
||||
return (
|
||||
<Alert color="red" title={t("common.error")}>
|
||||
<Alert color="red" title={tCommon("error")}>
|
||||
<Stack gap="sm">
|
||||
<Text size="sm">{t("integration.grid.loadError")}</Text>
|
||||
<Text size="sm">{tIntegration("grid.loadError")}</Text>
|
||||
<Button variant="light" color="red" onClick={() => void refetch()}>
|
||||
{t("common.action.tryAgain")}
|
||||
{tCommon("action.tryAgain")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Alert>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Indicator, Menu, Text } from "@mantine/core";
|
||||
import { IconBellRinging } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface UpdateIndicatorProps extends PropsWithChildren {
|
||||
availableUpdates: RouterOutputs["updateChecker"]["getAvailableUpdates"] | undefined;
|
||||
@@ -29,7 +29,7 @@ interface AvailableUpdatesMenuItemProps {
|
||||
}
|
||||
|
||||
export const AvailableUpdatesMenuItem = ({ availableUpdates }: AvailableUpdatesMenuItemProps) => {
|
||||
const t = useScopedI18n("common.userAvatar.menu");
|
||||
const t = useI18n("common.userAvatar.menu");
|
||||
if (availableUpdates === undefined || availableUpdates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
import { Fragment } from "react";
|
||||
import { Card, CardSection, Divider, Group, Stack, Text, Title } from "@mantine/core";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface DangerZoneRootProps {
|
||||
children: React.ReactNode[] | React.ReactNode;
|
||||
}
|
||||
|
||||
export const DangerZoneRoot = ({ children }: DangerZoneRootProps) => {
|
||||
const t = useScopedI18n("common");
|
||||
const t = useI18n("common");
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
|
||||
@@ -10,8 +10,9 @@ describe("DynamicBreadcrumb", () => {
|
||||
|
||||
// Dynamic ids (e.g. /manage/custom-widgets/workshop/<id>) have no message and are
|
||||
// not mapped while their page loads; resolving them must not raise MISSING_MESSAGE.
|
||||
expect(source).toContain("let label = pathnamePart");
|
||||
expect(source).toContain("if (mappedValue !== undefined)");
|
||||
expect(source).toContain("t.has(labelKey)");
|
||||
expect(source).toContain("mappedValue ?? (t.has(labelKey) ? t(labelKey) : pathnamePart)");
|
||||
expect(source).not.toMatch(/\{t\(`\$\{translationKey\}\.label`/u);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { Anchor, Badge, Breadcrumbs, Text } from "@mantine/core";
|
||||
import { IconHomeFilled } from "@tabler/icons-react";
|
||||
|
||||
import type { TranslationKeys } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface DynamicBreadcrumbProps {
|
||||
customHome?: string | null;
|
||||
@@ -15,6 +15,52 @@ interface DynamicBreadcrumbProps {
|
||||
}
|
||||
|
||||
const categorySegments = new Set(["tools"]);
|
||||
const commonActionLabels: Record<string, "create" | "edit"> = {
|
||||
"manage.integrations.edit": "edit",
|
||||
"manage.search-engines.edit": "edit",
|
||||
"manage.apps.edit": "edit",
|
||||
"manage.custom-widgets.edit": "edit",
|
||||
"manage.users.create": "create",
|
||||
};
|
||||
const commonLabelLabels: Record<string, "new"> = {
|
||||
"manage.integrations.new": "new",
|
||||
"manage.search-engines.new": "new",
|
||||
"manage.apps.new": "new",
|
||||
"manage.custom-widgets.new": "new",
|
||||
};
|
||||
const commonEntityLabels: Record<
|
||||
string,
|
||||
| "apps"
|
||||
| "boards"
|
||||
| "certificates"
|
||||
| "customWidgets"
|
||||
| "groups"
|
||||
| "integrations"
|
||||
| "invites"
|
||||
| "logs"
|
||||
| "media"
|
||||
| "searchEngines"
|
||||
| "tasks"
|
||||
| "users"
|
||||
> = {
|
||||
"manage.boards": "boards",
|
||||
"manage.integrations": "integrations",
|
||||
"manage.apps": "apps",
|
||||
"manage.custom-widgets": "customWidgets",
|
||||
"manage.medias": "media",
|
||||
"manage.search-engines": "searchEngines",
|
||||
"manage.users": "users",
|
||||
"manage.users.groups": "groups",
|
||||
"manage.users.invites": "invites",
|
||||
"manage.tools.certificates": "certificates",
|
||||
"manage.tools.docker.logs": "logs",
|
||||
"manage.tools.logs": "logs",
|
||||
"manage.tools.tasks": "tasks",
|
||||
};
|
||||
const invariantLabels: Record<string, "Docker" | "Kubernetes"> = {
|
||||
"manage.tools.docker": "Docker",
|
||||
"manage.tools.kubernetes": "Kubernetes",
|
||||
};
|
||||
|
||||
/**
|
||||
* Breadcrumb is client side rendered. Elements are automatically
|
||||
@@ -32,8 +78,11 @@ export const DynamicBreadcrumb = ({
|
||||
}: DynamicBreadcrumbProps) => {
|
||||
const pathname = usePathname();
|
||||
const pathnameParts = pathname.split("/").filter((part) => part.length > 0);
|
||||
const t = useScopedI18n("navigationStructure");
|
||||
const tNavbar = useScopedI18n("management.navbar.items");
|
||||
const t = useI18n("navigationStructure");
|
||||
const tNavbar = useI18n("management.navbar.items");
|
||||
const tCommonAction = useI18n("common.action");
|
||||
const tCommonEntity = useI18n("common.entity");
|
||||
const tCommonLabel = useI18n("common.label");
|
||||
|
||||
const length = pathnameParts.filter((part) => part !== customHome).length;
|
||||
|
||||
@@ -69,7 +118,24 @@ export const DynamicBreadcrumb = ({
|
||||
// their page is still loading. Fall back to the raw segment instead of
|
||||
// raising MISSING_MESSAGE.
|
||||
const labelKey = `${translationKey}.label` as TranslationKeys;
|
||||
const label = mappedValue ?? (t.has(labelKey) ? t(labelKey) : pathnamePart);
|
||||
const commonActionKey = commonActionLabels[translationKey];
|
||||
const commonLabelKey = commonLabelLabels[translationKey];
|
||||
const commonEntityKey = commonEntityLabels[translationKey];
|
||||
const invariantLabel = invariantLabels[translationKey];
|
||||
let label = pathnamePart;
|
||||
if (mappedValue !== undefined) {
|
||||
label = mappedValue;
|
||||
} else if (commonActionKey) {
|
||||
label = tCommonAction(commonActionKey);
|
||||
} else if (commonLabelKey) {
|
||||
label = tCommonLabel(commonLabelKey);
|
||||
} else if (commonEntityKey) {
|
||||
label = tCommonEntity(commonEntityKey);
|
||||
} else if (invariantLabel) {
|
||||
label = invariantLabel;
|
||||
} else if (t.has(labelKey)) {
|
||||
label = t(labelKey);
|
||||
}
|
||||
|
||||
if (isNonInteractable) {
|
||||
return <Text key={href}>{label}</Text>;
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { OnboardingTourStep } from "@gfazioli/mantine-onboarding-tour";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { createDocumentationLink } from "@homarr/definitions";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { useBoardPermissions } from "~/components/board/permissions/client";
|
||||
import { TourTargetsProvider } from "~/components/layout/header/tour-target";
|
||||
@@ -15,7 +15,7 @@ import { TourShell } from "./tour-shell";
|
||||
import { TourStepContent } from "./tour-step-content";
|
||||
|
||||
export const BoardTourProvider = ({ children }: PropsWithChildren) => {
|
||||
const t = useScopedI18n("onboardingTour.board");
|
||||
const t = useI18n("onboardingTour.board");
|
||||
const board = useRequiredBoard();
|
||||
const { hasChangeAccess } = useBoardPermissions(board);
|
||||
const [started, setStarted] = useState(true);
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { createDocumentationLink } from "@homarr/definitions";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { TourTargetsProvider } from "~/components/layout/header/tour-target";
|
||||
import { TourShell } from "./tour-shell";
|
||||
@@ -46,7 +46,7 @@ const usersStepFocusRevealProps: OnboardingTourFocusRevealProps = {
|
||||
};
|
||||
|
||||
export const ManageTourProvider = ({ children, isAdmin }: ManageTourProviderProps) => {
|
||||
const t = useScopedI18n("onboardingTour.manage");
|
||||
const t = useI18n("onboardingTour.manage");
|
||||
const { mutate: completeTour } = clientApi.user.completeTour.useMutation();
|
||||
const isMobile = useMediaQuery("(max-width: 48em)");
|
||||
const pathname = usePathname();
|
||||
|
||||
@@ -77,7 +77,8 @@ const TourDoneButton = ({ label, onClick }: TourDoneButtonProps) => {
|
||||
};
|
||||
|
||||
export const TourShell = ({ steps, started, onEnd, stepRoutes, position, children }: TourShellProps) => {
|
||||
const t = useI18n();
|
||||
const tCommon = useI18n("common.action");
|
||||
const tTour = useI18n("onboardingTour");
|
||||
const router = useRouter();
|
||||
const forwardActionRef = useRef<(() => void) | null>(null);
|
||||
|
||||
@@ -154,11 +155,11 @@ export const TourShell = ({ steps, started, onEnd, stepRoutes, position, childre
|
||||
const action = bindForwardAction(controller, () => {
|
||||
navigateAndAdvance(controller.currentStep?.id, nextStep?.id, () => controller.nextStep());
|
||||
});
|
||||
return <TourForwardButton label={t("onboardingTour.next")} onClick={action} />;
|
||||
return <TourForwardButton label={tCommon("next")} onClick={action} />;
|
||||
}}
|
||||
endStepNavigation={(controller) => {
|
||||
const action = bindForwardAction(controller, () => controller.endTour());
|
||||
return <TourDoneButton label={t("onboardingTour.done")} onClick={action} />;
|
||||
return <TourDoneButton label={tTour("done")} onClick={action} />;
|
||||
}}
|
||||
prevStepNavigation={(controller) => (
|
||||
<Button
|
||||
@@ -169,12 +170,12 @@ export const TourShell = ({ steps, started, onEnd, stepRoutes, position, childre
|
||||
navigateAndAdvance(controller.currentStep?.id, prevStep?.id, () => controller.prevStep());
|
||||
}}
|
||||
>
|
||||
{t("onboardingTour.prev")}
|
||||
{tTour("prev")}
|
||||
</Button>
|
||||
)}
|
||||
skipNavigation={(controller) => (
|
||||
<Button size="sm" variant="subtle" color="gray" onClick={() => controller.skipTour()}>
|
||||
{t("onboardingTour.skip")}
|
||||
{tTour("skip")}
|
||||
</Button>
|
||||
)}
|
||||
withPrevButton
|
||||
|
||||
@@ -13,7 +13,7 @@ interface TourStepContentProps {
|
||||
}
|
||||
|
||||
export const TourStepContent = ({ description, documentationHref, icon }: TourStepContentProps) => {
|
||||
const t = useI18n();
|
||||
const t = useI18n("onboardingTour");
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
@@ -31,7 +31,7 @@ export const TourStepContent = ({ description, documentationHref, icon }: TourSt
|
||||
<Anchor href={documentationHref} target="_blank" size="xs" fw={500}>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<IconExternalLink size={14} />
|
||||
{t("onboardingTour.learnMore")}
|
||||
{t("learnMore")}
|
||||
</Group>
|
||||
</Anchor>
|
||||
</Stack>
|
||||
|
||||
@@ -15,9 +15,9 @@ import {
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { signOut, useSession } from "@homarr/auth/client";
|
||||
import { hotkeys } from "@homarr/definitions";
|
||||
import { hotkeys, invariantTechnicalLabels } from "@homarr/definitions";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Link } from "@homarr/ui";
|
||||
|
||||
import { useAuthContext } from "~/app/[locale]/_client-providers/session";
|
||||
@@ -40,7 +40,7 @@ const formatHotkeyLabel = (hotkey: string, modifierLabel: string) =>
|
||||
.join(" + ");
|
||||
|
||||
export const UserAvatarMenu = ({ children, availableUpdates, isDockerEnabled }: UserAvatarMenuProps) => {
|
||||
const t = useScopedI18n("common.userAvatar.menu");
|
||||
const t = useI18n("common.userAvatar.menu");
|
||||
const session = useSession();
|
||||
|
||||
const { logoutUrl } = useAuthContext();
|
||||
@@ -110,7 +110,7 @@ export const UserAvatarMenu = ({ children, availableUpdates, isDockerEnabled }:
|
||||
</Menu.Item>
|
||||
{isDockerEnabled && (
|
||||
<Menu.Item leftSection={<IconBrandDocker size="1rem" />} onClick={() => openDockerModal()}>
|
||||
{t("docker")}
|
||||
{invariantTechnicalLabels.docker}
|
||||
</Menu.Item>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useId } from "react";
|
||||
|
||||
import { ReadOnlyCustomWidgetCode } from "@homarr/custom-widgets/workbench";
|
||||
import type { CustomWidgetEditorMessages } from "@homarr/custom-widgets/workbench";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { invariantTechnicalLabels } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
/** Read-only source view of a Workshop submission, used for both widget JSON and Custom CSS. */
|
||||
export function WorkshopCodeViewer({
|
||||
@@ -17,12 +18,13 @@ export function WorkshopCodeViewer({
|
||||
height?: string;
|
||||
}) {
|
||||
const id = useId();
|
||||
const t = useScopedI18n("customWidget.editor");
|
||||
const workshopT = useScopedI18n("workshop");
|
||||
const t = useI18n("customWidget.editor");
|
||||
const tCommon = useI18n("common");
|
||||
const workshopT = useI18n("workshop");
|
||||
const messages: CustomWidgetEditorMessages = {
|
||||
languageJsx: t("language.jsx"),
|
||||
languageJson: t("language.json"),
|
||||
undo: t("action.undo"),
|
||||
languageJsx: invariantTechnicalLabels.jsx,
|
||||
languageJson: invariantTechnicalLabels.json,
|
||||
undo: tCommon("action.undo"),
|
||||
redo: t("action.redo"),
|
||||
components: t("action.components"),
|
||||
componentSearch: t("componentReference.search"),
|
||||
@@ -30,10 +32,10 @@ export function WorkshopCodeViewer({
|
||||
componentCount: (count) => t("componentReference.count", { count }),
|
||||
insertStarter: t("action.insertStarter"),
|
||||
format: t("action.format"),
|
||||
copy: t("action.copy"),
|
||||
copy: tCommon("action.copy"),
|
||||
copied: t("action.copied"),
|
||||
schema: t("action.schema"),
|
||||
schemaTab: t("reference.schema"),
|
||||
schemaTab: invariantTechnicalLabels.jsonSchema,
|
||||
minimalTab: t("reference.minimal"),
|
||||
fullTab: t("reference.full"),
|
||||
errors: (count) => t("status.errors", { count }),
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IconDownload, IconExternalLink, IconSearch } from "@tabler/icons-react"
|
||||
|
||||
import { modalSizeSelect, useConfirmModal } from "@homarr/modals";
|
||||
import { showErrorNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { useWorkshopQuery } from "@homarr/workshop/backend";
|
||||
import type { WorkshopSubmissionSummary } from "@homarr/workshop/schema";
|
||||
import { validateWorkshopContent } from "@homarr/workshop/schema";
|
||||
@@ -24,7 +24,7 @@ import { WorkshopVoteControl } from "./workshop-vote-control";
|
||||
* board settings themselves are saved, so there is nothing to preview twice.
|
||||
*/
|
||||
export function WorkshopCssImportButton({ onImport }: { onImport(css: string): void }) {
|
||||
const t = useScopedI18n("workshop");
|
||||
const t = useI18n("workshop");
|
||||
const [opened, controls] = useDisclosure(false);
|
||||
|
||||
return (
|
||||
@@ -45,7 +45,7 @@ export function WorkshopCssImportButton({ onImport }: { onImport(css: string): v
|
||||
}
|
||||
|
||||
function WorkshopCssPicker({ onImport }: { onImport(css: string): void }) {
|
||||
const t = useScopedI18n("workshop");
|
||||
const t = useI18n("workshop");
|
||||
const session = useWorkshopSession();
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState } from "react";
|
||||
import { Alert, Button, Group, Modal, Select, Stack, Textarea } from "@mantine/core";
|
||||
|
||||
import { showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { WorkshopBackend } from "@homarr/workshop/backend";
|
||||
import { useWorkshopReportMutation } from "@homarr/workshop/backend";
|
||||
import type { WorkshopReport } from "@homarr/workshop/schema";
|
||||
@@ -19,7 +19,8 @@ interface WorkshopReportModalProps {
|
||||
}
|
||||
|
||||
export function WorkshopReportModal({ client, submissionId, opened, onClose }: WorkshopReportModalProps) {
|
||||
const t = useScopedI18n("workshop");
|
||||
const t = useI18n("workshop");
|
||||
const tCommon = useI18n("common");
|
||||
const [category, setCategory] = useState<WorkshopReport["category"]>("other");
|
||||
const [explanation, setExplanation] = useState("");
|
||||
const report = useWorkshopReportMutation(client);
|
||||
@@ -67,7 +68,7 @@ export function WorkshopReportModal({ client, submissionId, opened, onClose }: W
|
||||
{report.error && <Alert color="red">{report.error.message}</Alert>}
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="default" onClick={close}>
|
||||
{t("cancel")}
|
||||
{tCommon("action.cancel")}
|
||||
</Button>
|
||||
<Button color="red" loading={report.isPending} disabled={explanation.trim().length < 3} onClick={submit}>
|
||||
{t("reportSend")}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ActionIcon, Box, Group, Image, Modal, Paper, Tooltip, UnstyledButton }
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { IconChevronLeft, IconChevronRight, IconZoomIn } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { WorkshopBackend } from "@homarr/workshop/backend";
|
||||
|
||||
interface WorkshopScreenshotsProps {
|
||||
@@ -20,7 +20,7 @@ interface WorkshopScreenshotsProps {
|
||||
* click-to-zoom lightbox, so people can actually see what they are installing.
|
||||
*/
|
||||
export function WorkshopScreenshots({ client, submissionId, title, screenshots }: WorkshopScreenshotsProps) {
|
||||
const t = useScopedI18n("workshop");
|
||||
const t = useI18n("workshop");
|
||||
const [index, setIndex] = useState(0);
|
||||
const [zoomOpened, zoomControls] = useDisclosure(false);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button, Group, Text } from "@mantine/core";
|
||||
import { IconBrandGithub } from "@tabler/icons-react";
|
||||
|
||||
import { showErrorNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { WorkshopUser } from "@homarr/workshop/schema";
|
||||
|
||||
import { createWorkshopClient } from "./workshop-client";
|
||||
@@ -15,7 +15,7 @@ import { createWorkshopClient } from "./workshop-client";
|
||||
* never to install, so every surface can render this without gating its content.
|
||||
*/
|
||||
export function useWorkshopSession() {
|
||||
const t = useScopedI18n("workshop");
|
||||
const t = useI18n("workshop");
|
||||
const client = useMemo(createWorkshopClient, []);
|
||||
const [user, setUser] = useState<WorkshopUser | null>(null);
|
||||
const [pending, setPending] = useState(false);
|
||||
@@ -51,7 +51,7 @@ export function useWorkshopSession() {
|
||||
export type WorkshopSession = ReturnType<typeof useWorkshopSession>;
|
||||
|
||||
export function WorkshopAccountButton({ session }: { session: WorkshopSession }) {
|
||||
const t = useScopedI18n("workshop");
|
||||
const t = useI18n("workshop");
|
||||
|
||||
if (!session.user) {
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ReactNode } from "react";
|
||||
import { Badge, Box, Card, Group, Image, Stack, Text, Tooltip } from "@mantine/core";
|
||||
import { IconAlertTriangle, IconFlag, IconPhoto } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { WorkshopBackend } from "@homarr/workshop/backend";
|
||||
import type { WorkshopSubmissionSummary } from "@homarr/workshop/schema";
|
||||
|
||||
@@ -19,7 +19,7 @@ interface WorkshopSubmissionCardProps {
|
||||
}
|
||||
|
||||
export function WorkshopSubmissionCard({ client, item, actions }: WorkshopSubmissionCardProps) {
|
||||
const t = useScopedI18n("workshop");
|
||||
const t = useI18n("workshop");
|
||||
const screenshot = item.screenshots[0];
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { StyleProp } from "@mantine/core";
|
||||
import { IconAlertTriangle, IconRefresh } from "@tabler/icons-react";
|
||||
import type { UseQueryResult } from "@tanstack/react-query";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { WorkshopBackend, WorkshopPage } from "@homarr/workshop/backend";
|
||||
import type { WorkshopSubmissionSummary } from "@homarr/workshop/schema";
|
||||
|
||||
@@ -33,7 +33,8 @@ export function WorkshopSubmissionGrid({
|
||||
cols = { base: 1, sm: 2, lg: 3 },
|
||||
renderActions,
|
||||
}: WorkshopSubmissionGridProps) {
|
||||
const t = useScopedI18n("workshop");
|
||||
const t = useI18n("workshop");
|
||||
const tCommon = useI18n("common");
|
||||
|
||||
if (query.isLoading) {
|
||||
return (
|
||||
@@ -57,7 +58,7 @@ export function WorkshopSubmissionGrid({
|
||||
loading={query.isFetching}
|
||||
onClick={() => void query.refetch()}
|
||||
>
|
||||
{t("retry")}
|
||||
{tCommon("action.tryAgain")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Alert>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ActionIcon, Group, Text, Tooltip } from "@mantine/core";
|
||||
import { IconArrowBigDown, IconArrowBigDownFilled, IconArrowBigUp, IconArrowBigUpFilled } from "@tabler/icons-react";
|
||||
|
||||
import { showErrorNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { WorkshopBackend } from "@homarr/workshop/backend";
|
||||
import { useWorkshopUserVotesQuery, useWorkshopVoteMutation } from "@homarr/workshop/backend";
|
||||
|
||||
@@ -22,7 +22,7 @@ interface WorkshopVoteControlProps {
|
||||
* and clicking it again removes the vote.
|
||||
*/
|
||||
export function WorkshopVoteControl({ client, submissionId, score, canVote, size = "md" }: WorkshopVoteControlProps) {
|
||||
const t = useScopedI18n("workshop");
|
||||
const t = useI18n("workshop");
|
||||
const vote = useWorkshopVoteMutation(client);
|
||||
const userVotes = useWorkshopUserVotesQuery(client);
|
||||
const ownVote = userVotes.data?.find((entry) => entry.submission === submissionId)?.value ?? null;
|
||||
|
||||
Reference in new issue
Block a user