aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components/dashboard
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2024-11-17 00:33:28 +0000
committerGitHub <noreply@github.com>2024-11-17 00:33:28 +0000
commit4354ee7ba1c6ac9a9567944ae6169b1664e0ea8a (patch)
treee27c9070930514d77582bae00b3350274116179c /apps/web/components/dashboard
parent9f2c7be23769bb0f4102736a683710b1a1939661 (diff)
downloadkarakeep-4354ee7ba1c6ac9a9567944ae6169b1664e0ea8a.tar.zst
feature: Add i18n support. Fixes #57 (#635)
* feature(web): Add basic scaffolding for i18n * refactor: Switch most of the app's strings to use i18n strings * fix: Remove unused i18next-resources-for-ts command * Add user setting * More translations * Drop the german translation for now
Diffstat (limited to 'apps/web/components/dashboard')
-rw-r--r--apps/web/components/dashboard/BulkBookmarksAction.tsx28
-rw-r--r--apps/web/components/dashboard/ChangeLayout.tsx13
-rw-r--r--apps/web/components/dashboard/EditableText.tsx9
-rw-r--r--apps/web/components/dashboard/admin/AdminActions.tsx19
-rw-r--r--apps/web/components/dashboard/admin/ServerStats.tsx38
-rw-r--r--apps/web/components/dashboard/admin/UserList.tsx30
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx43
-rw-r--r--apps/web/components/dashboard/bookmarks/EditorCard.tsx26
-rw-r--r--apps/web/components/dashboard/bookmarks/ManageListsModal.tsx16
-rw-r--r--apps/web/components/dashboard/bookmarks/SummarizeBookmarkArea.tsx4
-rw-r--r--apps/web/components/dashboard/bookmarks/TagModal.tsx6
-rw-r--r--apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx13
-rw-r--r--apps/web/components/dashboard/header/ProfileOptions.tsx13
-rw-r--r--apps/web/components/dashboard/lists/AllListsView.tsx8
-rw-r--r--apps/web/components/dashboard/lists/EditListModal.tsx14
-rw-r--r--apps/web/components/dashboard/lists/ListOptions.tsx9
-rw-r--r--apps/web/components/dashboard/preview/ActionBar.tsx10
-rw-r--r--apps/web/components/dashboard/preview/AttachmentBox.tsx4
-rw-r--r--apps/web/components/dashboard/preview/BookmarkPreview.tsx8
-rw-r--r--apps/web/components/dashboard/preview/LinkContentSection.tsx12
-rw-r--r--apps/web/components/dashboard/search/SearchInput.tsx4
-rw-r--r--apps/web/components/dashboard/sidebar/AllLists.tsx6
-rw-r--r--apps/web/components/dashboard/sidebar/Sidebar.tsx10
-rw-r--r--apps/web/components/dashboard/tags/AllTagsView.tsx25
-rw-r--r--apps/web/components/dashboard/tags/TagOptions.tsx6
25 files changed, 226 insertions, 148 deletions
diff --git a/apps/web/components/dashboard/BulkBookmarksAction.tsx b/apps/web/components/dashboard/BulkBookmarksAction.tsx
index c78b65db..eb0e0cec 100644
--- a/apps/web/components/dashboard/BulkBookmarksAction.tsx
+++ b/apps/web/components/dashboard/BulkBookmarksAction.tsx
@@ -9,6 +9,7 @@ import {
import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog";
import { useToast } from "@/components/ui/use-toast";
import useBulkActionsStore from "@/lib/bulkActions";
+import { useTranslation } from "@/lib/i18n/client";
import {
CheckCheck,
FileDown,
@@ -33,6 +34,7 @@ import BulkTagModal from "./bookmarks/BulkTagModal";
import { ArchivedActionIcon, FavouritedActionIcon } from "./bookmarks/icons";
export default function BulkBookmarksAction() {
+ const { t } = useTranslation();
const { selectedBookmarks, isBulkEditEnabled } = useBulkActionsStore();
const setIsBulkEditEnabled = useBulkActionsStore(
(state) => state.setIsBulkEditEnabled,
@@ -179,7 +181,7 @@ export default function BulkBookmarksAction() {
const actionList = [
{
name: isClipboardAvailable()
- ? "Copy Links"
+ ? t("actions.copy_link")
: "Copying is only available over https",
icon: <Link size={18} />,
action: () => copyLinks(),
@@ -187,55 +189,57 @@ export default function BulkBookmarksAction() {
hidden: !isBulkEditEnabled,
},
{
- name: "Add to List",
+ name: t("actions.add_to_list"),
icon: <List size={18} />,
action: () => setManageListsModalOpen(true),
isPending: false,
hidden: !isBulkEditEnabled,
},
{
- name: "Edit Tags",
+ name: t("actions.edit_tags"),
icon: <Hash size={18} />,
action: () => setBulkTagModalOpen(true),
isPending: false,
hidden: !isBulkEditEnabled,
},
{
- name: alreadyFavourited ? "Unfavourite" : "Favourite",
+ name: alreadyFavourited ? t("actions.unfavorite") : t("actions.favorite"),
icon: <FavouritedActionIcon favourited={!!alreadyFavourited} size={18} />,
action: () => updateBookmarks({ favourited: !alreadyFavourited }),
isPending: updateBookmarkMutator.isPending,
hidden: !isBulkEditEnabled,
},
{
- name: alreadyArchived ? "Un-archive" : "Archive",
+ name: alreadyArchived ? t("actions.unarchive") : t("actions.archive"),
icon: <ArchivedActionIcon size={18} archived={!!alreadyArchived} />,
action: () => updateBookmarks({ archived: !alreadyArchived }),
isPending: updateBookmarkMutator.isPending,
hidden: !isBulkEditEnabled,
},
{
- name: "Download Full Page Archive",
+ name: t("actions.download_full_page_archive"),
icon: <FileDown size={18} />,
action: () => recrawlBookmarks(true),
isPending: recrawlBookmarkMutator.isPending,
hidden: !isBulkEditEnabled,
},
{
- name: "Refresh",
+ name: t("actions.refresh"),
icon: <RotateCw size={18} />,
action: () => recrawlBookmarks(false),
isPending: recrawlBookmarkMutator.isPending,
hidden: !isBulkEditEnabled,
},
{
- name: "Delete",
+ name: t("actions.delete"),
icon: <Trash2 size={18} color="red" />,
action: () => setIsDeleteDialogOpen(true),
hidden: !isBulkEditEnabled,
},
{
- name: isEverythingSelected() ? "Unselect All" : "Select All",
+ name: isEverythingSelected()
+ ? t("actions.unselect_all")
+ : t("actions.select_all"),
icon: (
<p className="flex items-center gap-2">
( <CheckCheck size={18} /> {selectedBookmarks.length} )
@@ -247,14 +251,14 @@ export default function BulkBookmarksAction() {
hidden: !isBulkEditEnabled,
},
{
- name: "Close bulk edit",
+ name: t("actions.close_bulk_edit"),
icon: <X size={18} />,
action: () => setIsBulkEditEnabled(false),
alwaysEnable: true,
hidden: !isBulkEditEnabled,
},
{
- name: "Bulk Edit",
+ name: t("actions.bulk_edit"),
icon: <Pencil size={18} />,
action: () => setIsBulkEditEnabled(true),
alwaysEnable: true,
@@ -276,7 +280,7 @@ export default function BulkBookmarksAction() {
loading={deleteBookmarkMutator.isPending}
onClick={() => deleteBookmarks()}
>
- Delete
+ {t("actions.delete")}
</ActionButton>
)}
/>
diff --git a/apps/web/components/dashboard/ChangeLayout.tsx b/apps/web/components/dashboard/ChangeLayout.tsx
index 6ec38245..c7f44a73 100644
--- a/apps/web/components/dashboard/ChangeLayout.tsx
+++ b/apps/web/components/dashboard/ChangeLayout.tsx
@@ -8,6 +8,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
+import { useTranslation } from "@/lib/i18n/client";
import { useBookmarkLayout } from "@/lib/userLocalSettings/bookmarksLayout";
import { updateBookmarksLayout } from "@/lib/userLocalSettings/userLocalSettings";
import {
@@ -16,11 +17,12 @@ import {
LayoutGrid,
LayoutList,
List,
+ LucideIcon,
} from "lucide-react";
-type LayoutType = "masonry" | "grid" | "list";
+type LayoutType = "masonry" | "grid" | "list" | "compact";
-const iconMap = {
+const iconMap: Record<LayoutType, LucideIcon> = {
masonry: LayoutDashboard,
grid: LayoutGrid,
list: LayoutList,
@@ -28,13 +30,14 @@ const iconMap = {
};
export default function ChangeLayout() {
+ const { t } = useTranslation();
const layout = useBookmarkLayout();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonWithTooltip
- tooltip="Change layout"
+ tooltip={t("actions.change_layout")}
delayDuration={100}
variant="ghost"
>
@@ -42,7 +45,7 @@ export default function ChangeLayout() {
</ButtonWithTooltip>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-fit">
- {Object.keys(iconMap).map((key) => (
+ {(Object.keys(iconMap) as LayoutType[]).map((key) => (
<DropdownMenuItem
key={key}
className="cursor-pointer justify-between"
@@ -50,7 +53,7 @@ export default function ChangeLayout() {
>
<div className="flex items-center gap-2">
{React.createElement(iconMap[key as LayoutType], { size: 18 })}
- <span className="capitalize">{key}</span>
+ <span>{t(`layouts.${key}`)}</span>
</div>
{layout == key && <Check className="ml-2 size-4" />}
</DropdownMenuItem>
diff --git a/apps/web/components/dashboard/EditableText.tsx b/apps/web/components/dashboard/EditableText.tsx
index 55ce10c6..e5027b93 100644
--- a/apps/web/components/dashboard/EditableText.tsx
+++ b/apps/web/components/dashboard/EditableText.tsx
@@ -7,6 +7,7 @@ import {
TooltipPortal,
TooltipTrigger,
} from "@/components/ui/tooltip";
+import { useTranslation } from "@/lib/i18n/client";
import { Check, Pencil, X } from "lucide-react";
interface Props {
@@ -26,6 +27,7 @@ function EditMode({
originalText,
setEditable,
}: Props) {
+ const { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -63,7 +65,7 @@ function EditMode({
}}
/>
<ActionButtonWithTooltip
- tooltip="Save"
+ tooltip={t("actions.save")}
delayDuration={500}
size="none"
variant="ghost"
@@ -74,7 +76,7 @@ function EditMode({
<Check className="size-4" />
</ActionButtonWithTooltip>
<ButtonWithTooltip
- tooltip="Cancel"
+ tooltip={t("actions.cancel")}
delayDuration={500}
size="none"
variant="ghost"
@@ -95,6 +97,7 @@ function ViewMode({
viewClassName,
untitledClassName,
}: Props) {
+ const { t } = useTranslation();
return (
<Tooltip delayDuration={500}>
<div className="flex max-w-full items-center gap-3">
@@ -107,7 +110,7 @@ function ViewMode({
</TooltipTrigger>
<ButtonWithTooltip
delayDuration={500}
- tooltip="Edit title"
+ tooltip={t("actions.edit_title")}
size="none"
variant="ghost"
className="align-middle text-gray-400"
diff --git a/apps/web/components/dashboard/admin/AdminActions.tsx b/apps/web/components/dashboard/admin/AdminActions.tsx
index a97552f8..3b95045c 100644
--- a/apps/web/components/dashboard/admin/AdminActions.tsx
+++ b/apps/web/components/dashboard/admin/AdminActions.tsx
@@ -2,9 +2,11 @@
import { ActionButton } from "@/components/ui/action-button";
import { toast } from "@/components/ui/use-toast";
+import { useTranslation } from "@/lib/i18n/client";
import { api } from "@/lib/trpc";
export default function AdminActions() {
+ const { t } = useTranslation();
const { mutate: recrawlLinks, isPending: isRecrawlPending } =
api.admin.recrawlLinks.useMutation({
onSuccess: () => {
@@ -69,7 +71,7 @@ export default function AdminActions() {
return (
<div>
- <div className="mb-2 mt-8 text-xl font-medium">Actions</div>
+ <div className="mb-2 mt-8 text-xl font-medium">{t("common.actions")}</div>
<div className="flex flex-col gap-2 sm:w-1/2">
<ActionButton
variant="destructive"
@@ -78,7 +80,7 @@ export default function AdminActions() {
recrawlLinks({ crawlStatus: "failure", runInference: true })
}
>
- Recrawl Failed Links Only
+ {t("admin.actions.recrawl_failed_links_only")}
</ActionButton>
<ActionButton
variant="destructive"
@@ -87,7 +89,7 @@ export default function AdminActions() {
recrawlLinks({ crawlStatus: "all", runInference: true })
}
>
- Recrawl All Links
+ {t("admin.actions.recrawl_all_links")}
</ActionButton>
<ActionButton
variant="destructive"
@@ -96,7 +98,8 @@ export default function AdminActions() {
recrawlLinks({ crawlStatus: "all", runInference: false })
}
>
- Recrawl All Links (Without Inference)
+ {t("admin.actions.recrawl_all_links")} (
+ {t("admin.actions.without_inference")})
</ActionButton>
<ActionButton
variant="destructive"
@@ -105,28 +108,28 @@ export default function AdminActions() {
reRunInferenceOnAllBookmarks({ taggingStatus: "failure" })
}
>
- Regenerate AI Tags for Failed Bookmarks Only
+ {t("admin.actions.regenerate_ai_tags_for_failed_bookmarks_only")}
</ActionButton>
<ActionButton
variant="destructive"
loading={isInferencePending}
onClick={() => reRunInferenceOnAllBookmarks({ taggingStatus: "all" })}
>
- Regenerate AI Tags for All Bookmarks
+ {t("admin.actions.regenerate_ai_tags_for_all_bookmarks")}
</ActionButton>
<ActionButton
variant="destructive"
loading={isReindexPending}
onClick={() => reindexBookmarks()}
>
- Reindex All Bookmarks
+ {t("admin.actions.reindex_all_bookmarks")}
</ActionButton>
<ActionButton
variant="destructive"
loading={isTidyAssetsPending}
onClick={() => tidyAssets()}
>
- Compact Assets
+ {t("admin.actions.compact_assets")}
</ActionButton>
</div>
</div>
diff --git a/apps/web/components/dashboard/admin/ServerStats.tsx b/apps/web/components/dashboard/admin/ServerStats.tsx
index f45d86c5..da69390b 100644
--- a/apps/web/components/dashboard/admin/ServerStats.tsx
+++ b/apps/web/components/dashboard/admin/ServerStats.tsx
@@ -10,6 +10,7 @@ import {
TableRow,
} from "@/components/ui/table";
import { useClientConfig } from "@/lib/clientConfig";
+import { useTranslation } from "@/lib/i18n/client";
import { api } from "@/lib/trpc";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
@@ -61,6 +62,7 @@ function ReleaseInfo() {
}
export default function ServerStats() {
+ const { t } = useTranslation();
const { data: serverStats } = api.admin.stats.useQuery(undefined, {
refetchInterval: 1000,
placeholderData: keepPreviousData,
@@ -72,15 +74,19 @@ export default function ServerStats() {
return (
<>
- <div className="mb-2 text-xl font-medium">Server Stats</div>
+ <div className="mb-2 text-xl font-medium">
+ {t("admin.server_stats.server_stats")}
+ </div>
<div className="flex flex-col gap-4 sm:flex-row">
<div className="rounded-md border bg-background p-4 sm:w-1/4">
- <div className="text-sm font-medium text-gray-400">Total Users</div>
+ <div className="text-sm font-medium text-gray-400">
+ {t("admin.server_stats.total_users")}
+ </div>
<div className="text-3xl font-semibold">{serverStats.numUsers}</div>
</div>
<div className="rounded-md border bg-background p-4 sm:w-1/4">
<div className="text-sm font-medium text-gray-400">
- Total Bookmarks
+ {t("admin.server_stats.total_bookmarks")}
</div>
<div className="text-3xl font-semibold">
{serverStats.numBookmarks}
@@ -88,42 +94,48 @@ export default function ServerStats() {
</div>
<div className="rounded-md border bg-background p-4 sm:w-1/4">
<div className="text-sm font-medium text-gray-400">
- Server Version
+ {t("admin.server_stats.server_version")}
</div>
<ReleaseInfo />
</div>
</div>
<div className="sm:w-1/2">
- <div className="mb-2 mt-8 text-xl font-medium">Background Jobs</div>
+ <div className="mb-2 mt-8 text-xl font-medium">
+ {t("admin.background_jobs.background_jobs")}
+ </div>
<Table className="rounded-md border">
<TableHeader className="bg-gray-200">
- <TableHead>Job</TableHead>
- <TableHead>Queued</TableHead>
- <TableHead>Pending</TableHead>
- <TableHead>Failed</TableHead>
+ <TableHead>{t("admin.background_jobs.job")}</TableHead>
+ <TableHead>{t("admin.background_jobs.queued")}</TableHead>
+ <TableHead>{t("admin.background_jobs.pending")}</TableHead>
+ <TableHead>{t("admin.background_jobs.failed")}</TableHead>
</TableHeader>
<TableBody>
<TableRow>
- <TableCell className="lg:w-2/3">Crawling Jobs</TableCell>
+ <TableCell className="lg:w-2/3">
+ {t("admin.background_jobs.crawler_jobs")}
+ </TableCell>
<TableCell>{serverStats.crawlStats.queued}</TableCell>
<TableCell>{serverStats.crawlStats.pending}</TableCell>
<TableCell>{serverStats.crawlStats.failed}</TableCell>
</TableRow>
<TableRow>
- <TableCell>Indexing Jobs</TableCell>
+ <TableCell>{t("admin.background_jobs.indexing_jobs")}</TableCell>
<TableCell>{serverStats.indexingStats.queued}</TableCell>
<TableCell>-</TableCell>
<TableCell>-</TableCell>
</TableRow>
<TableRow>
- <TableCell>Inference Jobs</TableCell>
+ <TableCell>{t("admin.background_jobs.inference_jobs")}</TableCell>
<TableCell>{serverStats.inferenceStats.queued}</TableCell>
<TableCell>{serverStats.inferenceStats.pending}</TableCell>
<TableCell>{serverStats.inferenceStats.failed}</TableCell>
</TableRow>
<TableRow>
- <TableCell>Tidy Assets Jobs</TableCell>
+ <TableCell>
+ {t("admin.background_jobs.tidy_assets_jobs")}
+ </TableCell>
<TableCell>{serverStats.tidyAssetsStats.queued}</TableCell>
<TableCell>-</TableCell>
<TableCell>-</TableCell>
diff --git a/apps/web/components/dashboard/admin/UserList.tsx b/apps/web/components/dashboard/admin/UserList.tsx
index 2937df28..8c788ef4 100644
--- a/apps/web/components/dashboard/admin/UserList.tsx
+++ b/apps/web/components/dashboard/admin/UserList.tsx
@@ -12,6 +12,7 @@ import {
TableRow,
} from "@/components/ui/table";
import { toast } from "@/components/ui/use-toast";
+import { useTranslation } from "@/lib/i18n/client";
import { api } from "@/lib/trpc";
import { Check, KeyRound, Pencil, Trash, UserPlus, X } from "lucide-react";
import { useSession } from "next-auth/react";
@@ -28,6 +29,7 @@ function toHumanReadableSize(size: number) {
}
export default function UsersSection() {
+ const { t } = useTranslation();
const { data: session } = useSession();
const invalidateUserList = api.useUtils().users.list.invalidate;
const { data: users } = api.users.list.useQuery();
@@ -55,7 +57,7 @@ export default function UsersSection() {
return (
<>
<div className="mb-2 flex items-center justify-between text-xl font-medium">
- <span>Users List</span>
+ <span>{t("admin.users_list.users_list")}</span>
<AddUserDialog>
<ButtonWithTooltip tooltip="Create User" variant="outline">
<UserPlus size={16} />
@@ -65,13 +67,13 @@ export default function UsersSection() {
<Table>
<TableHeader className="bg-gray-200">
- <TableHead>Name</TableHead>
- <TableHead>Email</TableHead>
- <TableHead>Num Bookmarks</TableHead>
- <TableHead>Asset Sizes</TableHead>
- <TableHead>Role</TableHead>
- <TableHead>Local User</TableHead>
- <TableHead>Actions</TableHead>
+ <TableHead>{t("common.name")}</TableHead>
+ <TableHead>{t("common.email")}</TableHead>
+ <TableHead>{t("admin.users_list.num_bookmarks")}</TableHead>
+ <TableHead>{t("admin.users_list.asset_sizes")}</TableHead>
+ <TableHead>{t("common.role")}</TableHead>
+ <TableHead>{t("admin.users_list.local_user")}</TableHead>
+ <TableHead>{t("common.actions")}</TableHead>
</TableHeader>
<TableBody>
{users.users.map((u) => (
@@ -84,13 +86,15 @@ export default function UsersSection() {
<TableCell className="py-1">
{toHumanReadableSize(userStats[u.id].assetSizes)}
</TableCell>
- <TableCell className="py-1 capitalize">{u.role}</TableCell>
- <TableCell className="py-1 capitalize">
+ <TableCell className="py-1">
+ {u.role && t(`common.roles.${u.role}`)}
+ </TableCell>
+ <TableCell className="py-1">
{u.localUser ? <Check /> : <X />}
</TableCell>
<TableCell className="flex gap-1 py-1">
<ActionButtonWithTooltip
- tooltip="Delete user"
+ tooltip={t("admin.users_list.delete_user")}
variant="outline"
onClick={() => deleteUser({ userId: u.id })}
loading={isDeletionPending}
@@ -100,7 +104,7 @@ export default function UsersSection() {
</ActionButtonWithTooltip>
<ResetPasswordDialog userId={u.id}>
<ButtonWithTooltip
- tooltip="Reset password"
+ tooltip={t("admin.users_list.reset_password")}
variant="outline"
disabled={session!.user.id == u.id || !u.localUser}
>
@@ -109,7 +113,7 @@ export default function UsersSection() {
</ResetPasswordDialog>
<ChangeRoleDialog userId={u.id} currentRole={u.role!}>
<ButtonWithTooltip
- tooltip="Change role"
+ tooltip={t("admin.users_list.change_role")}
variant="outline"
disabled={session!.user.id == u.id}
>
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx
index c09d2e50..8dfb96fd 100644
--- a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx
+++ b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx
@@ -10,6 +10,7 @@ import {
} from "@/components/ui/dropdown-menu";
import { useToast } from "@/components/ui/use-toast";
import { useClientConfig } from "@/lib/clientConfig";
+import { useTranslation } from "@/lib/i18n/client";
import {
FileDown,
Link,
@@ -41,6 +42,7 @@ import { useManageListsModal } from "./ManageListsModal";
import { useTagModel } from "./TagModal";
export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
+ const { t } = useTranslation();
const { toast } = useToast();
const linkId = bookmark.id;
@@ -58,14 +60,13 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
const onError = () => {
toast({
variant: "destructive",
- title: "Something went wrong",
- description: "There was a problem with your request.",
+ title: t("common.something_went_wrong"),
});
};
const deleteBookmarkMutator = useDeleteBookmark({
onSuccess: () => {
toast({
- description: "The bookmark has been deleted!",
+ description: t("toasts.bookmarks.deleted"),
});
},
onError,
@@ -74,7 +75,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
const updateBookmarkMutator = useUpdateBookmark({
onSuccess: () => {
toast({
- description: "The bookmark has been updated!",
+ description: t("toasts.bookmarks.updated"),
});
},
onError,
@@ -83,7 +84,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
const crawlBookmarkMutator = useRecrawlBookmark({
onSuccess: () => {
toast({
- description: "Re-fetch has been enqueued!",
+ description: t("toasts.bookmarks.refetch"),
});
},
onError,
@@ -92,7 +93,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
const fullPageArchiveBookmarkMutator = useRecrawlBookmark({
onSuccess: () => {
toast({
- description: "Full Page Archive creation has been triggered",
+ description: t("toasts.bookmarks.full_page_archive"),
});
},
onError,
@@ -101,7 +102,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
const removeFromListMutator = useRemoveBookmarkFromList({
onSuccess: () => {
toast({
- description: "The bookmark has been deleted from the list",
+ description: t("toasts.bookmarks.delete_from_list"),
});
},
onError,
@@ -145,7 +146,11 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
className="mr-2 size-4"
favourited={bookmark.favourited}
/>
- <span>{bookmark.favourited ? "Un-favourite" : "Favourite"}</span>
+ <span>
+ {bookmark.favourited
+ ? t("actions.unfavorite")
+ : t("actions.favorite")}
+ </span>
</DropdownMenuItem>
<DropdownMenuItem
disabled={demoMode}
@@ -160,7 +165,11 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
className="mr-2 size-4"
archived={bookmark.archived}
/>
- <span>{bookmark.archived ? "Un-archive" : "Archive"}</span>
+ <span>
+ {bookmark.archived
+ ? t("actions.unarchive")
+ : t("actions.archive")}
+ </span>
</DropdownMenuItem>
{bookmark.content.type === BookmarkTypes.LINK && (
@@ -173,7 +182,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
}}
>
<FileDown className="mr-2 size-4" />
- <span>Download Full Page Archive</span>
+ <span>{t("actions.download_full_page_archive")}</span>
</DropdownMenuItem>
)}
@@ -184,22 +193,22 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
(bookmark.content as ZBookmarkedLink).url,
);
toast({
- description: "Link was added to your clipboard!",
+ description: t("toasts.bookmarks.clipboard_copied"),
});
}}
>
<Link className="mr-2 size-4" />
- <span>Copy Link</span>
+ <span>{t("actions.copy_link")}</span>
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => setTagModalIsOpen(true)}>
<Tags className="mr-2 size-4" />
- <span>Edit Tags</span>
+ <span>{t("actions.edit_tags")}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setManageListsModalOpen(true)}>
<List className="mr-2 size-4" />
- <span>Manage Lists</span>
+ <span>{t("actions.manage_lists")}</span>
</DropdownMenuItem>
{listId && (
@@ -213,7 +222,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
}
>
<ListX className="mr-2 size-4" />
- <span>Remove from List</span>
+ <span>{t("actions.remove_from_list")}</span>
</DropdownMenuItem>
)}
@@ -225,7 +234,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
}
>
<RotateCw className="mr-2 size-4" />
- <span>Refresh</span>
+ <span>{t("actions.refresh")}</span>
</DropdownMenuItem>
)}
<DropdownMenuItem
@@ -236,7 +245,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
}
>
<Trash2 className="mr-2 size-4" />
- <span>Delete</span>
+ <span>{t("actions.delete")}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
diff --git a/apps/web/components/dashboard/bookmarks/EditorCard.tsx b/apps/web/components/dashboard/bookmarks/EditorCard.tsx
index 4d851d1c..cb4bfdce 100644
--- a/apps/web/components/dashboard/bookmarks/EditorCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/EditorCard.tsx
@@ -9,6 +9,7 @@ import { Textarea } from "@/components/ui/textarea";
import { toast } from "@/components/ui/use-toast";
import BookmarkAlreadyExistsToast from "@/components/utils/BookmarkAlreadyExistsToast";
import { useClientConfig } from "@/lib/clientConfig";
+import { useTranslation } from "@/lib/i18n/client";
import {
useBookmarkLayout,
useBookmarkLayoutSwitch,
@@ -48,6 +49,7 @@ interface MultiUrlImportState {
}
export default function EditorCard({ className }: { className?: string }) {
+ const { t } = useTranslation();
const inputRef = useRef<HTMLTextAreaElement>(null);
const [multiUrlImportState, setMultiUrlImportState] =
@@ -181,11 +183,9 @@ export default function EditorCard({ className }: { className?: string }) {
onSubmit={form.handleSubmit(onSubmit, onError)}
>
<div className="flex justify-between">
- <p className="text-sm">NEW ITEM</p>
+ <p className="text-sm">{t("editor.new_item")}</p>
<InfoTooltip size={15}>
- <p className="text-center">
- You can quickly focus on this field by pressing ⌘ + E
- </p>
+ <p className="text-center">{t("editor.quickly_focus")}</p>
</InfoTooltip>
</div>
<Separator />
@@ -198,9 +198,7 @@ export default function EditorCard({ className }: { className?: string }) {
"h-full w-full border-none p-0 text-lg focus-visible:ring-0",
{ "resize-none": bookmarkLayout !== "list" },
)}
- placeholder={
- "Paste a link or an image, write a note or drag and drop an image in here ..."
- }
+ placeholder={t("editor.placeholder")}
onKeyDown={(e) => {
if (demoMode) {
return;
@@ -223,16 +221,16 @@ export default function EditorCard({ className }: { className?: string }) {
<ActionButton loading={isPending} type="submit" variant="default">
{form.formState.dirtyFields.text
? demoMode
- ? "Submissions are disabled"
- : `Save (${OS === "macos" ? "⌘" : "Ctrl"} + Enter)`
- : "Save"}
+ ? t("editor.disabled_submissions")
+ : `${t("actions.save")} (${OS === "macos" ? "⌘" : "Ctrl"} + Enter)`
+ : t("actions.save")}
</ActionButton>
{multiUrlImportState && (
<MultipleChoiceDialog
open={true}
- title={`Import URLs as separate Bookmarks?`}
- description={`The input contains multiple URLs on separate lines. Do you want to import them as separate bookmarks?`}
+ title={t("editor.multiple_urls_dialog_title")}
+ description={t("editor.multiple_urls_dialog_desc")}
onOpenChange={(open) => {
if (!open) {
setMultiUrlImportState(null);
@@ -252,7 +250,7 @@ export default function EditorCard({ className }: { className?: string }) {
setMultiUrlImportState(null);
}}
>
- Import as Text Bookmark
+ {t("editor.import_as_text")}
</ActionButton>
),
() => (
@@ -267,7 +265,7 @@ export default function EditorCard({ className }: { className?: string }) {
setMultiUrlImportState(null);
}}
>
- Import as separate Bookmarks
+ {t("editor.import_as_separate_bookmarks")}
</ActionButton>
),
]}
diff --git a/apps/web/components/dashboard/bookmarks/ManageListsModal.tsx b/apps/web/components/dashboard/bookmarks/ManageListsModal.tsx
index c1a75a43..dfbd6d45 100644
--- a/apps/web/components/dashboard/bookmarks/ManageListsModal.tsx
+++ b/apps/web/components/dashboard/bookmarks/ManageListsModal.tsx
@@ -18,6 +18,7 @@ import {
} from "@/components/ui/form";
import LoadingSpinner from "@/components/ui/spinner";
import { toast } from "@/components/ui/use-toast";
+import { useTranslation } from "@/lib/i18n/client";
import { api } from "@/lib/trpc";
import { zodResolver } from "@hookform/resolvers/zod";
import { Archive, X } from "lucide-react";
@@ -42,6 +43,7 @@ export default function ManageListsModal({
open: boolean;
setOpen: (open: boolean) => void;
}) {
+ const { t } = useTranslation();
const formSchema = z.object({
listId: z.string({
required_error: "Please select a list",
@@ -73,7 +75,7 @@ export default function ManageListsModal({
useAddBookmarkToList({
onSuccess: () => {
toast({
- description: "List has been updated!",
+ description: t("toasts.lists.updated"),
});
form.resetField("listId");
},
@@ -86,7 +88,7 @@ export default function ManageListsModal({
} else {
toast({
variant: "destructive",
- title: "Something went wrong",
+ title: t("common.something_went_wrong"),
});
}
},
@@ -96,7 +98,7 @@ export default function ManageListsModal({
useRemoveBookmarkFromList({
onSuccess: () => {
toast({
- description: "List has been updated!",
+ description: t("toasts.lists.updated"),
});
form.resetField("listId");
},
@@ -109,7 +111,7 @@ export default function ManageListsModal({
} else {
toast({
variant: "destructive",
- title: "Something went wrong",
+ title: t("common.something_went_wrong"),
});
}
},
@@ -188,7 +190,7 @@ export default function ManageListsModal({
<DialogFooter className="sm:justify-end">
<DialogClose asChild>
<Button type="button" variant="secondary">
- Close
+ {t("actions.close")}
</Button>
</DialogClose>
<ArchiveBookmarkButton
@@ -196,14 +198,14 @@ export default function ManageListsModal({
bookmarkId={bookmarkId}
onDone={() => setOpen(false)}
>
- <Archive className="mr-2 size-4" /> Archive
+ <Archive className="mr-2 size-4" /> {t("actions.archive")}
</ArchiveBookmarkButton>
<ActionButton
type="submit"
loading={isAddingToListPending}
disabled={isAddingToListPending}
>
- Add
+ {t("actions.add")}
</ActionButton>
</DialogFooter>
</form>
diff --git a/apps/web/components/dashboard/bookmarks/SummarizeBookmarkArea.tsx b/apps/web/components/dashboard/bookmarks/SummarizeBookmarkArea.tsx
index 5dfa3166..21554556 100644
--- a/apps/web/components/dashboard/bookmarks/SummarizeBookmarkArea.tsx
+++ b/apps/web/components/dashboard/bookmarks/SummarizeBookmarkArea.tsx
@@ -2,6 +2,7 @@ import React from "react";
import { ActionButton } from "@/components/ui/action-button";
import LoadingSpinner from "@/components/ui/spinner";
import { toast } from "@/components/ui/use-toast";
+import { useTranslation } from "@/lib/i18n/client";
import { cn } from "@/lib/utils";
import { ChevronUp, RefreshCw, Sparkles, Trash2 } from "lucide-react";
@@ -99,6 +100,7 @@ export default function SummarizeBookmarkArea({
}: {
bookmark: ZBookmark;
}) {
+ const { t } = useTranslation();
const { mutate, isPending } = useSummarizeBookmark({
onError: () => {
toast({
@@ -132,7 +134,7 @@ export default function SummarizeBookmarkArea({
</div>
)}
<span className="relative z-10 flex items-center gap-1.5">
- Summarize with AI
+ {t("actions.summarize_with_ai")}
<Sparkles className="size-4" />
</span>
</ActionButton>
diff --git a/apps/web/components/dashboard/bookmarks/TagModal.tsx b/apps/web/components/dashboard/bookmarks/TagModal.tsx
index c2f081be..61f462b1 100644
--- a/apps/web/components/dashboard/bookmarks/TagModal.tsx
+++ b/apps/web/components/dashboard/bookmarks/TagModal.tsx
@@ -8,6 +8,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
+import { useTranslation } from "@/lib/i18n/client";
import type { ZBookmark } from "@hoarder/shared/types/bookmarks";
@@ -22,17 +23,18 @@ export default function TagModal({
open: boolean;
setOpen: (open: boolean) => void;
}) {
+ const { t } = useTranslation();
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
- <DialogTitle>Edit Tags</DialogTitle>
+ <DialogTitle>{t("actions.edit_tags")}</DialogTitle>
</DialogHeader>
<BookmarkTagsEditor bookmark={bookmark} />
<DialogFooter className="sm:justify-end">
<DialogClose asChild>
<Button type="button" variant="secondary">
- Close
+ {t("actions.close")}
</Button>
</DialogClose>
</DialogFooter>
diff --git a/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx b/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx
index 61132a60..c9db3dfa 100644
--- a/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx
+++ b/apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx
@@ -21,6 +21,7 @@ import {
TableRow,
} from "@/components/ui/table";
import { toast } from "@/components/ui/use-toast";
+import { useTranslation } from "@/lib/i18n/client";
import { api } from "@/lib/trpc";
import { cn } from "@/lib/utils";
import { distance } from "fastest-levenshtein";
@@ -56,6 +57,7 @@ const useSuggestions = () => {
};
function ApplyAllButton({ suggestions }: { suggestions: Suggestion[] }) {
+ const { t } = useTranslation();
const [applying, setApplying] = useState(false);
const { mutateAsync } = useMergeTag({
onError: (e) => {
@@ -91,7 +93,7 @@ function ApplyAllButton({ suggestions }: { suggestions: Suggestion[] }) {
return (
<ActionConfirmingDialog
- title="Merge all suggestions?"
+ title={t("cleanups.duplicate_tags.merge_all_suggestions")}
description={`Are you sure you want to apply all ${suggestions.length} suggestions?`}
actionButton={(setDialogOpen) => (
<ActionButton
@@ -100,13 +102,13 @@ function ApplyAllButton({ suggestions }: { suggestions: Suggestion[] }) {
onClick={() => applyAll(setDialogOpen)}
>
<Check className="mr-2 size-4" />
- Apply All
+ {t("actions.apply_all")}
</ActionButton>
)}
>
<Button variant="destructive">
<Check className="mr-2 size-4" />
- Apply All
+ {t("actions.apply_all")}
</Button>
</ActionConfirmingDialog>
);
@@ -121,6 +123,7 @@ function SuggestionRow({
updateMergeInto: (suggestion: Suggestion, newMergeIntoId: string) => void;
deleteSuggestion: (suggestion: Suggestion) => void;
}) {
+ const { t } = useTranslation();
const { mutate, isPending } = useMergeTag({
onSuccess: () => {
toast({
@@ -180,7 +183,7 @@ function SuggestionRow({
}
>
<Combine className="mr-2 size-4" />
- Merge
+ {t("actions.merge")}
</ActionButton>
<Button
@@ -188,7 +191,7 @@ function SuggestionRow({
onClick={() => deleteSuggestion(suggestion)}
>
<X className="mr-2 size-4" />
- Ignore
+ {t("actions.ignore")}
</Button>
</TableCell>
</TableRow>
diff --git a/apps/web/components/dashboard/header/ProfileOptions.tsx b/apps/web/components/dashboard/header/ProfileOptions.tsx
index ee6cac01..e715048e 100644
--- a/apps/web/components/dashboard/header/ProfileOptions.tsx
+++ b/apps/web/components/dashboard/header/ProfileOptions.tsx
@@ -11,31 +11,34 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
+import { useTranslation } from "@/lib/i18n/client";
import { LogOut, Moon, Paintbrush, Settings, Shield, Sun } from "lucide-react";
import { signOut, useSession } from "next-auth/react";
import { useTheme } from "next-themes";
function DarkModeToggle() {
+ const { t } = useTranslation();
const { theme } = useTheme();
if (theme == "dark") {
return (
<>
<Sun className="mr-2 size-4" />
- <span>Light Mode</span>
+ <span>{t("options.light_mode")}</span>
</>
);
} else {
return (
<>
<Moon className="mr-2 size-4" />
- <span>Dark Mode</span>
+ <span>{t("options.dark_mode")}</span>
</>
);
}
}
export default function SidebarProfileOptions() {
+ const { t } = useTranslation();
const toggleTheme = useToggleTheme();
const { data: session } = useSession();
if (!session) return redirect("/");
@@ -64,14 +67,14 @@ export default function SidebarProfileOptions() {
<DropdownMenuItem asChild>
<Link href="/settings">
<Settings className="mr-2 size-4" />
- User Settings
+ {t("settings.user_settings")}
</Link>
</DropdownMenuItem>
{session.user.role == "admin" && (
<DropdownMenuItem asChild>
<Link href="/dashboard/admin">
<Shield className="mr-2 size-4" />
- Admin Settings
+ {t("admin.admin_settings")}
</Link>
</DropdownMenuItem>
)}
@@ -94,7 +97,7 @@ export default function SidebarProfileOptions() {
}
>
<LogOut className="mr-2 size-4" />
- <span>Sign Out</span>
+ <span>{t("actions.sign_out")}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
diff --git a/apps/web/components/dashboard/lists/AllListsView.tsx b/apps/web/components/dashboard/lists/AllListsView.tsx
index 308af5db..ab6e31d3 100644
--- a/apps/web/components/dashboard/lists/AllListsView.tsx
+++ b/apps/web/components/dashboard/lists/AllListsView.tsx
@@ -4,6 +4,7 @@ import Link from "next/link";
import { EditListModal } from "@/components/dashboard/lists/EditListModal";
import { Button } from "@/components/ui/button";
import { CollapsibleTriggerChevron } from "@/components/ui/collapsible";
+import { useTranslation } from "@/lib/i18n/client";
import { MoreHorizontal, Plus } from "lucide-react";
import type { ZBookmarkList } from "@hoarder/shared/types/lists";
@@ -62,23 +63,24 @@ export default function AllListsView({
}: {
initialData: ZBookmarkList[];
}) {
+ const { t } = useTranslation();
return (
<ul>
<EditListModal>
<Button className="mb-2 flex h-full w-full items-center">
<Plus />
- <span>New List</span>
+ <span>{t("lists.new_list")}</span>
</Button>
</EditListModal>
<ListItem
collapsible={false}
- name="Favourites"
+ name={t("lists.favourites")}
icon="⭐️"
path={`/dashboard/favourites`}
/>
<ListItem
collapsible={false}
- name="Archive"
+ name={t("common.archive")}
icon="🗄️"
path={`/dashboard/archive`}
/>
diff --git a/apps/web/components/dashboard/lists/EditListModal.tsx b/apps/web/components/dashboard/lists/EditListModal.tsx
index cba1a0e6..d66d7096 100644
--- a/apps/web/components/dashboard/lists/EditListModal.tsx
+++ b/apps/web/components/dashboard/lists/EditListModal.tsx
@@ -26,6 +26,7 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { toast } from "@/components/ui/use-toast";
+import { useTranslation } from "@/lib/i18n/client";
import data from "@emoji-mart/data";
import Picker from "@emoji-mart/react";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -54,6 +55,7 @@ export function EditListModal({
parent?: ZBookmarkList;
children?: React.ReactNode;
}) {
+ const { t } = useTranslation();
const router = useRouter();
if (
(userOpen !== undefined && !userSetOpen) ||
@@ -91,7 +93,7 @@ export function EditListModal({
const { mutate: createList, isPending: isCreating } = useCreateBookmarkList({
onSuccess: (resp) => {
toast({
- description: "List has been created!",
+ description: t("toasts.lists.created"),
});
setOpen(false);
router.push(`/dashboard/lists/${resp.id}`);
@@ -115,7 +117,7 @@ export function EditListModal({
} else {
toast({
variant: "destructive",
- title: "Something went wrong",
+ title: t("common.something_went_wrong"),
});
}
},
@@ -124,7 +126,7 @@ export function EditListModal({
const { mutate: editList, isPending: isEditing } = useEditBookmarkList({
onSuccess: () => {
toast({
- description: "List has been updated!",
+ description: t("toasts.lists.updated"),
});
setOpen(false);
form.reset();
@@ -147,7 +149,7 @@ export function EditListModal({
} else {
toast({
variant: "destructive",
- title: "Something went wrong",
+ title: t("common.something_went_wrong"),
});
}
},
@@ -259,7 +261,7 @@ export function EditListModal({
<DialogFooter className="sm:justify-end">
<DialogClose asChild>
<Button type="button" variant="secondary">
- Close
+ {t("actions.close")}
</Button>
</DialogClose>
<ActionButton
@@ -267,7 +269,7 @@ export function EditListModal({
onClick={onSubmit}
loading={isPending}
>
- {list ? "Save" : "Create"}
+ {list ? t("actions.save") : t("actions.create")}
</ActionButton>
</DialogFooter>
</form>
diff --git a/apps/web/components/dashboard/lists/ListOptions.tsx b/apps/web/components/dashboard/lists/ListOptions.tsx
index b44d8a23..e663a2e0 100644
--- a/apps/web/components/dashboard/lists/ListOptions.tsx
+++ b/apps/web/components/dashboard/lists/ListOptions.tsx
@@ -7,6 +7,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
+import { useTranslation } from "@/lib/i18n/client";
import { Pencil, Plus, Trash2 } from "lucide-react";
import { ZBookmarkList } from "@hoarder/shared/types/lists";
@@ -21,6 +22,8 @@ export function ListOptions({
list: ZBookmarkList;
children?: React.ReactNode;
}) {
+ const { t } = useTranslation();
+
const [deleteListDialogOpen, setDeleteListDialogOpen] = useState(false);
const [newNestedListModalOpen, setNewNestedListModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
@@ -49,21 +52,21 @@ export function ListOptions({
onClick={() => setEditModalOpen(true)}
>
<Pencil className="size-4" />
- <span>Edit</span>
+ <span>{t("actions.edit")}</span>
</DropdownMenuItem>
<DropdownMenuItem
className="flex gap-2"
onClick={() => setNewNestedListModalOpen(true)}
>
<Plus className="size-4" />
- <span>New nested list</span>
+ <span>{t("lists.new_nested_list")}</span>
</DropdownMenuItem>
<DropdownMenuItem
className="flex gap-2"
onClick={() => setDeleteListDialogOpen(true)}
>
<Trash2 className="size-4" />
- <span>Delete</span>
+ <span>{t("actions.delete")}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
diff --git a/apps/web/components/dashboard/preview/ActionBar.tsx b/apps/web/components/dashboard/preview/ActionBar.tsx
index 3505d0a5..38ad8fa2 100644
--- a/apps/web/components/dashboard/preview/ActionBar.tsx
+++ b/apps/web/components/dashboard/preview/ActionBar.tsx
@@ -6,6 +6,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { toast } from "@/components/ui/use-toast";
+import { useTranslation } from "@/lib/i18n/client";
import { Trash2 } from "lucide-react";
import type { ZBookmark } from "@hoarder/shared/types/bookmarks";
@@ -17,6 +18,7 @@ import {
import { ArchivedActionIcon, FavouritedActionIcon } from "../bookmarks/icons";
export default function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
+ const { t } = useTranslation();
const router = useRouter();
const onError = () => {
toast({
@@ -72,7 +74,9 @@ export default function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
</ActionButton>
</TooltipTrigger>
<TooltipContent side="bottom">
- {bookmark.favourited ? "Un-favourite" : "Favourite"}
+ {bookmark.favourited
+ ? t("actions.unfavorite")
+ : t("actions.favorite")}
</TooltipContent>
</Tooltip>
<Tooltip delayDuration={0}>
@@ -92,7 +96,7 @@ export default function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
</ActionButton>
</TooltipTrigger>
<TooltipContent side="bottom">
- {bookmark.archived ? "Un-archive" : "Archive"}
+ {bookmark.archived ? t("actions.unarchive") : t("actions.archive")}
</TooltipContent>
</Tooltip>
<Tooltip delayDuration={0}>
@@ -108,7 +112,7 @@ export default function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
<Trash2 />
</ActionButton>
</TooltipTrigger>
- <TooltipContent side="bottom">Delete</TooltipContent>
+ <TooltipContent side="bottom">{t("actions.delete")}</TooltipContent>
</Tooltip>
</div>
);
diff --git a/apps/web/components/dashboard/preview/AttachmentBox.tsx b/apps/web/components/dashboard/preview/AttachmentBox.tsx
index d631f4d9..32184c30 100644
--- a/apps/web/components/dashboard/preview/AttachmentBox.tsx
+++ b/apps/web/components/dashboard/preview/AttachmentBox.tsx
@@ -10,6 +10,7 @@ import {
import FilePickerButton from "@/components/ui/file-picker-button";
import { toast } from "@/components/ui/use-toast";
import useUpload from "@/lib/hooks/upload-file";
+import { useTranslation } from "@/lib/i18n/client";
import {
Archive,
Camera,
@@ -41,6 +42,7 @@ import {
} from "@hoarder/trpc/lib/attachments";
export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) {
+ const { t } = useTranslation();
const typeToIcon: Record<ZAssetType, React.ReactNode> = {
screenshot: <Camera className="size-4" />,
fullPageArchive: <Archive className="size-4" />,
@@ -109,7 +111,7 @@ export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) {
return (
<Collapsible>
<CollapsibleTrigger className="flex w-full items-center justify-between gap-2 text-sm text-gray-400">
- Attachments
+ {t("common.attachments")}
<ChevronsDownUp className="size-4" />
</CollapsibleTrigger>
<CollapsibleContent className="flex flex-col gap-1 py-2 text-sm">
diff --git a/apps/web/components/dashboard/preview/BookmarkPreview.tsx b/apps/web/components/dashboard/preview/BookmarkPreview.tsx
index e37c4b86..ff6330fa 100644
--- a/apps/web/components/dashboard/preview/BookmarkPreview.tsx
+++ b/apps/web/components/dashboard/preview/BookmarkPreview.tsx
@@ -12,6 +12,7 @@ import {
TooltipPortal,
TooltipTrigger,
} from "@/components/ui/tooltip";
+import { useTranslation } from "@/lib/i18n/client";
import { api } from "@/lib/trpc";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
@@ -75,6 +76,7 @@ export default function BookmarkPreview({
bookmarkId: string;
initialData?: ZBookmark;
}) {
+ const { t } = useTranslation();
const { data: bookmark } = api.bookmarks.getBookmark.useQuery(
{
bookmarkId,
@@ -130,7 +132,7 @@ export default function BookmarkPreview({
href={sourceUrl}
className="flex items-center gap-2 text-gray-400"
>
- <span>View Original</span>
+ <span>{t("preview.view_original")}</span>
<ExternalLink />
</Link>
)}
@@ -140,11 +142,11 @@ export default function BookmarkPreview({
<CreationTime createdAt={bookmark.createdAt} />
<SummarizeBookmarkArea bookmark={bookmark} />
<div className="flex items-center gap-4">
- <p className="text-sm text-gray-400">Tags</p>
+ <p className="text-sm text-gray-400">{t("common.tags")}</p>
<BookmarkTagsEditor bookmark={bookmark} />
</div>
<div className="flex gap-4">
- <p className="pt-2 text-sm text-gray-400">Note</p>
+ <p className="pt-2 text-sm text-gray-400">{t("common.note")}</p>
<NoteEditor bookmark={bookmark} />
</div>
<AttachmentBox bookmark={bookmark} />
diff --git a/apps/web/components/dashboard/preview/LinkContentSection.tsx b/apps/web/components/dashboard/preview/LinkContentSection.tsx
index bf0d8f90..320fc561 100644
--- a/apps/web/components/dashboard/preview/LinkContentSection.tsx
+++ b/apps/web/components/dashboard/preview/LinkContentSection.tsx
@@ -8,6 +8,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
+import { useTranslation } from "@/lib/i18n/client";
import { ScrollArea } from "@radix-ui/react-scroll-area";
import {
@@ -79,6 +80,7 @@ export default function LinkContentSection({
}: {
bookmark: ZBookmark;
}) {
+ const { t } = useTranslation();
const [section, setSection] = useState<string>("cached");
if (bookmark.content.type != BookmarkTypes.LINK) {
@@ -104,21 +106,23 @@ export default function LinkContentSection({
</SelectTrigger>
<SelectContent>
<SelectGroup>
- <SelectItem value="cached">Cached Content</SelectItem>
+ <SelectItem value="cached">
+ {t("preview.cached_content")}
+ </SelectItem>
<SelectItem
value="screenshot"
disabled={!bookmark.content.screenshotAssetId}
>
- Screenshot
+ {t("common.screenshot")}
</SelectItem>
<SelectItem
value="archive"
disabled={!bookmark.content.fullPageArchiveAssetId}
>
- Archive
+ {t("common.archive")}
</SelectItem>
<SelectItem value="video" disabled={!bookmark.content.videoAssetId}>
- Video
+ {t("common.video")}
</SelectItem>
</SelectGroup>
</SelectContent>
diff --git a/apps/web/components/dashboard/search/SearchInput.tsx b/apps/web/components/dashboard/search/SearchInput.tsx
index a7caf44e..55f304e3 100644
--- a/apps/web/components/dashboard/search/SearchInput.tsx
+++ b/apps/web/components/dashboard/search/SearchInput.tsx
@@ -3,6 +3,7 @@
import React, { useEffect, useImperativeHandle, useRef } from "react";
import { Input } from "@/components/ui/input";
import { useDoBookmarkSearch } from "@/lib/hooks/bookmark-search";
+import { useTranslation } from "@/lib/i18n/client";
function useFocusSearchOnKeyPress(
inputRef: React.RefObject<HTMLInputElement>,
@@ -45,6 +46,7 @@ const SearchInput = React.forwardRef<
HTMLInputElement,
React.HTMLAttributes<HTMLInputElement> & { loading?: boolean }
>(({ className, ...props }, ref) => {
+ const { t } = useTranslation();
const { debounceSearch, searchQuery, isInSearchPage } = useDoBookmarkSearch();
const [value, setValue] = React.useState(searchQuery);
@@ -69,7 +71,7 @@ const SearchInput = React.forwardRef<
ref={inputRef}
value={value}
onChange={onChange}
- placeholder="Search"
+ placeholder={t("common.search")}
className={className}
{...props}
/>
diff --git a/apps/web/components/dashboard/sidebar/AllLists.tsx b/apps/web/components/dashboard/sidebar/AllLists.tsx
index c48ddb0f..7341e118 100644
--- a/apps/web/components/dashboard/sidebar/AllLists.tsx
+++ b/apps/web/components/dashboard/sidebar/AllLists.tsx
@@ -6,6 +6,7 @@ import { usePathname } from "next/navigation";
import SidebarItem from "@/components/shared/sidebar/SidebarItem";
import { Button } from "@/components/ui/button";
import { CollapsibleTriggerTriangle } from "@/components/ui/collapsible";
+import { useTranslation } from "@/lib/i18n/client";
import { MoreHorizontal, Plus } from "lucide-react";
import type { ZBookmarkList } from "@hoarder/shared/types/lists";
@@ -20,6 +21,7 @@ export default function AllLists({
}: {
initialData: { lists: ZBookmarkList[] };
}) {
+ const { t } = useTranslation();
const pathName = usePathname();
const isNodeOpen = useCallback(
(node: ZBookmarkListTreeNode) => pathName.includes(node.item.id),
@@ -37,13 +39,13 @@ export default function AllLists({
</li>
<SidebarItem
logo={<span className="text-lg">📋</span>}
- name="All Lists"
+ name={t("lists.all_lists")}
path={`/dashboard/lists`}
linkClassName="py-0.5"
/>
<SidebarItem
logo={<span className="text-lg">⭐️</span>}
- name="Favourites"
+ name={t("lists.favourites")}
path={`/dashboard/favourites`}
linkClassName="py-0.5"
/>
diff --git a/apps/web/components/dashboard/sidebar/Sidebar.tsx b/apps/web/components/dashboard/sidebar/Sidebar.tsx
index 8021ad36..8891d9bc 100644
--- a/apps/web/components/dashboard/sidebar/Sidebar.tsx
+++ b/apps/web/components/dashboard/sidebar/Sidebar.tsx
@@ -1,6 +1,7 @@
import { redirect } from "next/navigation";
import SidebarItem from "@/components/shared/sidebar/SidebarItem";
import { Separator } from "@/components/ui/separator";
+import { useTranslation } from "@/lib/i18n/server";
import { api } from "@/server/api/client";
import { getServerAuthSession } from "@/server/auth";
import { Archive, Home, Search, Tag } from "lucide-react";
@@ -10,6 +11,7 @@ import serverConfig from "@hoarder/shared/config";
import AllLists from "./AllLists";
export default async function Sidebar() {
+ const { t } = await useTranslation();
const session = await getServerAuthSession();
if (!session) {
redirect("/");
@@ -20,7 +22,7 @@ export default async function Sidebar() {
const searchItem = serverConfig.meilisearch
? [
{
- name: "Search",
+ name: t("common.search"),
icon: <Search size={18} />,
path: "/dashboard/search",
},
@@ -33,18 +35,18 @@ export default async function Sidebar() {
path: string;
}[] = [
{
- name: "Home",
+ name: t("common.home"),
icon: <Home size={18} />,
path: "/dashboard/bookmarks",
},
...searchItem,
{
- name: "Tags",
+ name: t("common.tags"),
icon: <Tag size={18} />,
path: "/dashboard/tags",
},
{
- name: "Archive",
+ name: t("common.archive"),
icon: <Archive size={18} />,
path: "/dashboard/archive",
},
diff --git a/apps/web/components/dashboard/tags/AllTagsView.tsx b/apps/web/components/dashboard/tags/AllTagsView.tsx
index 72f4dc11..1efc6090 100644
--- a/apps/web/components/dashboard/tags/AllTagsView.tsx
+++ b/apps/web/components/dashboard/tags/AllTagsView.tsx
@@ -13,6 +13,7 @@ import InfoTooltip from "@/components/ui/info-tooltip";
import { Separator } from "@/components/ui/separator";
import { Toggle } from "@/components/ui/toggle";
import { toast } from "@/components/ui/use-toast";
+import { useTranslation } from "@/lib/i18n/client";
import { api } from "@/lib/trpc";
import { ArrowDownAZ, Combine } from "lucide-react";
@@ -23,6 +24,7 @@ import DeleteTagConfirmationDialog from "./DeleteTagConfirmationDialog";
import { TagPill } from "./TagPill";
function DeleteAllUnusedTags({ numUnusedTags }: { numUnusedTags: number }) {
+ const { t } = useTranslation();
const { mutate, isPending } = useDeleteUnusedTags({
onSuccess: () => {
toast({
@@ -38,7 +40,7 @@ function DeleteAllUnusedTags({ numUnusedTags }: { numUnusedTags: number }) {
});
return (
<ActionConfirmingDialog
- title="Delete all unused tags?"
+ title={t("tags.delete_all_unused_tags")}
description={`Are you sure you want to delete the ${numUnusedTags} unused tags?`}
actionButton={() => (
<ActionButton
@@ -51,7 +53,7 @@ function DeleteAllUnusedTags({ numUnusedTags }: { numUnusedTags: number }) {
)}
>
<Button variant="destructive" disabled={numUnusedTags == 0}>
- Delete All Unused Tags
+ {t("tags.delete_all_unused_tags")}
</Button>
</ActionConfirmingDialog>
);
@@ -72,6 +74,7 @@ export default function AllTagsView({
}: {
initialData: ZGetTagResponse[];
}) {
+ const { t } = useTranslation();
interface Tag {
id: string;
name: string;
@@ -154,9 +157,9 @@ export default function AllTagsView({
onPressedChange={toggleDraggingEnabled}
>
<Combine className="mr-2 size-4" />
- Drag & Drop Merging
+ {t("tags.drag_and_drop_merging")}
<InfoTooltip size={15} className="my-auto ml-2" variant="explain">
- <p>Drag and drop tags on each other to merge them</p>
+ <p>{t("tags.drag_and_drop_merging_info")}</p>
</InfoTooltip>
</Toggle>
<Toggle
@@ -165,29 +168,29 @@ export default function AllTagsView({
pressed={sortByName}
onPressedChange={toggleSortByName}
>
- <ArrowDownAZ className="mr-2 size-4" /> Sort by Name
+ <ArrowDownAZ className="mr-2 size-4" /> {t("tags.sort_by_name")}
</Toggle>
</div>
<span className="flex items-center gap-2">
- <p className="text-lg">Your Tags</p>
+ <p className="text-lg">{t("tags.your_tags")}</p>
<InfoTooltip size={15} className="my-auto" variant="explain">
- <p>Tags that were attached at least once by you</p>
+ <p>{t("tags.your_tags_info")}</p>
</InfoTooltip>
</span>
{tagsToPill(humanTags)}
<Separator />
<span className="flex items-center gap-2">
- <p className="text-lg">AI Tags</p>
+ <p className="text-lg">{t("tags.ai_tags")}</p>
<InfoTooltip size={15} className="my-auto" variant="explain">
- <p>Tags that were only attached automatically (by AI)</p>
+ <p>{t("tags.ai_tags_info")}</p>
</InfoTooltip>
</span>
{tagsToPill(aiTags)}
<Separator />
<span className="flex items-center gap-2">
- <p className="text-lg">Unused Tags</p>
+ <p className="text-lg">{t("tags.unused_tags")}</p>
<InfoTooltip size={15} className="my-auto" variant="explain">
- <p>Tags that are not attached to any bookmarks</p>
+ <p>{t("tags.unused_tags_info")}</p>
</InfoTooltip>
</span>
<Collapsible>
diff --git a/apps/web/components/dashboard/tags/TagOptions.tsx b/apps/web/components/dashboard/tags/TagOptions.tsx
index 1bd17902..8d8cc9db 100644
--- a/apps/web/components/dashboard/tags/TagOptions.tsx
+++ b/apps/web/components/dashboard/tags/TagOptions.tsx
@@ -7,6 +7,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
+import { useTranslation } from "@/lib/i18n/client";
import { Combine, Trash2 } from "lucide-react";
import DeleteTagConfirmationDialog from "./DeleteTagConfirmationDialog";
@@ -19,6 +20,7 @@ export function TagOptions({
tag: { id: string; name: string };
children?: React.ReactNode;
}) {
+ const { t } = useTranslation();
const [deleteTagDialogOpen, setDeleteTagDialogOpen] = useState(false);
const [mergeTagDialogOpen, setMergeTagDialogOpen] = useState(false);
@@ -41,7 +43,7 @@ export function TagOptions({
onClick={() => setMergeTagDialogOpen(true)}
>
<Combine className="size-4" />
- <span>Merge</span>
+ <span>{t("actions.merge")}</span>
</DropdownMenuItem>
<DropdownMenuItem
@@ -49,7 +51,7 @@ export function TagOptions({
onClick={() => setDeleteTagDialogOpen(true)}
>
<Trash2 className="size-4" />
- <span>Delete</span>
+ <span>{t("actions.delete")}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>