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