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/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 ++++---- 12 files changed, 176 insertions(+), 75 deletions(-) create mode 100644 apps/web/components/settings/UserOptions.tsx (limited to 'apps/web/components/settings') 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 (