aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/landing/package.json2
-rw-r--r--apps/mobile/app.json4
-rw-r--r--apps/mobile/package.json2
-rw-r--r--apps/web/app/dashboard/admin/page.tsx15
-rw-r--r--apps/web/components/dashboard/admin/AdminCard.tsx3
-rw-r--r--apps/web/components/dashboard/admin/AdminNotices.tsx71
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkMarkdownComponent.tsx43
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx67
-rw-r--r--apps/web/components/dashboard/bookmarks/TextCard.tsx11
-rw-r--r--apps/web/components/dashboard/header/ProfileOptions.tsx11
-rw-r--r--apps/web/components/dashboard/preview/TextContentSection.tsx7
-rw-r--r--apps/web/components/ui/alert.tsx60
-rw-r--r--apps/web/components/ui/markdown/markdown-editor.tsx124
-rw-r--r--apps/web/components/ui/markdown/markdown-readonly.tsx (renamed from apps/web/components/ui/markdown-component.tsx)118
-rw-r--r--apps/web/components/ui/markdown/plugins/toolbar-plugin.tsx290
-rw-r--r--apps/web/components/ui/markdown/theme.ts35
-rw-r--r--apps/web/components/ui/switch.tsx28
-rw-r--r--apps/web/lib/exportBookmarks.ts2
-rw-r--r--apps/web/lib/i18n/locales/de/translation.json400
-rw-r--r--apps/web/lib/i18n/locales/en/translation.json50
-rw-r--r--apps/web/lib/i18n/locales/fr/translation.json15
-rw-r--r--apps/web/lib/i18n/locales/nl/translation.json110
-rw-r--r--apps/web/lib/i18n/locales/sv/translation.json328
-rw-r--r--apps/web/package.json11
24 files changed, 1247 insertions, 560 deletions
diff --git a/apps/landing/package.json b/apps/landing/package.json
index be96b848..ca5c7a98 100644
--- a/apps/landing/package.json
+++ b/apps/landing/package.json
@@ -19,7 +19,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"lucide-react": "^0.330.0",
- "next": "14.2.13",
+ "next": "14.2.15",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-select": "^5.8.0",
diff --git a/apps/mobile/app.json b/apps/mobile/app.json
index 8a6e9aaf..4772c25b 100644
--- a/apps/mobile/app.json
+++ b/apps/mobile/app.json
@@ -30,7 +30,7 @@
"NSAllowsLocalNetworking": true
}
},
- "buildNumber": "19"
+ "buildNumber": "20"
},
"android": {
"adaptiveIcon": {
@@ -48,7 +48,7 @@
}
},
"package": "app.hoarder.hoardermobile",
- "versionCode": 19
+ "versionCode": 20
},
"plugins": [
"expo-router",
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index e4d69d9f..5d4b6089 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -65,7 +65,7 @@
"ajv": "latest",
"eslint": "^8.57.0",
"eslint-config-universe": "^12.0.0",
- "prettier": "^3.2.5",
+ "prettier": "^3.4.2",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
},
diff --git a/apps/web/app/dashboard/admin/page.tsx b/apps/web/app/dashboard/admin/page.tsx
index 18efc889..cf97698b 100644
--- a/apps/web/app/dashboard/admin/page.tsx
+++ b/apps/web/app/dashboard/admin/page.tsx
@@ -1,5 +1,7 @@
import { redirect } from "next/navigation";
import AdminActions from "@/components/dashboard/admin/AdminActions";
+import { AdminCard } from "@/components/dashboard/admin/AdminCard";
+import { AdminNotices } from "@/components/dashboard/admin/AdminNotices";
import ServerStats from "@/components/dashboard/admin/ServerStats";
import UserList from "@/components/dashboard/admin/UserList";
import { getServerAuthSession } from "@/server/auth";
@@ -10,14 +12,15 @@ export default async function AdminPage() {
redirect("/");
}
return (
- <>
- <div className="rounded-md border bg-background p-4">
+ <div className="flex flex-col gap-4">
+ <AdminNotices />
+ <AdminCard>
<ServerStats />
<AdminActions />
- </div>
- <div className="mt-4 rounded-md border bg-background p-4">
+ </AdminCard>
+ <AdminCard>
<UserList />
- </div>
- </>
+ </AdminCard>
+ </div>
);
}
diff --git a/apps/web/components/dashboard/admin/AdminCard.tsx b/apps/web/components/dashboard/admin/AdminCard.tsx
new file mode 100644
index 00000000..3a52b5e5
--- /dev/null
+++ b/apps/web/components/dashboard/admin/AdminCard.tsx
@@ -0,0 +1,3 @@
+export function AdminCard({ children }: { children: React.ReactNode }) {
+ return <div className="rounded-md border bg-background p-4">{children}</div>;
+}
diff --git a/apps/web/components/dashboard/admin/AdminNotices.tsx b/apps/web/components/dashboard/admin/AdminNotices.tsx
new file mode 100644
index 00000000..4977736f
--- /dev/null
+++ b/apps/web/components/dashboard/admin/AdminNotices.tsx
@@ -0,0 +1,71 @@
+"use client";
+
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Badge } from "@/components/ui/badge";
+import { api } from "@/lib/trpc";
+import { AlertCircle } from "lucide-react";
+
+import { AdminCard } from "./AdminCard";
+
+interface AdminNotice {
+ level: "info" | "warning" | "error";
+ message: React.ReactNode;
+ title: string;
+}
+
+function useAdminNotices() {
+ const { data } = api.admin.getAdminNoticies.useQuery();
+ if (!data) {
+ return [];
+ }
+ const ret: AdminNotice[] = [];
+ if (data.legacyContainersNotice) {
+ ret.push({
+ level: "warning",
+ message: (
+ <p>
+ You&apos;re using the legacy docker container images. Those will stop
+ getting supported soon. Please follow{" "}
+ <a
+ href="https://docs.hoarder.app/next/Guides/legacy-container-upgrade"
+ className="underline"
+ >
+ this guide
+ </a>{" "}
+ to upgrade.
+ </p>
+ ),
+ title: "Legacy Container Images",
+ });
+ }
+ return ret;
+}
+
+export function AdminNotices() {
+ const notices = useAdminNotices();
+
+ if (notices.length === 0) {
+ return null;
+ }
+ return (
+ <AdminCard>
+ <div className="flex flex-col gap-2">
+ {notices.map((n, i) => (
+ <Alert key={i} variant="destructive">
+ <AlertCircle className="h-4 w-4" />
+ <AlertTitle>{n.title}</AlertTitle>
+ <AlertDescription>{n.message}</AlertDescription>
+ </Alert>
+ ))}
+ </div>
+ </AdminCard>
+ );
+}
+
+export function AdminNoticeBadge() {
+ const notices = useAdminNotices();
+ if (notices.length === 0) {
+ return null;
+ }
+ return <Badge variant="destructive">{notices.length}</Badge>;
+}
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkMarkdownComponent.tsx b/apps/web/components/dashboard/bookmarks/BookmarkMarkdownComponent.tsx
new file mode 100644
index 00000000..74eb0868
--- /dev/null
+++ b/apps/web/components/dashboard/bookmarks/BookmarkMarkdownComponent.tsx
@@ -0,0 +1,43 @@
+import MarkdownEditor from "@/components/ui/markdown/markdown-editor";
+import { MarkdownReadonly } from "@/components/ui/markdown/markdown-readonly";
+import { toast } from "@/components/ui/use-toast";
+
+import type { ZBookmarkTypeText } from "@hoarder/shared/types/bookmarks";
+import { useUpdateBookmarkText } from "@hoarder/shared-react/hooks/bookmarks";
+
+export function BookmarkMarkdownComponent({
+ children: bookmark,
+ readOnly = true,
+}: {
+ children: ZBookmarkTypeText;
+ readOnly?: boolean;
+}) {
+ const { mutate: updateBookmarkMutator, isPending } = useUpdateBookmarkText({
+ onSuccess: () => {
+ toast({
+ description: "Note updated!",
+ });
+ },
+ onError: () => {
+ toast({ description: "Something went wrong", variant: "destructive" });
+ },
+ });
+
+ const onSave = (text: string) => {
+ updateBookmarkMutator({
+ bookmarkId: bookmark.id,
+ text,
+ });
+ };
+ return (
+ <div className="h-full overflow-hidden">
+ {readOnly ? (
+ <MarkdownReadonly>{bookmark.content.text}</MarkdownReadonly>
+ ) : (
+ <MarkdownEditor onSave={onSave} isSaving={isPending}>
+ {bookmark.content.text}
+ </MarkdownEditor>
+ )}
+ </div>
+ );
+}
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx b/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx
index e0434943..b2c27c7e 100644
--- a/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx
+++ b/apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx
@@ -1,20 +1,12 @@
-import { useState } from "react";
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
+import { BookmarkMarkdownComponent } from "@/components/dashboard/bookmarks/BookmarkMarkdownComponent";
import {
Dialog,
- DialogClose,
DialogContent,
- DialogDescription,
- DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
-import { Textarea } from "@/components/ui/textarea";
-import { toast } from "@/components/ui/use-toast";
-import { useUpdateBookmarkText } from "@hoarder/shared-react/hooks/bookmarks";
-import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks";
+import { ZBookmark, ZBookmarkTypeText } from "@hoarder/shared/types/bookmarks";
export function BookmarkedTextEditor({
bookmark,
@@ -26,55 +18,20 @@ export function BookmarkedTextEditor({
setOpen: (open: boolean) => void;
}) {
const isNewBookmark = bookmark === undefined;
- const [noteText, setNoteText] = useState(
- bookmark && bookmark.content.type == BookmarkTypes.TEXT
- ? bookmark.content.text
- : "",
- );
-
- const { mutate: updateBookmarkMutator, isPending } = useUpdateBookmarkText({
- onSuccess: () => {
- toast({
- description: "Note updated!",
- });
- setOpen(false);
- },
- onError: () => {
- toast({ description: "Something went wrong", variant: "destructive" });
- },
- });
-
- const onSave = () => {
- updateBookmarkMutator({
- bookmarkId: bookmark.id,
- text: noteText,
- });
- };
return (
<Dialog open={open} onOpenChange={setOpen}>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>{isNewBookmark ? "New Note" : "Edit Note"}</DialogTitle>
- <DialogDescription>
- Write your note with markdown support
- </DialogDescription>
+ <DialogContent className="max-w-[80%]">
+ <DialogHeader className="flex">
+ <DialogTitle className="w-fit">
+ {isNewBookmark ? "New Note" : "Edit Note"}
+ </DialogTitle>
</DialogHeader>
- <Textarea
- value={noteText}
- onChange={(e) => setNoteText(e.target.value)}
- className="h-52 grow"
- />
- <DialogFooter className="flex-shrink gap-1 sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton type="button" loading={isPending} onClick={onSave}>
- Save
- </ActionButton>
- </DialogFooter>
+ <div className="h-[80vh]">
+ <BookmarkMarkdownComponent readOnly={false}>
+ {bookmark as ZBookmarkTypeText}
+ </BookmarkMarkdownComponent>
+ </div>
</DialogContent>
</Dialog>
);
diff --git a/apps/web/components/dashboard/bookmarks/TextCard.tsx b/apps/web/components/dashboard/bookmarks/TextCard.tsx
index 14a4f905..9d168910 100644
--- a/apps/web/components/dashboard/bookmarks/TextCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/TextCard.tsx
@@ -2,7 +2,7 @@
import Image from "next/image";
import Link from "next/link";
-import { MarkdownComponent } from "@/components/ui/markdown-component";
+import { BookmarkMarkdownComponent } from "@/components/dashboard/bookmarks/BookmarkMarkdownComponent";
import { bookmarkLayoutSwitch } from "@/lib/userLocalSettings/bookmarksLayout";
import { cn } from "@/lib/utils";
@@ -20,15 +20,16 @@ export default function TextCard({
bookmark: ZBookmarkTypeText;
className?: string;
}) {
- const bookmarkedText = bookmark.content;
-
const banner = bookmark.assets.find((a) => a.assetType == "bannerImage");
-
return (
<>
<BookmarkLayoutAdaptingCard
title={bookmark.title}
- content={<MarkdownComponent>{bookmarkedText.text}</MarkdownComponent>}
+ content={
+ <BookmarkMarkdownComponent readOnly={true}>
+ {bookmark}
+ </BookmarkMarkdownComponent>
+ }
footer={
getSourceUrl(bookmark) && (
<FooterLinkURL url={getSourceUrl(bookmark)} />
diff --git a/apps/web/components/dashboard/header/ProfileOptions.tsx b/apps/web/components/dashboard/header/ProfileOptions.tsx
index 3dbfcf04..fc18e9d2 100644
--- a/apps/web/components/dashboard/header/ProfileOptions.tsx
+++ b/apps/web/components/dashboard/header/ProfileOptions.tsx
@@ -16,6 +16,8 @@ import { LogOut, Moon, Paintbrush, Settings, Shield, Sun } from "lucide-react";
import { signOut, useSession } from "next-auth/react";
import { useTheme } from "next-themes";
+import { AdminNoticeBadge } from "../admin/AdminNotices";
+
function DarkModeToggle() {
const { t } = useTranslation();
const { theme } = useTheme();
@@ -72,9 +74,12 @@ export default function SidebarProfileOptions() {
</DropdownMenuItem>
{session.user.role == "admin" && (
<DropdownMenuItem asChild>
- <Link href="/dashboard/admin">
- <Shield className="mr-2 size-4" />
- {t("admin.admin_settings")}
+ <Link href="/dashboard/admin" className="flex justify-between">
+ <div className="items-cente flex gap-2">
+ <Shield className="size-4" />
+ {t("admin.admin_settings")}
+ </div>
+ <AdminNoticeBadge />
</Link>
</DropdownMenuItem>
)}
diff --git a/apps/web/components/dashboard/preview/TextContentSection.tsx b/apps/web/components/dashboard/preview/TextContentSection.tsx
index 327436c6..a58bc717 100644
--- a/apps/web/components/dashboard/preview/TextContentSection.tsx
+++ b/apps/web/components/dashboard/preview/TextContentSection.tsx
@@ -1,7 +1,8 @@
import Image from "next/image";
-import { MarkdownComponent } from "@/components/ui/markdown-component";
+import { BookmarkMarkdownComponent } from "@/components/dashboard/bookmarks/BookmarkMarkdownComponent";
import { ScrollArea } from "@radix-ui/react-scroll-area";
+import type { ZBookmarkTypeText } from "@hoarder/shared/types/bookmarks";
import { getAssetUrl } from "@hoarder/shared-react/utils/assetUtils";
import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks";
@@ -27,7 +28,9 @@ export function TextContentSection({ bookmark }: { bookmark: ZBookmark }) {
/>
</div>
)}
- <MarkdownComponent>{bookmark.content.text}</MarkdownComponent>
+ <BookmarkMarkdownComponent>
+ {bookmark as ZBookmarkTypeText}
+ </BookmarkMarkdownComponent>
</ScrollArea>
);
}
diff --git a/apps/web/components/ui/alert.tsx b/apps/web/components/ui/alert.tsx
new file mode 100644
index 00000000..706b711e
--- /dev/null
+++ b/apps/web/components/ui/alert.tsx
@@ -0,0 +1,60 @@
+import type { VariantProps } from "class-variance-authority";
+import * as React from "react";
+import { cn } from "@/lib/utils";
+import { cva } from "class-variance-authority";
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
+>(({ className, variant, ...props }, ref) => (
+ <div
+ ref={ref}
+ role="alert"
+ className={cn(alertVariants({ variant }), className)}
+ {...props}
+ />
+));
+Alert.displayName = "Alert";
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes<HTMLHeadingElement>
+>(({ className, ...props }, ref) => (
+ // eslint-disable-next-line jsx-a11y/heading-has-content
+ <h5
+ ref={ref}
+ className={cn("mb-1 font-medium leading-none tracking-tight", className)}
+ {...props}
+ />
+));
+AlertTitle.displayName = "AlertTitle";
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes<HTMLParagraphElement>
+>(({ className, ...props }, ref) => (
+ <div
+ ref={ref}
+ className={cn("text-sm [&_p]:leading-relaxed", className)}
+ {...props}
+ />
+));
+AlertDescription.displayName = "AlertDescription";
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/apps/web/components/ui/markdown/markdown-editor.tsx b/apps/web/components/ui/markdown/markdown-editor.tsx
new file mode 100644
index 00000000..85f0c878
--- /dev/null
+++ b/apps/web/components/ui/markdown/markdown-editor.tsx
@@ -0,0 +1,124 @@
+import { memo, useMemo, useState } from "react";
+import ToolbarPlugin from "@/components/ui/markdown/plugins/toolbar-plugin";
+import { MarkdownEditorTheme } from "@/components/ui/markdown/theme";
+import {
+ CodeHighlightNode,
+ CodeNode,
+ registerCodeHighlighting,
+} from "@lexical/code";
+import { LinkNode } from "@lexical/link";
+import { ListItemNode, ListNode } from "@lexical/list";
+import {
+ $convertFromMarkdownString,
+ $convertToMarkdownString,
+ TRANSFORMERS,
+} from "@lexical/markdown";
+import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
+import {
+ InitialConfigType,
+ LexicalComposer,
+} from "@lexical/react/LexicalComposer";
+import { ContentEditable } from "@lexical/react/LexicalContentEditable";
+import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
+import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
+import { HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode";
+import { ListPlugin } from "@lexical/react/LexicalListPlugin";
+import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
+import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
+import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
+import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
+import { TabIndentationPlugin } from "@lexical/react/LexicalTabIndentationPlugin";
+import { HeadingNode, QuoteNode } from "@lexical/rich-text";
+import { $getRoot, EditorState, LexicalEditor } from "lexical";
+
+function onError(error: Error) {
+ console.error(error);
+}
+
+const EDITOR_NODES = [
+ HeadingNode,
+ ListNode,
+ ListItemNode,
+ QuoteNode,
+ LinkNode,
+ CodeNode,
+ HorizontalRuleNode,
+ CodeHighlightNode,
+];
+
+interface MarkdownEditorProps {
+ children: string;
+ onSave?: (markdown: string) => void;
+ isSaving?: boolean;
+}
+
+const MarkdownEditor = memo(
+ ({ children: initialMarkdown, onSave, isSaving }: MarkdownEditorProps) => {
+ const [isRawMarkdownMode, setIsRawMarkdownMode] = useState(false);
+ const [rawMarkdown, setRawMarkdown] = useState(initialMarkdown);
+
+ const initialConfig: InitialConfigType = useMemo(
+ () => ({
+ namespace: "editor",
+ onError,
+ theme: MarkdownEditorTheme,
+ nodes: EDITOR_NODES,
+ editorState: (editor: LexicalEditor) => {
+ registerCodeHighlighting(editor);
+ $convertFromMarkdownString(initialMarkdown, TRANSFORMERS);
+ },
+ }),
+ [initialMarkdown],
+ );
+
+ const handleOnChange = (editorState: EditorState) => {
+ editorState.read(() => {
+ let markdownString;
+ if (isRawMarkdownMode) {
+ markdownString = $getRoot()?.getFirstChild()?.getTextContent() ?? "";
+ } else {
+ markdownString = $convertToMarkdownString(TRANSFORMERS);
+ }
+ setRawMarkdown(markdownString);
+ });
+ };
+
+ return (
+ <LexicalComposer initialConfig={initialConfig}>
+ <div className="flex h-full flex-col justify-stretch">
+ <ToolbarPlugin
+ isRawMarkdownMode={isRawMarkdownMode}
+ setIsRawMarkdownMode={setIsRawMarkdownMode}
+ onSave={onSave && (() => onSave(rawMarkdown))}
+ isSaving={!!isSaving}
+ />
+ {isRawMarkdownMode ? (
+ <PlainTextPlugin
+ contentEditable={
+ <ContentEditable className="h-full w-full min-w-full overflow-auto p-2" />
+ }
+ ErrorBoundary={LexicalErrorBoundary}
+ />
+ ) : (
+ <RichTextPlugin
+ contentEditable={
+ <ContentEditable className="prose h-full w-full min-w-full overflow-auto p-2 dark:prose-invert prose-p:m-0" />
+ }
+ ErrorBoundary={LexicalErrorBoundary}
+ />
+ )}
+ </div>
+ <HistoryPlugin />
+ <AutoFocusPlugin />
+ <TabIndentationPlugin />
+ <MarkdownShortcutPlugin transformers={TRANSFORMERS} />
+ <OnChangePlugin onChange={handleOnChange} />
+ <ListPlugin />
+ </LexicalComposer>
+ );
+ },
+);
+// needed for linter because of memo
+MarkdownEditor.displayName = "MarkdownEditor";
+
+export default MarkdownEditor;
diff --git a/apps/web/components/ui/markdown-component.tsx b/apps/web/components/ui/markdown/markdown-readonly.tsx
index d3c832ac..29077480 100644
--- a/apps/web/components/ui/markdown-component.tsx
+++ b/apps/web/components/ui/markdown/markdown-readonly.tsx
@@ -1,61 +1,57 @@
-import React from "react";
-import CopyBtn from "@/components/ui/copy-button";
-import { cn } from "@/lib/utils";
-import Markdown from "react-markdown";
-import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
-import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism";
-import remarkBreaks from "remark-breaks";
-import remarkGfm from "remark-gfm";
-
-function PreWithCopyBtn({ className, ...props }: React.ComponentProps<"pre">) {
- const ref = React.useRef<HTMLPreElement>(null);
- return (
- <span className="group relative">
- <CopyBtn
- className="absolute right-1 top-1 m-1 hidden text-white group-hover:block"
- getStringToCopy={() => {
- return ref.current?.textContent ?? "";
- }}
- />
- <pre ref={ref} className={cn(className, "")} {...props} />
- </span>
- );
-}
-
-export function MarkdownComponent({
- children: markdown,
-}: {
- children: string;
-}) {
- return (
- <Markdown
- remarkPlugins={[remarkGfm, remarkBreaks]}
- className="prose dark:prose-invert"
- components={{
- pre({ ...props }) {
- return <PreWithCopyBtn {...props} />;
- },
- code({ className, children, ...props }) {
- const match = /language-(\w+)/.exec(className ?? "");
- return match ? (
- // @ts-expect-error -- Refs are not compatible for some reason
- <SyntaxHighlighter
- PreTag="div"
- language={match[1]}
- {...props}
- style={dracula}
- >
- {String(children).replace(/\n$/, "")}
- </SyntaxHighlighter>
- ) : (
- <code className={className} {...props}>
- {children}
- </code>
- );
- },
- }}
- >
- {markdown}
- </Markdown>
- );
-}
+import React from "react";
+import CopyBtn from "@/components/ui/copy-button";
+import { cn } from "@/lib/utils";
+import Markdown from "react-markdown";
+import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
+import { dracula } from "react-syntax-highlighter/dist/cjs/styles/prism";
+import remarkBreaks from "remark-breaks";
+import remarkGfm from "remark-gfm";
+
+function PreWithCopyBtn({ className, ...props }: React.ComponentProps<"pre">) {
+ const ref = React.useRef<HTMLPreElement>(null);
+ return (
+ <span className="group relative">
+ <CopyBtn
+ className="absolute right-1 top-1 m-1 hidden text-white group-hover:block"
+ getStringToCopy={() => {
+ return ref.current?.textContent ?? "";
+ }}
+ />
+ <pre ref={ref} className={cn(className, "")} {...props} />
+ </span>
+ );
+}
+
+export function MarkdownReadonly({ children: markdown }: { children: string }) {
+ return (
+ <Markdown
+ remarkPlugins={[remarkGfm, remarkBreaks]}
+ className="prose dark:prose-invert"
+ components={{
+ pre({ ...props }) {
+ return <PreWithCopyBtn {...props} />;
+ },
+ code({ className, children, ...props }) {
+ const match = /language-(\w+)/.exec(className ?? "");
+ return match ? (
+ // @ts-expect-error -- Refs are not compatible for some reason
+ <SyntaxHighlighter
+ PreTag="div"
+ language={match[1]}
+ {...props}
+ style={dracula}
+ >
+ {String(children).replace(/\n$/, "")}
+ </SyntaxHighlighter>
+ ) : (
+ <code className={className} {...props}>
+ {children}
+ </code>
+ );
+ },
+ }}
+ >
+ {markdown}
+ </Markdown>
+ );
+}
diff --git a/apps/web/components/ui/markdown/plugins/toolbar-plugin.tsx b/apps/web/components/ui/markdown/plugins/toolbar-plugin.tsx
new file mode 100644
index 00000000..28e265e2
--- /dev/null
+++ b/apps/web/components/ui/markdown/plugins/toolbar-plugin.tsx
@@ -0,0 +1,290 @@
+import { useCallback, useEffect, useState } from "react";
+import { Button } from "@/components/ui/button";
+import { useTranslation } from "@/lib/i18n/client";
+import {
+ $convertFromMarkdownString,
+ $convertToMarkdownString,
+ TRANSFORMERS,
+} from "@lexical/markdown";
+import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
+import { mergeRegister } from "@lexical/utils";
+import {
+ $createParagraphNode,
+ $createTextNode,
+ $getRoot,
+ $getSelection,
+ $isRangeSelection,
+ FORMAT_TEXT_COMMAND,
+ LexicalCommand,
+ SELECTION_CHANGE_COMMAND,
+ TextFormatType,
+} from "lexical";
+import {
+ Bold,
+ Code,
+ Highlighter,
+ Italic,
+ LucideIcon,
+ Save,
+ Strikethrough,
+} from "lucide-react";
+
+import { ActionButton } from "../../action-button";
+import InfoTooltip from "../../info-tooltip";
+import { Label } from "../../label";
+import { Switch } from "../../switch";
+
+const LowPriority = 1;
+
+function MarkdownToolTip() {
+ const { t } = useTranslation();
+ return (
+ <InfoTooltip size={15}>
+ <table className="w-full table-auto text-left text-sm">
+ <thead>
+ <tr>
+ <th>{t("editor.text_toolbar.markdown_shortcuts.label")}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr className="border-b">
+ <td className="py-2">
+ {t("editor.text_toolbar.markdown_shortcuts.heading.label")}
+ </td>
+ <td className="py-2">
+ {t("editor.text_toolbar.markdown_shortcuts.heading.example")}
+ </td>
+ </tr>
+ <tr className="border-b">
+ <td className="py-2">
+ {t("editor.text_toolbar.markdown_shortcuts.bold.label")}
+ </td>
+ <td className="py-2">
+ {t("editor.text_toolbar.markdown_shortcuts.bold.example")}
+ </td>
+ </tr>
+ <tr className="border-b">
+ <td className="py-2">
+ {t("editor.text_toolbar.markdown_shortcuts.italic.label")}
+ </td>
+ <td className="py-2">
+ {t("editor.text_toolbar.markdown_shortcuts.italic.example")}
+ </td>
+ </tr>
+ <tr className="border-b">
+ <td className="py-2">
+ {t("editor.text_toolbar.markdown_shortcuts.blockquote.label")}
+ </td>
+ <td className="py-2">
+ {t("editor.text_toolbar.markdown_shortcuts.blockquote.example")}
+ </td>
+ </tr>
+ <tr className="border-b">
+ <td className="py-2">
+ {t("editor.text_toolbar.markdown_shortcuts.ordered_list.label")}
+ </td>
+ <td className="py-2">
+ {t("editor.text_toolbar.markdown_shortcuts.ordered_list.example")}
+ </td>
+ </tr>
+ <tr className="border-b">
+ <td className="py-2">
+ {t("editor.text_toolbar.markdown_shortcuts.unordered_list.label")}
+ </td>
+ <td className="py-2">
+ {t(
+ "editor.text_toolbar.markdown_shortcuts.unordered_list.example",
+ )}
+ </td>
+ </tr>
+ <tr className="border-b">
+ <td className="py-2">
+ {t("editor.text_toolbar.markdown_shortcuts.inline_code.label")}
+ </td>
+ <td className="py-2">
+ {t("editor.text_toolbar.markdown_shortcuts.inline_code.example")}
+ </td>
+ </tr>
+ <tr className="border-b">
+ <td className="py-2">
+ {t("editor.text_toolbar.markdown_shortcuts.block_code.label")}
+ </td>
+ <td className="py-2">
+ {t("editor.text_toolbar.markdown_shortcuts.block_code.example")}
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </InfoTooltip>
+ );
+}
+
+export default function ToolbarPlugin({
+ isRawMarkdownMode = false,
+ setIsRawMarkdownMode,
+ onSave,
+ isSaving,
+}: {
+ isRawMarkdownMode: boolean;
+ setIsRawMarkdownMode: (value: boolean) => void;
+ onSave?: () => void;
+ isSaving: boolean;
+}) {
+ const { t } = useTranslation();
+ const [editor] = useLexicalComposerContext();
+ const [editorToolbarState, setEditorToolbarState] = useState<{
+ isBold: boolean;
+ isItalic: boolean;
+ isStrikethrough: boolean;
+ isHighlight: boolean;
+ isCode: boolean;
+ }>({
+ isBold: false,
+ isItalic: false,
+ isStrikethrough: false,
+ isHighlight: false,
+ isCode: false,
+ });
+
+ const $updateToolbar = useCallback(() => {
+ const selection = $getSelection();
+ if ($isRangeSelection(selection)) {
+ setEditorToolbarState({
+ isBold: selection.hasFormat("bold"),
+ isItalic: selection.hasFormat("italic"),
+ isStrikethrough: selection.hasFormat("strikethrough"),
+ isHighlight: selection.hasFormat("highlight"),
+ isCode: selection.hasFormat("code"),
+ });
+ }
+ }, []);
+
+ useEffect(() => {
+ return mergeRegister(
+ editor.registerUpdateListener(({ editorState }) => {
+ editorState.read(() => {
+ $updateToolbar();
+ });
+ }),
+ editor.registerCommand(
+ SELECTION_CHANGE_COMMAND,
+ (_payload, _newEditor) => {
+ $updateToolbar();
+ return false;
+ },
+ LowPriority,
+ ),
+ );
+ }, [editor, $updateToolbar]);
+
+ const formatButtons: {
+ command: LexicalCommand<TextFormatType>;
+ format: TextFormatType;
+ isActive?: boolean;
+ icon: LucideIcon;
+ label: string;
+ }[] = [
+ {
+ command: FORMAT_TEXT_COMMAND,
+ format: "bold",
+ icon: Bold,
+ isActive: editorToolbarState.isBold,
+ label: t("editor.text_toolbar.bold"),
+ },
+ {
+ command: FORMAT_TEXT_COMMAND,
+ format: "italic",
+ icon: Italic,
+ isActive: editorToolbarState.isItalic,
+ label: t("editor.text_toolbar.italic"),
+ },
+ {
+ command: FORMAT_TEXT_COMMAND,
+ format: "strikethrough",
+ icon: Strikethrough,
+ isActive: editorToolbarState.isStrikethrough,
+ label: t("editor.text_toolbar.strikethrough"),
+ },
+ {
+ command: FORMAT_TEXT_COMMAND,
+ format: "code",
+ icon: Code,
+ isActive: editorToolbarState.isCode,
+ label: t("editor.text_toolbar.code"),
+ },
+ {
+ command: FORMAT_TEXT_COMMAND,
+ format: "highlight",
+ icon: Highlighter,
+ isActive: editorToolbarState.isHighlight,
+ label: t("editor.text_toolbar.highlight"),
+ },
+ ];
+
+ const handleRawMarkdownToggle = useCallback(() => {
+ editor.update(() => {
+ console.log(isRawMarkdownMode);
+ const root = $getRoot();
+ const firstChild = root.getFirstChild();
+ if (isRawMarkdownMode) {
+ if (firstChild) {
+ $convertFromMarkdownString(firstChild.getTextContent(), TRANSFORMERS);
+ }
+ setIsRawMarkdownMode(false);
+ } else {
+ const markdown = $convertToMarkdownString(TRANSFORMERS);
+ const pNode = $createParagraphNode();
+ pNode.append($createTextNode(markdown));
+ root.clear().append(pNode);
+ setIsRawMarkdownMode(true);
+ }
+ });
+ }, [editor, isRawMarkdownMode]);
+
+ return (
+ <div className="mb-1 flex items-center justify-between rounded-t-lg p-1">
+ <div className="flex">
+ {formatButtons.map(
+ ({ command, format, icon: Icon, isActive, label }) => (
+ <Button
+ key={format}
+ disabled={isRawMarkdownMode}
+ size={"sm"}
+ onClick={() => {
+ editor.dispatchCommand(command, format);
+ }}
+ variant={isActive ? "default" : "ghost"}
+ aria-label={label}
+ >
+ <Icon className="h-4 w-4" />
+ </Button>
+ ),
+ )}
+ </div>
+ <div className="flex items-center gap-2">
+ <div className="flex items-center space-x-2">
+ <Switch
+ id="editor-raw-markdown"
+ onCheckedChange={handleRawMarkdownToggle}
+ checked={isRawMarkdownMode}
+ />
+ <Label htmlFor="editor-raw-markdown">Raw Markdown</Label>
+ </div>
+ {onSave && (
+ <ActionButton
+ loading={isSaving}
+ className="flex items-center gap-2"
+ size={"sm"}
+ onClick={() => {
+ onSave?.();
+ }}
+ >
+ <Save className="size-4" />
+ Save
+ </ActionButton>
+ )}
+ <MarkdownToolTip />
+ </div>
+ </div>
+ );
+}
diff --git a/apps/web/components/ui/markdown/theme.ts b/apps/web/components/ui/markdown/theme.ts
new file mode 100644
index 00000000..ff088e32
--- /dev/null
+++ b/apps/web/components/ui/markdown/theme.ts
@@ -0,0 +1,35 @@
+export const MarkdownEditorTheme = {
+ code: "bg-[#282A36] text-[#F8F8F2] font-mono block px-4 py-2 my-2 text-sm overflow-x-auto relative rounded-md shadow-sm",
+ codeHighlight: {
+ atrule: "text-[#8BE9FD]",
+ attr: "text-[#8BE9FD]",
+ boolean: "text-[#FF79C6]",
+ builtin: "text-[#50FA7B]",
+ cdata: "text-[#6272A4]",
+ char: "text-[#50FA7B]",
+ class: "text-[#FF79C6]",
+ "class-name": "text-[#FF79C6]",
+ comment: "text-[#6272A4]",
+ constant: "text-[#FF79C6]",
+ deleted: "text-[#FF5555]",
+ doctype: "text-[#6272A4]",
+ entity: "text-[#FFB86C]",
+ function: "text-[#50FA7B]",
+ important: "text-[#F1FA8C]",
+ inserted: "text-[#50FA7B]",
+ keyword: "text-[#FF79C6]",
+ namespace: "text-[#F1FA8C]",
+ number: "text-[#BD93F9]",
+ operator: "text-[#FFB86C]",
+ prolog: "text-[#6272A4]",
+ property: "text-[#FFB86C]",
+ punctuation: "text-[#F8F8F2]",
+ regex: "text-[#FF5555]",
+ selector: "text-[#50FA7B]",
+ string: "text-[#F1FA8C]",
+ symbol: "text-[#FF79C6]",
+ tag: "text-[#FF79C6]",
+ url: "text-[#8BE9FD]",
+ variable: "text-[#F1FA8C]",
+ },
+};
diff --git a/apps/web/components/ui/switch.tsx b/apps/web/components/ui/switch.tsx
new file mode 100644
index 00000000..2438dc86
--- /dev/null
+++ b/apps/web/components/ui/switch.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import * as React from "react";
+import { cn } from "@/lib/utils";
+import * as SwitchPrimitives from "@radix-ui/react-switch";
+
+const Switch = React.forwardRef<
+ React.ElementRef<typeof SwitchPrimitives.Root>,
+ React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
+>(({ className, ...props }, ref) => (
+ <SwitchPrimitives.Root
+ className={cn(
+ "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
+ className,
+ )}
+ {...props}
+ ref={ref}
+ >
+ <SwitchPrimitives.Thumb
+ className={cn(
+ "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
+ )}
+ />
+ </SwitchPrimitives.Root>
+));
+Switch.displayName = SwitchPrimitives.Root.displayName;
+
+export { Switch };
diff --git a/apps/web/lib/exportBookmarks.ts b/apps/web/lib/exportBookmarks.ts
index dd1913fb..f651b897 100644
--- a/apps/web/lib/exportBookmarks.ts
+++ b/apps/web/lib/exportBookmarks.ts
@@ -51,7 +51,7 @@ export function toExportFormat(
title:
bookmark.title ??
(bookmark.content.type === BookmarkTypes.LINK
- ? bookmark.content.title ?? null
+ ? (bookmark.content.title ?? null)
: null),
tags: bookmark.tags.map((t) => t.name),
content,
diff --git a/apps/web/lib/i18n/locales/de/translation.json b/apps/web/lib/i18n/locales/de/translation.json
index 73e8839e..cb07914a 100644
--- a/apps/web/lib/i18n/locales/de/translation.json
+++ b/apps/web/lib/i18n/locales/de/translation.json
@@ -1,213 +1,213 @@
{
- "common": {
- "url": "URL",
- "name": "Name",
- "email": "E-Mail",
- "password": "Passwort",
- "action": "Aktion",
- "actions": "Aktionen",
- "created_at": "Erstellt am",
- "key": "Schlüssel",
- "role": "Rolle",
- "roles": {
- "user": "Benutzer",
- "admin": "Administrator"
- },
- "something_went_wrong": "Etwas ist schief gelaufen",
- "experimental": "Experimentell",
- "search": "Suche",
- "tags": "Tags",
- "note": "Notiz",
- "attachments": "Anhänge",
- "screenshot": "Screenshot",
- "video": "Video",
- "archive": "Archiv",
- "home": "Startseite"
+ "common": {
+ "url": "URL",
+ "name": "Name",
+ "email": "E-Mail",
+ "password": "Passwort",
+ "action": "Aktion",
+ "actions": "Aktionen",
+ "created_at": "Erstellt am",
+ "key": "Schlüssel",
+ "role": "Rolle",
+ "roles": {
+ "user": "Benutzer",
+ "admin": "Administrator"
},
- "layouts": {
- "masonry": "Mauerwerk",
- "grid": "Raster",
- "list": "Liste",
- "compact": "Kompakt"
+ "something_went_wrong": "Etwas ist schief gelaufen",
+ "experimental": "Experimentell",
+ "search": "Suche",
+ "tags": "Tags",
+ "note": "Notiz",
+ "attachments": "Anhänge",
+ "screenshot": "Screenshot",
+ "video": "Video",
+ "archive": "Archiv",
+ "home": "Startseite"
+ },
+ "layouts": {
+ "masonry": "Mauerwerk",
+ "grid": "Raster",
+ "list": "Liste",
+ "compact": "Kompakt"
+ },
+ "actions": {
+ "change_layout": "Layout ändern",
+ "archive": "Archivieren",
+ "unarchive": "Archivierung aufheben",
+ "favorite": "Favorit",
+ "unfavorite": "Favorit entfernen",
+ "delete": "Löschen",
+ "refresh": "Aktualisieren",
+ "download_full_page_archive": "Vollständiges Seitenarchiv herunterladen",
+ "edit_tags": "Tags bearbeiten",
+ "add_to_list": "Zur Liste hinzufügen",
+ "select_all": "Alle auswählen",
+ "unselect_all": "Auswahl aufheben",
+ "copy_link": "Link kopieren",
+ "close_bulk_edit": "Massenbearbeitung schließen",
+ "bulk_edit": "Massenbearbeitung",
+ "manage_lists": "Listen verwalten",
+ "remove_from_list": "Aus Liste entfernen",
+ "save": "Speichern",
+ "add": "Hinzufügen",
+ "edit": "Bearbeiten",
+ "create": "Erstellen",
+ "fetch_now": "Jetzt abrufen",
+ "summarize_with_ai": "Mit KI zusammenfassen",
+ "edit_title": "Titel bearbeiten",
+ "sign_out": "Abmelden",
+ "close": "Schließen",
+ "merge": "Zusammenführen",
+ "cancel": "Abbrechen",
+ "apply_all": "Alle anwenden",
+ "ignore": "Ignorieren",
+ "recrawl": "Erneutes Crawlen"
+ },
+ "settings": {
+ "back_to_app": "Zurück zur App",
+ "user_settings": "Benutzereinstellungen",
+ "info": {
+ "user_info": "Benutzerinformationen",
+ "basic_details": "Grundlegende Details",
+ "change_password": "Passwort ändern",
+ "current_password": "Aktuelles Passwort",
+ "new_password": "Neues Passwort",
+ "confirm_new_password": "Neues Passwort bestätigen",
+ "options": "Optionen",
+ "interface_lang": "Oberflächensprache"
},
- "actions": {
- "change_layout": "Layout ändern",
- "archive": "Archivieren",
- "unarchive": "Archivierung aufheben",
- "favorite": "Favorit",
- "unfavorite": "Favorit entfernen",
- "delete": "Löschen",
- "refresh": "Aktualisieren",
- "download_full_page_archive": "Vollständiges Seitenarchiv herunterladen",
- "edit_tags": "Tags bearbeiten",
- "add_to_list": "Zur Liste hinzufügen",
- "select_all": "Alle auswählen",
- "unselect_all": "Auswahl aufheben",
- "copy_link": "Link kopieren",
- "close_bulk_edit": "Massenbearbeitung schließen",
- "bulk_edit": "Massenbearbeitung",
- "manage_lists": "Listen verwalten",
- "remove_from_list": "Aus Liste entfernen",
- "save": "Speichern",
- "add": "Hinzufügen",
- "edit": "Bearbeiten",
- "create": "Erstellen",
- "fetch_now": "Jetzt abrufen",
- "summarize_with_ai": "Mit KI zusammenfassen",
- "edit_title": "Titel bearbeiten",
- "sign_out": "Abmelden",
- "close": "Schließen",
- "merge": "Zusammenführen",
- "cancel": "Abbrechen",
- "apply_all": "Alle anwenden",
- "ignore": "Ignorieren",
- "recrawl": "Erneutes Crawlen"
- },
- "settings": {
- "back_to_app": "Zurück zur App",
- "user_settings": "Benutzereinstellungen",
- "info": {
- "user_info": "Benutzerinformationen",
- "basic_details": "Grundlegende Details",
- "change_password": "Passwort ändern",
- "current_password": "Aktuelles Passwort",
- "new_password": "Neues Passwort",
- "confirm_new_password": "Neues Passwort bestätigen",
- "options": "Optionen",
- "interface_lang": "Oberflächensprache"
- },
- "ai": {
- "ai_settings": "KI-Einstellungen",
- "tagging_rules": "Tagging-Regeln",
- "tagging_rule_description": "Eingabeaufforderungen, die Sie hier hinzufügen, werden als Regeln für das Modell bei der Tag-Generierung verwendet. Sie können die endgültigen Aufforderungen im Bereich Vorschau anzeigen.",
- "prompt_preview": "Aufforderungsvorschau",
- "text_prompt": "Text-Aufforderung",
- "images_prompt": "Bild-Aufforderung"
- },
- "feeds": {
- "rss_subscriptions": "RSS-Abonnements",
- "add_a_subscription": "Ein Abonnement hinzufügen"
- },
- "import": {
- "import_export": "Import / Export",
- "import_export_bookmarks": "Lesezeichen importieren / exportieren",
- "import_bookmarks_from_html_file": "Lesezeichen aus HTML-Datei importieren",
- "import_bookmarks_from_pocket_export": "Lesezeichen aus Pocket-Export importieren",
- "import_bookmarks_from_omnivore_export": "Lesezeichen aus Omnivore-Export importieren",
- "import_bookmarks_from_hoarder_export": "Lesezeichen aus Hoarder-Export importieren",
- "export_links_and_notes": "Links und Notizen exportieren",
- "imported_bookmarks": "Importierte Lesezeichen"
- },
- "api_keys": {
- "api_keys": "API-Schlüssel",
- "new_api_key": "Neuer API-Schlüssel",
- "new_api_key_desc": "Geben Sie Ihrem API-Schlüssel einen eindeutigen Namen",
- "key_success": "Schlüssel wurde erfolgreich erstellt",
- "key_success_please_copy": "Bitte kopieren Sie den Schlüssel und speichern Sie ihn an einem sicheren Ort. Sobald Sie das Dialogfeld schließen, können Sie nicht mehr darauf zugreifen."
- },
- "broken_links": {
- "broken_links": "Defekter Link",
- "last_crawled_at": "Zuletzt gecrawlt am",
- "crawling_status": "Crawling-Status",
- "crawling_failed": "Crawling fehlgeschlagen"
- }
+ "ai": {
+ "ai_settings": "KI-Einstellungen",
+ "tagging_rules": "Tagging-Regeln",
+ "tagging_rule_description": "Eingabeaufforderungen, die Sie hier hinzufügen, werden als Regeln für das Modell bei der Tag-Generierung verwendet. Sie können die endgültigen Aufforderungen im Bereich Vorschau anzeigen.",
+ "prompt_preview": "Aufforderungsvorschau",
+ "text_prompt": "Text-Aufforderung",
+ "images_prompt": "Bild-Aufforderung"
},
- "admin": {
- "admin_settings": "Admin-Einstellungen",
- "server_stats": {
- "server_stats": "Serverstatistiken",
- "total_users": "Gesamte Benutzer",
- "total_bookmarks": "Gesamte Lesezeichen",
- "server_version": "Serverversion"
- },
- "background_jobs": {
- "background_jobs": "Hintergrundaufgaben",
- "crawler_jobs": "Crawler-Aufgaben",
- "indexing_jobs": "Indexierungsaufgaben",
- "inference_jobs": "Inferenzaufgaben",
- "tidy_assets_jobs": "Aufräumaufgaben",
- "job": "Aufgabe",
- "queued": "Warteschlange",
- "pending": "Ausstehend",
- "failed": "Fehlgeschlagen"
- },
- "actions": {
- "recrawl_failed_links_only": "Nur fehlgeschlagene Links erneut durchsuchen",
- "recrawl_all_links": "Alle Links erneut durchsuchen",
- "without_inference": "Ohne Inferenz",
- "regenerate_ai_tags_for_failed_bookmarks_only": "KI-Tags nur für fehlgeschlagene Lesezeichen neu generieren",
- "regenerate_ai_tags_for_all_bookmarks": "KI-Tags für alle Lesezeichen neu generieren",
- "reindex_all_bookmarks": "Alle Lesezeichen neu indizieren",
- "compact_assets": "Assets komprimieren"
- },
- "users_list": {
- "users_list": "Benutzerliste",
- "create_user": "Benutzer erstellen",
- "change_role": "Rolle ändern",
- "reset_password": "Passwort zurücksetzen",
- "delete_user": "Benutzer löschen",
- "num_bookmarks": "Anzahl der Lesezeichen",
- "asset_sizes": "Asset-Größen",
- "local_user": "Lokaler Benutzer",
- "confirm_password": "Passwort bestätigen"
- }
+ "feeds": {
+ "rss_subscriptions": "RSS-Abonnements",
+ "add_a_subscription": "Ein Abonnement hinzufügen"
},
- "options": {
- "dark_mode": "Dunkelmodus",
- "light_mode": "Hellmodus"
+ "import": {
+ "import_export": "Import / Export",
+ "import_export_bookmarks": "Lesezeichen importieren / exportieren",
+ "import_bookmarks_from_html_file": "Lesezeichen aus HTML-Datei importieren",
+ "import_bookmarks_from_pocket_export": "Lesezeichen aus Pocket-Export importieren",
+ "import_bookmarks_from_omnivore_export": "Lesezeichen aus Omnivore-Export importieren",
+ "import_bookmarks_from_hoarder_export": "Lesezeichen aus Hoarder-Export importieren",
+ "export_links_and_notes": "Links und Notizen exportieren",
+ "imported_bookmarks": "Importierte Lesezeichen"
},
- "lists": {
- "all_lists": "Alle Listen",
- "favourites": "Favoriten",
- "new_list": "Neue Liste",
- "new_nested_list": "Neue verschachtelte Liste"
+ "api_keys": {
+ "api_keys": "API-Schlüssel",
+ "new_api_key": "Neuer API-Schlüssel",
+ "new_api_key_desc": "Geben Sie Ihrem API-Schlüssel einen eindeutigen Namen",
+ "key_success": "Schlüssel wurde erfolgreich erstellt",
+ "key_success_please_copy": "Bitte kopieren Sie den Schlüssel und speichern Sie ihn an einem sicheren Ort. Sobald Sie das Dialogfeld schließen, können Sie nicht mehr darauf zugreifen."
},
- "tags": {
- "all_tags": "Alle Tags",
- "your_tags": "Ihre Tags",
- "your_tags_info": "Tags, die Sie mindestens einmal angehängt haben",
- "ai_tags": "KI-Tags",
- "ai_tags_info": "Tags, die nur automatisch (von der KI) angehängt wurden",
- "unused_tags": "Ungeliebte Tags",
- "unused_tags_info": "Tags, die an keine Lesezeichen angehängt sind",
- "delete_all_unused_tags": "Alle ungeliebten Tags löschen",
- "drag_and_drop_merging": "Ziehen & Ablegen zum Zusammenführen",
- "drag_and_drop_merging_info": "Ziehen und ablegen, um Tags zusammenzuführen",
- "sort_by_name": "Nach Name sortieren"
+ "broken_links": {
+ "broken_links": "Defekter Link",
+ "last_crawled_at": "Zuletzt gecrawlt am",
+ "crawling_status": "Crawling-Status",
+ "crawling_failed": "Crawling fehlgeschlagen"
+ }
+ },
+ "admin": {
+ "admin_settings": "Admin-Einstellungen",
+ "server_stats": {
+ "server_stats": "Serverstatistiken",
+ "total_users": "Gesamte Benutzer",
+ "total_bookmarks": "Gesamte Lesezeichen",
+ "server_version": "Serverversion"
},
- "preview": {
- "view_original": "Original anzeigen",
- "cached_content": "Gecachte Inhalte"
+ "background_jobs": {
+ "background_jobs": "Hintergrundaufgaben",
+ "crawler_jobs": "Crawler-Aufgaben",
+ "indexing_jobs": "Indexierungsaufgaben",
+ "inference_jobs": "Inferenzaufgaben",
+ "tidy_assets_jobs": "Aufräumaufgaben",
+ "job": "Aufgabe",
+ "queued": "Warteschlange",
+ "pending": "Ausstehend",
+ "failed": "Fehlgeschlagen"
},
- "editor": {
- "quickly_focus": "Sie können schnell auf dieses Feld fokussieren, indem Sie ⌘ + E drücken",
- "multiple_urls_dialog_title": "URLs als separate Lesezeichen importieren?",
- "multiple_urls_dialog_desc": "Die Eingabe enthält mehrere URLs in separaten Zeilen. Möchten Sie sie als separate Lesezeichen importieren?",
- "import_as_text": "Als Text-Lesezeichen importieren",
- "import_as_separate_bookmarks": "Als separate Lesezeichen importieren",
- "placeholder": "Fügen Sie einen Link oder ein Bild ein, schreiben Sie eine Notiz oder ziehen Sie ein Bild hierher ...",
- "new_item": "NEUER EINTRAG",
- "disabled_submissions": "Einsendungen sind deaktiviert"
+ "actions": {
+ "recrawl_failed_links_only": "Nur fehlgeschlagene Links erneut durchsuchen",
+ "recrawl_all_links": "Alle Links erneut durchsuchen",
+ "without_inference": "Ohne Inferenz",
+ "regenerate_ai_tags_for_failed_bookmarks_only": "KI-Tags nur für fehlgeschlagene Lesezeichen neu generieren",
+ "regenerate_ai_tags_for_all_bookmarks": "KI-Tags für alle Lesezeichen neu generieren",
+ "reindex_all_bookmarks": "Alle Lesezeichen neu indizieren",
+ "compact_assets": "Assets komprimieren"
},
- "toasts": {
- "bookmarks": {
- "updated": "Das Lesezeichen wurde aktualisiert!",
- "deleted": "Das Lesezeichen wurde gelöscht!",
- "refetch": "Neuabruf wurde in die Warteschlange gestellt!",
- "full_page_archive": "Erstellung des vollständigen Seitenarchivs wurde ausgelöst",
- "delete_from_list": "Das Lesezeichen wurde aus der Liste gelöscht",
- "clipboard_copied": "Link wurde in Ihre Zwischenablage kopiert!"
- },
- "lists": {
- "created": "Liste wurde erstellt!",
- "updated": "Liste wurde aktualisiert!"
- }
+ "users_list": {
+ "users_list": "Benutzerliste",
+ "create_user": "Benutzer erstellen",
+ "change_role": "Rolle ändern",
+ "reset_password": "Passwort zurücksetzen",
+ "delete_user": "Benutzer löschen",
+ "num_bookmarks": "Anzahl der Lesezeichen",
+ "asset_sizes": "Asset-Größen",
+ "local_user": "Lokaler Benutzer",
+ "confirm_password": "Passwort bestätigen"
+ }
+ },
+ "options": {
+ "dark_mode": "Dunkelmodus",
+ "light_mode": "Hellmodus"
+ },
+ "lists": {
+ "all_lists": "Alle Listen",
+ "favourites": "Favoriten",
+ "new_list": "Neue Liste",
+ "new_nested_list": "Neue verschachtelte Liste"
+ },
+ "tags": {
+ "all_tags": "Alle Tags",
+ "your_tags": "Ihre Tags",
+ "your_tags_info": "Tags, die Sie mindestens einmal angehängt haben",
+ "ai_tags": "KI-Tags",
+ "ai_tags_info": "Tags, die nur automatisch (von der KI) angehängt wurden",
+ "unused_tags": "Ungeliebte Tags",
+ "unused_tags_info": "Tags, die an keine Lesezeichen angehängt sind",
+ "delete_all_unused_tags": "Alle ungeliebten Tags löschen",
+ "drag_and_drop_merging": "Ziehen & Ablegen zum Zusammenführen",
+ "drag_and_drop_merging_info": "Ziehen und ablegen, um Tags zusammenzuführen",
+ "sort_by_name": "Nach Name sortieren"
+ },
+ "preview": {
+ "view_original": "Original anzeigen",
+ "cached_content": "Gecachte Inhalte"
+ },
+ "editor": {
+ "quickly_focus": "Sie können schnell auf dieses Feld fokussieren, indem Sie ⌘ + E drücken",
+ "multiple_urls_dialog_title": "URLs als separate Lesezeichen importieren?",
+ "multiple_urls_dialog_desc": "Die Eingabe enthält mehrere URLs in separaten Zeilen. Möchten Sie sie als separate Lesezeichen importieren?",
+ "import_as_text": "Als Text-Lesezeichen importieren",
+ "import_as_separate_bookmarks": "Als separate Lesezeichen importieren",
+ "placeholder": "Fügen Sie einen Link oder ein Bild ein, schreiben Sie eine Notiz oder ziehen Sie ein Bild hierher ...",
+ "new_item": "NEUER EINTRAG",
+ "disabled_submissions": "Einsendungen sind deaktiviert"
+ },
+ "toasts": {
+ "bookmarks": {
+ "updated": "Das Lesezeichen wurde aktualisiert!",
+ "deleted": "Das Lesezeichen wurde gelöscht!",
+ "refetch": "Neuabruf wurde in die Warteschlange gestellt!",
+ "full_page_archive": "Erstellung des vollständigen Seitenarchivs wurde ausgelöst",
+ "delete_from_list": "Das Lesezeichen wurde aus der Liste gelöscht",
+ "clipboard_copied": "Link wurde in Ihre Zwischenablage kopiert!"
},
- "cleanups": {
- "cleanups": "Bereinigungen",
- "duplicate_tags": {
- "title": "Doppelte Tags",
- "merge_all_suggestions": "Alle Vorschläge zusammenführen?"
- }
+ "lists": {
+ "created": "Liste wurde erstellt!",
+ "updated": "Liste wurde aktualisiert!"
+ }
+ },
+ "cleanups": {
+ "cleanups": "Bereinigungen",
+ "duplicate_tags": {
+ "title": "Doppelte Tags",
+ "merge_all_suggestions": "Alle Vorschläge zusammenführen?"
}
+ }
}
diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json
index 9f12487f..a871489a 100644
--- a/apps/web/lib/i18n/locales/en/translation.json
+++ b/apps/web/lib/i18n/locales/en/translation.json
@@ -187,7 +187,55 @@
"import_as_separate_bookmarks": "Import as separate Bookmarks",
"placeholder": "Paste a link or an image, write a note or drag and drop an image in here ...",
"new_item": "NEW ITEM",
- "disabled_submissions": "Submissions are disabled"
+ "disabled_submissions": "Submissions are disabled",
+ "text_toolbar": {
+ "undo": "Undo",
+ "redo": "Redo",
+ "bold": "Bold",
+ "italic": "Italic",
+ "underline": "Underline",
+ "strikethrough": "Strikethrough",
+ "code": "Code",
+ "highlight": "Highlight",
+ "align_left": "Left Align",
+ "align_center": "Center Align",
+ "align_right": "Right Align",
+ "markdown_shortcuts": {
+ "label": "Markdown shortcuts",
+ "heading": {
+ "label": "Heading",
+ "example": "# H1, ## H2, ### H3"
+ },
+ "bold": {
+ "label": "Bold",
+ "example": "**text** or CTRL+b"
+ },
+ "italic": {
+ "label": "Italic",
+ "example": "*Italic* or _Italic_ or CTRL+i"
+ },
+ "blockquote": {
+ "label": "Blockquote",
+ "example": "> Blockquote"
+ },
+ "ordered_list": {
+ "label": "Ordered List",
+ "example": "1. List item"
+ },
+ "unordered_list": {
+ "label": "Unordered List",
+ "example": "- List item"
+ },
+ "inline_code": {
+ "label": "Inline Code",
+ "example": "`Code`"
+ },
+ "block_code": {
+ "label": "Block Code",
+ "example": "``` + space"
+ }
+ }
+ }
},
"toasts": {
"bookmarks": {
diff --git a/apps/web/lib/i18n/locales/fr/translation.json b/apps/web/lib/i18n/locales/fr/translation.json
index d369ad44..142e9148 100644
--- a/apps/web/lib/i18n/locales/fr/translation.json
+++ b/apps/web/lib/i18n/locales/fr/translation.json
@@ -180,7 +180,20 @@
"import_as_separate_bookmarks": "Importer comme favoris séparés",
"placeholder": "Collez un lien ou une image, écrivez une note ou glissez-déposez une image ici ...",
"new_item": "NOUVEL ÉLÉMENT",
- "disabled_submissions": "Les soumissions sont désactivées"
+ "disabled_submissions": "Les soumissions sont désactivées",
+ "text_toolbar": {
+ "undo": "Annuler",
+ "redo": "Rétablir",
+ "bold": "Gras",
+ "italic": "Italique",
+ "underline": "Souligné",
+ "strikethrough": "Barré",
+ "code": "Code",
+ "highlight": "Surligner",
+ "align_left": "Aligner à gauche",
+ "align_center": "Aligner au centre",
+ "align_right": "Aligner à droite"
+ }
},
"toasts": {
"bookmarks": {
diff --git a/apps/web/lib/i18n/locales/nl/translation.json b/apps/web/lib/i18n/locales/nl/translation.json
index 664bbdc3..c835fdcd 100644
--- a/apps/web/lib/i18n/locales/nl/translation.json
+++ b/apps/web/lib/i18n/locales/nl/translation.json
@@ -1,58 +1,58 @@
{
- "common": {
- "created_at": "Gemaakt op",
- "archive": "Archiveer",
- "url": "URL",
- "name": "Naam",
- "email": "Email",
- "actions": "Acties",
- "role": "Rol",
- "roles": {
- "user": "Gebruiker",
- "admin": "Admin"
- },
- "something_went_wrong": "Er is iets fout gegaagn",
- "experimental": "Experimenteel",
- "search": "Zoeken",
- "tags": "Tags",
- "note": "Notitie",
- "attachments": "Bijlagen",
- "screenshot": "Schermopname",
- "video": "Video",
- "home": "Home",
- "password": "Wachtwoord",
- "action": "Actie",
- "key": "Sleutel"
+ "common": {
+ "created_at": "Gemaakt op",
+ "archive": "Archiveer",
+ "url": "URL",
+ "name": "Naam",
+ "email": "Email",
+ "actions": "Acties",
+ "role": "Rol",
+ "roles": {
+ "user": "Gebruiker",
+ "admin": "Admin"
},
- "layouts": {
- "list": "Lijst",
- "masonry": "Metselwerk",
- "grid": "Rooster",
- "compact": "Compact"
- },
- "actions": {
- "close_bulk_edit": "Bulkbewerking sluiten",
- "delete": "Wissen",
- "change_layout": "Verander Layout",
- "archive": "Archiveer",
- "unarchive": "Verwijderen uit het archief",
- "unfavorite": "Verwijder uit favoriet",
- "refresh": "Verversen",
- "download_full_page_archive": "Volledige pagina archief downloaden",
- "edit_tags": "Labels bewerken",
- "add_to_list": "Toevoegen aan lijst",
- "select_all": "Selecteer alles",
- "unselect_all": "Deselecteer alles",
- "bulk_edit": "Bulkbewerking",
- "remove_from_list": "Verwijderen uit lijst",
- "add": "Toevoegen",
- "edit": "Wijzigen",
- "create": "Creëer",
- "fetch_now": "Nu ophalen",
- "summarize_with_ai": "Samenvatten met AI",
- "manage_lists": "Lijsten beheren",
- "favorite": "Favoriet",
- "copy_link": "Kopieer link",
- "save": "Bewaar"
- }
+ "something_went_wrong": "Er is iets fout gegaagn",
+ "experimental": "Experimenteel",
+ "search": "Zoeken",
+ "tags": "Tags",
+ "note": "Notitie",
+ "attachments": "Bijlagen",
+ "screenshot": "Schermopname",
+ "video": "Video",
+ "home": "Home",
+ "password": "Wachtwoord",
+ "action": "Actie",
+ "key": "Sleutel"
+ },
+ "layouts": {
+ "list": "Lijst",
+ "masonry": "Metselwerk",
+ "grid": "Rooster",
+ "compact": "Compact"
+ },
+ "actions": {
+ "close_bulk_edit": "Bulkbewerking sluiten",
+ "delete": "Wissen",
+ "change_layout": "Verander Layout",
+ "archive": "Archiveer",
+ "unarchive": "Verwijderen uit het archief",
+ "unfavorite": "Verwijder uit favoriet",
+ "refresh": "Verversen",
+ "download_full_page_archive": "Volledige pagina archief downloaden",
+ "edit_tags": "Labels bewerken",
+ "add_to_list": "Toevoegen aan lijst",
+ "select_all": "Selecteer alles",
+ "unselect_all": "Deselecteer alles",
+ "bulk_edit": "Bulkbewerking",
+ "remove_from_list": "Verwijderen uit lijst",
+ "add": "Toevoegen",
+ "edit": "Wijzigen",
+ "create": "Creëer",
+ "fetch_now": "Nu ophalen",
+ "summarize_with_ai": "Samenvatten met AI",
+ "manage_lists": "Lijsten beheren",
+ "favorite": "Favoriet",
+ "copy_link": "Kopieer link",
+ "save": "Bewaar"
+ }
}
diff --git a/apps/web/lib/i18n/locales/sv/translation.json b/apps/web/lib/i18n/locales/sv/translation.json
index 52e3bb68..9adf3bec 100644
--- a/apps/web/lib/i18n/locales/sv/translation.json
+++ b/apps/web/lib/i18n/locales/sv/translation.json
@@ -1,177 +1,177 @@
{
- "common": {
- "url": "URL",
- "email": "Email",
- "password": "Lösenord",
- "created_at": "Skapad den",
- "key": "Nyckel",
- "action": "Åtgärd",
- "actions": "Åtgärder",
- "experimental": "Experimentell",
- "attachments": "Bilagor",
- "screenshot": "Skärmdump",
- "roles": {
- "admin": "Admin",
- "user": "Användare"
- },
- "note": "Anteckning",
- "video": "Video",
- "archive": "Arkiv",
- "name": "Namn",
- "role": "Roll",
- "something_went_wrong": "Något gick fel",
- "search": "Sök",
- "tags": "Taggar",
- "home": "Hem"
+ "common": {
+ "url": "URL",
+ "email": "Email",
+ "password": "Lösenord",
+ "created_at": "Skapad den",
+ "key": "Nyckel",
+ "action": "Åtgärd",
+ "actions": "Åtgärder",
+ "experimental": "Experimentell",
+ "attachments": "Bilagor",
+ "screenshot": "Skärmdump",
+ "roles": {
+ "admin": "Admin",
+ "user": "Användare"
},
- "layouts": {
- "grid": "Rutnät",
- "compact": "Kompakt",
- "list": "Lista"
+ "note": "Anteckning",
+ "video": "Video",
+ "archive": "Arkiv",
+ "name": "Namn",
+ "role": "Roll",
+ "something_went_wrong": "Något gick fel",
+ "search": "Sök",
+ "tags": "Taggar",
+ "home": "Hem"
+ },
+ "layouts": {
+ "grid": "Rutnät",
+ "compact": "Kompakt",
+ "list": "Lista"
+ },
+ "actions": {
+ "delete": "Radera",
+ "archive": "Arkivera",
+ "unarchive": "Avarkivera",
+ "add_to_list": "Lägg till i lista",
+ "download_full_page_archive": "Ladda ner fullsidesarkiv",
+ "select_all": "Markera allt",
+ "manage_lists": "Hantera listor",
+ "remove_from_list": "Ta bort från lista",
+ "unselect_all": "Avmarkera allt",
+ "add": "Lägg till",
+ "fetch_now": "Hämta nu",
+ "edit": "Ändra",
+ "sign_out": "Logga ut",
+ "close": "Stäng",
+ "edit_title": "Ändra titel",
+ "cancel": "Avbryt",
+ "ignore": "Ignorera",
+ "refresh": "Uppdatera",
+ "favorite": "Favorit",
+ "copy_link": "Kopiera länk",
+ "create": "Skapa",
+ "change_layout": "Andra layout",
+ "edit_tags": "Ändra taggar",
+ "summarize_with_ai": "Sammanfatta med AI",
+ "save": "Spara",
+ "merge": "Sammanfoga"
+ },
+ "settings": {
+ "back_to_app": "Tillbaka till app",
+ "info": {
+ "user_info": "Användarinfo",
+ "basic_details": "Grundläggande detaljer",
+ "interface_lang": "Gränsnittsspråk",
+ "current_password": "Nuvarande lösenord",
+ "confirm_new_password": "Bekräfta nytt lösenord",
+ "change_password": "Ändra lösenord",
+ "new_password": "Nytt lösenord"
},
- "actions": {
- "delete": "Radera",
- "archive": "Arkivera",
- "unarchive": "Avarkivera",
- "add_to_list": "Lägg till i lista",
- "download_full_page_archive": "Ladda ner fullsidesarkiv",
- "select_all": "Markera allt",
- "manage_lists": "Hantera listor",
- "remove_from_list": "Ta bort från lista",
- "unselect_all": "Avmarkera allt",
- "add": "Lägg till",
- "fetch_now": "Hämta nu",
- "edit": "Ändra",
- "sign_out": "Logga ut",
- "close": "Stäng",
- "edit_title": "Ändra titel",
- "cancel": "Avbryt",
- "ignore": "Ignorera",
- "refresh": "Uppdatera",
- "favorite": "Favorit",
- "copy_link": "Kopiera länk",
- "create": "Skapa",
- "change_layout": "Andra layout",
- "edit_tags": "Ändra taggar",
- "summarize_with_ai": "Sammanfatta med AI",
- "save": "Spara",
- "merge": "Sammanfoga"
- },
- "settings": {
- "back_to_app": "Tillbaka till app",
- "info": {
- "user_info": "Användarinfo",
- "basic_details": "Grundläggande detaljer",
- "interface_lang": "Gränsnittsspråk",
- "current_password": "Nuvarande lösenord",
- "confirm_new_password": "Bekräfta nytt lösenord",
- "change_password": "Ändra lösenord",
- "new_password": "Nytt lösenord"
- },
- "feeds": {
- "rss_subscriptions": "RSS-prenumerationer",
- "add_a_subscription": "Lägg till en prenumeration"
- },
- "ai": {
- "images_prompt": "Bildprompt",
- "text_prompt": "Textprompt",
- "tagging_rules": "Taggningsregler",
- "ai_settings": "AI-inställningar"
- },
- "import": {
- "import_export": "Importera / exportera",
- "import_export_bookmarks": "Exportera bokmärken",
- "import_bookmarks_from_omnivore_export": "Importera bokmärken från Omnivore-export",
- "imported_bookmarks": "Importerade bokmärken",
- "import_bookmarks_from_hoarder_export": "Importera bokmärken från Hoarder-export",
- "import_bookmarks_from_html_file": "Importera bokmärken från HTML-fil",
- "import_bookmarks_from_pocket_export": "Importera bokmärken från Pocket-export",
- "export_links_and_notes": "Exportera länkar och anteckningar"
- },
- "api_keys": {
- "api_keys": "API-nycklar",
- "new_api_key": "Ny API-nyckel",
- "new_api_key_desc": "Ge din API-nyckel ett unikt namn",
- "key_success_please_copy": "Kopiera nyckeln och lagra den på en saker plats. Efter att du stängt denna dialogruta kommer su inte att kunna nå den igen."
- },
- "user_settings": "Användarinställningar"
+ "feeds": {
+ "rss_subscriptions": "RSS-prenumerationer",
+ "add_a_subscription": "Lägg till en prenumeration"
},
- "admin": {
- "server_stats": {
- "server_stats": "Serverstatistik",
- "server_version": "Serverversion"
- },
- "admin_settings": "Admin-inställningar",
- "background_jobs": {
- "background_jobs": "Bakgrundsjobb",
- "job": "Jobb",
- "pending": "Pågående",
- "indexing_jobs": "Indexeringsjobb",
- "inference_jobs": "Inferensjobb",
- "queued": "Köade"
- },
- "actions": {
- "reindex_all_bookmarks": "Återindexera alla bokmärken",
- "without_inference": "Utan inferens"
- },
- "users_list": {
- "reset_password": "Återställ lösenord",
- "delete_user": "Radera användare",
- "users_list": "Användarlista",
- "local_user": "Lokal användare",
- "confirm_password": "Bekräfta lösenord",
- "create_user": "Skapa användare",
- "change_role": "Ändra roll"
- }
+ "ai": {
+ "images_prompt": "Bildprompt",
+ "text_prompt": "Textprompt",
+ "tagging_rules": "Taggningsregler",
+ "ai_settings": "AI-inställningar"
},
- "options": {
- "light_mode": "Ljust läge",
- "dark_mode": "Mörkt läge"
+ "import": {
+ "import_export": "Importera / exportera",
+ "import_export_bookmarks": "Exportera bokmärken",
+ "import_bookmarks_from_omnivore_export": "Importera bokmärken från Omnivore-export",
+ "imported_bookmarks": "Importerade bokmärken",
+ "import_bookmarks_from_hoarder_export": "Importera bokmärken från Hoarder-export",
+ "import_bookmarks_from_html_file": "Importera bokmärken från HTML-fil",
+ "import_bookmarks_from_pocket_export": "Importera bokmärken från Pocket-export",
+ "export_links_and_notes": "Exportera länkar och anteckningar"
},
- "lists": {
- "new_nested_list": "Ny nästlad lista",
- "all_lists": "Alla listor",
- "favourites": "Favoriter",
- "new_list": "Ny lista"
+ "api_keys": {
+ "api_keys": "API-nycklar",
+ "new_api_key": "Ny API-nyckel",
+ "new_api_key_desc": "Ge din API-nyckel ett unikt namn",
+ "key_success_please_copy": "Kopiera nyckeln och lagra den på en saker plats. Efter att du stängt denna dialogruta kommer su inte att kunna nå den igen."
},
- "tags": {
- "drag_and_drop_merging_info": "Dra och släpp taggar på varandra för att sammanfoga dem",
- "delete_all_unused_tags": "Radera alla oanvända taggar",
- "all_tags": "Alla taggar",
- "unused_tags": "Oanvända taggar",
- "sort_by_name": "Sortera efter namn",
- "your_tags": "Dina taggar",
- "ai_tags": "AI-taggar"
+ "user_settings": "Användarinställningar"
+ },
+ "admin": {
+ "server_stats": {
+ "server_stats": "Serverstatistik",
+ "server_version": "Serverversion"
},
- "editor": {
- "import_as_separate_bookmarks": "Importera som separata bokmärken",
- "import_as_text": "Importera som textbokmärke",
- "placeholder": "Klistra in en länk eller bild, skriv en anteckning eller dra och släpp en bild här...",
- "new_item": "NY POST",
- "multiple_urls_dialog_title": "Importera URL:er som separata bokmärken?",
- "multiple_urls_dialog_desc": "Inmatningen innehåller flera URL:er på separata rader. Vill du importera dem som separata bokmärken?"
+ "admin_settings": "Admin-inställningar",
+ "background_jobs": {
+ "background_jobs": "Bakgrundsjobb",
+ "job": "Jobb",
+ "pending": "Pågående",
+ "indexing_jobs": "Indexeringsjobb",
+ "inference_jobs": "Inferensjobb",
+ "queued": "Köade"
},
- "toasts": {
- "bookmarks": {
- "updated": "Bokmärket har uppdaters!",
- "full_page_archive": "Skapande av fullsidesarkiv har startats",
- "deleted": "Bokmärket har raderats!",
- "delete_from_list": "Bokmärket har raderats från listan",
- "clipboard_copied": "Länken har lags till i ditt urklipp!"
- },
- "lists": {
- "created": "Listan har skapats!",
- "updated": "Listan har uppdaterats!"
- }
+ "actions": {
+ "reindex_all_bookmarks": "Återindexera alla bokmärken",
+ "without_inference": "Utan inferens"
},
- "cleanups": {
- "duplicate_tags": {
- "title": "Dublettaggar",
- "merge_all_suggestions": "Slå ihop alla förslag?"
- },
- "cleanups": "Rensningar"
+ "users_list": {
+ "reset_password": "Återställ lösenord",
+ "delete_user": "Radera användare",
+ "users_list": "Användarlista",
+ "local_user": "Lokal användare",
+ "confirm_password": "Bekräfta lösenord",
+ "create_user": "Skapa användare",
+ "change_role": "Ändra roll"
+ }
+ },
+ "options": {
+ "light_mode": "Ljust läge",
+ "dark_mode": "Mörkt läge"
+ },
+ "lists": {
+ "new_nested_list": "Ny nästlad lista",
+ "all_lists": "Alla listor",
+ "favourites": "Favoriter",
+ "new_list": "Ny lista"
+ },
+ "tags": {
+ "drag_and_drop_merging_info": "Dra och släpp taggar på varandra för att sammanfoga dem",
+ "delete_all_unused_tags": "Radera alla oanvända taggar",
+ "all_tags": "Alla taggar",
+ "unused_tags": "Oanvända taggar",
+ "sort_by_name": "Sortera efter namn",
+ "your_tags": "Dina taggar",
+ "ai_tags": "AI-taggar"
+ },
+ "editor": {
+ "import_as_separate_bookmarks": "Importera som separata bokmärken",
+ "import_as_text": "Importera som textbokmärke",
+ "placeholder": "Klistra in en länk eller bild, skriv en anteckning eller dra och släpp en bild här...",
+ "new_item": "NY POST",
+ "multiple_urls_dialog_title": "Importera URL:er som separata bokmärken?",
+ "multiple_urls_dialog_desc": "Inmatningen innehåller flera URL:er på separata rader. Vill du importera dem som separata bokmärken?"
+ },
+ "toasts": {
+ "bookmarks": {
+ "updated": "Bokmärket har uppdaters!",
+ "full_page_archive": "Skapande av fullsidesarkiv har startats",
+ "deleted": "Bokmärket har raderats!",
+ "delete_from_list": "Bokmärket har raderats från listan",
+ "clipboard_copied": "Länken har lags till i ditt urklipp!"
},
- "preview": {
- "view_original": "Visa orginal"
+ "lists": {
+ "created": "Listan har skapats!",
+ "updated": "Listan har uppdaterats!"
}
+ },
+ "cleanups": {
+ "duplicate_tags": {
+ "title": "Dublettaggar",
+ "merge_all_suggestions": "Slå ihop alla förslag?"
+ },
+ "cleanups": "Rensningar"
+ },
+ "preview": {
+ "view_original": "Visa orginal"
+ }
}
diff --git a/apps/web/package.json b/apps/web/package.json
index 3affe516..98b3a434 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -23,6 +23,11 @@
"@hoarder/shared-react": "workspace:^0.1.0",
"@hoarder/trpc": "workspace:^0.1.0",
"@hookform/resolvers": "^3.3.4",
+ "@lexical/list": "^0.20.2",
+ "@lexical/markdown": "^0.20.2",
+ "@lexical/plain-text": "^0.20.2",
+ "@lexical/react": "^0.20.2",
+ "@lexical/rich-text": "^0.20.2",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
@@ -33,6 +38,7 @@
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-toggle": "^1.0.3",
@@ -53,13 +59,14 @@
"fastest-levenshtein": "^1.0.16",
"i18next": "^23.16.5",
"i18next-resources-to-backend": "^1.2.1",
+ "lexical": "^0.20.2",
"lucide-react": "^0.330.0",
- "next": "14.2.13",
+ "next": "14.2.15",
"next-auth": "^4.24.5",
"next-i18next": "^15.3.1",
"next-pwa": "^5.6.0",
"next-themes": "^0.3.0",
- "prettier": "^3.2.5",
+ "prettier": "^3.4.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-draggable": "^4.4.6",