diff options
| author | Mohamed Bassem <me@mbassem.com> | 2024-11-17 00:33:28 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-11-17 00:33:28 +0000 |
| commit | 4354ee7ba1c6ac9a9567944ae6169b1664e0ea8a (patch) | |
| tree | e27c9070930514d77582bae00b3350274116179c /apps/web/components/dashboard | |
| parent | 9f2c7be23769bb0f4102736a683710b1a1939661 (diff) | |
| download | karakeep-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')
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> |
