aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/components')
-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
-rw-r--r--apps/web/components/settings/AISettings.tsx34
-rw-r--r--apps/web/components/settings/AddApiKey.tsx32
-rw-r--r--apps/web/components/settings/ApiKeySettings.tsx14
-rw-r--r--apps/web/components/settings/ChangePassword.tsx20
-rw-r--r--apps/web/components/settings/DeleteApiKey.tsx4
-rw-r--r--apps/web/components/settings/FeedSettings.tsx35
-rw-r--r--apps/web/components/settings/ImportExport.tsx20
-rw-r--r--apps/web/components/settings/UserDetails.tsx10
-rw-r--r--apps/web/components/settings/UserOptions.tsx55
-rw-r--r--apps/web/components/settings/sidebar/ModileSidebar.tsx4
-rw-r--r--apps/web/components/settings/sidebar/Sidebar.tsx4
-rw-r--r--apps/web/components/settings/sidebar/items.tsx19
-rw-r--r--apps/web/components/ui/action-confirming-dialog.tsx4
38 files changed, 405 insertions, 224 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>
diff --git a/apps/web/components/settings/AISettings.tsx b/apps/web/components/settings/AISettings.tsx
index 0a8db147..79a9e558 100644
--- a/apps/web/components/settings/AISettings.tsx
+++ b/apps/web/components/settings/AISettings.tsx
@@ -20,6 +20,7 @@ import {
} from "@/components/ui/select";
import { toast } from "@/components/ui/use-toast";
import { useClientConfig } from "@/lib/clientConfig";
+import { useTranslation } from "@/lib/i18n/client";
import { api } from "@/lib/trpc";
import { zodResolver } from "@hookform/resolvers/zod";
import { Plus, Save, Trash2 } from "lucide-react";
@@ -34,6 +35,7 @@ import {
} from "@hoarder/shared/types/prompts";
export function PromptEditor() {
+ const { t } = useTranslation();
const apiUtils = api.useUtils();
const form = useForm<z.infer<typeof zNewPromptSchema>>({
@@ -117,7 +119,7 @@ export function PromptEditor() {
className="items-center"
>
<Plus className="mr-2 size-4" />
- Add
+ {t("actions.add")}
</ActionButton>
</form>
</Form>
@@ -125,6 +127,7 @@ export function PromptEditor() {
}
export function PromptRow({ prompt }: { prompt: ZPrompt }) {
+ const { t } = useTranslation();
const apiUtils = api.useUtils();
const { mutateAsync: updatePrompt, isPending: isUpdating } =
api.prompts.update.useMutation({
@@ -169,11 +172,7 @@ export function PromptRow({ prompt }: { prompt: ZPrompt }) {
return (
<FormItem className="hidden">
<FormControl>
- <Input
- placeholder="Add a custom prompt"
- type="hidden"
- {...field}
- />
+ <Input type="hidden" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -234,7 +233,7 @@ export function PromptRow({ prompt }: { prompt: ZPrompt }) {
className="items-center"
>
<Save className="mr-2 size-4" />
- Save
+ {t("actions.save")}
</ActionButton>
<ActionButton
loading={isDeleting}
@@ -244,7 +243,7 @@ export function PromptRow({ prompt }: { prompt: ZPrompt }) {
type="button"
>
<Trash2 className="mr-2 size-4" />
- Delete
+ {t("actions.delete")}
</ActionButton>
</form>
</Form>
@@ -252,15 +251,16 @@ export function PromptRow({ prompt }: { prompt: ZPrompt }) {
}
export function TaggingRules() {
+ const { t } = useTranslation();
const { data: prompts, isLoading } = api.prompts.list.useQuery();
return (
<div className="mt-2 flex flex-col gap-2">
- <div className="w-full text-xl font-medium sm:w-1/3">Tagging Rules</div>
+ <div className="w-full text-xl font-medium sm:w-1/3">
+ {t("settings.ai.tagging_rules")}
+ </div>
<p className="mb-1 text-xs italic text-muted-foreground">
- Prompts that you add here will be included as rules to the model during
- tag generation. You can view the final prompts in the prompt preview
- section.
+ {t("settings.ai.tagging_rule_description")}
</p>
{isLoading && <FullPageSpinner />}
{prompts && prompts.length == 0 && (
@@ -276,14 +276,15 @@ export function TaggingRules() {
}
export function PromptDemo() {
+ const { t } = useTranslation();
const { data: prompts } = api.prompts.list.useQuery();
const clientConfig = useClientConfig();
return (
<div className="flex flex-col gap-2">
<div className="mb-4 w-full text-xl font-medium sm:w-1/3">
- Prompt Preview
+ {t("settings.ai.prompt_preview")}
</div>
- <p>Text Prompt</p>
+ <p>{t("settings.ai.text_prompt")}</p>
<code className="whitespace-pre-wrap rounded-md bg-muted p-3 text-sm text-muted-foreground">
{buildTextPrompt(
clientConfig.inference.inferredTagLang,
@@ -294,7 +295,7 @@ export function PromptDemo() {
/* context length */ 1024 /* The value here doesn't matter */,
).trim()}
</code>
- <p>Image Prompt</p>
+ <p>{t("settings.ai.images_prompt")}</p>
<code className="whitespace-pre-wrap rounded-md bg-muted p-3 text-sm text-muted-foreground">
{buildImagePrompt(
clientConfig.inference.inferredTagLang,
@@ -308,12 +309,13 @@ export function PromptDemo() {
}
export default function AISettings() {
+ const { t } = useTranslation();
return (
<>
<div className="rounded-md border bg-background p-4">
<div className="mb-2 flex flex-col gap-3">
<div className="w-full text-2xl font-medium sm:w-1/3">
- AI Settings
+ {t("settings.ai.ai_settings")}
</div>
<TaggingRules />
</div>
diff --git a/apps/web/components/settings/AddApiKey.tsx b/apps/web/components/settings/AddApiKey.tsx
index 34fd2df7..00e70d3f 100644
--- a/apps/web/components/settings/AddApiKey.tsx
+++ b/apps/web/components/settings/AddApiKey.tsx
@@ -27,17 +27,18 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
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 { useForm } from "react-hook-form";
import { z } from "zod";
function ApiKeySuccess({ apiKey }: { apiKey: string }) {
+ const { t } = useTranslation();
return (
<div>
<div className="py-4">
- Note: please copy the key and store it somewhere safe. Once you close
- the dialog, you won&apos;t be able to access it again.
+ {t("settings.api_keys.key_success_please_copy")}
</div>
<div className="flex space-x-2 pt-2">
<Input value={apiKey} readOnly />
@@ -52,6 +53,7 @@ function ApiKeySuccess({ apiKey }: { apiKey: string }) {
}
function AddApiKeyForm({ onSuccess }: { onSuccess: (key: string) => void }) {
+ const { t } = useTranslation();
const formSchema = z.object({
name: z.string(),
});
@@ -62,7 +64,10 @@ function AddApiKeyForm({ onSuccess }: { onSuccess: (key: string) => void }) {
router.refresh();
},
onError: () => {
- toast({ description: "Something went wrong", variant: "destructive" });
+ toast({
+ description: t("common.something_went_wrong"),
+ variant: "destructive",
+ });
},
});
@@ -95,12 +100,16 @@ function AddApiKeyForm({ onSuccess }: { onSuccess: (key: string) => void }) {
render={({ field }) => {
return (
<FormItem className="flex-1">
- <FormLabel>Name</FormLabel>
+ <FormLabel>{t("common.name")}</FormLabel>
<FormControl>
- <Input type="text" placeholder="Name" {...field} />
+ <Input
+ type="text"
+ placeholder={t("common.name")}
+ {...field}
+ />
</FormControl>
<FormDescription>
- Give your API key a unique name
+ {t("settings.api_keys.new_api_key_desc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -112,7 +121,7 @@ function AddApiKeyForm({ onSuccess }: { onSuccess: (key: string) => void }) {
type="submit"
loading={mutator.isPending}
>
- Create
+ {t("actions.create")}
</ActionButton>
</form>
</Form>
@@ -120,17 +129,20 @@ function AddApiKeyForm({ onSuccess }: { onSuccess: (key: string) => void }) {
}
export default function AddApiKey() {
+ const { t } = useTranslation();
const [key, setKey] = useState<string | undefined>(undefined);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
return (
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
- <Button>New API Key</Button>
+ <Button>{t("settings.api_keys.new_api_key")}</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
- {key ? "Key was successfully created" : "Create API key"}
+ {key
+ ? t("settings.api_keys.key_success")
+ : t("settings.api_keys.new_api_key")}
</DialogTitle>
<DialogDescription>
{key ? (
@@ -147,7 +159,7 @@ export default function AddApiKey() {
variant="outline"
onClick={() => setKey(undefined)}
>
- Close
+ {t("actions.close")}
</Button>
</DialogClose>
</DialogFooter>
diff --git a/apps/web/components/settings/ApiKeySettings.tsx b/apps/web/components/settings/ApiKeySettings.tsx
index 4d43be7a..8f07e5a4 100644
--- a/apps/web/components/settings/ApiKeySettings.tsx
+++ b/apps/web/components/settings/ApiKeySettings.tsx
@@ -6,27 +6,31 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
+import { useTranslation } from "@/lib/i18n/server";
import { api } from "@/server/api/client";
import AddApiKey from "./AddApiKey";
import DeleteApiKey from "./DeleteApiKey";
export default async function ApiKeys() {
+ const { t } = await useTranslation();
const keys = await api.apiKeys.list();
return (
<div>
<div className="flex items-center justify-between">
- <div className="mb-2 text-lg font-medium">API Keys</div>
+ <div className="mb-2 text-lg font-medium">
+ {t("settings.api_keys.api_keys")}
+ </div>
<AddApiKey />
</div>
<div className="mt-2">
<Table>
<TableHeader>
<TableRow>
- <TableHead>Name</TableHead>
- <TableHead>Key</TableHead>
- <TableHead>Created At</TableHead>
- <TableHead>Action</TableHead>
+ <TableHead>{t("common.name")}</TableHead>
+ <TableHead>{t("common.key")}</TableHead>
+ <TableHead>{t("common.created_at")}</TableHead>
+ <TableHead>{t("common.action")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
diff --git a/apps/web/components/settings/ChangePassword.tsx b/apps/web/components/settings/ChangePassword.tsx
index aa27f223..e9f426a6 100644
--- a/apps/web/components/settings/ChangePassword.tsx
+++ b/apps/web/components/settings/ChangePassword.tsx
@@ -12,6 +12,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
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 { useForm } from "react-hook-form";
@@ -19,6 +20,7 @@ import { useForm } from "react-hook-form";
import { zChangePasswordSchema } from "@hoarder/shared/types/users";
export function ChangePassword() {
+ const { t } = useTranslation();
const form = useForm<z.infer<typeof zChangePasswordSchema>>({
resolver: zodResolver(zChangePasswordSchema),
defaultValues: {
@@ -55,7 +57,7 @@ export function ChangePassword() {
return (
<div className="flex flex-col sm:flex-row">
<div className="mb-4 w-full text-lg font-medium sm:w-1/3">
- Change Password
+ {t("settings.info.change_password")}
</div>
<Form {...form}>
<form
@@ -68,11 +70,11 @@ export function ChangePassword() {
render={({ field }) => {
return (
<FormItem className="flex-1">
- <FormLabel>Current Password</FormLabel>
+ <FormLabel>{t("settings.info.current_password")}</FormLabel>
<FormControl>
<Input
type="password"
- placeholder="Current Password"
+ placeholder={t("settings.info.current_password")}
{...field}
/>
</FormControl>
@@ -87,11 +89,11 @@ export function ChangePassword() {
render={({ field }) => {
return (
<FormItem className="flex-1">
- <FormLabel>New Password</FormLabel>
+ <FormLabel>{t("settings.info.new_password")}</FormLabel>
<FormControl>
<Input
type="password"
- placeholder="New Password"
+ placeholder={t("settings.info.new_password")}
{...field}
/>
</FormControl>
@@ -106,11 +108,13 @@ export function ChangePassword() {
render={({ field }) => {
return (
<FormItem className="flex-1">
- <FormLabel>Confirm New Password</FormLabel>
+ <FormLabel>
+ {t("settings.info.confirm_new_password")}
+ </FormLabel>
<FormControl>
<Input
type="Password"
- placeholder="Confirm New Password"
+ placeholder={t("settings.info.confirm_new_password")}
{...field}
/>
</FormControl>
@@ -124,7 +128,7 @@ export function ChangePassword() {
type="submit"
loading={mutator.isPending}
>
- Save
+ {t("actions.save")}
</ActionButton>
</form>
</Form>
diff --git a/apps/web/components/settings/DeleteApiKey.tsx b/apps/web/components/settings/DeleteApiKey.tsx
index e2334c44..4efb7ea8 100644
--- a/apps/web/components/settings/DeleteApiKey.tsx
+++ b/apps/web/components/settings/DeleteApiKey.tsx
@@ -5,6 +5,7 @@ import { ActionButton } from "@/components/ui/action-button";
import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog";
import { Button } from "@/components/ui/button";
import { toast } from "@/components/ui/use-toast";
+import { useTranslation } from "@/lib/i18n/client";
import { api } from "@/lib/trpc";
import { Trash } from "lucide-react";
@@ -15,6 +16,7 @@ export default function DeleteApiKey({
name: string;
id: string;
}) {
+ const { t } = useTranslation();
const router = useRouter();
const mutator = api.apiKeys.revoke.useMutation({
onSuccess: () => {
@@ -43,7 +45,7 @@ export default function DeleteApiKey({
mutator.mutate({ id }, { onSuccess: () => setDialogOpen(false) })
}
>
- Delete
+ {t("actions.delete")}
</ActionButton>
)}
>
diff --git a/apps/web/components/settings/FeedSettings.tsx b/apps/web/components/settings/FeedSettings.tsx
index 4880132c..e3999cb5 100644
--- a/apps/web/components/settings/FeedSettings.tsx
+++ b/apps/web/components/settings/FeedSettings.tsx
@@ -14,6 +14,7 @@ import {
import { FullPageSpinner } from "@/components/ui/full-page-spinner";
import { Input } from "@/components/ui/input";
import { toast } from "@/components/ui/use-toast";
+import { useTranslation } from "@/lib/i18n/client";
import { api } from "@/lib/trpc";
import { cn } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -59,6 +60,7 @@ import {
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
export function FeedsEditorDialog() {
+ const { t } = useTranslation();
const [open, setOpen] = React.useState(false);
const apiUtils = api.useUtils();
@@ -92,7 +94,7 @@ export function FeedsEditorDialog() {
<DialogTrigger asChild>
<Button>
<Plus className="mr-2 size-4" />
- Add a Subscription
+ {t("settings.feeds.add_a_subscription")}
</Button>
</DialogTrigger>
<DialogContent>
@@ -164,6 +166,7 @@ export function FeedsEditorDialog() {
}
export function EditFeedDialog({ feed }: { feed: ZFeed }) {
+ const { t } = useTranslation();
const apiUtils = api.useUtils();
const [open, setOpen] = React.useState(false);
React.useEffect(() => {
@@ -198,7 +201,7 @@ export function EditFeedDialog({ feed }: { feed: ZFeed }) {
<DialogTrigger asChild>
<Button variant="secondary">
<Edit className="mr-2 size-4" />
- Edit
+ {t("actions.edit")}
</Button>
</DialogTrigger>
<DialogContent>
@@ -233,7 +236,7 @@ export function EditFeedDialog({ feed }: { feed: ZFeed }) {
render={({ field }) => {
return (
<FormItem className="flex-1">
- <FormLabel>Name</FormLabel>
+ <FormLabel>{t("common.name")}</FormLabel>
<FormControl>
<Input placeholder="Feed name" type="text" {...field} />
</FormControl>
@@ -248,7 +251,7 @@ export function EditFeedDialog({ feed }: { feed: ZFeed }) {
render={({ field }) => {
return (
<FormItem className="flex-1">
- <FormLabel>URL</FormLabel>
+ <FormLabel>{t("common.url")}</FormLabel>
<FormControl>
<Input placeholder="Feed url" type="text" {...field} />
</FormControl>
@@ -262,7 +265,7 @@ export function EditFeedDialog({ feed }: { feed: ZFeed }) {
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="secondary">
- Close
+ {t("actions.close")}
</Button>
</DialogClose>
<ActionButton
@@ -274,7 +277,7 @@ export function EditFeedDialog({ feed }: { feed: ZFeed }) {
className="items-center"
>
<Save className="mr-2 size-4" />
- Save
+ {t("actions.save")}
</ActionButton>
</DialogFooter>
</DialogContent>
@@ -283,6 +286,7 @@ export function EditFeedDialog({ feed }: { feed: ZFeed }) {
}
export function FeedRow({ feed }: { feed: ZFeed }) {
+ const { t } = useTranslation();
const apiUtils = api.useUtils();
const { mutate: deleteFeed, isPending: isDeleting } =
api.feeds.delete.useMutation({
@@ -340,7 +344,7 @@ export function FeedRow({ feed }: { feed: ZFeed }) {
onClick={() => fetchNow({ feedId: feed.id })}
>
<ArrowDownToLine className="mr-2 size-4" />
- Fetch Now
+ {t("actions.fetch_now")}
</ActionButton>
<ActionConfirmingDialog
title={`Delete Feed "${feed.name}"?`}
@@ -354,13 +358,13 @@ export function FeedRow({ feed }: { feed: ZFeed }) {
type="button"
>
<Trash2 className="mr-2 size-4" />
- Delete
+ {t("actions.delete")}
</ActionButton>
)}
>
<Button variant="destructive" disabled={isDeleting}>
<Trash2 className="mr-2 size-4" />
- Delete
+ {t("actions.delete")}
</Button>
</ActionConfirmingDialog>
</TableCell>
@@ -369,6 +373,7 @@ export function FeedRow({ feed }: { feed: ZFeed }) {
}
export default function FeedSettings() {
+ const { t } = useTranslation();
const { data: feeds, isLoading } = api.feeds.list.useQuery();
return (
<>
@@ -376,12 +381,14 @@ export default function FeedSettings() {
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className="flex items-center gap-2 text-lg font-medium">
- RSS Subscriptions
+ {t("settings.feeds.rss_subscriptions")}
<Tooltip>
<TooltipTrigger className="text-muted-foreground">
<FlaskConical size={15} />
</TooltipTrigger>
- <TooltipContent side="bottom">Experimental</TooltipContent>
+ <TooltipContent side="bottom">
+ {t("common.experimental")}
+ </TooltipContent>
</Tooltip>
</span>
<FeedsEditorDialog />
@@ -396,11 +403,11 @@ export default function FeedSettings() {
<Table>
<TableHeader>
<TableRow>
- <TableHead>Name</TableHead>
- <TableHead>URL</TableHead>
+ <TableHead>{t("common.name")}</TableHead>
+ <TableHead>{t("common.url")}</TableHead>
<TableHead>Last Fetch</TableHead>
<TableHead>Last Status</TableHead>
- <TableHead>Actions</TableHead>
+ <TableHead>{t("common.actions")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
diff --git a/apps/web/components/settings/ImportExport.tsx b/apps/web/components/settings/ImportExport.tsx
index 7889b4d8..5cb35def 100644
--- a/apps/web/components/settings/ImportExport.tsx
+++ b/apps/web/components/settings/ImportExport.tsx
@@ -7,6 +7,7 @@ import { buttonVariants } from "@/components/ui/button";
import FilePickerButton from "@/components/ui/file-picker-button";
import { Progress } from "@/components/ui/progress";
import { toast } from "@/components/ui/use-toast";
+import { useTranslation } from "@/lib/i18n/client";
import {
ParsedBookmark,
parseHoarderBookmarkFile,
@@ -31,6 +32,7 @@ import {
import { BookmarkTypes } from "@hoarder/shared/types/bookmarks";
export function ExportButton() {
+ const { t } = useTranslation();
return (
<Link
href="/api/bookmarks/export"
@@ -40,12 +42,13 @@ export function ExportButton() {
)}
>
<Download />
- <p>Export Links and Notes</p>
+ <p>{t("settings.import.export_links_and_notes")}</p>
</Link>
);
}
export function ImportExportRow() {
+ const { t } = useTranslation();
const router = useRouter();
const [importProgress, setImportProgress] = useState<{
@@ -145,7 +148,7 @@ export function ImportExportRow() {
},
onSuccess: async (resp) => {
const importList = await createList({
- name: `Imported Bookmarks`,
+ name: t("settings.import.imported_bookmarks"),
icon: "⬆️",
});
setImportProgress({ done: 0, total: resp.length });
@@ -211,7 +214,7 @@ export function ImportExportRow() {
}
>
<Upload />
- <p>Import Bookmarks from HTML file</p>
+ <p>{t("settings.import.import_bookmarks_from_html_file")}</p>
</FilePickerButton>
<FilePickerButton
@@ -224,7 +227,7 @@ export function ImportExportRow() {
}
>
<Upload />
- <p>Import Bookmarks from Pocket export</p>
+ <p>{t("settings.import.import_bookmarks_from_pocket_export")}</p>
</FilePickerButton>
<FilePickerButton
loading={false}
@@ -236,7 +239,7 @@ export function ImportExportRow() {
}
>
<Upload />
- <p>Import Bookmarks from Omnivore export</p>
+ <p>{t("settings.import.import_bookmarks_from_omnivore_export")}</p>
</FilePickerButton>
<FilePickerButton
loading={false}
@@ -248,7 +251,7 @@ export function ImportExportRow() {
}
>
<Upload />
- <p>Import Bookmarks from Hoarder export</p>
+ <p>{t("settings.import.import_bookmarks_from_hoarder_export")}</p>
</FilePickerButton>
<ExportButton />
</div>
@@ -269,9 +272,12 @@ export function ImportExportRow() {
}
export default function ImportExport() {
+ const { t } = useTranslation();
return (
<div className="flex w-full flex-col gap-2">
- <p className="mb-4 text-lg font-medium">Import / Export Bookmarks</p>
+ <p className="mb-4 text-lg font-medium">
+ {t("settings.import.import_export_bookmarks")}
+ </p>
<ImportExportRow />
</div>
);
diff --git a/apps/web/components/settings/UserDetails.tsx b/apps/web/components/settings/UserDetails.tsx
index 471a6e09..af6698ad 100644
--- a/apps/web/components/settings/UserDetails.tsx
+++ b/apps/web/components/settings/UserDetails.tsx
@@ -1,24 +1,26 @@
import { Input } from "@/components/ui/input";
+import { useTranslation } from "@/lib/i18n/server";
import { api } from "@/server/api/client";
export default async function UserDetails() {
+ const { t } = await useTranslation();
const whoami = await api.users.whoami();
const details = [
{
- label: "Name",
+ label: t("common.name"),
value: whoami.name ?? undefined,
},
{
- label: "Email",
+ label: t("common.email"),
value: whoami.email ?? undefined,
},
];
return (
- <div className="mb-8 flex w-full flex-col sm:flex-row">
+ <div className="flex w-full flex-col sm:flex-row">
<div className="mb-4 w-full text-lg font-medium sm:w-1/3">
- Basic Details
+ {t("settings.info.basic_details")}
</div>
<div className="w-full">
{details.map(({ label, value }) => (
diff --git a/apps/web/components/settings/UserOptions.tsx b/apps/web/components/settings/UserOptions.tsx
new file mode 100644
index 00000000..38dc1520
--- /dev/null
+++ b/apps/web/components/settings/UserOptions.tsx
@@ -0,0 +1,55 @@
+"use client";
+
+import { useTranslation } from "@/lib/i18n/client";
+import { useInterfaceLang } from "@/lib/userLocalSettings/bookmarksLayout";
+import { updateInterfaceLang } from "@/lib/userLocalSettings/userLocalSettings";
+
+import { langNameMappings } from "@hoarder/shared/langs";
+
+import { Label } from "../ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "../ui/select";
+
+const LanguageSelect = () => {
+ const lang = useInterfaceLang();
+ return (
+ <Select
+ value={lang}
+ onValueChange={async (val) => {
+ await updateInterfaceLang(val);
+ }}
+ >
+ <SelectTrigger>
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ {Object.entries(langNameMappings).map(([lang, name]) => (
+ <SelectItem key={lang} value={lang}>
+ {name}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ );
+};
+
+export function UserOptions() {
+ const { t } = useTranslation();
+
+ return (
+ <div className="flex flex-col sm:flex-row">
+ <div className="mb-4 w-full text-lg font-medium sm:w-1/3">
+ {t("settings.info.options")}
+ </div>
+ <div className="flex w-full flex-col gap-2">
+ <Label>{t("settings.info.interface_lang")}</Label>
+ <LanguageSelect />
+ </div>
+ </div>
+ );
+}
diff --git a/apps/web/components/settings/sidebar/ModileSidebar.tsx b/apps/web/components/settings/sidebar/ModileSidebar.tsx
index 2016c931..cbed9ef9 100644
--- a/apps/web/components/settings/sidebar/ModileSidebar.tsx
+++ b/apps/web/components/settings/sidebar/ModileSidebar.tsx
@@ -1,12 +1,14 @@
import MobileSidebarItem from "@/components/shared/sidebar/ModileSidebarItem";
+import { useTranslation } from "@/lib/i18n/server";
import { settingsSidebarItems } from "./items";
export default async function MobileSidebar() {
+ const { t } = await useTranslation();
return (
<aside className="w-full">
<ul className="flex justify-between space-x-2 border-b-black px-5 py-2 pt-5">
- {settingsSidebarItems.map((item) => (
+ {settingsSidebarItems(t).map((item) => (
<MobileSidebarItem
key={item.name}
logo={item.icon}
diff --git a/apps/web/components/settings/sidebar/Sidebar.tsx b/apps/web/components/settings/sidebar/Sidebar.tsx
index 247e0916..a1b61e98 100644
--- a/apps/web/components/settings/sidebar/Sidebar.tsx
+++ b/apps/web/components/settings/sidebar/Sidebar.tsx
@@ -1,5 +1,6 @@
import { redirect } from "next/navigation";
import SidebarItem from "@/components/shared/sidebar/SidebarItem";
+import { useTranslation } from "@/lib/i18n/server";
import { getServerAuthSession } from "@/server/auth";
import serverConfig from "@hoarder/shared/config";
@@ -7,6 +8,7 @@ import serverConfig from "@hoarder/shared/config";
import { settingsSidebarItems } from "./items";
export default async function Sidebar() {
+ const { t } = await useTranslation();
const session = await getServerAuthSession();
if (!session) {
redirect("/");
@@ -16,7 +18,7 @@ export default async function Sidebar() {
<aside className="flex h-[calc(100vh-64px)] w-60 flex-col gap-5 border-r p-4 ">
<div>
<ul className="space-y-2 text-sm font-medium">
- {settingsSidebarItems.map((item) => (
+ {settingsSidebarItems(t).map((item) => (
<SidebarItem
key={item.name}
logo={item.icon}
diff --git a/apps/web/components/settings/sidebar/items.tsx b/apps/web/components/settings/sidebar/items.tsx
index 047ee233..43dfabdd 100644
--- a/apps/web/components/settings/sidebar/items.tsx
+++ b/apps/web/components/settings/sidebar/items.tsx
@@ -1,4 +1,5 @@
import React from "react";
+import { TFunction } from "i18next";
import {
ArrowLeft,
Download,
@@ -8,38 +9,40 @@ import {
User,
} from "lucide-react";
-export const settingsSidebarItems: {
+export const settingsSidebarItems = (
+ t: TFunction,
+): {
name: string;
icon: JSX.Element;
path: string;
-}[] = [
+}[] => [
{
- name: "Back To App",
+ name: t("settings.back_to_app"),
icon: <ArrowLeft size={18} />,
path: "/dashboard/bookmarks",
},
{
- name: "User Info",
+ name: t("settings.info.user_info"),
icon: <User size={18} />,
path: "/settings/info",
},
{
- name: "AI Settings",
+ name: t("settings.ai.ai_settings"),
icon: <Sparkles size={18} />,
path: "/settings/ai",
},
{
- name: "RSS Subscriptions",
+ name: t("settings.feeds.rss_subscriptions"),
icon: <Rss size={18} />,
path: "/settings/feeds",
},
{
- name: "Import / Export",
+ name: t("settings.import.import_export"),
icon: <Download size={18} />,
path: "/settings/import",
},
{
- name: "API Keys",
+ name: t("settings.api_keys.api_keys"),
icon: <KeyRound size={18} />,
path: "/settings/api-keys",
},
diff --git a/apps/web/components/ui/action-confirming-dialog.tsx b/apps/web/components/ui/action-confirming-dialog.tsx
index cfd38fc3..e624d287 100644
--- a/apps/web/components/ui/action-confirming-dialog.tsx
+++ b/apps/web/components/ui/action-confirming-dialog.tsx
@@ -8,6 +8,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
+import { useTranslation } from "@/lib/i18n/client";
import { Button } from "./button";
@@ -26,6 +27,7 @@ export default function ActionConfirmingDialog({
actionButton: (setDialogOpen: (open: boolean) => void) => React.ReactNode;
children?: React.ReactNode;
}) {
+ const { t } = useTranslation();
const [customIsOpen, setCustomIsOpen] = useState(false);
const [isDialogOpen, setDialogOpen] = [
userIsOpen ?? customIsOpen,
@@ -42,7 +44,7 @@ export default function ActionConfirmingDialog({
<DialogFooter className="sm:justify-end">
<DialogClose asChild>
<Button type="button" variant="secondary">
- Close
+ {t("actions.close")}
</Button>
</DialogClose>
{actionButton(setDialogOpen)}