diff --git a/src/components/command-palette/index.tsx b/src/components/command-palette/index.tsx index bf82cc973..d67068a3a 100644 --- a/src/components/command-palette/index.tsx +++ b/src/components/command-palette/index.tsx @@ -1,3 +1,4 @@ +import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { useRef } from "react"; import { useHotkeys } from "react-hotkeys-hook"; @@ -81,33 +82,65 @@ export function CommandPalette() { - Builder Command Palette + + Builder Command Palette + - Type a command or search... + + Type a command or search... + - The command you're looking for doesn't exist. + + The command you're looking for doesn't exist. + diff --git a/src/components/command-palette/pages/resumes.tsx b/src/components/command-palette/pages/resumes.tsx index c28d93298..e89e17c20 100644 --- a/src/components/command-palette/pages/resumes.tsx +++ b/src/components/command-palette/pages/resumes.tsx @@ -73,7 +73,9 @@ export function ResumesCommandGroup() { {resume.name} - Press Enter to open + + Press Enter to open + )) diff --git a/src/components/input/chip-input.tsx b/src/components/input/chip-input.tsx index 22ac6d17e..ded50bbea 100644 --- a/src/components/input/chip-input.tsx +++ b/src/components/input/chip-input.tsx @@ -14,6 +14,7 @@ import { useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; +import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { PencilSimpleIcon, XIcon } from "@phosphor-icons/react"; import { motion } from "motion/react"; @@ -78,7 +79,11 @@ function ChipItem({ id, chip, index, isEditing, onEdit, onRemove }: ChipItemProp ); } @@ -260,10 +268,16 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { @@ -206,7 +216,7 @@ const CopyApiKeyForm = ({ apiKey }: CopyApiKeyFormProps) => { diff --git a/src/dialogs/auth/change-password.tsx b/src/dialogs/auth/change-password.tsx index 906ba7f32..8748361db 100644 --- a/src/dialogs/auth/change-password.tsx +++ b/src/dialogs/auth/change-password.tsx @@ -14,6 +14,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from " import { Input } from "@/components/ui/input"; import { useFormBlocker } from "@/hooks/use-form-blocker"; import { authClient } from "@/integrations/auth/client"; +import { getReadableErrorMessage } from "@/utils/error-message"; import { type DialogProps, useDialogStore } from "../store"; @@ -55,7 +56,16 @@ export function ChangePasswordDialog(_: DialogProps<"auth.change-password">) { }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + getReadableErrorMessage( + error, + t({ + comment: "Fallback toast when changing account password fails", + message: "Failed to update your password. Please try again.", + }), + ), + { id: toastId }, + ); return; } @@ -100,6 +110,17 @@ export function ChangePasswordDialog(_: DialogProps<"auth.change-password">) { /> @@ -130,6 +151,17 @@ export function ChangePasswordDialog(_: DialogProps<"auth.change-password">) { /> @@ -140,7 +172,7 @@ export function ChangePasswordDialog(_: DialogProps<"auth.change-password">) { diff --git a/src/dialogs/auth/disable-two-factor.tsx b/src/dialogs/auth/disable-two-factor.tsx index 2f69ada88..9cfdc98b9 100644 --- a/src/dialogs/auth/disable-two-factor.tsx +++ b/src/dialogs/auth/disable-two-factor.tsx @@ -14,6 +14,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from " import { Input } from "@/components/ui/input"; import { useFormBlocker } from "@/hooks/use-form-blocker"; import { authClient } from "@/integrations/auth/client"; +import { getReadableErrorMessage } from "@/utils/error-message"; import { type DialogProps, useDialogStore } from "../store"; @@ -43,7 +44,16 @@ export function DisableTwoFactorDialog(_: DialogProps<"auth.two-factor.disable"> const { error } = await authClient.twoFactor.disable({ password: data.password }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + getReadableErrorMessage( + error, + t({ + comment: "Fallback toast when disabling two-factor authentication fails", + message: "Failed to disable two-factor authentication. Please try again.", + }), + ), + { id: toastId }, + ); return; } @@ -92,6 +102,19 @@ export function DisableTwoFactorDialog(_: DialogProps<"auth.two-factor.disable"> /> @@ -102,7 +125,7 @@ export function DisableTwoFactorDialog(_: DialogProps<"auth.two-factor.disable"> diff --git a/src/dialogs/auth/enable-two-factor.tsx b/src/dialogs/auth/enable-two-factor.tsx index b67108835..f47919aa1 100644 --- a/src/dialogs/auth/enable-two-factor.tsx +++ b/src/dialogs/auth/enable-two-factor.tsx @@ -17,6 +17,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from " import { Input } from "@/components/ui/input"; import { useFormBlocker } from "@/hooks/use-form-blocker"; import { authClient } from "@/integrations/auth/client"; +import { getReadableErrorMessage } from "@/utils/error-message"; import { type DialogProps, useDialogStore } from "../store"; @@ -76,7 +77,16 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">) }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + getReadableErrorMessage( + error, + t({ + comment: "Fallback toast when enabling two-factor authentication fails", + message: "Failed to enable two-factor authentication. Please try again.", + }), + ), + { id: toastId }, + ); return; } @@ -96,7 +106,16 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">) const { error } = await authClient.twoFactor.verifyTotp({ code: data.code }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + getReadableErrorMessage( + error, + t({ + comment: "Fallback toast when verifying two-factor setup code fails", + message: "Failed to verify your code. Please try again.", + }), + ), + { id: toastId }, + ); return; } @@ -202,6 +221,19 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">) /> @@ -264,10 +296,12 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">) @@ -290,11 +324,11 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">)
@@ -302,7 +336,7 @@ export function EnableTwoFactorDialog(_: DialogProps<"auth.two-factor.enable">) @@ -328,7 +362,10 @@ function TwoFactorQRCode({ totpUri }: { totpUri: string }) { size={256} marginSize={2} className="rounded-md" - title="Two-Factor Authentication QR Code" + title={t({ + comment: "Accessible title for QR code image shown during two-factor setup", + message: "Two-Factor Authentication QR Code", + })} /> ); } diff --git a/src/dialogs/resume/import.tsx b/src/dialogs/resume/import.tsx index 4e2a1f64b..fdda2d04f 100644 --- a/src/dialogs/resume/import.tsx +++ b/src/dialogs/resume/import.tsx @@ -24,6 +24,7 @@ import { JSONResumeImporter } from "@/integrations/import/json-resume"; import { ReactiveResumeJSONImporter } from "@/integrations/import/reactive-resume-json"; import { ReactiveResumeV4JSONImporter } from "@/integrations/import/reactive-resume-v4-json"; import { client, orpc } from "@/integrations/orpc/client"; +import { getOrpcErrorMessage } from "@/utils/error-message"; import { cn } from "@/utils/style"; import { type DialogProps, useDialogStore } from "../store"; @@ -186,18 +187,39 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) { }); } - if (!data) throw new Error("No data was returned from the AI provider."); + if (!data) { + throw new Error( + t({ + comment: "Error shown when AI import endpoint returns no parsed resume data", + message: "No data was returned from the AI provider.", + }), + ); + } const id = await importResume({ data }); toast.success(t`Your resume has been imported successfully.`, { id: toastId, description: null }); closeDialog(); void navigate({ to: `/builder/$resumeId`, params: { resumeId: id } }); } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message, { id: toastId, description: null }); - } else { - toast.error(t`An unknown error occurred while importing your resume.`, { id: toastId, description: null }); - } + toast.error( + getOrpcErrorMessage(error, { + byCode: { + BAD_REQUEST: t({ + comment: "Error shown when AI parsing returns invalid resume structure during import", + message: "The imported file could not be parsed into a valid resume.", + }), + BAD_GATEWAY: t({ + comment: "Error shown when AI provider is unreachable during PDF/DOCX resume import", + message: "Could not reach the AI provider. Please try again.", + }), + }, + fallback: t({ + comment: "Fallback toast when importing a resume fails for an unknown reason", + message: "An unknown error occurred while importing your resume.", + }), + }), + { id: toastId, description: null }, + ); } finally { setIsLoading(false); } @@ -235,14 +257,36 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) { value={field.value} onValueChange={field.onChange} options={[ - { value: "reactive-resume-json", label: "Reactive Resume (JSON)" }, - { value: "reactive-resume-v4-json", label: "Reactive Resume v4 (JSON)" }, - { value: "json-resume-json", label: "JSON Resume" }, + { + value: "reactive-resume-json", + label: t({ + comment: "Import source option for current Reactive Resume JSON format", + message: "Reactive Resume (JSON)", + }), + }, + { + value: "reactive-resume-v4-json", + label: t({ + comment: "Import source option for legacy Reactive Resume v4 JSON format", + message: "Reactive Resume v4 (JSON)", + }), + }, + { + value: "json-resume-json", + label: t({ + comment: "Import source option for standard JSON Resume format", + message: "JSON Resume", + }), + }, { value: "pdf", label: (
- PDF {t`AI`} + {t({ + comment: "File format label in import source selector", + message: "PDF", + })}{" "} + {t`AI`}
), }, @@ -250,7 +294,11 @@ export function ImportResumeDialog(_: DialogProps<"resume.import">) { value: "docx", label: (
- Microsoft Word {t`AI`} + {t({ + comment: "File format label in import source selector", + message: "Microsoft Word", + })}{" "} + {t`AI`}
), }, diff --git a/src/dialogs/resume/index.tsx b/src/dialogs/resume/index.tsx index 211b57f01..0640db3e7 100644 --- a/src/dialogs/resume/index.tsx +++ b/src/dialogs/resume/index.tsx @@ -25,6 +25,7 @@ import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@/ import { useFormBlocker } from "@/hooks/use-form-blocker"; import { authClient } from "@/integrations/auth/client"; import { orpc, type RouterInput } from "@/integrations/orpc/client"; +import { getResumeErrorMessage } from "@/utils/error-message"; import { generateId, generateRandomName, slugify } from "@/utils/string"; import { type DialogProps, useDialogStore } from "../store"; @@ -70,12 +71,7 @@ export function CreateResumeDialog(_: DialogProps<"resume.create">) { closeDialog(); }, onError: (error) => { - if (error.message === "RESUME_SLUG_ALREADY_EXISTS") { - toast.error(t`A resume with this slug already exists.`, { id: toastId }); - return; - } - - toast.error(error.message, { id: toastId }); + toast.error(getResumeErrorMessage(error), { id: toastId }); }, }); }; @@ -99,7 +95,7 @@ export function CreateResumeDialog(_: DialogProps<"resume.create">) { closeDialog(); }, onError: (error) => { - toast.error(error.message, { id: toastId }); + toast.error(getResumeErrorMessage(error), { id: toastId }); }, }); }; @@ -121,7 +117,13 @@ export function CreateResumeDialog(_: DialogProps<"resume.create">) { - + @@ -183,12 +185,7 @@ export function UpdateResumeDialog({ data }: DialogProps<"resume.update">) { closeDialog(); }, onError: (error) => { - if (error.message === "RESUME_SLUG_ALREADY_EXISTS") { - toast.error(t`A resume with this slug already exists.`, { id: toastId }); - return; - } - - toast.error(error.message, { id: toastId }); + toast.error(getResumeErrorMessage(error), { id: toastId }); }, }); }; @@ -257,7 +254,7 @@ export function DuplicateResumeDialog({ data }: DialogProps<"resume.duplicate">) void navigate({ to: `/builder/$resumeId`, params: { resumeId: id } }); }, onError: (error) => { - toast.error(error.message, { id: toastId }); + toast.error(getResumeErrorMessage(error), { id: toastId }); }, }); }; diff --git a/src/routes/$username/$slug.tsx b/src/routes/$username/$slug.tsx index d7e6a3f49..3733ac19b 100644 --- a/src/routes/$username/$slug.tsx +++ b/src/routes/$username/$slug.tsx @@ -1,26 +1,25 @@ -import { Trans } from "@lingui/react/macro"; +import { t } from "@lingui/core/macro"; import { ORPCError } from "@orpc/client"; -import { DownloadSimpleIcon } from "@phosphor-icons/react"; -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { createFileRoute, notFound, redirect } from "@tanstack/react-router"; -import { useCallback, useEffect } from "react"; +import { useEffect } from "react"; import type { ResumeData } from "@/schema/resume/data"; import { LoadingScreen } from "@/components/layout/loading-screen"; import { ResumePreview } from "@/components/resume/preview"; import { useResumeStore } from "@/components/resume/store/resume"; -import { Button } from "@/components/ui/button"; -import { Spinner } from "@/components/ui/spinner"; import { orpc, type RouterOutput } from "@/integrations/orpc/client"; -import { downloadFromUrl } from "@/utils/file"; import { cn } from "@/utils/style"; type LoaderData = Omit & { data: ResumeData }; export const Route = createFileRoute("/$username/$slug")({ component: RouteComponent, - loader: async ({ context, params: { username, slug } }) => { + loader: async ({ context, params, ...rest }) => { + console.log("$username/$slug loader", JSON.stringify({ params, context, rest }, null, 2)); + + const { username, slug } = params; const resume = await context.queryClient.ensureQueryData( orpc.resume.getBySlug.queryOptions({ input: { username, slug } }), ); @@ -28,7 +27,19 @@ export const Route = createFileRoute("/$username/$slug")({ return { resume: resume as LoaderData }; }, head: ({ loaderData }) => ({ - meta: [{ title: loaderData ? `${loaderData.resume.name} - Reactive Resume` : "Reactive Resume" }], + meta: [ + { + title: loaderData + ? `${loaderData.resume.name} - ${t({ + comment: "Brand name suffix in browser tab title for public resume pages", + message: "Reactive Resume", + })}` + : t({ + comment: "Browser tab title before the public resume finishes loading", + message: "Reactive Resume", + }), + }, + ], }), onError: (error) => { if (error instanceof ORPCError && error.code === "NEED_PASSWORD") { @@ -54,9 +65,6 @@ function RouteComponent() { const initialize = useResumeStore((state) => state.initialize); const { data: resume } = useQuery(orpc.resume.getBySlug.queryOptions({ input: { username, slug } })); - const { mutateAsync: printResumeAsPDF, isPending: isPrinting } = useMutation( - orpc.printer.printResumeAsPDF.mutationOptions(), - ); useEffect(() => { if (!resume) return; @@ -64,12 +72,6 @@ function RouteComponent() { return () => initialize(null); }, [resume, initialize]); - const handleDownload = useCallback(async () => { - if (!resume) return; - const { url } = await printResumeAsPDF({ id: resume.id }); - await downloadFromUrl(url, `${resume.name}.pdf`); - }, [resume, printResumeAsPDF]); - if (!isReady) return ; return ( @@ -79,17 +81,6 @@ function RouteComponent() { > - - ); } diff --git a/src/routes/_home/-sections/faq.tsx b/src/routes/_home/-sections/faq.tsx index 797a24bce..a7c9bf903 100644 --- a/src/routes/_home/-sections/faq.tsx +++ b/src/routes/_home/-sections/faq.tsx @@ -40,7 +40,12 @@ const getFaqItems = (): FAQItemData[] => [ className={buttonVariants({ variant: "link", className: "h-auto px-0!" })} > contribute to the translations on Crowdin - (opens in new tab) + + + {" "} + (opens in new tab) + + . @@ -78,7 +83,7 @@ export function FAQ() { viewport={{ once: true }} transition={{ duration: 0.45 }} > - + Frequently Asked Questions diff --git a/src/routes/auth/-components/social-auth.tsx b/src/routes/auth/-components/social-auth.tsx index 983678489..d17ba33a1 100644 --- a/src/routes/auth/-components/social-auth.tsx +++ b/src/routes/auth/-components/social-auth.tsx @@ -3,7 +3,6 @@ import { Trans } from "@lingui/react/macro"; import { FingerprintIcon, GithubLogoIcon, GoogleLogoIcon, LinkedinLogoIcon, VaultIcon } from "@phosphor-icons/react"; import { useQuery } from "@tanstack/react-query"; import { useRouter } from "@tanstack/react-router"; -import { useEffect, useRef } from "react"; import { toast } from "sonner"; import type { RouterOutput } from "@/integrations/orpc/client"; @@ -51,7 +50,6 @@ type SocialAuthButtonsProps = { function SocialAuthButtons({ providers }: SocialAuthButtonsProps) { const router = useRouter(); - const hasStartedConditionalPasskeyRef = useRef(false); const handleSocialLogin = async (provider: string) => { const toastId = toast.loading(t`Signing in...`); @@ -62,7 +60,14 @@ function SocialAuthButtons({ providers }: SocialAuthButtonsProps) { }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + error.message || + t({ + comment: "Fallback toast when social sign-in fails without a provider error message", + message: "Failed to sign in. Please try again.", + }), + { id: toastId }, + ); return; } @@ -79,7 +84,14 @@ function SocialAuthButtons({ providers }: SocialAuthButtonsProps) { }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + error.message || + t({ + comment: "Fallback toast when custom OAuth sign-in fails without a provider error message", + message: "Failed to sign in. Please try again.", + }), + { id: toastId }, + ); return; } @@ -93,7 +105,14 @@ function SocialAuthButtons({ providers }: SocialAuthButtonsProps) { const { error } = await authClient.signIn.passkey({ autoFill: false }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + error.message || + t({ + comment: "Fallback toast when passkey sign-in fails without an error message", + message: "Failed to sign in. Please try again.", + }), + { id: toastId }, + ); return; } @@ -101,25 +120,6 @@ function SocialAuthButtons({ providers }: SocialAuthButtonsProps) { await router.invalidate(); }; - useEffect(() => { - if (!("passkey" in providers)) return; - if (typeof window === "undefined") return; - if (!("PublicKeyCredential" in window)) return; - if (!PublicKeyCredential.isConditionalMediationAvailable) return; - if (hasStartedConditionalPasskeyRef.current) return; - - hasStartedConditionalPasskeyRef.current = true; - - void PublicKeyCredential.isConditionalMediationAvailable().then(async (isAvailable) => { - if (!isAvailable) return; - - const { error } = await authClient.signIn.passkey({ autoFill: true }); - if (error) return; - - await router.invalidate(); - }); - }, [providers, router]); - return (
); diff --git a/src/routes/auth/forgot-password.tsx b/src/routes/auth/forgot-password.tsx index 00553381c..ccedcd84c 100644 --- a/src/routes/auth/forgot-password.tsx +++ b/src/routes/auth/forgot-password.tsx @@ -45,7 +45,14 @@ function RouteComponent() { }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + error.message || + t({ + comment: "Fallback toast when requesting password reset email fails without backend message", + message: "Failed to send password reset email. Please try again.", + }), + { id: toastId }, + ); return; } @@ -71,7 +78,8 @@ function RouteComponent() { nativeButton={false} render={ - Sign in now + Sign in now{" "} + } /> @@ -87,10 +95,20 @@ function RouteComponent() { render={({ field }) => ( - Email Address + Email Address } + render={ + + } /> @@ -98,7 +116,7 @@ function RouteComponent() { /> @@ -122,7 +140,7 @@ function PostForgotPasswordScreen() { nativeButton={false} render={ - Open Email Client + Open Email Client } /> diff --git a/src/routes/auth/login.tsx b/src/routes/auth/login.tsx index 04dbcc60f..36025116e 100644 --- a/src/routes/auth/login.tsx +++ b/src/routes/auth/login.tsx @@ -2,7 +2,9 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { ArrowRightIcon, EyeIcon, EyeSlashIcon } from "@phosphor-icons/react"; +import { useQuery } from "@tanstack/react-query"; import { createFileRoute, Link, redirect, useNavigate, useRouter } from "@tanstack/react-router"; +import { useEffect, useRef } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { useToggle } from "usehooks-ts"; @@ -12,6 +14,7 @@ import { Button } from "@/components/ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { authClient } from "@/integrations/auth/client"; +import { orpc } from "@/integrations/orpc/client"; import { SocialAuth } from "./-components/social-auth"; @@ -33,9 +36,13 @@ type FormValues = z.infer; function RouteComponent() { const router = useRouter(); const navigate = useNavigate(); - const [showPassword, toggleShowPassword] = useToggle(false); const { flags } = Route.useRouteContext(); + const hasStartedConditionalPasskeyRef = useRef(false); + const [showPassword, toggleShowPassword] = useToggle(false); + + const { data: providers = {} } = useQuery(orpc.auth.providers.list.queryOptions()); + const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { @@ -55,7 +62,14 @@ function RouteComponent() { : await authClient.signIn.username({ username: data.identifier, password: data.password }); if (result.error) { - toast.error(result.error.message, { id: toastId }); + toast.error( + result.error.message || + t({ + comment: "Fallback toast when sign-in fails and no server error message is available", + message: "Failed to sign in. Please try again.", + }), + { id: toastId }, + ); return; } @@ -81,11 +95,30 @@ function RouteComponent() { } }; + useEffect(() => { + if (!("passkey" in providers)) return; + if (typeof window === "undefined") return; + if (!("PublicKeyCredential" in window)) return; + if (!PublicKeyCredential.isConditionalMediationAvailable) return; + if (hasStartedConditionalPasskeyRef.current) return; + + hasStartedConditionalPasskeyRef.current = true; + + void PublicKeyCredential.isConditionalMediationAvailable().then(async (isAvailable) => { + if (!isAvailable) return; + + const { error } = await authClient.signIn.passkey({ autoFill: true }); + if (error) return; + + await router.invalidate(); + }); + }, [providers, router]); + return ( <>

- Sign in to your account + Sign in to your account

{!flags.disableSignups && ( @@ -98,7 +131,10 @@ function RouteComponent() { className="h-auto gap-1.5 px-1! py-0" render={ - Create one now + + Create one now + {" "} + } /> @@ -116,14 +152,18 @@ function RouteComponent() { render={({ field }) => ( - Email Address + + Email Address + @@ -144,7 +184,7 @@ function RouteComponent() {
- Password + Password
@@ -182,7 +237,7 @@ function RouteComponent() { /> diff --git a/src/routes/auth/register.tsx b/src/routes/auth/register.tsx index 8d89ceee4..94b5efc74 100644 --- a/src/routes/auth/register.tsx +++ b/src/routes/auth/register.tsx @@ -71,7 +71,14 @@ function RouteComponent() { }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + error.message || + t({ + comment: "Fallback toast when account registration fails without a server error message", + message: "Failed to create your account. Please try again.", + }), + { id: toastId }, + ); return; } @@ -97,7 +104,8 @@ function RouteComponent() { className="h-auto gap-1.5 px-1! py-0" render={ - Sign in now + Sign in now{" "} + } /> @@ -114,11 +122,20 @@ function RouteComponent() { render={({ field }) => ( - Name + Name + } /> @@ -132,7 +149,7 @@ function RouteComponent() { render={({ field }) => ( - Username + Username @@ -157,14 +177,17 @@ function RouteComponent() { render={({ field }) => ( - Email Address + Email Address @@ -181,7 +204,7 @@ function RouteComponent() { render={({ field }) => ( - Password + Password
-
@@ -206,7 +244,7 @@ function RouteComponent() { /> @@ -242,7 +280,8 @@ function PostSignupScreen() { nativeButton={false} render={ - Continue + Continue{" "} + } /> diff --git a/src/routes/auth/reset-password.tsx b/src/routes/auth/reset-password.tsx index e09fd6d09..9d4f2c248 100644 --- a/src/routes/auth/reset-password.tsx +++ b/src/routes/auth/reset-password.tsx @@ -53,7 +53,14 @@ function RouteComponent() { const { error } = await authClient.resetPassword({ token, newPassword: data.password }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + error.message || + t({ + comment: "Fallback toast when resetting password fails and no backend message is available", + message: "Failed to reset your password. Please try again.", + }), + { id: toastId }, + ); return; } @@ -84,7 +91,7 @@ function RouteComponent() { render={({ field }) => ( - New Password + New Password
-
@@ -109,7 +131,7 @@ function RouteComponent() { /> diff --git a/src/routes/auth/resume-password.tsx b/src/routes/auth/resume-password.tsx index b9477eadb..837ca475d 100644 --- a/src/routes/auth/resume-password.tsx +++ b/src/routes/auth/resume-password.tsx @@ -16,6 +16,7 @@ import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { orpc } from "@/integrations/orpc/client"; +import { getReadableErrorMessage } from "@/utils/error-message"; const searchSchema = z.object({ redirect: z @@ -75,7 +76,16 @@ function RouteComponent() { toast.dismiss(toastId); form.setError("password", { message: t`The password you entered is incorrect` }); } else { - toast.error(error.message, { id: toastId }); + toast.error( + getReadableErrorMessage( + error, + t({ + comment: "Fallback toast when resume password verification fails unexpectedly", + message: "Failed to verify the password. Please try again.", + }), + ), + { id: toastId }, + ); } }, }, @@ -102,7 +112,7 @@ function RouteComponent() { render={({ field }) => ( - Password + Password
-
@@ -128,7 +153,7 @@ function RouteComponent() { diff --git a/src/routes/auth/verify-2fa-backup.tsx b/src/routes/auth/verify-2fa-backup.tsx index 9b29d3763..1793f5c41 100644 --- a/src/routes/auth/verify-2fa-backup.tsx +++ b/src/routes/auth/verify-2fa-backup.tsx @@ -43,7 +43,14 @@ function RouteComponent() { const { error } = await authClient.twoFactor.verifyBackupCode({ code: formattedCode }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + error.message || + t({ + comment: "Fallback toast when verifying a backup two-factor authentication code fails", + message: "Failed to verify your backup code. Please try again.", + }), + { id: toastId }, + ); return; } @@ -86,14 +93,14 @@ function RouteComponent() { render={ - Go Back + Go Back } />
diff --git a/src/routes/auth/verify-2fa.tsx b/src/routes/auth/verify-2fa.tsx index 54dfd8e3e..52878d823 100644 --- a/src/routes/auth/verify-2fa.tsx +++ b/src/routes/auth/verify-2fa.tsx @@ -44,7 +44,14 @@ function RouteComponent() { }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + error.message || + t({ + comment: "Fallback toast when verifying a two-factor authentication code fails", + message: "Failed to verify your code. Please try again.", + }), + { id: toastId }, + ); return; } @@ -95,14 +102,14 @@ function RouteComponent() { render={ - Back to Login + Back to Login } /> @@ -113,7 +120,9 @@ function RouteComponent() { className="h-auto justify-self-center p-0 text-sm" render={ - Lost access to your authenticator? + + Lost access to your authenticator? + } /> diff --git a/src/routes/builder/$resumeId/-components/header.tsx b/src/routes/builder/$resumeId/-components/header.tsx index 5c438939b..fc84df544 100644 --- a/src/routes/builder/$resumeId/-components/header.tsx +++ b/src/routes/builder/$resumeId/-components/header.tsx @@ -26,6 +26,7 @@ import { import { useDialogStore } from "@/dialogs/store"; import { useConfirm } from "@/hooks/use-confirm"; import { orpc } from "@/integrations/orpc/client"; +import { getResumeErrorMessage } from "@/utils/error-message"; import { useBuilderSidebar } from "../-store/sidebar"; @@ -38,12 +39,21 @@ export function BuilderHeader() {
); @@ -99,7 +114,7 @@ function BuilderHeaderDropdown() { { id, isLocked: !isLocked }, { onError: (error) => { - toast.error(error.message); + toast.error(getResumeErrorMessage(error)); }, }, ); @@ -122,7 +137,7 @@ function BuilderHeaderDropdown() { void navigate({ to: "/dashboard/resumes", search: { sort: "lastUpdatedAt", tags: [] } }); }, onError: (error) => { - toast.error(error.message, { id: toastId }); + toast.error(getResumeErrorMessage(error), { id: toastId }); }, }, ); diff --git a/src/routes/builder/$resumeId/-sidebar/left/sections/custom-fields.tsx b/src/routes/builder/$resumeId/-sidebar/left/sections/custom-fields.tsx index 719de6a1d..f4754ef3b 100644 --- a/src/routes/builder/$resumeId/-sidebar/left/sections/custom-fields.tsx +++ b/src/routes/builder/$resumeId/-sidebar/left/sections/custom-fields.tsx @@ -1,5 +1,6 @@ import type z from "zod"; +import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { DotsSixVerticalIcon, LinkIcon, ListPlusIcon, XIcon } from "@phosphor-icons/react"; import { Reorder, useDragControls } from "motion/react"; @@ -119,7 +120,10 @@ export function CustomFieldsSection({ onSubmit }: Props) { type="url" value={field.value} id={`customFields.${index}.link`} - placeholder="Must start with https://" + placeholder={t({ + comment: "Placeholder text for custom link URL field in resume builder", + message: "Must start with https://", + })} onChange={(e) => { field.onChange(e.target.value); void form.handleSubmit(onSubmit)(); diff --git a/src/routes/builder/$resumeId/-sidebar/left/sections/custom.tsx b/src/routes/builder/$resumeId/-sidebar/left/sections/custom.tsx index d6f2cbecd..e54214f6a 100644 --- a/src/routes/builder/$resumeId/-sidebar/left/sections/custom.tsx +++ b/src/routes/builder/$resumeId/-sidebar/left/sections/custom.tsx @@ -1,3 +1,4 @@ +import { t } from "@lingui/core/macro"; import { Plural, Trans } from "@lingui/react/macro"; import { ColumnsIcon, @@ -46,9 +47,18 @@ function getItemTitle(type: CustomSectionType, item: CustomSectionItemType): str .with("summary", () => { if ("content" in item) { const stripped = stripHtml(item.content); - return stripped.length > 50 ? `${stripped.slice(0, 50)}...` : stripped || "Summary"; + return stripped.length > 50 + ? `${stripped.slice(0, 50)}...` + : stripped || + t({ + comment: "Fallback title for a custom summary item in resume builder when content is empty", + message: "Summary", + }); } - return "Summary"; + return t({ + comment: "Fallback title for a custom summary item in resume builder when content is unavailable", + message: "Summary", + }); }) .with("profiles", () => ("network" in item ? item.network : "")) .with("experience", () => ("company" in item ? item.company : "")) @@ -65,9 +75,18 @@ function getItemTitle(type: CustomSectionType, item: CustomSectionItemType): str .with("cover-letter", () => { if ("recipient" in item) { const stripped = stripHtml(item.recipient); - return stripped.length > 50 ? `${stripped.slice(0, 50)}...` : stripped || "Cover Letter"; + return stripped.length > 50 + ? `${stripped.slice(0, 50)}...` + : stripped || + t({ + comment: "Fallback title for a custom cover letter item in resume builder when recipient is empty", + message: "Cover Letter", + }); } - return "Cover Letter"; + return t({ + comment: "Fallback title for a custom cover letter item in resume builder when recipient is unavailable", + message: "Cover Letter", + }); }) .exhaustive(); } @@ -216,9 +235,15 @@ function CustomSectionDropdownMenu({ section }: { section: CustomSection }) { }; const onDeleteSection = async () => { - const confirmed = await confirm("Are you sure you want to delete this custom section?", { - confirmText: "Delete", - cancelText: "Cancel", + const confirmed = await confirm(t`Are you sure you want to delete this custom section?`, { + confirmText: t({ + comment: "Destructive confirmation button label when deleting a custom section in resume builder", + message: "Delete", + }), + cancelText: t({ + comment: "Confirmation dialog button label to abort deleting a custom section in resume builder", + message: "Cancel", + }), }); if (!confirmed) return; diff --git a/src/routes/builder/$resumeId/-sidebar/left/sections/picture.tsx b/src/routes/builder/$resumeId/-sidebar/left/sections/picture.tsx index 51fd1cad6..f781649f3 100644 --- a/src/routes/builder/$resumeId/-sidebar/left/sections/picture.tsx +++ b/src/routes/builder/$resumeId/-sidebar/left/sections/picture.tsx @@ -18,6 +18,7 @@ import { Input } from "@/components/ui/input"; import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@/components/ui/input-group"; import { orpc } from "@/integrations/orpc/client"; import { pictureSchema } from "@/schema/resume/data"; +import { getReadableErrorMessage } from "@/utils/error-message"; import { SectionBase } from "../shared/section-base"; @@ -59,9 +60,10 @@ function PictureSectionForm() { if (!picture.url) return; const appOrigin = window.location.origin; - const pictureOrigin = new URL(picture.url).origin; + const pictureUrl = new URL(picture.url, appOrigin); + const pictureOrigin = pictureUrl.origin; - const filename = picture.url.split("/").pop(); + const filename = pictureUrl.pathname.split("/").pop(); if (!filename) return; // If the picture is from the same origin, attempt to delete it @@ -85,7 +87,16 @@ function PictureSectionForm() { if (fileInputRef.current) fileInputRef.current.value = ""; }, onError: (error) => { - toast.error(error.message, { id: toastId }); + toast.error( + getReadableErrorMessage( + error, + t({ + comment: "Fallback toast when uploading profile picture for resume fails", + message: "Failed to upload picture. Please try again.", + }), + ), + { id: toastId }, + ); }, }); }; @@ -96,8 +107,10 @@ function PictureSectionForm() {
-
{picture.url && ( @@ -111,7 +124,7 @@ function PictureSectionForm() {
{picture.url ? : }
-
+ URL
- } /> + } /> + {hasApplyLink ? ( + + ) : ( + + )} diff --git a/src/routes/dashboard/job-search/-components/use-job-search.ts b/src/routes/dashboard/job-search/-components/use-job-search.ts index 9e7dc7f4f..a17ebf580 100644 --- a/src/routes/dashboard/job-search/-components/use-job-search.ts +++ b/src/routes/dashboard/job-search/-components/use-job-search.ts @@ -7,6 +7,7 @@ import type { JobResult, RapidApiQuota } from "@/schema/jobs"; import { useJobsStore } from "@/integrations/jobs/store"; import { orpc } from "@/integrations/orpc/client"; +import { getOrpcErrorMessage } from "@/utils/error-message"; import { buildPostFilters, @@ -71,8 +72,21 @@ export function useJobSearch() { }, onError: (error) => { if (requestId !== requestIdRef.current) return; - setError(error.message); - toast.error(error.message); + const message = getOrpcErrorMessage(error, { + byCode: { + BAD_GATEWAY: t({ + comment: "Error shown when job search API is unavailable while searching jobs", + message: "Could not fetch jobs from JSearch API. Please try again.", + }), + }, + fallback: t({ + comment: "Fallback error shown when job search request fails", + message: "Failed to search jobs. Please try again.", + }), + }); + + setError(message); + toast.error(message); }, }, ); diff --git a/src/routes/dashboard/job-search/index.tsx b/src/routes/dashboard/job-search/index.tsx index 9e5e731fb..1783b3bcb 100644 --- a/src/routes/dashboard/job-search/index.tsx +++ b/src/routes/dashboard/job-search/index.tsx @@ -96,11 +96,17 @@ function RouteComponent() { type="text" value={query} onChange={(e) => setQuery(e.target.value)} - placeholder={t`e.g. frontend developer jobs in Berlin`} + placeholder={t({ + comment: "Example placeholder text in job search input field", + message: "e.g. frontend developer jobs in Berlin", + })} + aria-label={t({ + comment: "Accessible label for free-text job search query field", + message: "Job search query", + })} autoCorrect="off" autoComplete="off" spellCheck="false" - autoFocus />
diff --git a/src/routes/dashboard/resumes/-components/menus/context-menu.tsx b/src/routes/dashboard/resumes/-components/menus/context-menu.tsx index 8ff4fbf87..a806ffffb 100644 --- a/src/routes/dashboard/resumes/-components/menus/context-menu.tsx +++ b/src/routes/dashboard/resumes/-components/menus/context-menu.tsx @@ -22,6 +22,7 @@ import { import { useDialogStore } from "@/dialogs/store"; import { useConfirm } from "@/hooks/use-confirm"; import { orpc, type RouterOutput } from "@/integrations/orpc/client"; +import { getResumeErrorMessage } from "@/utils/error-message"; type Props = { resume: RouterOutput["resume"]["list"][number]; @@ -56,7 +57,7 @@ export function ResumeContextMenu({ resume, children }: Props) { { id: resume.id, isLocked: !resume.isLocked }, { onError: (error) => { - toast.error(error.message); + toast.error(getResumeErrorMessage(error)); }, }, ); @@ -78,7 +79,7 @@ export function ResumeContextMenu({ resume, children }: Props) { toast.success(t`Your resume has been deleted successfully.`, { id: toastId }); }, onError: (error) => { - toast.error(error.message, { id: toastId }); + toast.error(getResumeErrorMessage(error), { id: toastId }); }, }, ); @@ -93,7 +94,7 @@ export function ResumeContextMenu({ resume, children }: Props) { render={ - Open + Open } /> @@ -102,24 +103,28 @@ export function ResumeContextMenu({ resume, children }: Props) { - Update + Update - Duplicate + Duplicate {resume.isLocked ? : } - {resume.isLocked ? Unlock : Lock} + {resume.isLocked ? ( + Unlock + ) : ( + Lock + )} - Delete + Delete diff --git a/src/routes/dashboard/resumes/-components/menus/dropdown-menu.tsx b/src/routes/dashboard/resumes/-components/menus/dropdown-menu.tsx index 0f4495273..ca05fcd7a 100644 --- a/src/routes/dashboard/resumes/-components/menus/dropdown-menu.tsx +++ b/src/routes/dashboard/resumes/-components/menus/dropdown-menu.tsx @@ -22,6 +22,7 @@ import { import { useDialogStore } from "@/dialogs/store"; import { useConfirm } from "@/hooks/use-confirm"; import { orpc, type RouterOutput } from "@/integrations/orpc/client"; +import { getResumeErrorMessage } from "@/utils/error-message"; type Props = Omit, "children"> & { resume: RouterOutput["resume"]["list"][number]; @@ -56,7 +57,7 @@ export function ResumeDropdownMenu({ resume, children, ...props }: Props) { { id: resume.id, isLocked: !resume.isLocked }, { onError: (error) => { - toast.error(error.message); + toast.error(getResumeErrorMessage(error)); }, }, ); @@ -78,7 +79,7 @@ export function ResumeDropdownMenu({ resume, children, ...props }: Props) { toast.success(t`Your resume has been deleted successfully.`, { id: toastId }); }, onError: (error) => { - toast.error(error.message, { id: toastId }); + toast.error(getResumeErrorMessage(error), { id: toastId }); }, }, ); @@ -92,7 +93,7 @@ export function ResumeDropdownMenu({ resume, children, ...props }: Props) { - Open + Open @@ -100,24 +101,28 @@ export function ResumeDropdownMenu({ resume, children, ...props }: Props) { - Update + Update - Duplicate + Duplicate {resume.isLocked ? : } - {resume.isLocked ? Unlock : Lock} + {resume.isLocked ? ( + Unlock + ) : ( + Lock + )} - Delete + Delete diff --git a/src/routes/dashboard/settings/ai.tsx b/src/routes/dashboard/settings/ai.tsx index 58ef04367..2ca802e3a 100644 --- a/src/routes/dashboard/settings/ai.tsx +++ b/src/routes/dashboard/settings/ai.tsx @@ -17,6 +17,7 @@ import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; import { type AIProvider, useAIStore } from "@/integrations/ai/store"; import { orpc } from "@/integrations/orpc/client"; +import { getOrpcErrorMessage } from "@/utils/error-message"; import { cn } from "@/utils/style"; import { DashboardHeader } from "../-components/header"; @@ -28,31 +29,46 @@ export const Route = createFileRoute("/dashboard/settings/ai")({ const providerOptions: (ComboboxOption & { defaultBaseURL: string })[] = [ { value: "openai", - label: "OpenAI", + label: t({ + comment: "AI provider option label in dashboard AI settings", + message: "OpenAI", + }), keywords: ["openai", "gpt", "chatgpt"], defaultBaseURL: "https://api.openai.com/v1", }, { value: "ollama", - label: "Ollama", + label: t({ + comment: "AI provider option label in dashboard AI settings", + message: "Ollama", + }), keywords: ["ollama", "ai", "local"], defaultBaseURL: "http://localhost:11434", }, { value: "anthropic", - label: "Anthropic Claude", + label: t({ + comment: "AI provider option label in dashboard AI settings", + message: "Anthropic Claude", + }), keywords: ["anthropic", "claude", "ai"], defaultBaseURL: "https://api.anthropic.com/v1", }, { value: "vercel-ai-gateway", - label: "Vercel AI Gateway", + label: t({ + comment: "AI provider option label in dashboard AI settings", + message: "Vercel AI Gateway", + }), keywords: ["vercel", "gateway", "ai"], defaultBaseURL: "https://ai-gateway.vercel.sh/v1/ai", }, { value: "gemini", - label: "Google Gemini", + label: t({ + comment: "AI provider option label in dashboard AI settings", + message: "Google Gemini", + }), keywords: ["gemini", "google", "bard"], defaultBaseURL: "https://generativelanguage.googleapis.com/v1beta", }, @@ -106,7 +122,24 @@ function AIForm() { draft.testStatus = "failure"; }); - toast.error(error.message); + toast.error( + getOrpcErrorMessage(error, { + byCode: { + BAD_REQUEST: t({ + comment: "Error shown when AI provider credentials or base URL are invalid in AI settings", + message: "Invalid AI provider configuration. Please check your settings.", + }), + BAD_GATEWAY: t({ + comment: "Error shown when the configured AI provider cannot be reached during connection test", + message: "Could not reach the AI provider. Please try again.", + }), + }, + fallback: t({ + comment: "Fallback toast when testing AI provider connection fails", + message: "Failed to test AI provider connection. Please try again.", + }), + }), + ); }, }, ); @@ -138,7 +171,10 @@ function AIForm() { value={model} disabled={enabled} onChange={(e) => handleModelChange(e.target.value)} - placeholder="e.g., gpt-4, claude-3-opus, gemini-pro" + placeholder={t({ + comment: "Example model-name placeholder in AI settings", + message: "e.g., gpt-4, claude-3-opus, gemini-pro", + })} autoCorrect="off" autoComplete="off" spellCheck="false" diff --git a/src/routes/dashboard/settings/api-keys.tsx b/src/routes/dashboard/settings/api-keys.tsx index 7661c4ce1..2b401558e 100644 --- a/src/routes/dashboard/settings/api-keys.tsx +++ b/src/routes/dashboard/settings/api-keys.tsx @@ -11,6 +11,7 @@ import { Separator } from "@/components/ui/separator"; import { useDialogStore } from "@/dialogs/store"; import { useConfirm } from "@/hooks/use-confirm"; import { authClient } from "@/integrations/auth/client"; +import { getReadableErrorMessage } from "@/utils/error-message"; import { DashboardHeader } from "../-components/header"; @@ -38,8 +39,14 @@ function RouteComponent() { const onDelete = async (id: string) => { const confirmation = await confirm(t`Are you sure you want to delete this API key?`, { description: t`The API key will no longer be able to access your data after deletion. This action cannot be undone.`, - confirmText: t`Delete`, - cancelText: t`Cancel`, + confirmText: t({ + comment: "API key deletion confirmation dialog confirm action in settings", + message: "Delete", + }), + cancelText: t({ + comment: "API key deletion confirmation dialog cancel action in settings", + message: "Cancel", + }), }); if (!confirmation) return; @@ -49,7 +56,16 @@ function RouteComponent() { const { error } = await authClient.apiKey.delete({ keyId: id }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + getReadableErrorMessage( + error, + t({ + comment: "Fallback toast when deleting an API key fails", + message: "Failed to delete the API key. Please try again.", + }), + ), + { id: toastId }, + ); return; } diff --git a/src/routes/dashboard/settings/authentication/-components/hooks.tsx b/src/routes/dashboard/settings/authentication/-components/hooks.tsx index 7128c1818..5f0e9df5d 100644 --- a/src/routes/dashboard/settings/authentication/-components/hooks.tsx +++ b/src/routes/dashboard/settings/authentication/-components/hooks.tsx @@ -18,18 +18,49 @@ import type { AuthProvider } from "@/integrations/auth/types"; import { authClient } from "@/integrations/auth/client"; import { orpc } from "@/integrations/orpc/client"; +import { getReadableErrorMessage } from "@/utils/error-message"; /** * Get the display name for a social provider */ export function getProviderName(providerId: AuthProvider): string { return match(providerId) - .with("credential", () => "Password") - .with("passkey", () => "Passkey") - .with("google", () => "Google") - .with("github", () => "GitHub") - .with("linkedin", () => "LinkedIn") - .with("custom", () => "Custom OAuth") + .with("credential", () => + t({ + comment: "Authentication provider display name in account settings", + message: "Password", + }), + ) + .with("passkey", () => + t({ + comment: "Authentication provider display name in account settings", + message: "Passkey", + }), + ) + .with("google", () => + t({ + comment: "Authentication provider display name in account settings", + message: "Google", + }), + ) + .with("github", () => + t({ + comment: "Authentication provider display name in account settings", + message: "GitHub", + }), + ) + .with("linkedin", () => + t({ + comment: "Authentication provider display name in account settings", + message: "LinkedIn", + }), + ) + .with("custom", () => + t({ + comment: "Authentication provider display name in account settings", + message: "Custom OAuth", + }), + ) .exhaustive(); } @@ -85,7 +116,16 @@ export function useAuthProviderActions() { const { error } = await authClient.linkSocial({ provider, callbackURL: "/dashboard/settings/authentication" }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + getReadableErrorMessage( + error, + t({ + comment: "Fallback toast when linking a social authentication provider fails", + message: "Failed to link provider. Please try again.", + }), + ), + { id: toastId }, + ); return; } @@ -99,7 +139,16 @@ export function useAuthProviderActions() { const { error } = await authClient.unlinkAccount({ providerId: provider, accountId }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + getReadableErrorMessage( + error, + t({ + comment: "Fallback toast when unlinking a social authentication provider fails", + message: "Failed to unlink provider. Please try again.", + }), + ), + { id: toastId }, + ); return; } diff --git a/src/routes/dashboard/settings/authentication/-components/passkeys.tsx b/src/routes/dashboard/settings/authentication/-components/passkeys.tsx index e00268edd..dc3fb87ee 100644 --- a/src/routes/dashboard/settings/authentication/-components/passkeys.tsx +++ b/src/routes/dashboard/settings/authentication/-components/passkeys.tsx @@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { usePrompt } from "@/hooks/use-prompt"; import { authClient } from "@/integrations/auth/client"; +import { getReadableErrorMessage } from "@/utils/error-message"; export function PasskeysSection() { const queryClient = useQueryClient(); @@ -26,7 +27,15 @@ export function PasskeysSection() { }, onSuccess: async ({ data, error }) => { if (error) { - toast.error(error.message); + toast.error( + getReadableErrorMessage( + error, + t({ + comment: "Fallback toast when passkey registration fails", + message: "Failed to register passkey. Please try again.", + }), + ), + ); return; } @@ -36,7 +45,10 @@ export function PasskeysSection() { const name = await prompt(t`Enter a name for your passkey.`, { description: t`This will help you identify it later, if you plan to have multiple passkeys.`, defaultValue: "", - confirmText: t`Save`, + confirmText: t({ + comment: "Passkey rename prompt confirm action in authentication settings", + message: "Save", + }), }); if (name === null) return; @@ -46,7 +58,15 @@ export function PasskeysSection() { const { error: renameError } = await authClient.passkey.updatePasskey({ id: passkeyId, name: passkeyName }); if (renameError) { - toast.error(renameError.message); + toast.error( + getReadableErrorMessage( + renameError, + t({ + comment: "Fallback toast when renaming a passkey fails", + message: "Failed to rename passkey. Please try again.", + }), + ), + ); return; } @@ -63,7 +83,15 @@ export function PasskeysSection() { }, onSuccess: async ({ error }) => { if (error) { - toast.error(error.message); + toast.error( + getReadableErrorMessage( + error, + t({ + comment: "Fallback toast when deleting a passkey fails", + message: "Failed to delete passkey. Please try again.", + }), + ), + ); return; } @@ -131,7 +159,7 @@ export function PasskeysSection() { disabled={deletePasskeyMutation.isPending} > - Delete + Delete
diff --git a/src/routes/dashboard/settings/authentication/-components/social-provider.tsx b/src/routes/dashboard/settings/authentication/-components/social-provider.tsx index c6c5fc73c..e7c5f4930 100644 --- a/src/routes/dashboard/settings/authentication/-components/social-provider.tsx +++ b/src/routes/dashboard/settings/authentication/-components/social-provider.tsx @@ -61,7 +61,9 @@ export function SocialProviderSection({ provider, name, animationDelay = 0 }: So > )) @@ -74,7 +76,7 @@ export function SocialProviderSection({ provider, name, animationDelay = 0 }: So > )) diff --git a/src/routes/dashboard/settings/danger-zone.tsx b/src/routes/dashboard/settings/danger-zone.tsx index fed4d927e..c819e6ae3 100644 --- a/src/routes/dashboard/settings/danger-zone.tsx +++ b/src/routes/dashboard/settings/danger-zone.tsx @@ -13,6 +13,7 @@ import { Separator } from "@/components/ui/separator"; import { useConfirm } from "@/hooks/use-confirm"; import { authClient } from "@/integrations/auth/client"; import { orpc } from "@/integrations/orpc/client"; +import { getReadableErrorMessage } from "@/utils/error-message"; import { DashboardHeader } from "../-components/header"; @@ -33,8 +34,14 @@ function RouteComponent() { const handleDeleteAccount = async () => { const confirmed = await confirm(t`Are you sure you want to delete your account?`, { description: t`This action cannot be undone. All your data will be permanently deleted.`, - confirmText: t`Confirm`, - cancelText: t`Cancel`, + confirmText: t({ + comment: "Account deletion confirmation dialog confirm action in danger zone", + message: "Confirm", + }), + cancelText: t({ + comment: "Account deletion confirmation dialog cancel action in danger zone", + message: "Cancel", + }), }); if (!confirmed) return; @@ -48,7 +55,16 @@ function RouteComponent() { void navigate({ to: "/" }); }, onError: (error) => { - toast.error(error.message, { id: toastId }); + toast.error( + getReadableErrorMessage( + error, + t({ + comment: "Fallback toast when account deletion fails", + message: "Failed to delete your account. Please try again.", + }), + ), + { id: toastId }, + ); }, }); }; diff --git a/src/routes/dashboard/settings/job-search.tsx b/src/routes/dashboard/settings/job-search.tsx index f85948847..abf307ee0 100644 --- a/src/routes/dashboard/settings/job-search.tsx +++ b/src/routes/dashboard/settings/job-search.tsx @@ -16,6 +16,7 @@ import { Separator } from "@/components/ui/separator"; import { Spinner } from "@/components/ui/spinner"; import { useJobsStore } from "@/integrations/jobs/store"; import { orpc } from "@/integrations/orpc/client"; +import { getOrpcErrorMessage } from "@/utils/error-message"; import { DashboardHeader } from "../-components/header"; @@ -50,7 +51,20 @@ function RapidAPIKeyForm() { draft.rapidApiQuota = null; }); - toast.error(error.message); + toast.error( + getOrpcErrorMessage(error, { + byCode: { + BAD_GATEWAY: t({ + comment: "Error shown when JSearch API connection test fails in job search settings", + message: "Could not reach JSearch API. Check your API key and try again.", + }), + }, + fallback: t({ + comment: "Fallback toast when testing RapidAPI job search connection fails", + message: "Failed to test RapidAPI connection. Please try again.", + }), + }), + ); }, }, ); diff --git a/src/routes/dashboard/settings/profile.tsx b/src/routes/dashboard/settings/profile.tsx index bdd8dc991..ed0095a22 100644 --- a/src/routes/dashboard/settings/profile.tsx +++ b/src/routes/dashboard/settings/profile.tsx @@ -15,6 +15,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from " import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { authClient } from "@/integrations/auth/client"; +import { getReadableErrorMessage } from "@/utils/error-message"; import { DashboardHeader } from "../-components/header"; @@ -66,7 +67,15 @@ function RouteComponent() { }); if (error) { - toast.error(error.message); + toast.error( + getReadableErrorMessage( + error, + t({ + comment: "Fallback toast when updating profile details fails", + message: "Failed to update your profile. Please try again.", + }), + ), + ); return; } @@ -81,7 +90,15 @@ function RouteComponent() { }); if (error) { - toast.error(error.message); + toast.error( + getReadableErrorMessage( + error, + t({ + comment: "Fallback toast when requesting email change confirmation fails", + message: "Failed to request email change. Please try again.", + }), + ), + ); return; } @@ -102,7 +119,16 @@ function RouteComponent() { }); if (error) { - toast.error(error.message, { id: toastId }); + toast.error( + getReadableErrorMessage( + error, + t({ + comment: "Fallback toast when resending account verification email fails", + message: "Failed to resend verification email. Please try again.", + }), + ), + { id: toastId }, + ); return; } @@ -136,7 +162,18 @@ function RouteComponent() { Name } + render={ + + } /> @@ -157,7 +194,10 @@ function RouteComponent() { min={3} max={64} autoComplete="username" - placeholder="john.doe" + placeholder={t({ + comment: "Example username placeholder on profile settings form", + message: "john.doe", + })} className="lowercase" {...field} /> @@ -181,7 +221,10 @@ function RouteComponent() { @@ -224,7 +267,7 @@ function RouteComponent() { className="flex items-center gap-x-4 justify-self-end will-change-[transform,opacity]" >