From 4354ee7ba1c6ac9a9567944ae6169b1664e0ea8a Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 17 Nov 2024 00:33:28 +0000 Subject: 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 --- apps/web/@types/i18next.d.ts | 13 ++ apps/web/app/dashboard/cleanups/page.tsx | 9 +- apps/web/app/dashboard/lists/page.tsx | 4 +- apps/web/app/dashboard/tags/page.tsx | 4 +- apps/web/app/layout.tsx | 13 +- apps/web/app/settings/info/page.tsx | 4 +- .../components/dashboard/BulkBookmarksAction.tsx | 28 +-- apps/web/components/dashboard/ChangeLayout.tsx | 13 +- apps/web/components/dashboard/EditableText.tsx | 9 +- .../components/dashboard/admin/AdminActions.tsx | 19 +- .../web/components/dashboard/admin/ServerStats.tsx | 38 ++-- apps/web/components/dashboard/admin/UserList.tsx | 30 +-- .../dashboard/bookmarks/BookmarkOptions.tsx | 43 +++-- .../components/dashboard/bookmarks/EditorCard.tsx | 26 ++- .../dashboard/bookmarks/ManageListsModal.tsx | 16 +- .../dashboard/bookmarks/SummarizeBookmarkArea.tsx | 4 +- .../components/dashboard/bookmarks/TagModal.tsx | 6 +- .../dashboard/cleanups/TagDuplicationDetention.tsx | 13 +- .../components/dashboard/header/ProfileOptions.tsx | 13 +- .../components/dashboard/lists/AllListsView.tsx | 8 +- .../components/dashboard/lists/EditListModal.tsx | 14 +- .../web/components/dashboard/lists/ListOptions.tsx | 9 +- .../web/components/dashboard/preview/ActionBar.tsx | 10 +- .../components/dashboard/preview/AttachmentBox.tsx | 4 +- .../dashboard/preview/BookmarkPreview.tsx | 8 +- .../dashboard/preview/LinkContentSection.tsx | 12 +- .../components/dashboard/search/SearchInput.tsx | 4 +- apps/web/components/dashboard/sidebar/AllLists.tsx | 6 +- apps/web/components/dashboard/sidebar/Sidebar.tsx | 10 +- apps/web/components/dashboard/tags/AllTagsView.tsx | 25 +-- apps/web/components/dashboard/tags/TagOptions.tsx | 6 +- apps/web/components/settings/AISettings.tsx | 34 ++-- apps/web/components/settings/AddApiKey.tsx | 32 +++- apps/web/components/settings/ApiKeySettings.tsx | 14 +- apps/web/components/settings/ChangePassword.tsx | 20 +- apps/web/components/settings/DeleteApiKey.tsx | 4 +- apps/web/components/settings/FeedSettings.tsx | 35 ++-- apps/web/components/settings/ImportExport.tsx | 20 +- apps/web/components/settings/UserDetails.tsx | 10 +- apps/web/components/settings/UserOptions.tsx | 55 ++++++ .../components/settings/sidebar/ModileSidebar.tsx | 4 +- apps/web/components/settings/sidebar/Sidebar.tsx | 4 +- apps/web/components/settings/sidebar/items.tsx | 19 +- .../web/components/ui/action-confirming-dialog.tsx | 4 +- apps/web/lib/i18n/client.ts | 33 ++++ apps/web/lib/i18n/locales/en/translation.json | 206 +++++++++++++++++++++ apps/web/lib/i18n/provider.tsx | 18 ++ apps/web/lib/i18n/server.ts | 36 ++++ apps/web/lib/i18n/settings.ts | 17 ++ apps/web/lib/providers.tsx | 21 ++- apps/web/lib/userLocalSettings/bookmarksLayout.tsx | 7 + apps/web/lib/userLocalSettings/types.ts | 1 + .../web/lib/userLocalSettings/userLocalSettings.ts | 21 ++- apps/web/package.json | 4 + 54 files changed, 790 insertions(+), 250 deletions(-) create mode 100644 apps/web/@types/i18next.d.ts create mode 100644 apps/web/components/settings/UserOptions.tsx create mode 100644 apps/web/lib/i18n/client.ts create mode 100644 apps/web/lib/i18n/locales/en/translation.json create mode 100644 apps/web/lib/i18n/provider.tsx create mode 100644 apps/web/lib/i18n/server.ts create mode 100644 apps/web/lib/i18n/settings.ts (limited to 'apps') diff --git a/apps/web/@types/i18next.d.ts b/apps/web/@types/i18next.d.ts new file mode 100644 index 00000000..62c42b7b --- /dev/null +++ b/apps/web/@types/i18next.d.ts @@ -0,0 +1,13 @@ +import "i18next"; + +import translation from "../lib/i18n/locales/en/translation.json"; + +declare module "i18next" { + // Extend CustomTypeOptions + interface CustomTypeOptions { + defaultNS: "translation"; + resources: { + translation: typeof translation; + }; + } +} diff --git a/apps/web/app/dashboard/cleanups/page.tsx b/apps/web/app/dashboard/cleanups/page.tsx index ca9187ee..1974d2a7 100644 --- a/apps/web/app/dashboard/cleanups/page.tsx +++ b/apps/web/app/dashboard/cleanups/page.tsx @@ -1,18 +1,21 @@ import { TagDuplicationDetection } from "@/components/dashboard/cleanups/TagDuplicationDetention"; import { Separator } from "@/components/ui/separator"; +import { useTranslation } from "@/lib/i18n/server"; import { Paintbrush, Tags } from "lucide-react"; -export default function Cleanups() { +export default async function Cleanups() { + const { t } = await useTranslation(); + return (
- Cleanups + {t("cleanups.cleanups")} - Duplicate Tags + {t("cleanups.duplicate_tags.title")} diff --git a/apps/web/app/dashboard/lists/page.tsx b/apps/web/app/dashboard/lists/page.tsx index 1c22ac32..36eb8b7a 100644 --- a/apps/web/app/dashboard/lists/page.tsx +++ b/apps/web/app/dashboard/lists/page.tsx @@ -1,13 +1,15 @@ import AllListsView from "@/components/dashboard/lists/AllListsView"; import { Separator } from "@/components/ui/separator"; +import { useTranslation } from "@/lib/i18n/server"; import { api } from "@/server/api/client"; export default async function ListsPage() { + const { t } = await useTranslation(); const lists = await api.lists.list(); return (
-

📋 All Lists

+

📋 {t("lists.all_lists")}

diff --git a/apps/web/app/dashboard/tags/page.tsx b/apps/web/app/dashboard/tags/page.tsx index 6caea513..1639e4c5 100644 --- a/apps/web/app/dashboard/tags/page.tsx +++ b/apps/web/app/dashboard/tags/page.tsx @@ -1,13 +1,15 @@ import AllTagsView from "@/components/dashboard/tags/AllTagsView"; import { Separator } from "@/components/ui/separator"; +import { useTranslation } from "@/lib/i18n/server"; import { api } from "@/server/api/client"; export default async function TagsPage() { + const { t } = await useTranslation(); const allTags = (await api.tags.list()).tags; return (
- All Tags + {t("tags.all_tags")}
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 6d8b10ed..7d3858eb 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -5,14 +5,9 @@ import "@hoarder/tailwind-config/globals.css"; import type { Viewport } from "next"; import React from "react"; -import { cookies } from "next/headers"; import { Toaster } from "@/components/ui/toaster"; import Providers from "@/lib/providers"; -import { - defaultUserLocalSettings, - parseUserLocalSettings, - USER_LOCAL_SETTINGS_COOKIE_NAME, -} from "@/lib/userLocalSettings/types"; +import { getUserLocalSettings } from "@/lib/userLocalSettings/userLocalSettings"; import { getServerAuthSession } from "@/server/auth"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; @@ -57,11 +52,7 @@ export default async function RootLayout({ {children} diff --git a/apps/web/app/settings/info/page.tsx b/apps/web/app/settings/info/page.tsx index 8027b09f..c7d8f808 100644 --- a/apps/web/app/settings/info/page.tsx +++ b/apps/web/app/settings/info/page.tsx @@ -1,11 +1,13 @@ import { ChangePassword } from "@/components/settings/ChangePassword"; import UserDetails from "@/components/settings/UserDetails"; +import { UserOptions } from "@/components/settings/UserOptions"; export default async function InfoPage() { return ( -
+
+
); } 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: , action: () => copyLinks(), @@ -187,55 +189,57 @@ export default function BulkBookmarksAction() { hidden: !isBulkEditEnabled, }, { - name: "Add to List", + name: t("actions.add_to_list"), icon: , action: () => setManageListsModalOpen(true), isPending: false, hidden: !isBulkEditEnabled, }, { - name: "Edit Tags", + name: t("actions.edit_tags"), icon: , action: () => setBulkTagModalOpen(true), isPending: false, hidden: !isBulkEditEnabled, }, { - name: alreadyFavourited ? "Unfavourite" : "Favourite", + name: alreadyFavourited ? t("actions.unfavorite") : t("actions.favorite"), icon: , action: () => updateBookmarks({ favourited: !alreadyFavourited }), isPending: updateBookmarkMutator.isPending, hidden: !isBulkEditEnabled, }, { - name: alreadyArchived ? "Un-archive" : "Archive", + name: alreadyArchived ? t("actions.unarchive") : t("actions.archive"), icon: , action: () => updateBookmarks({ archived: !alreadyArchived }), isPending: updateBookmarkMutator.isPending, hidden: !isBulkEditEnabled, }, { - name: "Download Full Page Archive", + name: t("actions.download_full_page_archive"), icon: , action: () => recrawlBookmarks(true), isPending: recrawlBookmarkMutator.isPending, hidden: !isBulkEditEnabled, }, { - name: "Refresh", + name: t("actions.refresh"), icon: , action: () => recrawlBookmarks(false), isPending: recrawlBookmarkMutator.isPending, hidden: !isBulkEditEnabled, }, { - name: "Delete", + name: t("actions.delete"), icon: , action: () => setIsDeleteDialogOpen(true), hidden: !isBulkEditEnabled, }, { - name: isEverythingSelected() ? "Unselect All" : "Select All", + name: isEverythingSelected() + ? t("actions.unselect_all") + : t("actions.select_all"), icon: (

( {selectedBookmarks.length} ) @@ -247,14 +251,14 @@ export default function BulkBookmarksAction() { hidden: !isBulkEditEnabled, }, { - name: "Close bulk edit", + name: t("actions.close_bulk_edit"), icon: , action: () => setIsBulkEditEnabled(false), alwaysEnable: true, hidden: !isBulkEditEnabled, }, { - name: "Bulk Edit", + name: t("actions.bulk_edit"), icon: , action: () => setIsBulkEditEnabled(true), alwaysEnable: true, @@ -276,7 +280,7 @@ export default function BulkBookmarksAction() { loading={deleteBookmarkMutator.isPending} onClick={() => deleteBookmarks()} > - Delete + {t("actions.delete")} )} /> 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 = { masonry: LayoutDashboard, grid: LayoutGrid, list: LayoutList, @@ -28,13 +30,14 @@ const iconMap = { }; export default function ChangeLayout() { + const { t } = useTranslation(); const layout = useBookmarkLayout(); return ( @@ -42,7 +45,7 @@ export default function ChangeLayout() { - {Object.keys(iconMap).map((key) => ( + {(Object.keys(iconMap) as LayoutType[]).map((key) => (

{React.createElement(iconMap[key as LayoutType], { size: 18 })} - {key} + {t(`layouts.${key}`)}
{layout == key && } 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(null); useEffect(() => { @@ -63,7 +65,7 @@ function EditMode({ }} />
@@ -107,7 +110,7 @@ function ViewMode({ { @@ -69,7 +71,7 @@ export default function AdminActions() { return (
-
Actions
+
{t("common.actions")}
- Recrawl Failed Links Only + {t("admin.actions.recrawl_failed_links_only")} - Recrawl All Links + {t("admin.actions.recrawl_all_links")} - Recrawl All Links (Without Inference) + {t("admin.actions.recrawl_all_links")} ( + {t("admin.actions.without_inference")}) - Regenerate AI Tags for Failed Bookmarks Only + {t("admin.actions.regenerate_ai_tags_for_failed_bookmarks_only")} reRunInferenceOnAllBookmarks({ taggingStatus: "all" })} > - Regenerate AI Tags for All Bookmarks + {t("admin.actions.regenerate_ai_tags_for_all_bookmarks")} reindexBookmarks()} > - Reindex All Bookmarks + {t("admin.actions.reindex_all_bookmarks")} tidyAssets()} > - Compact Assets + {t("admin.actions.compact_assets")}
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 ( <> -
Server Stats
+
+ {t("admin.server_stats.server_stats")} +
-
Total Users
+
+ {t("admin.server_stats.total_users")} +
{serverStats.numUsers}
- Total Bookmarks + {t("admin.server_stats.total_bookmarks")}
{serverStats.numBookmarks} @@ -88,42 +94,48 @@ export default function ServerStats() {
- Server Version + {t("admin.server_stats.server_version")}
-
Background Jobs
+
+ {t("admin.background_jobs.background_jobs")} +
- Job - Queued - Pending - Failed + {t("admin.background_jobs.job")} + {t("admin.background_jobs.queued")} + {t("admin.background_jobs.pending")} + {t("admin.background_jobs.failed")} - Crawling Jobs + + {t("admin.background_jobs.crawler_jobs")} + {serverStats.crawlStats.queued} {serverStats.crawlStats.pending} {serverStats.crawlStats.failed} - Indexing Jobs + {t("admin.background_jobs.indexing_jobs")} {serverStats.indexingStats.queued} - - - Inference Jobs + {t("admin.background_jobs.inference_jobs")} {serverStats.inferenceStats.queued} {serverStats.inferenceStats.pending} {serverStats.inferenceStats.failed} - Tidy Assets Jobs + + {t("admin.background_jobs.tidy_assets_jobs")} + {serverStats.tidyAssetsStats.queued} - - 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 ( <>
- Users List + {t("admin.users_list.users_list")} @@ -65,13 +67,13 @@ export default function UsersSection() {
- Name - Email - Num Bookmarks - Asset Sizes - Role - Local User - Actions + {t("common.name")} + {t("common.email")} + {t("admin.users_list.num_bookmarks")} + {t("admin.users_list.asset_sizes")} + {t("common.role")} + {t("admin.users_list.local_user")} + {t("common.actions")} {users.users.map((u) => ( @@ -84,13 +86,15 @@ export default function UsersSection() { {toHumanReadableSize(userStats[u.id].assetSizes)} - {u.role} - + + {u.role && t(`common.roles.${u.role}`)} + + {u.localUser ? : } deleteUser({ userId: u.id })} loading={isDeletionPending} @@ -100,7 +104,7 @@ export default function UsersSection() { @@ -109,7 +113,7 @@ export default function UsersSection() { 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} /> - {bookmark.favourited ? "Un-favourite" : "Favourite"} + + {bookmark.favourited + ? t("actions.unfavorite") + : t("actions.favorite")} + - {bookmark.archived ? "Un-archive" : "Archive"} + + {bookmark.archived + ? t("actions.unarchive") + : t("actions.archive")} + {bookmark.content.type === BookmarkTypes.LINK && ( @@ -173,7 +182,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { }} > - Download Full Page Archive + {t("actions.download_full_page_archive")} )} @@ -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"), }); }} > - Copy Link + {t("actions.copy_link")} )} setTagModalIsOpen(true)}> - Edit Tags + {t("actions.edit_tags")} setManageListsModalOpen(true)}> - Manage Lists + {t("actions.manage_lists")} {listId && ( @@ -213,7 +222,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { } > - Remove from List + {t("actions.remove_from_list")} )} @@ -225,7 +234,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { } > - Refresh + {t("actions.refresh")} )} - Delete + {t("actions.delete")} 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(null); const [multiUrlImportState, setMultiUrlImportState] = @@ -181,11 +183,9 @@ export default function EditorCard({ className }: { className?: string }) { onSubmit={form.handleSubmit(onSubmit, onError)} >
-

NEW ITEM

+

{t("editor.new_item")}

-

- You can quickly focus on this field by pressing ⌘ + E -

+

{t("editor.quickly_focus")}

@@ -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 }) { {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")} {multiUrlImportState && ( { 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")} ), () => ( @@ -267,7 +265,7 @@ export default function EditorCard({ className }: { className?: string }) { setMultiUrlImportState(null); }} > - Import as separate Bookmarks + {t("editor.import_as_separate_bookmarks")} ), ]} 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({ setOpen(false)} > - Archive + {t("actions.archive")} - Add + {t("actions.add")} 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({ )} - Summarize with AI + {t("actions.summarize_with_ai")} 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 ( - Edit Tags + {t("actions.edit_tags")} 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 ( ( applyAll(setDialogOpen)} > - Apply All + {t("actions.apply_all")} )} > ); @@ -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({ } > - Merge + {t("actions.merge")}
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 ( <> - Light Mode + {t("options.light_mode")} ); } else { return ( <> - Dark Mode + {t("options.dark_mode")} ); } } 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() { - User Settings + {t("settings.user_settings")} {session.user.role == "admin" && ( - Admin Settings + {t("admin.admin_settings")} )} @@ -94,7 +97,7 @@ export default function SidebarProfileOptions() { } > - Sign Out + {t("actions.sign_out")} 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 (
    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({ - {list ? "Save" : "Create"} + {list ? t("actions.save") : t("actions.create")} 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)} > - Edit + {t("actions.edit")} setNewNestedListModalOpen(true)} > - New nested list + {t("lists.new_nested_list")} setDeleteListDialogOpen(true)} > - Delete + {t("actions.delete")} 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 }) { - {bookmark.favourited ? "Un-favourite" : "Favourite"} + {bookmark.favourited + ? t("actions.unfavorite") + : t("actions.favorite")} @@ -92,7 +96,7 @@ export default function ActionBar({ bookmark }: { bookmark: ZBookmark }) { - {bookmark.archived ? "Un-archive" : "Archive"} + {bookmark.archived ? t("actions.unarchive") : t("actions.archive")} @@ -108,7 +112,7 @@ export default function ActionBar({ bookmark }: { bookmark: ZBookmark }) { - Delete + {t("actions.delete")} ); 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 = { screenshot: , fullPageArchive: , @@ -109,7 +111,7 @@ export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) { return ( - Attachments + {t("common.attachments")} 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" > - View Original + {t("preview.view_original")} )} @@ -140,11 +142,11 @@ export default function BookmarkPreview({
    -

    Tags

    +

    {t("common.tags")}

    -

    Note

    +

    {t("common.note")}

    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("cached"); if (bookmark.content.type != BookmarkTypes.LINK) { @@ -104,21 +106,23 @@ export default function LinkContentSection({ - Cached Content + + {t("preview.cached_content")} + - Screenshot + {t("common.screenshot")} - Archive + {t("common.archive")} - Video + {t("common.video")} 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, @@ -45,6 +46,7 @@ const SearchInput = React.forwardRef< HTMLInputElement, React.HTMLAttributes & { 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({ 📋} - name="All Lists" + name={t("lists.all_lists")} path={`/dashboard/lists`} linkClassName="py-0.5" /> ⭐️} - 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: , path: "/dashboard/search", }, @@ -33,18 +35,18 @@ export default async function Sidebar() { path: string; }[] = [ { - name: "Home", + name: t("common.home"), icon: , path: "/dashboard/bookmarks", }, ...searchItem, { - name: "Tags", + name: t("common.tags"), icon: , path: "/dashboard/tags", }, { - name: "Archive", + name: t("common.archive"), icon: , 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 ( ( ); @@ -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} > - Drag & Drop Merging + {t("tags.drag_and_drop_merging")} -

    Drag and drop tags on each other to merge them

    +

    {t("tags.drag_and_drop_merging_info")}

    - Sort by Name + {t("tags.sort_by_name")} -

    Your Tags

    +

    {t("tags.your_tags")}

    -

    Tags that were attached at least once by you

    +

    {t("tags.your_tags_info")}

    {tagsToPill(humanTags)} -

    AI Tags

    +

    {t("tags.ai_tags")}

    -

    Tags that were only attached automatically (by AI)

    +

    {t("tags.ai_tags_info")}

    {tagsToPill(aiTags)} -

    Unused Tags

    +

    {t("tags.unused_tags")}

    -

    Tags that are not attached to any bookmarks

    +

    {t("tags.unused_tags_info")}

    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)} > - Merge + {t("actions.merge")} setDeleteTagDialogOpen(true)} > - Delete + {t("actions.delete")} 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>({ @@ -117,7 +119,7 @@ export function PromptEditor() { className="items-center" > - Add + {t("actions.add")} @@ -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 ( - + @@ -234,7 +233,7 @@ export function PromptRow({ prompt }: { prompt: ZPrompt }) { className="items-center" > - Save + {t("actions.save")} - Delete + {t("actions.delete")} @@ -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 (
    -
    Tagging Rules
    +
    + {t("settings.ai.tagging_rules")} +

    - 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")}

    {isLoading && } {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 (
    - Prompt Preview + {t("settings.ai.prompt_preview")}
    -

    Text Prompt

    +

    {t("settings.ai.text_prompt")}

    {buildTextPrompt( clientConfig.inference.inferredTagLang, @@ -294,7 +295,7 @@ export function PromptDemo() { /* context length */ 1024 /* The value here doesn't matter */, ).trim()} -

    Image Prompt

    +

    {t("settings.ai.images_prompt")}

    {buildImagePrompt( clientConfig.inference.inferredTagLang, @@ -308,12 +309,13 @@ export function PromptDemo() { } export default function AISettings() { + const { t } = useTranslation(); return ( <>
    - AI Settings + {t("settings.ai.ai_settings")}
    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 (
    - Note: please copy the key and store it somewhere safe. Once you close - the dialog, you won't be able to access it again. + {t("settings.api_keys.key_success_please_copy")}
    @@ -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 ( - Name + {t("common.name")} - + - Give your API key a unique name + {t("settings.api_keys.new_api_key_desc")} @@ -112,7 +121,7 @@ function AddApiKeyForm({ onSuccess }: { onSuccess: (key: string) => void }) { type="submit" loading={mutator.isPending} > - Create + {t("actions.create")} @@ -120,17 +129,20 @@ function AddApiKeyForm({ onSuccess }: { onSuccess: (key: string) => void }) { } export default function AddApiKey() { + const { t } = useTranslation(); const [key, setKey] = useState(undefined); const [dialogOpen, setDialogOpen] = useState(false); return ( - + - {key ? "Key was successfully created" : "Create API key"} + {key + ? t("settings.api_keys.key_success") + : t("settings.api_keys.new_api_key")} {key ? ( @@ -147,7 +159,7 @@ export default function AddApiKey() { variant="outline" onClick={() => setKey(undefined)} > - Close + {t("actions.close")} 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 (
    -
    API Keys
    +
    + {t("settings.api_keys.api_keys")} +
- Name - Key - Created At - Action + {t("common.name")} + {t("common.key")} + {t("common.created_at")} + {t("common.action")} 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>({ resolver: zodResolver(zChangePasswordSchema), defaultValues: { @@ -55,7 +57,7 @@ export function ChangePassword() { return (
- Change Password + {t("settings.info.change_password")}
{ return ( - Current Password + {t("settings.info.current_password")} @@ -87,11 +89,11 @@ export function ChangePassword() { render={({ field }) => { return ( - New Password + {t("settings.info.new_password")} @@ -106,11 +108,13 @@ export function ChangePassword() { render={({ field }) => { return ( - Confirm New Password + + {t("settings.info.confirm_new_password")} + @@ -124,7 +128,7 @@ export function ChangePassword() { type="submit" loading={mutator.isPending} > - Save + {t("actions.save")} 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")} )} > 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() { @@ -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 }) { @@ -233,7 +236,7 @@ export function EditFeedDialog({ feed }: { feed: ZFeed }) { render={({ field }) => { return ( - Name + {t("common.name")} @@ -248,7 +251,7 @@ export function EditFeedDialog({ feed }: { feed: ZFeed }) { render={({ field }) => { return ( - URL + {t("common.url")} @@ -262,7 +265,7 @@ export function EditFeedDialog({ feed }: { feed: ZFeed }) { - Save + {t("actions.save")} @@ -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 })} > - Fetch Now + {t("actions.fetch_now")} - Delete + {t("actions.delete")} )} > @@ -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() {
- RSS Subscriptions + {t("settings.feeds.rss_subscriptions")} - Experimental + + {t("common.experimental")} + @@ -396,11 +403,11 @@ export default function FeedSettings() {
- Name - URL + {t("common.name")} + {t("common.url")} Last Fetch Last Status - Actions + {t("common.actions")} 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 ( -

Export Links and Notes

+

{t("settings.import.export_links_and_notes")}

); } 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() { } > -

Import Bookmarks from HTML file

+

{t("settings.import.import_bookmarks_from_html_file")}

-

Import Bookmarks from Pocket export

+

{t("settings.import.import_bookmarks_from_pocket_export")}

-

Import Bookmarks from Omnivore export

+

{t("settings.import.import_bookmarks_from_omnivore_export")}

-

Import Bookmarks from Hoarder export

+

{t("settings.import.import_bookmarks_from_hoarder_export")}

@@ -269,9 +272,12 @@ export function ImportExportRow() { } export default function ImportExport() { + const { t } = useTranslation(); return (
-

Import / Export Bookmarks

+

+ {t("settings.import.import_export_bookmarks")} +

); 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 ( -
+
- Basic Details + {t("settings.info.basic_details")}
{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 ( + + ); +}; + +export function UserOptions() { + const { t } = useTranslation(); + + return ( +
+
+ {t("settings.info.options")} +
+
+ + +
+
+ ); +} 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 (