feat(i18n): implement localization using LinguiJS

This commit is contained in:
Amruth Pillai
2023-11-10 09:07:47 +01:00
parent 13d91411e3
commit 6ad4358d70
108 changed files with 4631 additions and 798 deletions

7
.gitignore vendored
View File

@ -38,9 +38,14 @@ Thumbs.db
# Generated Files
.nx
.swc
stats.html
libs/prisma
# Environment Variables
*.env*
!.env.example
!.env.example
# Lingui Compiled Messages
apps/client/src/locales/_build/
apps/client/src/locales/**/*.js

View File

@ -9,5 +9,8 @@
"https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json": [
"tools/compose/*"
]
}
},
"i18n-ally.localesPaths": [
"apps/client/src/locales"
]
}

View File

@ -3,6 +3,7 @@
![App Version](https://img.shields.io/github/package-json/version/AmruthPillai/Reactive-Resume/v4?label=version)
[![Docker Pulls](https://img.shields.io/docker/pulls/amruthpillai/reactive-resume)](https://hub.docker.com/repository/docker/amruthpillai/reactive-resume)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/AmruthPillai)](https://github.com/sponsors/AmruthPillai)
[![Crowdin](https://badges.crowdin.net/reactive-resume/localized.svg)](https://crowdin.com/project/reactive-resume)
# Reactive Resume

View File

@ -14,9 +14,29 @@
"config": "tailwind.config.js"
}
},
"plugins": ["lingui"],
"rules": {
// react-hooks
"react-hooks/exhaustive-deps": "off"
"react-hooks/exhaustive-deps": "off",
// lingui
"lingui/no-unlocalized-strings": 2,
"lingui/t-call-in-function": 2,
"lingui/no-single-variables-to-translate": 2,
"lingui/no-expression-in-message": 2,
"lingui/no-single-tag-to-translate": 2,
"lingui/no-trans-inside-trans": 2,
"lingui/text-restrictions": [
2,
{
"rules": [
{
"patterns": ["''", "\"", "", "“"],
"message": "This string contains forbidden characters"
}
]
}
]
}
},
{

View File

@ -2,6 +2,7 @@
try {
if (
localStorage.theme === "dark" ||
// eslint-disable-next-line lingui/no-unlocalized-strings
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
document.documentElement.classList.add("dark");

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import {
CaretDown,
ChatTeardropText,
@ -39,15 +40,14 @@ export const AiActions = ({ value, onChange, className }: Props) => {
const onClick = async (action: Action, mood?: Mood) => {
setLoading(action);
let result = value;
// await new Promise((resolve) => setTimeout(resolve, 2000));
let result = value;
if (action === "improve") result = await improveWriting(value);
if (action === "fix") result = await fixGrammar(value);
if (action === "tone" && mood) result = await changeTone(value, mood);
onChange("Result" + result);
onChange(result);
setLoading(false);
};
@ -67,52 +67,52 @@ export const AiActions = ({ value, onChange, className }: Props) => {
className="-rotate-90 bg-background px-2 text-[10px] leading-[10px]"
>
<MagicWand size={10} className="mr-1" />
AI
{t`AI`}
</Badge>
</div>
<Button size="sm" variant="outline" disabled={!!loading} onClick={() => onClick("improve")}>
{loading === "improve" ? <CircleNotch className="animate-spin" /> : <PenNib />}
<span className="ml-2 text-xs">Improve Writing</span>
<span className="ml-2 text-xs">{t`Improve Writing`}</span>
</Button>
<Button size="sm" variant="outline" disabled={!!loading} onClick={() => onClick("fix")}>
{loading === "fix" ? <CircleNotch className="animate-spin" /> : <Exam />}
<span className="ml-2 text-xs">Fix Spelling & Grammar</span>
<span className="ml-2 text-xs">{t`Fix Spelling & Grammar`}</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="outline" disabled={!!loading}>
{loading === "tone" ? <CircleNotch className="animate-spin" /> : <ChatTeardropText />}
<span className="mx-2 text-xs">Change Tone</span>
<span className="mx-2 text-xs">{t`Change Tone`}</span>
<CaretDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => onClick("tone", "casual")}>
<span role="img" aria-label="Casual">
<span role="img" aria-label={t`Casual`}>
🙂
</span>
<span className="ml-2">Casual</span>
<span className="ml-2">{t`Casual`}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onClick("tone", "professional")}>
<span role="img" aria-label="Professional">
<span role="img" aria-label={t`Professional`}>
💼
</span>
<span className="ml-2">Professional</span>
<span className="ml-2">{t`Professional`}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onClick("tone", "confident")}>
<span role="img" aria-label="Confident">
<span role="img" aria-label={t`Confident`}>
😎
</span>
<span className="ml-2">Confident</span>
<span className="ml-2">{t`Confident`}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onClick("tone", "friendly")}>
<span role="img" aria-label="Friendly">
<span role="img" aria-label={t`Friendly`}>
😊
</span>
<span className="ml-2">Friendly</span>
<span className="ml-2">{t`Friendly`}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -1,3 +1,4 @@
import { t, Trans } from "@lingui/macro";
import { cn } from "@reactive-resume/utils";
type Props = {
@ -12,23 +13,29 @@ export const Copyright = ({ className }: Props) => (
)}
>
<span>
Licensed under{" "}
<a
target="_blank"
rel="noopener noreferrer nofollow"
href="https://github.com/AmruthPillai/Reactive-Resume/blob/main/LICENSE"
>
MIT
</a>
<Trans>
Licensed under{" "}
<a
target="_blank"
rel="noopener noreferrer nofollow"
href="https://github.com/AmruthPillai/Reactive-Resume/blob/main/LICENSE"
>
MIT
</a>
</Trans>
</span>
<span>By the community, for the community.</span>
<span>{t`By the community, for the community.`}</span>
<span>
A passion project by{" "}
<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.amruthpillai.com/">
Amruth Pillai
</a>
<Trans>
A passion project by{" "}
<a target="_blank" rel="noopener noreferrer nofollow" href="https://www.amruthpillai.com/">
Amruth Pillai
</a>
</Trans>
</span>
<span className="mt-2 font-bold">Reactive Resume v{appVersion}</span>
<span className="mt-4">
{t`Reactive Resume`} {"v" + appVersion}
</span>
</div>
);

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { useTheme } from "@reactive-resume/hooks";
import { cn } from "@reactive-resume/utils";
@ -25,7 +26,7 @@ export const Icon = ({ size = 32, className }: Props) => {
src={src}
width={size}
height={size}
alt="Reactive Resume"
alt={t`Reactive Resume`}
className={cn("rounded-sm", className)}
/>
);

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { useTheme } from "@reactive-resume/hooks";
import { cn } from "@reactive-resume/utils";
@ -25,7 +26,7 @@ export const Logo = ({ size = 32, className }: Props) => {
src={src}
width={size}
height={size}
alt="Reactive Resume"
alt={t`Reactive Resume`}
className={cn("rounded-sm", className)}
/>
);

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import {
DropdownMenu,
DropdownMenuContent,
@ -24,12 +25,14 @@ export const UserOptions = ({ children }: Props) => {
<DropdownMenuContent side="top" align="start" className="w-48">
<DropdownMenuItem onClick={() => navigate("/dashboard/settings")}>
Settings
{t`Settings`}
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
<KeyboardShortcut>S</KeyboardShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => logout()}>
Logout
{t`Logout`}
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
<KeyboardShortcut>Q</KeyboardShortcut>
</DropdownMenuItem>
</DropdownMenuContent>

View File

@ -0,0 +1,16 @@
import { i18n } from "@lingui/core";
import { t } from "@lingui/macro";
export const getLocales = () => ({
"en-US": t`English`,
"de-DE": t`German`,
});
export const defaultLocale = "en-US";
export async function dynamicActivate(locale: string) {
const { messages } = await import(`../locales/${locale}.po`);
i18n.load(locale, messages);
i18n.activate(locale);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { GithubLogo, GoogleLogo } from "@phosphor-icons/react";
import { Button } from "@reactive-resume/ui";
@ -5,15 +6,15 @@ export const SocialAuth = () => (
<div className="grid grid-cols-2 gap-4">
<Button asChild size="lg" className="w-full !bg-[#222] !text-white hover:!bg-[#222]/80">
<a href="/api/auth/github">
<GoogleLogo className="mr-3 h-4 w-4" />
GitHub
<GithubLogo className="mr-3 h-4 w-4" />
{t`GitHub`}
</a>
</Button>
<Button asChild size="lg" className="w-full !bg-[#4285F4] !text-white hover:!bg-[#4285F4]/80">
<a href="/api/auth/google">
<GithubLogo className="mr-3 h-4 w-4" />
Google
<GoogleLogo className="mr-3 h-4 w-4" />
{t`Google`}
</a>
</Button>
</div>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { Warning } from "@phosphor-icons/react";
import { twoFactorBackupSchema } from "@reactive-resume/dto";
import { usePasswordToggle } from "@reactive-resume/hooks";
@ -49,7 +50,7 @@ export const BackupOtpPage = () => {
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while trying to sign in",
title: t`An error occurred while trying to sign in to your account.`,
description: message,
});
}
@ -59,9 +60,9 @@ export const BackupOtpPage = () => {
return (
<div className="space-y-8">
<div className="space-y-1.5">
<h2 className="text-2xl font-semibold tracking-tight">Use your backup code</h2>
<h2 className="text-2xl font-semibold tracking-tight">{t`Use your backup code`}</h2>
<h6 className="leading-relaxed opacity-60">
Enter one of the 10 backup codes you saved when you enabled two-factor authentication.
{t`Enter one of the 10 backup codes you saved when you enabled two-factor authentication.`}
</h6>
</div>
@ -77,12 +78,12 @@ export const BackupOtpPage = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Backup Code</FormLabel>
<FormLabel>{t`Backup Code`}</FormLabel>
<FormControl>
<Input
pattern="[a-z0-9]{10}"
placeholder="a1b2c3d4e5"
title="may contain lowercase letters or numbers, and must be exactly 10 characters."
title={t`Backup Codes may contain only lowercase letters or numbers, and must be exactly 10 characters.`}
{...field}
/>
</FormControl>
@ -92,7 +93,7 @@ export const BackupOtpPage = () => {
/>
<Button type="submit" disabled={loading} className="mt-4 w-full">
Sign in
{t`Sign in`}
</Button>
</form>
</Form>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { Warning } from "@phosphor-icons/react";
import { forgotPasswordSchema } from "@reactive-resume/dto";
import {
@ -44,7 +45,7 @@ export const ForgotPasswordPage = () => {
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while trying to send your password recovery email",
title: t`An error occurred while trying to send your password recovery email.`,
description: message,
});
}
@ -55,11 +56,10 @@ export const ForgotPasswordPage = () => {
return (
<div className="space-y-8">
<div className="space-y-4">
<h2 className="text-2xl font-semibold tracking-tight">You've got mail!</h2>
<h2 className="text-2xl font-semibold tracking-tight">{t`You've got mail!`}</h2>
<Alert variant="success">
<AlertDescription className="pt-0">
A password reset link should have been sent to your inbox, if an account existed with
the email you provided.
{t`A password reset link should have been sent to your inbox, if an account existed with the email you provided.`}
</AlertDescription>
</Alert>
</div>
@ -70,10 +70,9 @@ export const ForgotPasswordPage = () => {
return (
<div className="space-y-8">
<div className="space-y-1.5">
<h2 className="text-2xl font-semibold tracking-tight">Forgot your password?</h2>
<h2 className="text-2xl font-semibold tracking-tight">{t`Forgot your password?`}</h2>
<h6 className="leading-relaxed opacity-75">
Enter your email address and we will send you a link to reset your password if the account
exists.
{t`Enter your email address and we will send you a link to reset your password if the account exists.`}
</h6>
</div>
@ -85,7 +84,7 @@ export const ForgotPasswordPage = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormLabel>{t`Email`}</FormLabel>
<FormControl>
<Input placeholder="john.doe@example.com" {...field} />
</FormControl>
@ -95,7 +94,7 @@ export const ForgotPasswordPage = () => {
/>
<Button type="submit" disabled={loading} className="mt-4 w-full">
Send Email
{t`Send Email`}
</Button>
</form>
</Form>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { useMemo } from "react";
import { Link, matchRoutes, Outlet, useLocation } from "react-router-dom";
@ -25,7 +26,13 @@ export const AuthLayout = () => {
<>
<div className="flex items-center gap-x-4">
<hr className="flex-1" />
<span className="text-xs font-medium">or continue with</span>
<span className="text-xs font-medium">
{t({
message: "or continue with",
context:
"The user can either login with email/password, or continue with GitHub or Google.",
})}
</span>
<hr className="flex-1" />
</div>
@ -38,18 +45,18 @@ export const AuthLayout = () => {
<img
width={1920}
height={1080}
alt="Open books on a table"
alt={t`Open books on a table`}
className="h-screen w-full object-cover object-center"
src="/backgrounds/patrick-tomasso-Oaqk7qqNh_c-unsplash.jpg"
/>
<div className="absolute bottom-5 right-5 z-10 bg-primary/30 px-4 py-2 text-xs font-medium text-primary-foreground backdrop-blur-sm">
<a
href="https://unsplash.com/photos/Oaqk7qqNh_c"
target="_blank"
rel="noopener noreferrer nofollow"
href="https://unsplash.com/photos/Oaqk7qqNh_c"
>
Photograph by Patrick Tomasso
{t`Photograph by Patrick Tomasso`}
</a>
</div>
</div>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t, Trans } from "@lingui/macro";
import { ArrowRight, Warning } from "@phosphor-icons/react";
import { loginSchema } from "@reactive-resume/dto";
import { usePasswordToggle } from "@reactive-resume/hooks";
@ -48,7 +49,7 @@ export const LoginPage = () => {
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while trying to sign in",
title: t`An error occurred while trying to sign in to your account.`,
description: message,
});
}
@ -58,12 +59,13 @@ export const LoginPage = () => {
return (
<div className="space-y-8">
<div className="space-y-1.5">
<h2 className="text-2xl font-semibold tracking-tight">Sign in to your account</h2>
<h2 className="text-2xl font-semibold tracking-tight">{t`Sign in to your account`}</h2>
<h6>
<span className="opacity-75">Don't have an account?</span>
<span className="opacity-75">{t`Don't have an account?`}</span>
<Button asChild variant="link" className="px-1.5">
<Link to="/auth/register">
Create one now <ArrowRight className="ml-1" />
{t({ message: "Create one now", context: "This is a link to create a new account" })}{" "}
<ArrowRight className="ml-1" />
</Link>
</Button>
</h6>
@ -81,11 +83,11 @@ export const LoginPage = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormLabel>{t`Email`}</FormLabel>
<FormControl>
<Input placeholder="john.doe@example.com" {...field} />
</FormControl>
<FormDescription>You can also enter your username.</FormDescription>
<FormDescription>{t`You can also enter your username.`}</FormDescription>
<FormMessage />
</FormItem>
)}
@ -96,13 +98,15 @@ export const LoginPage = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormLabel>{t`Password`}</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormDescription>
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
temporarily.
<Trans>
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
temporarily.
</Trans>
</FormDescription>
<FormMessage />
</FormItem>
@ -111,11 +115,11 @@ export const LoginPage = () => {
<div className="mt-4 flex items-center gap-x-4">
<Button type="submit" disabled={loading} className="flex-1">
Sign in
{t`Sign in`}
</Button>
<Button asChild variant="link" className="px-4">
<Link to="/auth/forgot-password">Forgot Password?</Link>
<Link to="/auth/forgot-password">{t`Forgot Password?`}</Link>
</Button>
</div>
</form>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t, Trans } from "@lingui/macro";
import { ArrowRight, Warning } from "@phosphor-icons/react";
import { registerSchema } from "@reactive-resume/dto";
import { usePasswordToggle } from "@reactive-resume/hooks";
@ -38,7 +39,7 @@ export const RegisterPage = () => {
username: "",
email: "",
password: "",
language: "en",
locale: "en",
},
});
@ -56,7 +57,7 @@ export const RegisterPage = () => {
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while trying to sign up",
title: t`An error occurred while trying to create a new account.`,
description: message,
});
}
@ -66,12 +67,12 @@ export const RegisterPage = () => {
return (
<div className="space-y-8">
<div className="space-y-1.5">
<h2 className="text-2xl font-semibold tracking-tight">Create a new account</h2>
<h2 className="text-2xl font-semibold tracking-tight">{t`Create a new account`}</h2>
<h6>
<span className="opacity-75">Already have an account?</span>
<span className="opacity-75">{t`Already have an account?`}</span>
<Button asChild variant="link" className="px-1.5">
<Link to="/auth/login">
Sign in now <ArrowRight className="ml-1" />
{t`Sign in now`} <ArrowRight className="ml-1" />
</Link>
</Button>
</h6>
@ -89,9 +90,16 @@ export const RegisterPage = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t`Name`}</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
<Input
placeholder={t({
message: "John Doe",
context:
"Localized version of a placeholder name. For example, Max Mustermann in German or Jan Kowalski in Polish.",
})}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@ -103,9 +111,16 @@ export const RegisterPage = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormLabel>{t`Username`}</FormLabel>
<FormControl>
<Input placeholder="john.doe" {...field} />
<Input
placeholder={t({
message: "john.doe",
context:
"Localized version of a placeholder username. For example, max.mustermann in German or jan.kowalski in Polish.",
})}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@ -117,9 +132,16 @@ export const RegisterPage = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormLabel>{t`Email`}</FormLabel>
<FormControl>
<Input placeholder="john.doe@example.com" {...field} />
<Input
placeholder={t({
message: "john.doe@example.com",
context:
"Localized version of a placeholder email. For example, max.mustermann@example.de in German or jan.kowalski@example.pl in Polish.",
})}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@ -131,13 +153,15 @@ export const RegisterPage = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormLabel>{t`Password`}</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormDescription>
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
temporarily.
<Trans>
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
temporarily.
</Trans>
</FormDescription>
<FormMessage />
</FormItem>
@ -145,7 +169,7 @@ export const RegisterPage = () => {
/>
<Button disabled={loading} className="mt-4 w-full">
Sign up
{t`Sign up`}
</Button>
</form>
</Form>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t, Trans } from "@lingui/macro";
import { Warning } from "@phosphor-icons/react";
import { resetPasswordSchema } from "@reactive-resume/dto";
import { usePasswordToggle } from "@reactive-resume/hooks";
@ -53,7 +54,7 @@ export const ResetPasswordPage = () => {
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while trying to reset your password",
title: t`An error occurred while trying to reset your password.`,
description: message,
});
}
@ -68,9 +69,9 @@ export const ResetPasswordPage = () => {
return (
<div className="space-y-8">
<div className="space-y-1.5">
<h2 className="text-2xl font-semibold tracking-tight">Reset your password</h2>
<h2 className="text-2xl font-semibold tracking-tight">{t`Reset your password`}</h2>
<h6 className="leading-relaxed opacity-75">
Enter a new password below, and make sure it's secure.
{t`Enter a new password below, and make sure it's secure.`}
</h6>
</div>
@ -86,13 +87,15 @@ export const ResetPasswordPage = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormLabel>{t`Password`}</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormDescription>
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
temporarily.
<Trans>
Hold <code className="text-xs font-bold">Ctrl</code> to display your password
temporarily.
</Trans>
</FormDescription>
<FormMessage />
</FormItem>
@ -100,7 +103,7 @@ export const ResetPasswordPage = () => {
/>
<Button type="submit" disabled={loading} className="mt-4 w-full">
Update Password
{t`Change Password`}
</Button>
</form>
</Form>

View File

@ -1,3 +1,4 @@
import { t, Trans } from "@lingui/macro";
import { ArrowRight, Info, SealCheck, Warning } from "@phosphor-icons/react";
import { Alert, AlertDescription, AlertTitle, Button } from "@reactive-resume/ui";
import { AxiosError } from "axios";
@ -25,7 +26,7 @@ export const VerifyEmailPage = () => {
toast({
variant: "success",
icon: <SealCheck size={16} weight="bold" />,
title: "Your email address has been verified successfully.",
title: t`Your email address has been verified successfully.`,
});
navigate("/dashboard/resumes", { replace: true });
@ -36,7 +37,7 @@ export const VerifyEmailPage = () => {
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while trying to verify your email address",
title: t`An error occurred while trying to verify your email address.`,
description: message,
});
}
@ -51,27 +52,26 @@ export const VerifyEmailPage = () => {
return (
<div className="space-y-6">
<div className="space-y-2">
<h2 className="text-2xl font-semibold tracking-tight">Verify your email address</h2>
<h2 className="text-2xl font-semibold tracking-tight">{t`Verify your email address`}</h2>
<p className="leading-relaxed opacity-75">
You should have received an email from <strong>Reactive Resume</strong> with a link to
verify your account.
<Trans>
You should have received an email from <strong>Reactive Resume</strong> with a link to
verify your account.
</Trans>
</p>
</div>
<Alert variant="info">
<Info size={18} />
<AlertTitle>Please note that this step is completely optional.</AlertTitle>
<AlertTitle>{t`Please note that this step is completely optional.`}</AlertTitle>
<AlertDescription>
We verify your email address only to ensure that we can send you a password reset link in
case you forget your password.
{t`We verify your email address only to ensure that we can send you a password reset link in case you forget your password.`}
</AlertDescription>
</Alert>
<Button asChild disabled={loading}>
<Link to="/dashboard">
Continue to Dashboard
{t`Go to Dashboard`}
<ArrowRight className="ml-2" />
</Link>
</Button>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { ArrowRight, Warning } from "@phosphor-icons/react";
import { twoFactorSchema } from "@reactive-resume/dto";
import { usePasswordToggle } from "@reactive-resume/hooks";
@ -49,7 +50,7 @@ export const VerifyOtpPage = () => {
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while trying to sign in",
title: t`An error occurred while trying to sign in to your account.`,
description: message,
});
}
@ -59,14 +60,14 @@ export const VerifyOtpPage = () => {
return (
<div className="space-y-8">
<div className="space-y-1.5">
<h2 className="text-2xl font-semibold tracking-tight">Two Step Verification</h2>
<h2 className="text-2xl font-semibold tracking-tight">{t`Two-Factor Authentication`}</h2>
<h6>
<span className="opacity-75">
Enter the one-time password provided by your authenticator app below.
{t`Enter the one-time password provided by your authenticator app below.`}
</span>
<Button asChild variant="link" className="px-1.5">
<Link to="/auth/backup-otp">
Lost your device? <ArrowRight className="ml-1" />
{t`Lost your device?`} <ArrowRight className="ml-1" />
</Link>
</Button>
</h6>
@ -84,7 +85,7 @@ export const VerifyOtpPage = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>One-Time Password</FormLabel>
<FormLabel>{t`One-Time Password`}</FormLabel>
<FormControl>
<Input placeholder="123456" {...field} />
</FormControl>
@ -94,7 +95,7 @@ export const VerifyOtpPage = () => {
/>
<Button type="submit" disabled={loading} className="mt-4 w-full">
Sign in
{t`Sign in`}
</Button>
</form>
</Form>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { HouseSimple, Lock, SidebarSimple } from "@phosphor-icons/react";
import { useBreakpoint } from "@reactive-resume/hooks";
import { Button, Tooltip } from "@reactive-resume/ui";
@ -52,7 +53,7 @@ export const BuilderHeader = () => {
<h1 className="font-medium">{title}</h1>
{locked && (
<Tooltip content="This resume is locked, please unlock to make further changes.">
<Tooltip content={t`This resume is locked, please unlock to make further changes.`}>
<Lock size={14} className="ml-2 opacity-75" />
</Tooltip>
)}

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import {
ArrowClockwise,
ArrowCounterClockwise,
@ -55,14 +56,14 @@ export const BuilderToolbar = () => {
>
<div className="inline-flex items-center justify-center rounded-full bg-background px-4 shadow-xl">
{/* Undo */}
<Tooltip content="Undo">
<Tooltip content={t`Undo`}>
<Button size="icon" variant="ghost" className="rounded-none" onClick={() => undo()}>
<ArrowCounterClockwise />
</Button>
</Tooltip>
{/* Redo */}
<Tooltip content="Redo">
<Tooltip content={t`Redo`}>
<Button size="icon" variant="ghost" className="rounded-none" onClick={() => redo()}>
<ArrowClockwise />
</Button>
@ -70,25 +71,25 @@ export const BuilderToolbar = () => {
<Separator orientation="vertical" className="h-9" />
<Tooltip content="Zoom In">
<Tooltip content={t`Zoom In`}>
<Button size="icon" variant="ghost" className="rounded-none" onClick={onZoomIn}>
<MagnifyingGlassPlus />
</Button>
</Tooltip>
<Tooltip content="Zoom Out">
<Tooltip content={t`Zoom Out`}>
<Button size="icon" variant="ghost" className="rounded-none" onClick={onZoomOut}>
<MagnifyingGlassMinus />
</Button>
</Tooltip>
<Tooltip content="Reset Zoom">
<Tooltip content={t`Reset Zoom`}>
<Button size="icon" variant="ghost" className="rounded-none" onClick={onResetView}>
<ClockClockwise />
</Button>
</Tooltip>
<Tooltip content="Center Artboard">
<Tooltip content={t`Center Artboard`}>
<Button size="icon" variant="ghost" className="rounded-none" onClick={onCenterView}>
<CubeFocus />
</Button>
@ -97,7 +98,7 @@ export const BuilderToolbar = () => {
<Separator orientation="vertical" className="h-9" />
{/* Toggle Page Break Line */}
<Tooltip content="Toggle Page Break Line">
<Tooltip content={t`Toggle Page Break Line`}>
<Toggle
className="rounded-none"
pressed={pageOptions.breakLine}
@ -110,7 +111,7 @@ export const BuilderToolbar = () => {
</Tooltip>
{/* Toggle Page Numbers */}
<Tooltip content="Toggle Page Numbers">
<Tooltip content={t`Toggle Page Numbers`}>
<Toggle
className="rounded-none"
pressed={pageOptions.pageNumbers}
@ -125,14 +126,14 @@ export const BuilderToolbar = () => {
<Separator orientation="vertical" className="h-9" />
{/* Copy Link to Resume */}
<Tooltip content="Copy Link to Resume">
<Tooltip content={t`Copy Link to Resume`}>
<Button size="icon" variant="ghost" className="rounded-none" disabled={!isPublic}>
<LinkSimple />
</Button>
</Tooltip>
{/* Download PDF */}
<Tooltip content="Download PDF">
<Tooltip content={t`Download PDF`}>
<Button
size="icon"
variant="ghost"

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { ResumeDto } from "@reactive-resume/dto";
import { useCallback, useEffect } from "react";
import { Helmet } from "react-helmet-async";
@ -34,7 +35,9 @@ export const BuilderPage = () => {
return (
<>
<Helmet>
<title>{title} - Reactive Resume</title>
<title>
{title} - {t`Reactive Resume`}
</title>
</Helmet>
<iframe

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { awardSchema, defaultAward } from "@reactive-resume/schema";
import {
FormControl,
@ -35,9 +36,9 @@ export const AwardsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Title</FormLabel>
<FormLabel>{t({ message: "Title", context: "Name of the Award" })}</FormLabel>
<FormControl>
<Input {...field} placeholder="3rd Runner Up" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -49,9 +50,9 @@ export const AwardsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Awarder</FormLabel>
<FormLabel>{t`Awarder`}</FormLabel>
<FormControl>
<Input {...field} placeholder="TechCrunch Disrupt SF" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -63,9 +64,15 @@ export const AwardsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Date</FormLabel>
<FormLabel>{t`Date`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Aug 2019" />
<Input
{...field}
placeholder={t({
message: "March 2023",
comment: "The month and year should be uniform across all languages.",
})}
/>
</FormControl>
<FormMessage />
</FormItem>
@ -77,9 +84,9 @@ export const AwardsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Website</FormLabel>
<FormLabel>{t`Website`}</FormLabel>
<FormControl>
<URLInput {...field} placeholder="https://techcrunch.com/events/disrupt-sf-2019" />
<URLInput {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -91,7 +98,7 @@ export const AwardsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Summary</FormLabel>
<FormLabel>{t`Summary`}</FormLabel>
<FormControl>
<RichInput
{...field}

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { certificationSchema, defaultCertification } from "@reactive-resume/schema";
import {
FormControl,
@ -35,9 +36,9 @@ export const CertificationsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Name</FormLabel>
<FormLabel>{t({ message: "Name", context: "Name of the Certification" })}</FormLabel>
<FormControl>
<Input {...field} placeholder="Web Developer Bootcamp" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -49,9 +50,9 @@ export const CertificationsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Issuer</FormLabel>
<FormLabel>{t`Issuer`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Udemy" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -63,9 +64,9 @@ export const CertificationsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Date</FormLabel>
<FormLabel>{t`Date`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Aug 2019" />
<Input {...field} placeholder={t`March 2023`} />
</FormControl>
<FormMessage />
</FormItem>
@ -77,7 +78,7 @@ export const CertificationsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Website</FormLabel>
<FormLabel>{t`Website`}</FormLabel>
<FormControl>
<URLInput {...field} placeholder="https://udemy.com/certificate/UC-..." />
</FormControl>
@ -91,7 +92,7 @@ export const CertificationsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Summary</FormLabel>
<FormLabel>{t`Summary`}</FormLabel>
<FormControl>
<RichInput
{...field}

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { X } from "@phosphor-icons/react";
import { CustomSection, customSectionSchema, defaultCustomSection } from "@reactive-resume/schema";
import {
@ -49,7 +50,7 @@ export const CustomSectionDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Name</FormLabel>
<FormLabel>{t`Name`}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@ -63,7 +64,7 @@ export const CustomSectionDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Description</FormLabel>
<FormLabel>{t`Description`}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@ -77,7 +78,7 @@ export const CustomSectionDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Date</FormLabel>
<FormLabel>{t`Date`}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@ -91,7 +92,7 @@ export const CustomSectionDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Location</FormLabel>
<FormLabel>{t`Location`}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@ -105,7 +106,7 @@ export const CustomSectionDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Website</FormLabel>
<FormLabel>{t`Website`}</FormLabel>
<FormControl>
<URLInput {...field} />
</FormControl>
@ -119,7 +120,7 @@ export const CustomSectionDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Summary</FormLabel>
<FormLabel>{t`Summary`}</FormLabel>
<FormControl>
<RichInput
{...field}
@ -141,12 +142,12 @@ export const CustomSectionDialog = () => {
render={({ field }) => (
<div className="col-span-2 space-y-3">
<FormItem>
<FormLabel>Keywords</FormLabel>
<FormLabel>{t`Keywords`}</FormLabel>
<FormControl>
<BadgeInput {...field} />
</FormControl>
<FormDescription>
You can add multiple keywords by separating them with a comma.
{t`You can add multiple keywords by separating them with a comma or pressing enter.`}
</FormDescription>
<FormMessage />
</FormItem>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { defaultEducation, educationSchema } from "@reactive-resume/schema";
import {
FormControl,
@ -35,9 +36,9 @@ export const EducationDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Institution</FormLabel>
<FormLabel>{t`Institution`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Carnegie Mellon University" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -49,9 +50,14 @@ export const EducationDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Type of Study</FormLabel>
<FormLabel>
{t({
message: "Type of Study",
comment: "For example, Bachelor's Degree or Master's Degree",
})}
</FormLabel>
<FormControl>
<Input {...field} placeholder="Bachelor's Degree" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -63,9 +69,14 @@ export const EducationDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Area of Study</FormLabel>
<FormLabel>
{t({
message: "Area of Study",
comment: "For example, Computer Science or Business Administration",
})}
</FormLabel>
<FormControl>
<Input {...field} placeholder="Computer Science" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -77,7 +88,12 @@ export const EducationDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Score</FormLabel>
<FormLabel>
{t({
message: "Score",
comment: "Score or honors for the degree, for example, CGPA or magna cum laude",
})}
</FormLabel>
<FormControl>
<Input {...field} placeholder="9.2 GPA" />
</FormControl>
@ -91,9 +107,9 @@ export const EducationDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Date</FormLabel>
<FormLabel>{t`Date`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Aug 2006 - Oct 2012" />
<Input {...field} placeholder={t`March 2023 - Present`} />
</FormControl>
<FormMessage />
</FormItem>
@ -105,9 +121,9 @@ export const EducationDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Website</FormLabel>
<FormLabel>{t`Website`}</FormLabel>
<FormControl>
<URLInput {...field} placeholder="https://www.cmu.edu/" />
<URLInput {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -119,7 +135,7 @@ export const EducationDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Summary</FormLabel>
<FormLabel>{t`Summary`}</FormLabel>
<FormControl>
<RichInput
{...field}

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { defaultExperience, experienceSchema } from "@reactive-resume/schema";
import {
FormControl,
@ -35,9 +36,9 @@ export const ExperienceDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Company</FormLabel>
<FormLabel>{t`Company`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Alphabet Inc." />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -49,9 +50,14 @@ export const ExperienceDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Position</FormLabel>
<FormLabel>
{t({
message: "Position",
context: "Position held at a company, for example, Software Engineer",
})}
</FormLabel>
<FormControl>
<Input {...field} placeholder="Chief Executive Officer" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -63,9 +69,9 @@ export const ExperienceDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Date</FormLabel>
<FormLabel>{t`Date`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Dec 2019 - Present" />
<Input {...field} placeholder={t`March 2023 - Present`} />
</FormControl>
<FormMessage />
</FormItem>
@ -77,9 +83,9 @@ export const ExperienceDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Location</FormLabel>
<FormLabel>{t`Location`}</FormLabel>
<FormControl>
<Input {...field} placeholder="New York, NY" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -91,9 +97,9 @@ export const ExperienceDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Website</FormLabel>
<FormLabel>{t`Website`}</FormLabel>
<FormControl>
<URLInput {...field} placeholder="https://www.abc.xyz/" />
<URLInput {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -105,7 +111,7 @@ export const ExperienceDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Summary</FormLabel>
<FormLabel>{t`Summary`}</FormLabel>
<FormControl>
<RichInput
{...field}

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { X } from "@phosphor-icons/react";
import { defaultInterest, interestSchema } from "@reactive-resume/schema";
import {
@ -36,9 +37,9 @@ export const InterestsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Name</FormLabel>
<FormLabel>{t`Name`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Video Games" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -51,12 +52,12 @@ export const InterestsDialog = () => {
render={({ field }) => (
<div className="col-span-2 space-y-3">
<FormItem>
<FormLabel>Keywords</FormLabel>
<FormLabel>{t`Keywords`}</FormLabel>
<FormControl>
<BadgeInput {...field} placeholder="FIFA 23, Call of Duty, etc." />
<BadgeInput {...field} />
</FormControl>
<FormDescription>
You can add multiple keywords by separating them with a comma.
{t`You can add multiple keywords by separating them with a comma or pressing enter.`}
</FormDescription>
<FormMessage />
</FormItem>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { defaultLanguage, languageSchema } from "@reactive-resume/schema";
import {
FormControl,
@ -33,9 +34,9 @@ export const LanguagesDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Name</FormLabel>
<FormLabel>{t`Name`}</FormLabel>
<FormControl>
<Input {...field} placeholder="German" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -47,9 +48,9 @@ export const LanguagesDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Fluency</FormLabel>
<FormLabel>{t`Fluency`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Native Speaker" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -61,7 +62,7 @@ export const LanguagesDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Fluency (CEFR)</FormLabel>
<FormLabel>{t`Fluency (CEFR)`}</FormLabel>
<FormControl className="py-2">
<div className="flex items-center gap-x-4">
<Slider

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t, Trans } from "@lingui/macro";
import { defaultProfile, profileSchema } from "@reactive-resume/schema";
import {
Avatar,
@ -35,8 +36,9 @@ export const ProfilesDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Network</FormLabel>
<FormLabel>{t`Network`}</FormLabel>
<FormControl>
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
<Input {...field} placeholder="LinkedIn" />
</FormControl>
<FormMessage />
@ -49,9 +51,9 @@ export const ProfilesDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Username</FormLabel>
<FormLabel>{t`Username`}</FormLabel>
<FormControl>
<Input {...field} placeholder="johndoe" />
<Input {...field} placeholder="john.doe" />
</FormControl>
<FormMessage />
</FormItem>
@ -63,7 +65,7 @@ export const ProfilesDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>URL</FormLabel>
<FormLabel>{t`Website`}</FormLabel>
<FormControl>
<URLInput {...field} placeholder="https://linkedin.com/in/johndoe" />
</FormControl>
@ -77,7 +79,7 @@ export const ProfilesDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel htmlFor="iconSlug">Icon</FormLabel>
<FormLabel htmlFor="iconSlug">{t`Icon`}</FormLabel>
<FormControl>
<div className="flex items-center gap-x-2">
<Avatar className="h-8 w-8 bg-white">
@ -93,15 +95,17 @@ export const ProfilesDialog = () => {
</FormControl>
<FormMessage />
<FormDescription className="ml-10">
Powered by{" "}
<a
href="https://simpleicons.org/"
target="_blank"
rel="noopener noreferrer nofollow"
className="font-medium"
>
Simple Icons
</a>
<Trans>
Powered by{" "}
<a
href="https://simpleicons.org/"
target="_blank"
rel="noopener noreferrer nofollow"
className="font-medium"
>
Simple Icons
</a>
</Trans>
</FormDescription>
</FormItem>
)}

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { X } from "@phosphor-icons/react";
import { defaultProject, projectSchema } from "@reactive-resume/schema";
import {
@ -40,9 +41,9 @@ export const ProjectsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Name</FormLabel>
<FormLabel>{t`Name`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Reactive Resume" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -54,9 +55,9 @@ export const ProjectsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Description</FormLabel>
<FormLabel>{t`Description`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Open Source Resume Builder" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -68,9 +69,9 @@ export const ProjectsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Date</FormLabel>
<FormLabel>{t`Date`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Sep 2018 - Present" />
<Input {...field} placeholder={t`March 2023 - Present`} />
</FormControl>
<FormMessage />
</FormItem>
@ -82,7 +83,7 @@ export const ProjectsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Website</FormLabel>
<FormLabel>{t`Website`}</FormLabel>
<FormControl>
<URLInput {...field} placeholder="https://rxresu.me" />
</FormControl>
@ -96,7 +97,7 @@ export const ProjectsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Summary</FormLabel>
<FormLabel>{t`Summary`}</FormLabel>
<FormControl>
<RichInput
{...field}
@ -118,12 +119,12 @@ export const ProjectsDialog = () => {
render={({ field }) => (
<div className="col-span-2 space-y-3">
<FormItem>
<FormLabel>Keywords</FormLabel>
<FormLabel>{t`Keywords`}</FormLabel>
<FormControl>
<BadgeInput {...field} placeholder="FIFA 23, Call of Duty, etc." />
<BadgeInput {...field} />
</FormControl>
<FormDescription>
You can add multiple keywords by separating them with a comma.
{t`You can add multiple keywords by separating them with a comma or pressing enter.`}
</FormDescription>
<FormMessage />
</FormItem>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { defaultPublication, publicationSchema } from "@reactive-resume/schema";
import {
FormControl,
@ -35,9 +36,9 @@ export const PublicationsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Name</FormLabel>
<FormLabel>{t`Name`}</FormLabel>
<FormControl>
<Input {...field} placeholder="The Great Gatsby" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -49,9 +50,9 @@ export const PublicationsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Publisher</FormLabel>
<FormLabel>{t`Publisher`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Charles Scribner's Sons" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -63,9 +64,9 @@ export const PublicationsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Release Date</FormLabel>
<FormLabel>{t`Date`}</FormLabel>
<FormControl>
<Input {...field} placeholder="April 10, 1925" />
<Input {...field} placeholder={t`March 2023`} />
</FormControl>
<FormMessage />
</FormItem>
@ -77,9 +78,9 @@ export const PublicationsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Website</FormLabel>
<FormLabel>{t`Website`}</FormLabel>
<FormControl>
<URLInput {...field} placeholder="https://books.google.com/..." />
<URLInput {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -91,7 +92,7 @@ export const PublicationsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Summary</FormLabel>
<FormLabel>{t`Summary`}</FormLabel>
<FormControl>
<RichInput
{...field}

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { defaultReference, referenceSchema } from "@reactive-resume/schema";
import {
FormControl,
@ -35,9 +36,9 @@ export const ReferencesDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Name</FormLabel>
<FormLabel>{t`Name`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Cosmo Kramer" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -49,9 +50,9 @@ export const ReferencesDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Description</FormLabel>
<FormLabel>{t`Description`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Neighbour" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -63,9 +64,9 @@ export const ReferencesDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Website</FormLabel>
<FormLabel>{t`Website`}</FormLabel>
<FormControl>
<URLInput {...field} placeholder="https://linkedin.com/in/cosmo.kramer" />
<URLInput {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -77,7 +78,7 @@ export const ReferencesDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Summary</FormLabel>
<FormLabel>{t`Summary`}</FormLabel>
<FormControl>
<RichInput
{...field}

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { X } from "@phosphor-icons/react";
import { defaultSkill, skillSchema } from "@reactive-resume/schema";
import {
@ -37,9 +38,9 @@ export const SkillsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Name</FormLabel>
<FormLabel>{t`Name`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Content Management" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -51,9 +52,9 @@ export const SkillsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Description</FormLabel>
<FormLabel>{t`Description`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Advanced" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -65,7 +66,7 @@ export const SkillsDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Level</FormLabel>
<FormLabel>{t`Level`}</FormLabel>
<FormControl className="py-2">
<div className="flex items-center gap-x-4">
<Slider
@ -91,12 +92,12 @@ export const SkillsDialog = () => {
render={({ field }) => (
<div className="col-span-2 space-y-3">
<FormItem>
<FormLabel>Keywords</FormLabel>
<FormLabel>{t`Keywords`}</FormLabel>
<FormControl>
<BadgeInput {...field} placeholder="WordPress, Joomla, Webflow etc." />
<BadgeInput {...field} />
</FormControl>
<FormDescription>
You can add multiple keywords by separating them with a comma.
{t`You can add multiple keywords by separating them with a comma or pressing enter.`}
</FormDescription>
<FormMessage />
</FormItem>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { defaultVolunteer, volunteerSchema } from "@reactive-resume/schema";
import {
FormControl,
@ -35,9 +36,9 @@ export const VolunteerDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Organization</FormLabel>
<FormLabel>{t`Organization`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Amnesty International" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -49,9 +50,9 @@ export const VolunteerDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Position</FormLabel>
<FormLabel>{t`Position`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Recruiter" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -63,9 +64,9 @@ export const VolunteerDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Date</FormLabel>
<FormLabel>{t`Date`}</FormLabel>
<FormControl>
<Input {...field} placeholder="Dec 2016 - Aug 2017" />
<Input {...field} placeholder={t`March 2023 - Present`} />
</FormControl>
<FormMessage />
</FormItem>
@ -77,9 +78,9 @@ export const VolunteerDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1">
<FormLabel>Location</FormLabel>
<FormLabel>{t`Location`}</FormLabel>
<FormControl>
<Input {...field} placeholder="New York, NY" />
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -91,9 +92,9 @@ export const VolunteerDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Website</FormLabel>
<FormLabel>{t`Website`}</FormLabel>
<FormControl>
<URLInput {...field} placeholder="https://www.amnesty.org/" />
<URLInput {...field} />
</FormControl>
<FormMessage />
</FormItem>
@ -105,7 +106,7 @@ export const VolunteerDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem className="col-span-1 sm:col-span-2">
<FormLabel>Summary</FormLabel>
<FormLabel>{t`Summary`}</FormLabel>
<FormControl>
<RichInput
{...field}

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { Plus, PlusCircle } from "@phosphor-icons/react";
import {
Award,
@ -50,7 +51,15 @@ export const LeftSidebar = () => {
</Button>
<div className="flex flex-col items-center justify-center gap-y-2">
<SectionIcon id="basics" name="Basics" onClick={() => scrollIntoView("#basics")} />
<SectionIcon
id="basics"
onClick={() => scrollIntoView("#basics")}
name={t({
message: "Basics",
context:
"The Basics section of a Resume consists of User's Picture, Full Name, Location etc.",
})}
/>
<SectionIcon id="summary" onClick={() => scrollIntoView("#summary")} />
<SectionIcon id="profiles" onClick={() => scrollIntoView("#profiles")} />
<SectionIcon id="experience" onClick={() => scrollIntoView("#experience")} />
@ -68,10 +77,11 @@ export const LeftSidebar = () => {
<SectionIcon
id="custom"
variant="outline"
name="Add a new section"
name={t`Add a new section`}
icon={<Plus size={14} />}
onClick={() => {
addSection();
// eslint-disable-next-line lingui/no-unlocalized-strings
scrollIntoView("& > section:last-of-type");
}}
/>
@ -184,7 +194,7 @@ export const LeftSidebar = () => {
<Button size="lg" variant="outline" onClick={addSection}>
<PlusCircle />
<span className="ml-2">Add a new section</span>
<span className="ml-2">{t`Add a new section`}</span>
</Button>
</div>
</ScrollArea>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { basicsSchema } from "@reactive-resume/schema";
import { Input, Label } from "@reactive-resume/ui";
@ -17,7 +18,7 @@ export const BasicsSection = () => {
<header className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
{getSectionIcon("basics")}
<h2 className="line-clamp-1 text-3xl font-bold">Basics</h2>
<h2 className="line-clamp-1 text-3xl font-bold">{t`Basics`}</h2>
</div>
</header>
@ -27,10 +28,9 @@ export const BasicsSection = () => {
</div>
<div className="space-y-1.5 sm:col-span-2">
<Label htmlFor="basics.name">Full Name</Label>
<Label htmlFor="basics.name">{t`Full Name`}</Label>
<Input
id="basics.name"
placeholder="John Doe"
value={basics.name}
hasError={!basicsSchema.pick({ name: true }).safeParse({ name: basics.name }).success}
onChange={(event) => setValue("basics.name", event.target.value)}
@ -38,17 +38,16 @@ export const BasicsSection = () => {
</div>
<div className="space-y-1.5 sm:col-span-2">
<Label htmlFor="basics.headline">Headline</Label>
<Label htmlFor="basics.headline">{t`Headline`}</Label>
<Input
id="basics.headline"
placeholder="Highly Creative Frontend Web Developer"
value={basics.headline}
onChange={(event) => setValue("basics.headline", event.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="basics.email">Email Address</Label>
<Label htmlFor="basics.email">{t`Email`}</Label>
<Input
id="basics.email"
placeholder="john.doe@example.com"
@ -61,7 +60,7 @@ export const BasicsSection = () => {
</div>
<div className="space-y-1.5">
<Label htmlFor="basics.url">Website</Label>
<Label htmlFor="basics.url">{t`Website`}</Label>
<URLInput
id="basics.url"
value={basics.url}
@ -71,7 +70,7 @@ export const BasicsSection = () => {
</div>
<div className="space-y-1.5">
<Label htmlFor="basics.phone">Phone Number</Label>
<Label htmlFor="basics.phone">{t`Phone`}</Label>
<Input
id="basics.phone"
placeholder="+1 (123) 4567 7890"
@ -81,10 +80,9 @@ export const BasicsSection = () => {
</div>
<div className="space-y-1.5">
<Label htmlFor="basics.location">Location</Label>
<Label htmlFor="basics.location">{t`Location`}</Label>
<Input
id="basics.location"
placeholder="105 Cedarhurst Ave, Cedarhurst, NY 11516"
value={basics.location}
onChange={(event) => setValue("basics.location", event.target.value)}
/>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { createId } from "@paralleldrive/cuid2";
import { DotsSixVertical, Plus, X } from "@phosphor-icons/react";
import { CustomField as ICustomField } from "@reactive-resume/schema";
@ -38,21 +39,22 @@ export const CustomField = ({ field, onChange, onRemove }: CustomFieldProps) =>
<DotsSixVertical />
</Button>
<Input
placeholder="Icon"
{/* <Input
placeholder={t`Icon`}
value={field.icon}
className="!ml-0"
onChange={(event) => handleChange("icon", event.target.value)}
/>
/> */}
<Input
placeholder="Name"
placeholder={t`Name`}
value={field.name}
className="!ml-0"
onChange={(event) => handleChange("name", event.target.value)}
/>
<Input
placeholder="Value"
placeholder={t`Value`}
value={field.value}
onChange={(event) => handleChange("value", event.target.value)}
/>
@ -126,7 +128,7 @@ export const CustomFieldsSection = ({ className }: Props) => {
<Button variant="link" onClick={onAddCustomField}>
<Plus className="mr-2" />
<span>Add a custom field</span>
<span>{t`Add a custom field`}</span>
</Button>
</div>
);

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import {
AspectRatio,
Checkbox,
@ -69,7 +70,7 @@ export const PictureOptions = () => {
<div className="flex flex-col gap-y-5">
<div className="grid grid-cols-3 items-center gap-x-6">
<Label htmlFor="picture.size" className="col-span-1">
Size (in px)
{t`Size (in px)`}
</Label>
<Input
type="number"
@ -85,7 +86,7 @@ export const PictureOptions = () => {
<div className="grid grid-cols-3 items-center gap-x-6">
<Label htmlFor="picture.aspectRatio" className="col-span-1">
Aspect Ratio
{t`Aspect Ratio`}
</Label>
<div className="col-span-2 flex items-center justify-between">
<ToggleGroup
@ -94,19 +95,19 @@ export const PictureOptions = () => {
onValueChange={onAspectRatioChange}
className="flex items-center justify-center"
>
<Tooltip content="Square">
<Tooltip content={t`Square`}>
<ToggleGroupItem value="square">
<div className="h-3 w-3 border border-foreground" />
</ToggleGroupItem>
</Tooltip>
<Tooltip content="Horizontal">
<Tooltip content={t`Horizontal`}>
<ToggleGroupItem value="horizontal">
<div className="h-2 w-3 border border-foreground" />
</ToggleGroupItem>
</Tooltip>
<Tooltip content="Portrait">
<Tooltip content={t`Portrait`}>
<ToggleGroupItem value="portrait">
<div className="h-3 w-2 border border-foreground" />
</ToggleGroupItem>
@ -130,7 +131,7 @@ export const PictureOptions = () => {
<div className="grid grid-cols-3 items-center gap-x-6">
<Label htmlFor="picture.borderRadius" className="col-span-1">
Border Radius
{t`Border Radius`}
</Label>
<div className="col-span-2 flex items-center justify-between">
<ToggleGroup
@ -139,19 +140,19 @@ export const PictureOptions = () => {
onValueChange={onBorderRadiusChange}
className="flex items-center justify-center"
>
<Tooltip content="Square">
<Tooltip content={t`Square`}>
<ToggleGroupItem value="square">
<div className="h-3 w-3 border border-foreground" />
</ToggleGroupItem>
</Tooltip>
<Tooltip content="Rounded">
<Tooltip content={t`Rounded`}>
<ToggleGroupItem value="rounded">
<div className="h-3 w-3 rounded-sm border border-foreground" />
</ToggleGroupItem>
</Tooltip>
<Tooltip content="Circle">
<Tooltip content={t`Circle`}>
<ToggleGroupItem value="circle">
<div className="h-3 w-3 rounded-full border border-foreground" />
</ToggleGroupItem>
@ -176,7 +177,7 @@ export const PictureOptions = () => {
<div>
<div className="grid grid-cols-3 items-start gap-x-6">
<div className="col-span-1">
<Label>Effects</Label>
<Label>{t`Effects`}</Label>
</div>
<div className="col-span-2 space-y-4">
<div className="flex items-center space-x-2">
@ -187,7 +188,7 @@ export const PictureOptions = () => {
setValue("basics.picture.effects.hidden", checked);
}}
/>
<Label htmlFor="picture.effects.hidden">Hidden</Label>
<Label htmlFor="picture.effects.hidden">{t`Hidden`}</Label>
</div>
<div className="flex items-center space-x-2">
@ -198,7 +199,7 @@ export const PictureOptions = () => {
setValue("basics.picture.effects.border", checked);
}}
/>
<Label htmlFor="picture.effects.border">Border</Label>
<Label htmlFor="picture.effects.border">{t`Border`}</Label>
</div>
<div className="flex items-center space-x-2">
@ -209,7 +210,7 @@ export const PictureOptions = () => {
setValue("basics.picture.effects.grayscale", checked);
}}
/>
<Label htmlFor="picture.effects.grayscale">Grayscale</Label>
<Label htmlFor="picture.effects.grayscale">{t`Grayscale`}</Label>
</div>
</div>
</div>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { Aperture, UploadSimple } from "@phosphor-icons/react";
import {
Avatar,
@ -48,7 +49,7 @@ export const PictureSection = () => {
</Avatar>
<div className="flex w-full flex-col gap-y-1.5">
<Label htmlFor="basics.picture.url">Picture</Label>
<Label htmlFor="basics.picture.url">{t`Picture`}</Label>
<div className="flex items-center gap-x-2">
<Input
id="basics.picture.url"

View File

@ -14,6 +14,7 @@ import {
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { t } from "@lingui/macro";
import { Plus } from "@phosphor-icons/react";
import { SectionItem, SectionKey, SectionWithItem } from "@reactive-resume/schema";
import { Button } from "@reactive-resume/ui";
@ -103,7 +104,12 @@ export const SectionBase = <T extends SectionItem>({ id, title, description }: P
className="gap-x-2 border-dashed py-6 leading-relaxed hover:bg-secondary-accent"
>
<Plus size={14} />
<span className="font-medium">Add New {section.name}</span>
<span className="font-medium">
{t({
message: "Add New Item",
context: "For example, add a new work experience, or add a new profile.",
})}
</span>
</Button>
)}
@ -137,7 +143,12 @@ export const SectionBase = <T extends SectionItem>({ id, title, description }: P
<footer className="flex items-center justify-end">
<Button variant="outline" className="ml-auto gap-x-2" onClick={onCreate}>
<Plus />
<span>Add New {section.name}</span>
<span>
{t({
message: "Add New Item",
context: "For example, add a new work experience, or add a new profile.",
})}
</span>
</Button>
</footer>
)}

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { createId } from "@paralleldrive/cuid2";
import { CopySimple, PencilSimple, Plus } from "@phosphor-icons/react";
import { SectionItem, SectionWithItem } from "@reactive-resume/schema";
@ -20,7 +21,7 @@ import {
} from "@reactive-resume/ui";
import { produce } from "immer";
import get from "lodash.get";
import { useEffect, useMemo } from "react";
import { useEffect } from "react";
import { UseFormReturn } from "react-hook-form";
import { DialogName, useDialog } from "@/client/stores/dialog";
@ -40,12 +41,12 @@ export const SectionDialog = <T extends SectionItem>({
children,
}: Props<T>) => {
const { isOpen, mode, close, payload } = useDialog<T>(id);
const setValue = useResumeStore((state) => state.setValue);
const section = useResumeStore((state) => {
if (!id) return null;
return get(state.resume.data.sections, id);
}) as SectionWithItem<T> | null;
const name = useMemo(() => section?.name ?? "", [section?.name]);
const isCreate = mode === "create";
const isUpdate = mode === "update";
@ -111,18 +112,16 @@ export const SectionDialog = <T extends SectionItem>({
<Form {...form}>
<form>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure you want to delete this {name}?</AlertDialogTitle>
<AlertDialogTitle>{t`Are you sure you want to delete this item?`}</AlertDialogTitle>
<AlertDialogDescription>
This action can be reverted by clicking on the undo button in the floating
toolbar.
{t`This action can be reverted by clicking on the undo button in the floating toolbar.`}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>{t`Cancel`}</AlertDialogCancel>
<AlertDialogAction variant="error" onClick={form.handleSubmit(onSubmit)}>
Delete
{t`Delete`}
</AlertDialogAction>
</AlertDialogFooter>
</form>
@ -144,9 +143,9 @@ export const SectionDialog = <T extends SectionItem>({
{isUpdate && <PencilSimple />}
{isDuplicate && <CopySimple />}
<h2>
{isCreate && `Create a new ${name}`}
{isUpdate && `Update an existing ${name}`}
{isDuplicate && `Duplicate an existing ${name}`}
{isCreate && t`Create a new item`}
{isUpdate && t`Update an existing item`}
{isDuplicate && t`Duplicate an existing item`}
</h2>
</div>
</DialogTitle>
@ -156,9 +155,9 @@ export const SectionDialog = <T extends SectionItem>({
<DialogFooter>
<Button type="submit">
{isCreate && "Create"}
{isUpdate && "Save Changes"}
{isDuplicate && "Duplicate"}
{isCreate && t`Create`}
{isUpdate && t`Save Changes`}
{isDuplicate && t`Duplicate`}
</Button>
</DialogFooter>
</form>

View File

@ -1,5 +1,6 @@
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { t } from "@lingui/macro";
import { CopySimple, DotsSixVertical, PencilSimple, TrashSimple } from "@phosphor-icons/react";
import {
DropdownMenu,
@ -81,19 +82,19 @@ export const SectionListItem = ({
</DropdownMenuTrigger>
<DropdownMenuContent align="center" side="left" sideOffset={-16}>
<DropdownMenuCheckboxItem checked={visible} onCheckedChange={onToggleVisibility}>
<span className="-ml-0.5">Visible</span>
<span className="-ml-0.5">{t`Visible`}</span>
</DropdownMenuCheckboxItem>
<DropdownMenuItem onClick={onUpdate}>
<PencilSimple size={14} />
<span className="ml-2">Edit</span>
<span className="ml-2">{t`Edit`}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={onDuplicate}>
<CopySimple size={14} />
<span className="ml-2">Copy</span>
<span className="ml-2">{t`Copy`}</span>
</DropdownMenuItem>
<DropdownMenuItem className="text-error" onClick={onDelete}>
<TrashSimple size={14} />
<span className="ml-2">Remove</span>
<span className="ml-2">{t`Remove`}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -1,3 +1,4 @@
import { plural, t } from "@lingui/macro";
import {
ArrowCounterClockwise,
Broom,
@ -63,7 +64,7 @@ export const SectionOptions = ({ id }: Props) => {
<>
<DropdownMenuItem onClick={onCreate}>
<Plus />
<span className="ml-2">Add a new item</span>
<span className="ml-2">{t`Add a new item`}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
@ -73,12 +74,12 @@ export const SectionOptions = ({ id }: Props) => {
<DropdownMenuGroup>
<DropdownMenuItem onClick={toggleVisibility}>
{section.visible ? <Eye /> : <EyeSlash />}
<span className="ml-2">{section.visible ? "Hide" : "Show"}</span>
<span className="ml-2">{section.visible ? t`Hide` : t`Show`}</span>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<PencilSimple />
<span className="ml-2">Rename</span>
<span className="ml-2">{t`Rename`}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<div className="relative col-span-2">
@ -103,15 +104,15 @@ export const SectionOptions = ({ id }: Props) => {
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Columns />
<span className="ml-2">Columns</span>
<span className="ml-2">{t`Columns`}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup value={`${section.columns}`} onValueChange={onChangeColumns}>
<DropdownMenuRadioItem value="1">1 Column</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="2">2 Columns</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="3">3 Columns</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="4">4 Columns</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="5">5 Columns</DropdownMenuRadioItem>
{Array.from({ length: 5 }, (_, i) => i + 1).map((value) => (
<DropdownMenuRadioItem value={`${value}`}>
{plural(value, { one: "Column", other: "Columns" })}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
@ -119,12 +120,12 @@ export const SectionOptions = ({ id }: Props) => {
<DropdownMenuSeparator />
<DropdownMenuItem disabled={!hasItems} onClick={onResetItems}>
<Broom />
<span className="ml-2">Reset</span>
<span className="ml-2">{t`Reset`}</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-error" disabled={!isCustomSection} onClick={onRemove}>
<TrashSimple />
<span className="ml-2">Remove</span>
<span className="ml-2">{t`Remove`}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { Tag } from "@phosphor-icons/react";
import { URL, urlSchema } from "@reactive-resume/schema";
import { Button, Input, Popover, PopoverContent, PopoverTrigger } from "@reactive-resume/ui";
@ -36,14 +37,14 @@ export const URLInput = forwardRef<HTMLInputElement, Props>(
<PopoverContent className="p-1.5">
<Input
value={value.label}
placeholder="Label"
placeholder={t`Label`}
onChange={(event) => onChange({ ...value, label: event.target.value })}
/>
</PopoverContent>
</Popover>
</div>
{hasError && <small className="opacity-75">URL must start with https://</small>}
{hasError && <small className="opacity-75">{t`URL must start with https://`}</small>}
</>
);
},

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { ScrollArea, Separator } from "@reactive-resume/ui";
import { useRef } from "react";
@ -56,26 +57,30 @@ export const RightSidebar = () => {
<div />
<div className="flex flex-col items-center justify-center gap-y-2">
<SectionIcon id="template" name="Template" onClick={() => scrollIntoView("#template")} />
<SectionIcon id="layout" name="Layout" onClick={() => scrollIntoView("#layout")} />
<SectionIcon
id="template"
name={t`Template`}
onClick={() => scrollIntoView("#template")}
/>
<SectionIcon id="layout" name={t`Layout`} onClick={() => scrollIntoView("#layout")} />
<SectionIcon
id="typography"
name="Typography"
name={t`Typography`}
onClick={() => scrollIntoView("#typography")}
/>
<SectionIcon id="theme" name="Theme" onClick={() => scrollIntoView("#theme")} />
<SectionIcon id="page" name="Page" onClick={() => scrollIntoView("#page")} />
<SectionIcon id="sharing" name="Sharing" onClick={() => scrollIntoView("#sharing")} />
<SectionIcon id="theme" name={t`Theme`} onClick={() => scrollIntoView("#theme")} />
<SectionIcon id="page" name={t`Page`} onClick={() => scrollIntoView("#page")} />
<SectionIcon id="sharing" name={t`Sharing`} onClick={() => scrollIntoView("#sharing")} />
<SectionIcon
id="statistics"
name="Statistics"
name={t`Statistics`}
onClick={() => scrollIntoView("#statistics")}
/>
<SectionIcon id="export" name="Export" onClick={() => scrollIntoView("#export")} />
<SectionIcon id="notes" name="Notes" onClick={() => scrollIntoView("#notes")} />
<SectionIcon id="export" name={t`Export`} onClick={() => scrollIntoView("#export")} />
<SectionIcon id="notes" name={t`Notes`} onClick={() => scrollIntoView("#notes")} />
<SectionIcon
id="information"
name="Information"
name={t`Information`}
onClick={() => scrollIntoView("#information")}
/>
</div>

View File

@ -1,16 +1,15 @@
import { t } from "@lingui/macro";
import { CircleNotch, FileJs, FilePdf } from "@phosphor-icons/react";
import { buttonVariants, Card, CardContent, CardDescription, CardTitle } from "@reactive-resume/ui";
import { cn } from "@reactive-resume/utils";
import { saveAs } from "file-saver";
import { useToast } from "@/client/hooks/use-toast";
import { usePrintResume } from "@/client/services/resume/print";
import { useResumeStore } from "@/client/stores/resume";
import { getSectionIcon } from "../shared/section-icon";
export const ExportSection = () => {
const { toast } = useToast();
const { printResume, loading } = usePrintResume();
const onJsonExport = () => {
@ -19,11 +18,6 @@ export const ExportSection = () => {
const resumeJSON = JSON.stringify(resume.data, null, 2);
saveAs(new Blob([resumeJSON], { type: "application/json" }), filename);
toast({
variant: "success",
title: "A JSON snapshot of your resume has been successfully exported.",
});
};
const onPdfExport = async () => {
@ -43,7 +37,7 @@ export const ExportSection = () => {
<header className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
{getSectionIcon("export")}
<h2 className="line-clamp-1 text-3xl font-bold">Export</h2>
<h2 className="line-clamp-1 text-3xl font-bold">{t`Export`}</h2>
</div>
</header>
@ -57,10 +51,9 @@ export const ExportSection = () => {
>
<FileJs size={22} />
<CardContent className="flex-1">
<CardTitle className="text-sm">JSON</CardTitle>
<CardTitle className="text-sm">{t`JSON`}</CardTitle>
<CardDescription className="font-normal">
Download a JSON snapshot of your resume. This file can be used to import your resume
in the future, or can even be shared with others to collaborate.
{t`Download a JSON snapshot of your resume. This file can be used to import your resume in the future, or can even be shared with others to collaborate.`}
</CardDescription>
</CardContent>
</Card>
@ -76,10 +69,9 @@ export const ExportSection = () => {
{loading ? <CircleNotch size={22} className="animate-spin" /> : <FilePdf size={22} />}
<CardContent className="flex-1">
<CardTitle className="text-sm">PDF</CardTitle>
<CardTitle className="text-sm">{t`PDF`}</CardTitle>
<CardDescription className="font-normal">
Download a PDF of your resume. This file can be used to print your resume, send it to
recruiters, or upload on job portals.
{t`Download a PDF of your resume. This file can be used to print your resume, send it to recruiters, or upload on job portals.`}
</CardDescription>
</CardContent>
</Card>

View File

@ -1,3 +1,4 @@
import { t, Trans } from "@lingui/macro";
import { Book, EnvelopeSimpleOpen, GithubLogo, HandHeart } from "@phosphor-icons/react";
import {
buttonVariants,
@ -14,28 +15,30 @@ import { getSectionIcon } from "../shared/section-icon";
const DonateCard = () => (
<Card className="space-y-4 bg-info text-info-foreground">
<CardContent className="space-y-2">
<CardTitle>Support the app by donating what you can!</CardTitle>
<CardTitle>{t`Support the app by donating what you can!`}</CardTitle>
<CardDescription className="space-y-2">
<p>
I built Reactive Resume mostly by myself during my spare time, with a lot of help from
other great open-source contributors.
</p>
<p>
If you like the app and want to support keeping it free forever, please donate whatever
you can afford to give.
</p>
<Trans>
<p>
I built Reactive Resume mostly by myself during my spare time, with a lot of help from
other great open-source contributors.
</p>
<p>
If you like the app and want to support keeping it free forever, please donate whatever
you can afford to give.
</p>
<p>Your donations could be tax-deductible, depending on your location.</p>
</Trans>
</CardDescription>
</CardContent>
<CardFooter>
<a
href="https://opencollective.com/reactive-resume"
className={cn(buttonVariants({ size: "sm" }))}
href="https://github.com/sponsors/AmruthPillai"
target="_blank"
rel="noopener noreferrer nofollow"
target="_blank"
>
<HandHeart size={14} weight="bold" className="mr-2" />
<span>Donate to Reactive Resume</span>
<span>{t`Donate to Reactive Resume`}</span>
</a>
</CardFooter>
</Card>
@ -44,36 +47,37 @@ const DonateCard = () => (
const IssuesCard = () => (
<Card className="space-y-4">
<CardContent className="space-y-2">
<CardTitle>Found a bug, or have an idea for a new feature?</CardTitle>
<CardTitle>{t`Found a bug, or have an idea for a new feature?`}</CardTitle>
<CardDescription className="space-y-2">
<p>I'm sure the app is not perfect, but I'd like for it to be.</p>
<p>
If you faced any issues while creating your resume, or have an idea that would help you
and other users in creating your resume more easily, drop an issue on the repository or
send me an email about it.
</p>
<Trans>
<p>I'm sure the app is not perfect, but I'd like for it to be.</p>
<p>
If you faced any issues while creating your resume, or have an idea that would help you
and other users in creating your resume more easily, drop an issue on the repository or
send me an email about it.
</p>
</Trans>
</CardDescription>
</CardContent>
<CardFooter className="space-x-4">
<a
className={cn(buttonVariants({ size: "sm" }))}
href="https://github.com/AmruthPillai/Reactive-Resume/issues/new/choose"
target="_blank"
className={cn(buttonVariants({ size: "sm" }))}
rel="noopener noreferrer nofollow"
target="_blank"
>
<GithubLogo size={14} weight="bold" className="mr-2" />
<span>Raise an issue</span>
<span>{t`Raise an issue`}</span>
</a>
<a
className={cn(buttonVariants({ size: "sm" }))}
href="mailto:hello@amruthpillai.com"
target="_blank"
rel="noopener noreferrer nofollow"
target="_blank"
>
<EnvelopeSimpleOpen size={14} weight="bold" className="mr-2" />
<span>Send me a message</span>
<span>{t`Send me a message`}</span>
</a>
</CardFooter>
</Card>
@ -82,17 +86,18 @@ const IssuesCard = () => (
const DocumentationCard = () => (
<Card className="space-y-4">
<CardContent className="space-y-2">
<CardTitle>Don't know where to begin? Hit the docs!</CardTitle>
<CardTitle>{t`Don't know where to begin? Hit the docs!`}</CardTitle>
<CardDescription className="space-y-2">
<p>
The community has spent a lot of time writing the documentation for Reactive Resume, and
I'm sure it will help you get started with the app.
</p>
<p>
There are also a lot of examples to help you get started, and features that you might not
know about which could help you build your perfect resume.
</p>
<Trans>
<p>
The community has spent a lot of time writing the documentation for Reactive Resume, and
I'm sure it will help you get started with the app.
</p>
<p>
There are also a lot of examples to help you get started, and features that you might
not know about which could help you build your perfect resume.
</p>
</Trans>
</CardDescription>
</CardContent>
<CardFooter className="space-x-4">
@ -103,7 +108,7 @@ const DocumentationCard = () => (
rel="noopener noreferrer nofollow"
>
<Book size={14} weight="bold" className="mr-2" />
<span>Documentation</span>
<span>{t`Documentation`}</span>
</a>
</CardFooter>
</Card>
@ -115,7 +120,7 @@ export const InformationSection = () => {
<header className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
{getSectionIcon("information")}
<h2 className="line-clamp-1 text-3xl font-bold">Information</h2>
<h2 className="line-clamp-1 text-3xl font-bold">{t`Information`}</h2>
</div>
</header>

View File

@ -18,6 +18,7 @@ import {
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { t, Trans } from "@lingui/macro";
import { ArrowCounterClockwise, DotsSixVertical, Plus, TrashSimple } from "@phosphor-icons/react";
import { defaultMetadata } from "@reactive-resume/schema";
import { Button, Portal, Tooltip } from "@reactive-resume/ui";
@ -203,10 +204,10 @@ export const LayoutSection = () => {
<header className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
{getSectionIcon("layout")}
<h2 className="line-clamp-1 text-3xl font-bold">Layout</h2>
<h2 className="line-clamp-1 text-3xl font-bold">{t`Layout`}</h2>
</div>
<Tooltip content="Reset Layout">
<Tooltip content={t`Reset Layout`}>
<Button size="icon" variant="ghost" onClick={onResetLayout}>
<ArrowCounterClockwise />
</Button>
@ -232,7 +233,9 @@ export const LayoutSection = () => {
return (
<div key={pageIndex} className="rounded border p-3 pb-4">
<div className="flex items-center justify-between">
<p className="mb-3 text-xs font-bold">Page {pageIndex + 1}</p>
<p className="mb-3 text-xs font-bold">
<Trans>Page {pageIndex + 1}</Trans>
</p>
{pageIndex !== 0 && (
<Button
@ -247,8 +250,8 @@ export const LayoutSection = () => {
</div>
<div className="grid grid-cols-2 items-start gap-x-4">
<Column id={mainIndex} name="Main" items={main} />
<Column id={sidebarIndex} name="Sidebar" items={sidebar} />
<Column id={mainIndex} name={t`Main`} items={main} />
<Column id={sidebarIndex} name={t`Sidebar`} items={sidebar} />
</div>
</div>
);
@ -261,7 +264,7 @@ export const LayoutSection = () => {
<Button variant="outline" className="ml-auto" onClick={onAddPage}>
<Plus />
<span className="ml-2">Add New Page</span>
<span className="ml-2">{t`Add New Page`}</span>
</Button>
</main>
</section>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { RichInput } from "@reactive-resume/ui";
import { useResumeStore } from "@/client/stores/resume";
@ -13,22 +14,20 @@ export const NotesSection = () => {
<header className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
{getSectionIcon("notes")}
<h2 className="line-clamp-1 text-3xl font-bold">Notes</h2>
<h2 className="line-clamp-1 text-3xl font-bold">{t`Notes`}</h2>
</div>
</header>
<main className="grid gap-y-4">
<p className="leading-relaxed">
This section is reserved for your personal notes specific to this resume. The content here
remains private and is not shared with anyone else.
{t`This section is reserved for your personal notes specific to this resume. The content here remains private and is not shared with anyone else.`}
</p>
<div className="space-y-1.5">
<RichInput content={notes} onChange={(content) => setValue("metadata.notes", content)} />
<p className="text-xs leading-relaxed opacity-75">
For example, information regarding which companies you sent this resume to or the links
to the job descriptions can be noted down here.
{t`For example, information regarding which companies you sent this resume to or the links to the job descriptions can be noted down here.`}
</p>
</div>
</main>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import {
Label,
Select,
@ -22,13 +23,13 @@ export const PageSection = () => {
<header className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
{getSectionIcon("page")}
<h2 className="line-clamp-1 text-3xl font-bold">Page</h2>
<h2 className="line-clamp-1 text-3xl font-bold">{t`Page`}</h2>
</div>
</header>
<main className="grid gap-y-4">
<div className="space-y-1.5">
<Label>Format</Label>
<Label>{t`Format`}</Label>
<Select
value={page.format}
onValueChange={(value) => {
@ -36,17 +37,17 @@ export const PageSection = () => {
}}
>
<SelectTrigger>
<SelectValue placeholder="Format" />
<SelectValue placeholder={t`Format`} />
</SelectTrigger>
<SelectContent>
<SelectItem value="a4">A4</SelectItem>
<SelectItem value="letter">Letter</SelectItem>
<SelectItem value="a4">{t`A4`}</SelectItem>
<SelectItem value="letter">{t`Letter`}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label>Margin</Label>
<Label>{t`Margin`}</Label>
<div className="flex items-center gap-x-4 py-1">
<Slider
min={0}
@ -63,7 +64,7 @@ export const PageSection = () => {
</div>
<div className="space-y-1.5">
<Label>Options</Label>
<Label>{t`Options`}</Label>
<div className="py-2">
<div className="flex items-center gap-x-4">
@ -74,7 +75,7 @@ export const PageSection = () => {
setValue("metadata.page.options.breakLine", checked);
}}
/>
<Label htmlFor="metadata.page.options.breakLine">Show Break Line</Label>
<Label htmlFor="metadata.page.options.breakLine">{t`Show Break Line`}</Label>
</div>
</div>
@ -87,7 +88,7 @@ export const PageSection = () => {
setValue("metadata.page.options.pageNumbers", checked);
}}
/>
<Label htmlFor="metadata.page.options.pageNumbers">Show Page Numbers</Label>
<Label htmlFor="metadata.page.options.pageNumbers">{t`Show Page Numbers`}</Label>
</div>
</div>
</div>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { CopySimple } from "@phosphor-icons/react";
import { Button, Input, Label, Switch, Tooltip } from "@reactive-resume/ui";
import { AnimatePresence, motion } from "framer-motion";
@ -25,9 +26,8 @@ export const SharingSection = () => {
toast({
variant: "success",
title: "A link has been copied to your clipboard.",
description:
"Anyone with this link can view and download the resume. Share it on your profile or with recruiters.",
title: t`A link has been copied to your clipboard.`,
description: t`Anyone with this link can view and download the resume. Share it on your profile or with recruiters.`,
});
};
@ -36,7 +36,7 @@ export const SharingSection = () => {
<header className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
{getSectionIcon("sharing")}
<h2 className="line-clamp-1 text-3xl font-bold">Sharing</h2>
<h2 className="line-clamp-1 text-3xl font-bold">{t`Sharing`}</h2>
</div>
</header>
@ -52,9 +52,9 @@ export const SharingSection = () => {
/>
<div>
<Label htmlFor="visibility" className="space-y-1">
<p>Public</p>
<p>{t`Public`}</p>
<p className="text-xs opacity-60">
Anyone with the link can view and download the resume.
{t`Anyone with the link can view and download the resume.`}
</p>
</Label>
</div>
@ -70,12 +70,12 @@ export const SharingSection = () => {
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<Label htmlFor="resume-url">URL</Label>
<Label htmlFor="resume-url">{t`URL`}</Label>
<div className="flex gap-x-1.5">
<Input id="resume-url" readOnly value={url} className="flex-1" />
<Tooltip content="Copy to Clipboard">
<Tooltip content={t`Copy to Clipboard`}>
<Button size="icon" variant="ghost" onClick={onCopy}>
<CopySimple />
</Button>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { Info } from "@phosphor-icons/react";
import { Alert, AlertDescription, AlertTitle } from "@reactive-resume/ui";
import { cn } from "@reactive-resume/utils";
@ -19,7 +20,7 @@ export const StatisticsSection = () => {
<header className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
{getSectionIcon("statistics")}
<h2 className="line-clamp-1 text-3xl font-bold">Statistics</h2>
<h2 className="line-clamp-1 text-3xl font-bold">{t`Statistics`}</h2>
</div>
</header>
@ -34,12 +35,9 @@ export const StatisticsSection = () => {
>
<Alert variant="info">
<Info size={18} />
<AlertTitle>Statistics are available only for public resumes.</AlertTitle>
<AlertTitle>{t`Statistics are available only for public resumes.`}</AlertTitle>
<AlertDescription className="text-xs leading-relaxed">
You can track the number of views your resume has received, or how many people
have downloaded the resume by enabling public sharing.
{t`You can track the number of views your resume has received, or how many people have downloaded the resume by enabling public sharing.`}
</AlertDescription>
</Alert>
</motion.div>
@ -50,14 +48,14 @@ export const StatisticsSection = () => {
<h3 className={cn("text-4xl font-bold blur-none transition-all", !isPublic && "blur-sm")}>
{statistics?.views ?? 0}
</h3>
<p className="opacity-75">Views</p>
<p className="opacity-75">{t`Views`}</p>
</div>
<div>
<h3 className={cn("text-4xl font-bold blur-none transition-all", !isPublic && "blur-sm")}>
{statistics?.downloads ?? 0}
</h3>
<p className="opacity-75">Downloads</p>
<p className="opacity-75">{t`Downloads`}</p>
</div>
</main>
</section>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { Button, HoverCard, HoverCardContent, HoverCardTrigger } from "@reactive-resume/ui";
import { cn, templatesList } from "@reactive-resume/utils";
@ -14,7 +15,7 @@ export const TemplateSection = () => {
<header className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
{getSectionIcon("template")}
<h2 className="line-clamp-1 text-3xl font-bold">Template</h2>
<h2 className="line-clamp-1 text-3xl font-bold">{t`Template`}</h2>
</div>
</header>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { Input, Label, Popover, PopoverContent, PopoverTrigger } from "@reactive-resume/ui";
import { cn } from "@reactive-resume/utils";
import { HexColorPicker } from "react-colorful";
@ -16,7 +17,7 @@ export const ThemeSection = () => {
<header className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
{getSectionIcon("theme")}
<h2 className="line-clamp-1 text-3xl font-bold">Theme</h2>
<h2 className="line-clamp-1 text-3xl font-bold">{t`Theme`}</h2>
</div>
</header>
@ -39,7 +40,7 @@ export const ThemeSection = () => {
</div>
<div className="space-y-1.5">
<Label htmlFor="theme.primary">Primary Color</Label>
<Label htmlFor="theme.primary">{t`Primary Color`}</Label>
<div className="relative">
<Popover>
<PopoverTrigger asChild>
@ -69,7 +70,7 @@ export const ThemeSection = () => {
</div>
<div className="space-y-1.5">
<Label htmlFor="theme.primary">Background Color</Label>
<Label htmlFor="theme.primary">{t`Background Color`}</Label>
<div className="relative">
<Popover>
<PopoverTrigger asChild>
@ -99,7 +100,7 @@ export const ThemeSection = () => {
</div>
<div className="space-y-1.5">
<Label htmlFor="theme.primary">Text Color</Label>
<Label htmlFor="theme.primary">{t`Text Color`}</Label>
<div className="relative">
<Popover>
<PopoverTrigger asChild>

View File

@ -1,3 +1,6 @@
/* eslint-disable lingui/no-unlocalized-strings */
import { t } from "@lingui/macro";
import { Button, Combobox, ComboboxOption, Label, Slider, Switch } from "@reactive-resume/ui";
import { cn } from "@reactive-resume/utils";
import { fonts } from "@reactive-resume/utils";
@ -61,7 +64,7 @@ export const TypographySection = () => {
<header className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
{getSectionIcon("typography")}
<h2 className="line-clamp-1 text-3xl font-bold">Typography</h2>
<h2 className="line-clamp-1 text-3xl font-bold">{t`Typography`}</h2>
</div>
</header>
@ -89,11 +92,11 @@ export const TypographySection = () => {
</div>
<div className="space-y-1.5">
<Label>Font Family</Label>
<Label>{t`Font Family`}</Label>
<Combobox
options={families}
value={typography.font.family}
searchPlaceholder="Search for a font family"
searchPlaceholder={t`Search for a font family`}
onValueChange={(value) => {
setValue("metadata.typography.font.family", value);
setValue("metadata.typography.font.subset", "latin");
@ -104,11 +107,11 @@ export const TypographySection = () => {
<div className="grid grid-cols-2 gap-x-4">
<div className="space-y-1.5">
<Label>Font Subset</Label>
<Label>{t`Font Subset`}</Label>
<Combobox
options={subsets}
value={typography.font.subset}
searchPlaceholder="Search for a font subset"
searchPlaceholder={t`Search for a font subset`}
onValueChange={(value) => {
setValue("metadata.typography.font.subset", value);
}}
@ -116,12 +119,12 @@ export const TypographySection = () => {
</div>
<div className="space-y-1.5">
<Label>Font Variants</Label>
<Label>{t`Font Variants`}</Label>
<Combobox
multiple
options={variants}
value={typography.font.variants}
searchPlaceholder="Search for a font variant"
searchPlaceholder={t`Search for a font variant`}
onValueChange={(value) => {
setValue("metadata.typography.font.variants", value);
}}
@ -130,7 +133,7 @@ export const TypographySection = () => {
</div>
<div className="space-y-1.5">
<Label>Font Size</Label>
<Label>{t`Font Size`}</Label>
<div className="flex items-center gap-x-4 py-1">
<Slider
min={12}
@ -147,7 +150,7 @@ export const TypographySection = () => {
</div>
<div className="space-y-1.5">
<Label>Line Height</Label>
<Label>{t`Line Height`}</Label>
<div className="flex items-center gap-x-4 py-1">
<Slider
min={0}
@ -164,7 +167,7 @@ export const TypographySection = () => {
</div>
<div className="space-y-1.5">
<Label>Options</Label>
<Label>{t`Options`}</Label>
<div className="py-2">
<div className="flex items-center gap-x-4">
@ -175,7 +178,7 @@ export const TypographySection = () => {
setValue("metadata.typography.underlineLinks", checked);
}}
/>
<Label htmlFor="metadata.typography.underlineLinks">Underline Links</Label>
<Label htmlFor="metadata.typography.underlineLinks">{t`Underline Links`}</Label>
</div>
</div>
</div>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { FadersHorizontal, ReadCvLogo } from "@phosphor-icons/react";
import { Button, KeyboardShortcut, Separator } from "@reactive-resume/ui";
import { cn } from "@reactive-resume/utils";
@ -33,21 +34,6 @@ interface SidebarItem {
icon: React.ReactNode;
}
const sidebarItems: SidebarItem[] = [
{
path: "/dashboard/resumes",
name: "Resumes",
shortcut: "⇧R",
icon: <ReadCvLogo />,
},
{
path: "/dashboard/settings",
name: "Settings",
shortcut: "⇧S",
icon: <FadersHorizontal />,
},
];
type SidebarItemProps = SidebarItem & {
onClick?: () => void;
};
@ -94,6 +80,21 @@ export const Sidebar = ({ setOpen }: SidebarProps) => {
setOpen?.(false);
});
const sidebarItems: SidebarItem[] = [
{
path: "/dashboard/resumes",
name: t`Resumes`,
shortcut: "⇧R",
icon: <ReadCvLogo />,
},
{
path: "/dashboard/settings",
name: t`Settings`,
shortcut: "⇧S",
icon: <FadersHorizontal />,
},
];
return (
<div className="flex h-full flex-col gap-y-4">
<div className="ml-12 flex justify-center lg:ml-0">

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { Check, DownloadSimple, Warning } from "@phosphor-icons/react";
import {
JsonResume,
@ -141,7 +142,7 @@ export const ImportDialog = () => {
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while validating the file.",
title: t`An error occurred while validating the file.`,
});
}
}
@ -186,7 +187,7 @@ export const ImportDialog = () => {
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: "An error occurred while importing your resume.",
title: t`An error occurred while importing your resume.`,
description: importError?.message,
});
}
@ -206,12 +207,11 @@ export const ImportDialog = () => {
<DialogTitle>
<div className="flex items-center space-x-2.5">
<DownloadSimple />
<h2>Import an existing resume</h2>
<h2>{t`Import an existing resume`}</h2>
</div>
</DialogTitle>
<DialogDescription>
Upload a file from an external source to parse an existing resume and import it into
Reactive Resume for easier editing.
{t`Upload a file from one of the accepted sources to parse existing data and import it into Reactive Resume for easier editing.`}
</DialogDescription>
</DialogHeader>
@ -220,20 +220,24 @@ export const ImportDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Type</FormLabel>
<FormLabel>{t`Filetype`}</FormLabel>
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="Please select a file type" />
<SelectValue placeholder={t`Please select a file type`} />
</SelectTrigger>
<SelectContent>
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
<SelectItem value="reactive-resume-json">
Reactive Resume (.json)
</SelectItem>
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
<SelectItem value="reactive-resume-v3-json">
Reactive Resume v3 (.json)
</SelectItem>
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
<SelectItem value="json-resume-json">JSON Resume (.json)</SelectItem>
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
<SelectItem value="linkedin-data-export-zip">
LinkedIn Data Export (.zip)
</SelectItem>
@ -250,7 +254,7 @@ export const ImportDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>File</FormLabel>
<FormLabel>{t`File`}</FormLabel>
<FormControl>
<Input
type="file"
@ -263,14 +267,22 @@ export const ImportDialog = () => {
/>
</FormControl>
<FormMessage />
{accept && <FormDescription>Accepts only {accept} files</FormDescription>}
{accept && (
<FormDescription>
{t({
message: `Accepts only ${accept} files`,
comment:
"Helper text to let the user know what filetypes are accepted. {accept} can be .pdf or .json.",
})}
</FormDescription>
)}
</FormItem>
)}
/>
{validationResult?.isValid === false && validationResult.errors !== undefined && (
<div className="space-y-2">
<Label className="text-error">Errors during Validation</Label>
<Label className="text-error">{t`Errors`}</Label>
<ScrollArea orientation="vertical" className="h-[180px]">
<div className="whitespace-pre-wrap rounded bg-secondary-accent p-4 font-mono text-xs leading-relaxed">
{JSON.stringify(validationResult.errors, null, 4)}
@ -283,25 +295,25 @@ export const ImportDialog = () => {
<AnimatePresence presenceAffectsLayout>
{(!validationResult ?? false) && (
<Button type="button" onClick={onValidate}>
Validate
{t`Validate`}
</Button>
)}
{validationResult !== null && !validationResult.isValid && (
<Button type="button" variant="secondary" onClick={onReset}>
Reset
{t`Discard`}
</Button>
)}
{validationResult !== null && validationResult.isValid && (
<>
<Button type="button" onClick={onImport} disabled={loading}>
Import
{t`Import`}
</Button>
<Button disabled type="button" variant="success">
<Check size={16} weight="bold" className="mr-2" />
Validated
{t`Validated`}
</Button>
</>
)}

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { ResumeDto } from "@reactive-resume/dto";
import {
AlertDialog,
@ -34,22 +35,21 @@ export const LockDialog = () => {
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{isLockMode && "Are you sure you want to lock this resume?"}
{isUnlockMode && "Are you sure you want to unlock this resume?"}
{isLockMode && t`Are you sure you want to lock this resume?`}
{isUnlockMode && t`Are you sure you want to unlock this resume?`}
</AlertDialogTitle>
<AlertDialogDescription>
{isLockMode &&
"Locking a resume will prevent any further changes to it. This is useful when you have already shared your resume with someone and you don't want to accidentally make any changes to it."}
{isUnlockMode && "Unlocking a resume will allow you to make changes to it again."}
t`Locking a resume will prevent any further changes to it. This is useful when you have already shared your resume with someone and you don't want to accidentally make any changes to it.`}
{isUnlockMode && t`Unlocking a resume will allow you to make changes to it again.`}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>{t`Cancel`}</AlertDialogCancel>
<AlertDialogAction variant="info" disabled={loading} onClick={onSubmit}>
{isLockMode && "Lock"}
{isUnlockMode && "Unlock"}
{isLockMode && t`Lock`}
{isUnlockMode && t`Unlock`}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { CaretDown, Flask, MagicWand, Plus } from "@phosphor-icons/react";
import { createResumeSchema, ResumeDto } from "@reactive-resume/dto";
import { idSchema, sampleResume } from "@reactive-resume/schema";
@ -116,7 +117,7 @@ export const ResumeDialog = () => {
toast({
variant: "error",
title: "An error occurred while trying process your request.",
title: t`An error occurred while trying to create your resume.`,
description: message,
});
}
@ -159,18 +160,16 @@ export const ResumeDialog = () => {
<Form {...form}>
<form>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure you want to delete your resume?</AlertDialogTitle>
<AlertDialogTitle>{t`Are you sure you want to delete your resume?`}</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your resume and cannot
be recovered.
{t`This action cannot be undone. This will permanently delete your resume and cannot be recovered.`}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>{t`Cancel`}</AlertDialogCancel>
<AlertDialogAction variant="error" onClick={form.handleSubmit(onSubmit)}>
Delete
{t`Delete`}
</AlertDialogAction>
</AlertDialogFooter>
</form>
@ -190,16 +189,16 @@ export const ResumeDialog = () => {
<div className="flex items-center space-x-2.5">
<Plus />
<h2>
{isCreate && "Create a new resume"}
{isUpdate && "Update an existing resume"}
{isDuplicate && "Duplicate an existing resume"}
{isCreate && t`Create a new resume`}
{isUpdate && t`Update an existing resume`}
{isDuplicate && t`Duplicate an existing resume`}
</h2>
</div>
</DialogTitle>
<DialogDescription>
{isCreate && "Start building your resume by giving it a name."}
{isUpdate && "Changed your mind about the name? Give it a new one."}
{isDuplicate && "Give your old resume a new name."}
{isCreate && t`Start building your resume by giving it a name.`}
{isUpdate && t`Changed your mind about the name? Give it a new one.`}
{isDuplicate && t`Give your old resume a new name.`}
</DialogDescription>
</DialogHeader>
@ -208,13 +207,13 @@ export const ResumeDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormLabel>{t`Title`}</FormLabel>
<FormControl>
<div className="flex items-center justify-between gap-x-2">
<Input {...field} className="flex-1" />
{(isCreate || isDuplicate) && (
<Tooltip content="Generate a random name">
<Tooltip content={t`Generate a random title for your resume`}>
<Button
size="icon"
type="button"
@ -228,7 +227,7 @@ export const ResumeDialog = () => {
</div>
</FormControl>
<FormDescription>
Tip: You can name the resume referring to the position you are applying for.
{t`Tip: You can name the resume referring to the position you are applying for.`}
</FormDescription>
<FormMessage />
</FormItem>
@ -240,7 +239,7 @@ export const ResumeDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormLabel>{t`Slug`}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@ -252,9 +251,9 @@ export const ResumeDialog = () => {
<DialogFooter>
<div className="flex items-center">
<Button type="submit" disabled={loading} className="rounded-r-none">
{isCreate && "Create"}
{isUpdate && "Save Changes"}
{isDuplicate && "Duplicate"}
{isCreate && t`Create`}
{isUpdate && t`Save Changes`}
{isDuplicate && t`Duplicate`}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -265,7 +264,7 @@ export const ResumeDialog = () => {
<DropdownMenuContent side="right" align="center">
<DropdownMenuItem onClick={onCreateSample}>
<Flask className="mr-2" />
Create Sample Resume
{t`Create Sample Resume`}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { Plus } from "@phosphor-icons/react";
import { KeyboardShortcut } from "@reactive-resume/ui";
import { cn } from "@reactive-resume/utils";
@ -20,11 +21,12 @@ export const CreateResumeCard = () => {
)}
>
<h4 className="font-medium">
Create a new resume
<KeyboardShortcut className="ml-2">(^N)</KeyboardShortcut>
{t`Create a new resume`}
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
<KeyboardShortcut className="ml-2">^N</KeyboardShortcut>
</h4>
<p className="text-xs opacity-75">Start from scratch</p>
<p className="text-xs opacity-75">{t`Start from scratch`}</p>
</div>
</BaseCard>
);

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { DownloadSimple } from "@phosphor-icons/react";
import { KeyboardShortcut } from "@reactive-resume/ui";
import { cn } from "@reactive-resume/utils";
@ -20,11 +21,12 @@ export const ImportResumeCard = () => {
)}
>
<h4 className="line-clamp-1 font-medium">
Import an existing resume
<KeyboardShortcut className="ml-2">(^I)</KeyboardShortcut>
{t`Import an existing resume`}
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
<KeyboardShortcut className="ml-2">^I</KeyboardShortcut>
</h4>
<p className="line-clamp-1 text-xs opacity-75">LinkedIn, JSON Resume, etc.</p>
<p className="line-clamp-1 text-xs opacity-75">{t`LinkedIn, JSON Resume, etc.`}</p>
</div>
</BaseCard>
);

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import {
CircleNotch,
CopySimple,
@ -112,7 +113,7 @@ export const ResumeCard = ({ resume }: Props) => {
)}
>
<h4 className="line-clamp-2 font-medium">{resume.title}</h4>
<p className="line-clamp-1 text-xs opacity-75">{`Last updated ${lastUpdated}`}</p>
<p className="line-clamp-1 text-xs opacity-75">{t`Last updated ${lastUpdated}`}</p>
</div>
</BaseCard>
</ContextMenuTrigger>
@ -120,31 +121,31 @@ export const ResumeCard = ({ resume }: Props) => {
<ContextMenuContent>
<ContextMenuItem onClick={onOpen}>
<FolderOpen size={14} className="mr-2" />
Open
{t`Open`}
</ContextMenuItem>
<ContextMenuItem onClick={onUpdate}>
<PencilSimple size={14} className="mr-2" />
Rename
{t`Rename`}
</ContextMenuItem>
<ContextMenuItem onClick={onDuplicate}>
<CopySimple size={14} className="mr-2" />
Duplicate
{t`Duplicate`}
</ContextMenuItem>
{resume.locked ? (
<ContextMenuItem onClick={onLockChange}>
<LockOpen size={14} className="mr-2" />
Unlock
{t`Unlock`}
</ContextMenuItem>
) : (
<ContextMenuItem onClick={onLockChange}>
<Lock size={14} className="mr-2" />
Lock
{t`Lock`}
</ContextMenuItem>
)}
<ContextMenuSeparator />
<ContextMenuItem onClick={onDelete} className="text-error">
<TrashSimple size={14} className="mr-2" />
Delete
{t`Delete`}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { Plus } from "@phosphor-icons/react";
import { ResumeDto } from "@reactive-resume/dto";
import { KeyboardShortcut } from "@reactive-resume/ui";
@ -15,11 +16,12 @@ export const CreateResumeListItem = () => {
onClick={() => open("create")}
title={
<>
<span>Create a new resume</span>
<KeyboardShortcut className="ml-2">(^N)</KeyboardShortcut>
<span>{t`Create a new resume`}</span>
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
<KeyboardShortcut className="ml-2">^N</KeyboardShortcut>
</>
}
description="Start building from scratch"
description={t`Start building from scratch`}
/>
);
};

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { DownloadSimple } from "@phosphor-icons/react";
import { KeyboardShortcut } from "@reactive-resume/ui";
@ -14,11 +15,12 @@ export const ImportResumeListItem = () => {
onClick={() => open("create")}
title={
<>
<span>Import an existing resume</span>
<KeyboardShortcut className="ml-2">(^I)</KeyboardShortcut>
<span>{t`Import an existing resume`}</span>
{/* eslint-disable-next-line lingui/no-unlocalized-strings */}
<KeyboardShortcut className="ml-2">^I</KeyboardShortcut>
</>
}
description="LinkedIn, JSON Resume, etc."
description={t`LinkedIn, JSON Resume, etc.`}
/>
);
};

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import {
CopySimple,
DotsThreeVertical,
@ -73,7 +74,7 @@ export const ResumeListItem = ({ resume }: Props) => {
}}
>
<FolderOpen size={14} className="mr-2" />
Open
{t`Open`}
</DropdownMenuItem>
<DropdownMenuItem
onClick={(event) => {
@ -82,7 +83,7 @@ export const ResumeListItem = ({ resume }: Props) => {
}}
>
<PencilSimple size={14} className="mr-2" />
Rename
{t`Rename`}
</DropdownMenuItem>
<DropdownMenuItem
onClick={(event) => {
@ -91,7 +92,7 @@ export const ResumeListItem = ({ resume }: Props) => {
}}
>
<CopySimple size={14} className="mr-2" />
Duplicate
{t`Duplicate`}
</DropdownMenuItem>
<ContextMenuSeparator />
<DropdownMenuItem
@ -102,7 +103,7 @@ export const ResumeListItem = ({ resume }: Props) => {
}}
>
<TrashSimple size={14} className="mr-2" />
Delete
{t`Delete`}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -117,7 +118,7 @@ export const ResumeListItem = ({ resume }: Props) => {
onClick={onOpen}
className="group"
title={resume.title}
description={`Last updated ${lastUpdated}`}
description={t`Last updated ${lastUpdated}`}
end={dropdownMenu}
/>
</HoverCardTrigger>
@ -142,20 +143,20 @@ export const ResumeListItem = ({ resume }: Props) => {
<ContextMenuContent>
<ContextMenuItem onClick={onOpen}>
<FolderOpen size={14} className="mr-2" />
Open
{t`Open`}
</ContextMenuItem>
<ContextMenuItem onClick={onUpdate}>
<PencilSimple size={14} className="mr-2" />
Rename
{t`Rename`}
</ContextMenuItem>
<ContextMenuItem onClick={onDuplicate}>
<CopySimple size={14} className="mr-2" />
Duplicate
{t`Duplicate`}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={onDelete} className="text-error">
<TrashSimple size={14} className="mr-2" />
Delete
{t`Delete`}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { List, SquaresFour } from "@phosphor-icons/react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@reactive-resume/ui";
import { motion } from "framer-motion";
@ -15,7 +16,9 @@ export const ResumesPage = () => {
return (
<>
<Helmet>
<title>Resumes - Reactive Resume</title>
<title>
{t`Resumes`} - {t`Reactive Resume`}
</title>
</Helmet>
<Tabs value={layout} onValueChange={(value) => setLayout(value as Layout)}>
@ -25,17 +28,17 @@ export const ResumesPage = () => {
animate={{ opacity: 1, x: 0 }}
className="text-4xl font-bold tracking-tight"
>
Resumes
{t`Resumes`}
</motion.h1>
<TabsList>
<TabsTrigger value="grid" className="h-8 w-8 p-0 sm:h-8 sm:w-auto sm:px-4">
<SquaresFour />
<span className="ml-2 hidden sm:block">Grid</span>
<span className="ml-2 hidden sm:block">{t`Grid`}</span>
</TabsTrigger>
<TabsTrigger value="list" className="h-8 w-8 p-0 sm:h-8 sm:w-auto sm:px-4">
<List />
<span className="ml-2 hidden sm:block">List</span>
<span className="ml-2 hidden sm:block">{t`List`}</span>
</TabsTrigger>
</TabsList>
</div>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { QrCode } from "@phosphor-icons/react";
import {
Alert,
@ -46,7 +47,8 @@ import { useDialog } from "@/client/stores/dialog";
const formSchema = z.object({
uri: z.literal("").or(z.string().optional()),
code: z.literal("").or(z.string().regex(/^\d{6}$/, "Code must be exactly 6 digits long.")),
// eslint-disable-next-line lingui/t-call-in-function
code: z.literal("").or(z.string().regex(/^\d{6}$/, t`Code must be exactly 6 digits long.`)),
backupCodes: z.array(z.string()),
});
@ -103,7 +105,7 @@ export const TwoFactorDialog = () => {
toast({
variant: "error",
title: "An error occurred while trying to enable two-factor authentication.",
title: t`An error occurred while trying to enable two-factor authentication.`,
description: message,
});
}
@ -131,23 +133,21 @@ export const TwoFactorDialog = () => {
<form className="space-y-4">
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure you want to disable two-factor authentication?
{t`Are you sure you want to disable two-factor authentication?`}
</AlertDialogTitle>
<AlertDialogDescription>
If you disable two-factor authentication, you will no longer be required to enter
a verification code when logging in.
{t`If you disable two-factor authentication, you will no longer be required to enter a verification code when logging in.`}
</AlertDialogDescription>
</AlertDialogHeader>
<Alert variant="info">
<AlertDescription>Note: This will make your account less secure.</AlertDescription>
<AlertDescription>{t`Note: This will make your account less secure.`}</AlertDescription>
</Alert>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>{t`Cancel`}</AlertDialogCancel>
<AlertDialogAction variant="error" onClick={form.handleSubmit(onSubmit)}>
Disable
{t`Disable`}
</AlertDialogAction>
</AlertDialogFooter>
</form>
@ -167,19 +167,19 @@ export const TwoFactorDialog = () => {
<div className="flex items-center space-x-2.5">
<QrCode />
<h2>
{mode === "create" && "Setup two-factor authentication on your account"}
{mode === "create" && t`Setup two-factor authentication on your account`}
{mode === "update" &&
"Verify that two-factor authentication has been setup correctly"}
{mode === "duplicate" && "Store your backup codes securely"}
t`Verify that two-factor authentication has been setup correctly`}
{mode === "duplicate" && t`Store your backup codes securely`}
</h2>
</div>
</DialogTitle>
<DialogDescription>
{isCreate &&
"Scan the QR code below with your authenticator app to setup 2FA on your account."}
t`Scan the QR code below with your authenticator app to setup 2FA on your account.`}
{isUpdate &&
"Enter the 6-digit code from your authenticator app to verify that 2FA has been setup correctly."}
{isDuplicate && "You have enabled two-factor authentication successfully."}
t`Enter the 6-digit code from your authenticator app to verify that 2FA has been setup correctly.`}
{isDuplicate && t`You have enabled two-factor authentication successfully.`}
</DialogDescription>
</DialogHeader>
@ -196,8 +196,7 @@ export const TwoFactorDialog = () => {
</div>
</FormControl>
<FormDescription>
In case you don't have access to your camera, you can also copy-paste this URI
to your authenticator app.
{t`In case you are unable to scan this QR Code, you can also copy-paste this link into your authenticator app.`}
</FormDescription>
</FormItem>
)}
@ -210,7 +209,7 @@ export const TwoFactorDialog = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Code</FormLabel>
<FormLabel>{t`Code`}</FormLabel>
<FormControl>
<Input type="number" placeholder="123456" {...field} />
</FormControl>
@ -237,24 +236,23 @@ export const TwoFactorDialog = () => {
/>
<p className="text-xs leading-relaxed">
Please store your backup codes in a secure location. You can use one of these
one-time use codes to login in case you lose access to your authenticator app.
{t`Please store your backup codes in a secure location. You can use one of these one-time use codes to login in case you lose access to your authenticator app.`}
</p>
</>
)}
<DialogFooter>
{isCreate && <Button disabled={loading}>Continue</Button>}
{isCreate && <Button disabled={loading}>{t`Continue`}</Button>}
{isUpdate && (
<>
<Button variant="ghost" onClick={() => open("create")}>
Back
{t`Back`}
</Button>
<Button disabled={loading}>Continue</Button>
<Button disabled={loading}>{t`Continue`}</Button>
</>
)}
{isDuplicate && <Button disabled={loading}>Close</Button>}
{isDuplicate && <Button disabled={loading}>{t`Close`}</Button>}
</DialogFooter>
</form>
</Form>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { Check, UploadSimple, Warning } from "@phosphor-icons/react";
import { UpdateUserDto, updateUserSchema } from "@reactive-resume/dto";
import {
@ -65,7 +66,7 @@ export const AccountSettings = () => {
if (user.email !== data.email) {
toast({
variant: "info",
title: "Check your email for the confirmation link to update your email address.",
title: t`Check your email for the confirmation link to update your email address.`,
});
}
@ -100,10 +101,9 @@ export const AccountSettings = () => {
return (
<div className="space-y-6">
<div>
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">Account</h3>
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">{t`Account`}</h3>
<p className="leading-relaxed opacity-75">
Here, you can update your account information such as your profile picture, name and
username.
{t`Here, you can update your account information such as your profile picture, name and username.`}
</p>
</div>
@ -117,7 +117,7 @@ export const AccountSettings = () => {
<UserAvatar />
<FormItem className="flex-1">
<FormLabel>Picture</FormLabel>
<FormLabel>{t`Picture`}</FormLabel>
<FormControl>
<Input placeholder="https://..." {...field} value={field.value || ""} />
</FormControl>
@ -149,7 +149,7 @@ export const AccountSettings = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel>{t`Name`}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@ -162,7 +162,7 @@ export const AccountSettings = () => {
control={form.control}
render={({ field, fieldState }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormLabel>{t`Username`}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@ -180,7 +180,7 @@ export const AccountSettings = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormLabel>{t`Email`}</FormLabel>
<FormControl>
<Input type="email" {...field} />
</FormControl>
@ -191,14 +191,14 @@ export const AccountSettings = () => {
)}
>
{user.emailVerified ? <Check size={12} /> : <Warning size={12} />}
{user.emailVerified ? "Verified" : "Unverified"}
{user.emailVerified ? t`Verified` : t`Unverified`}
{!user.emailVerified && (
<Button
variant="link"
className="h-auto text-xs"
onClick={onResendVerificationEmail}
>
Resend confirmation link
{t`Resend email confirmation link`}
</Button>
)}
</FormDescription>
@ -216,10 +216,10 @@ export const AccountSettings = () => {
className="flex items-center space-x-2 self-center sm:col-start-2"
>
<Button type="submit" disabled={loading}>
Save Changes
{t`Save Changes`}
</Button>
<Button type="reset" variant="ghost" onClick={onReset}>
Reset
{t`Discard`}
</Button>
</motion.div>
)}

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t, Trans } from "@lingui/macro";
import {
Button,
Form,
@ -48,7 +49,7 @@ export const DangerZoneSettings = () => {
toast({
variant: "success",
title: "Your account has been deleted successfully.",
title: t`Your account and all your data has been deleted successfully. Goodbye!`,
});
navigate("/");
@ -58,11 +59,13 @@ export const DangerZoneSettings = () => {
return (
<div className="space-y-6">
<div>
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">Danger Zone</h3>
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">{t`Danger Zone`}</h3>
<p className="leading-relaxed opacity-75">
In this section, you can delete your account and all the data associated to your user, but
please keep in mind that{" "}
<span className="font-semibold">this action is irreversible</span>.
<Trans>
In this section, you can delete your account and all the data associated to your user,
but please keep in mind that{" "}
<span className="font-semibold">this action is irreversible</span>.
</Trans>
</p>
</div>
@ -73,12 +76,14 @@ export const DangerZoneSettings = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Delete Account</FormLabel>
<FormLabel>{t`Delete Account`}</FormLabel>
<FormControl>
<Input placeholder="delete" {...field} />
</FormControl>
<FormDescription>
Type <code className="font-bold">delete</code> to confirm deleting your account.
<Trans>
Type <code className="font-bold">delete</code> to confirm deleting your account.
</Trans>
</FormDescription>
</FormItem>
)}
@ -86,7 +91,7 @@ export const DangerZoneSettings = () => {
<div className="flex items-center space-x-2 self-center">
<Button type="submit" variant="error" disabled={!form.formState.isValid || loading}>
{count === 1 ? "Are you sure?" : "Delete Account"}
{count === 1 ? t`Are you sure?` : t`Delete Account`}
</Button>
</div>
</form>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t, Trans } from "@lingui/macro";
import { LockSimple, LockSimpleOpen, TrashSimple } from "@phosphor-icons/react";
import {
Alert,
@ -20,7 +21,8 @@ import { useOpenAiStore } from "@/client/stores/openai";
const formSchema = z.object({
apiKey: z
.string()
.regex(/^sk-[a-zA-Z0-9]+$/, "That doesn't look like a valid OpenAI API key.")
// eslint-disable-next-line lingui/t-call-in-function
.regex(/^sk-[a-zA-Z0-9]+$/, t`That doesn't look like a valid OpenAI API key.`)
.default(""),
});
@ -47,26 +49,27 @@ export const OpenAISettings = () => {
return (
<div className="space-y-6">
<div>
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">OpenAI Integration</h3>
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">{t`OpenAI Integration`}</h3>
<p className="leading-relaxed opacity-75">
You can make use of the OpenAI API to help you generate content, or improve your writing
while composing your resume.
{t`You can make use of the OpenAI API to help you generate content, or improve your writing while composing your resume.`}
</p>
</div>
<div className="prose prose-sm prose-zinc max-w-full dark:prose-invert">
<p>
You have the option to{" "}
<a
href="https://www.howtogeek.com/885918/how-to-get-an-openai-api-key/"
rel="noopener noreferrer nofollow"
target="_blank"
>
obtain your own OpenAI API key
</a>
. This key empowers you to leverage the API as you see fit. Alternatively, if you wish to
disable the AI features in Reactive Resume altogether, you can simply remove the key from
your settings.
<Trans>
You have the option to{" "}
<a
target="_blank"
rel="noopener noreferrer nofollow"
href="https://www.howtogeek.com/885918/how-to-get-an-openai-api-key/"
>
obtain your own OpenAI API key
</a>
. This key empowers you to leverage the API as you see fit. Alternatively, if you wish
to disable the AI features in Reactive Resume altogether, you can simply remove the key
from your settings.
</Trans>
</p>
</div>
@ -77,7 +80,7 @@ export const OpenAISettings = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>API Key</FormLabel>
<FormLabel>{t`API Key`}</FormLabel>
<FormControl>
<Input type="password" placeholder="sk-..." {...field} />
</FormControl>
@ -95,13 +98,13 @@ export const OpenAISettings = () => {
<Button type="submit" disabled={isEnabled || !form.formState.isDirty}>
{!isEnabled && <LockSimpleOpen className="mr-2" />}
{isEnabled && <LockSimple className="mr-2" />}
{isEnabled ? "Saved" : "Save Locally"}
{isEnabled ? t`Stored` : t`Store Locally`}
</Button>
{isEnabled && (
<Button type="reset" variant="ghost" onClick={onRemove}>
<TrashSimple className="mr-2" />
Remove
{t`Forget`}
</Button>
)}
</div>
@ -110,36 +113,38 @@ export const OpenAISettings = () => {
<div className="prose prose-sm prose-zinc max-w-full dark:prose-invert">
<p>
Your API key is securely stored in the browser's local storage and is only utilized when
making requests to OpenAI via their official SDK. Rest assured that your key is not
transmitted to any external server except when interacting with OpenAI's services.
<Trans>
Your API key is securely stored in the browser's local storage and is only utilized when
making requests to OpenAI via their official SDK. Rest assured that your key is not
transmitted to any external server except when interacting with OpenAI's services.
</Trans>
</p>
</div>
<Alert variant="warning">
<div className="prose prose-neutral max-w-full text-xs leading-relaxed text-primary dark:prose-invert">
<span className="font-medium">Note: </span>
<span>
<Trans>
<span className="font-medium">Note: </span>
By utilizing the OpenAI API, you acknowledge and accept the{" "}
<a
href="https://openai.com/policies/terms-of-use"
target="_blank"
rel="noopener noreferrer nofollow"
target="_blank"
>
terms of use
</a>{" "}
and{" "}
<a
href="https://openai.com/policies/privacy-policy"
target="_blank"
rel="noopener noreferrer nofollow"
target="_blank"
>
privacy policy
</a>{" "}
outlined by OpenAI. Please note that Reactive Resume bears no responsibility for any
improper or unauthorized utilization of the service, and any resulting repercussions or
liabilities solely rest on the user.
</span>
</Trans>
</div>
</Alert>
</div>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t, Trans } from "@lingui/macro";
import { useTheme } from "@reactive-resume/hooks";
import { Button } from "@reactive-resume/ui";
import { Combobox } from "@reactive-resume/ui";
@ -8,11 +9,12 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { dynamicActivate, getLocales } from "@/client/libs/lingui";
import { useUpdateUser, useUser } from "@/client/services/user";
const formSchema = z.object({
theme: z.enum(["system", "light", "dark"]).default("system"),
language: z.string().default("en"),
locale: z.string().default("en-US"),
});
type FormValues = z.infer<typeof formSchema>;
@ -24,7 +26,7 @@ export const ProfileSettings = () => {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: { theme, language: "en" },
defaultValues: { theme, locale: "en-US" },
});
useEffect(() => {
@ -34,7 +36,7 @@ export const ProfileSettings = () => {
const onReset = () => {
if (!user) return;
form.reset({ theme, language: user.language ?? "en" });
form.reset({ theme, locale: user.locale ?? "en-US" });
};
const onSubmit = async (data: FormValues) => {
@ -42,8 +44,9 @@ export const ProfileSettings = () => {
setTheme(data.theme);
if (user.language !== data.language) {
await updateUser({ language: data.language });
if (user.locale !== data.locale) {
await dynamicActivate(data.locale);
await updateUser({ locale: data.locale });
}
form.reset(data);
@ -52,9 +55,9 @@ export const ProfileSettings = () => {
return (
<div className="space-y-6">
<div>
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">Profile</h3>
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">{t`Profile`}</h3>
<p className="leading-relaxed opacity-75">
Here, you can update your profile to customize and personalize your experience.
{t`Here, you can update your profile to customize and personalize your experience.`}
</p>
</div>
@ -65,16 +68,16 @@ export const ProfileSettings = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Theme</FormLabel>
<FormLabel>{t`Theme`}</FormLabel>
<div className="w-full">
<Combobox
{...field}
value={field.value}
onValueChange={field.onChange}
options={[
{ label: "System", value: "system" },
{ label: "Light", value: "light" },
{ label: "Dark", value: "dark" },
{ label: t`System`, value: "system" },
{ label: t`Light`, value: "light" },
{ label: t`Dark`, value: "dark" },
]}
/>
</div>
@ -83,35 +86,35 @@ export const ProfileSettings = () => {
/>
<FormField
name="language"
name="locale"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Language</FormLabel>
<FormLabel>{t`Language`}</FormLabel>
<div className="w-full">
<Combobox
{...field}
value={field.value}
onValueChange={field.onChange}
options={[
{
value: "en",
label: <p>English</p>,
},
]}
options={Object.entries(getLocales()).map(([value, label]) => ({
label,
value,
}))}
/>
</div>
<FormDescription>
<span>
Don't see your language?{" "}
<a
target="_blank"
rel="noopener noreferrer nofollow"
href="https://translate.rxresu.me/"
className="font-medium underline underline-offset-2"
>
Help translate the app.
</a>
<Trans>
Don't see your locale?{" "}
<a
target="_blank"
rel="noopener noreferrer nofollow"
href="https://translate.rxresu.me/"
className="font-medium underline underline-offset-2"
>
Help translate the app.
</a>
</Trans>
</span>
</FormDescription>
</FormItem>
@ -125,10 +128,10 @@ export const ProfileSettings = () => {
)}
>
<Button type="submit" disabled={loading}>
Save Changes
{t`Save Changes`}
</Button>
<Button type="reset" variant="ghost" onClick={onReset}>
Reset
{t`Discard`}
</Button>
</div>
</form>

View File

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t, Trans } from "@lingui/macro";
import {
Accordion,
AccordionContent,
@ -29,7 +30,8 @@ const formSchema = z
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "The passwords you entered do not match.",
// eslint-disable-next-line lingui/t-call-in-function
message: t`The passwords you entered do not match.`,
});
type FormValues = z.infer<typeof formSchema>;
@ -54,7 +56,7 @@ export const SecuritySettings = () => {
toast({
variant: "success",
title: "Your password has been updated successfully.",
title: t`Your password has been updated successfully.`,
});
onReset();
@ -63,16 +65,15 @@ export const SecuritySettings = () => {
return (
<div className="space-y-6">
<div>
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">Security</h3>
<h3 className="text-2xl font-bold leading-relaxed tracking-tight">{t`Security`}</h3>
<p className="leading-relaxed opacity-75">
In this section, you can change your password and enable/disable two-factor
authentication.
{t`In this section, you can change your password and enable/disable two-factor authentication.`}
</p>
</div>
<Accordion type="multiple" defaultValue={["password", "two-factor"]}>
<AccordionItem value="password">
<AccordionTrigger>Password</AccordionTrigger>
<AccordionTrigger>{t`Password`}</AccordionTrigger>
<AccordionContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-6 sm:grid-cols-2">
@ -81,7 +82,7 @@ export const SecuritySettings = () => {
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>New Password</FormLabel>
<FormLabel>{t`New Password`}</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
@ -94,7 +95,7 @@ export const SecuritySettings = () => {
control={form.control}
render={({ field, fieldState }) => (
<FormItem>
<FormLabel>Confirm New Password</FormLabel>
<FormLabel>{t`Confirm New Password`}</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
@ -117,10 +118,10 @@ export const SecuritySettings = () => {
className="flex items-center space-x-2 self-center sm:col-start-2"
>
<Button type="submit" disabled={loading}>
Change Password
{t`Change Password`}
</Button>
<Button type="reset" variant="ghost" onClick={onReset}>
Reset
{t`Discard`}
</Button>
</motion.div>
)}
@ -131,27 +132,31 @@ export const SecuritySettings = () => {
</AccordionItem>
<AccordionItem value="two-factor">
<AccordionTrigger>Two-Factor Authentication</AccordionTrigger>
<AccordionTrigger>{t`Two-Factor Authentication`}</AccordionTrigger>
<AccordionContent>
{user?.twoFactorEnabled ? (
<p className="mb-4 leading-relaxed opacity-75">
<strong>Two-factor authentication is enabled.</strong> You will be asked to enter a
code every time you sign in.
<Trans>
<strong>Two-factor authentication is enabled.</strong> You will be asked to enter
a code every time you sign in.
</Trans>
</p>
) : (
<p className="mb-4 leading-relaxed opacity-75">
<strong>Two-factor authentication is currently disabled.</strong> You can enable it
by adding an authenticator app to your account.
<Trans>
<strong>Two-factor authentication is currently disabled.</strong> You can enable
it by adding an authenticator app to your account.
</Trans>
</p>
)}
{user?.twoFactorEnabled ? (
<Button variant="outline" onClick={() => open("delete")}>
Disable 2FA
{t`Disable 2FA`}
</Button>
) : (
<Button variant="outline" onClick={() => open("create")}>
Enable 2FA
{t`Enable 2FA`}
</Button>
)}
</AccordionContent>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { Separator } from "@reactive-resume/ui";
import { motion } from "framer-motion";
import { Helmet } from "react-helmet-async";
@ -11,7 +12,9 @@ import { SecuritySettings } from "./_sections/security";
export const SettingsPage = () => (
<>
<Helmet>
<title>Settings - Reactive Resume</title>
<title>
{t`Settings`} - {t`Reactive Resume`}
</title>
</Helmet>
<div className="max-w-2xl space-y-8 pb-12">
@ -20,7 +23,7 @@ export const SettingsPage = () => (
animate={{ opacity: 1, x: 0 }}
className="text-4xl font-bold tracking-tight"
>
Settings
{t`Settings`}
</motion.h1>
<AccountSettings />

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { Separator } from "@reactive-resume/ui";
import { Copyright } from "@/client/components/copyright";
@ -12,11 +13,10 @@ export const Footer = () => (
<div className="flex flex-col gap-y-2">
<Logo size={96} className="-ml-2" />
<h2 className="text-xl font-medium">Reactive Resume</h2>
<h2 className="text-xl font-medium">{t`Reactive Resume`}</h2>
<p className="prose prose-sm prose-zinc leading-relaxed opacity-60 dark:prose-invert">
A free and open-source resume builder that simplifies the tasks of creating, updating, and
sharing your resume.
{t`A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.`}
</p>
<Copyright className="mt-6" />

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { Helmet } from "react-helmet-async";
import { HeroSection } from "./sections/hero";
@ -7,7 +8,9 @@ import { StatisticsSection } from "./sections/statistics";
export const HomePage = () => (
<main className="relative isolate mb-[400px] overflow-hidden bg-background">
<Helmet>
<title>Reactive Resume - A free and open-source resume builder</title>
<title>
{t`Reactive Resume`} - {t`A free and open-source resume builder`}
</title>
</Helmet>
<HeroSection />

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { Book, SignOut } from "@phosphor-icons/react";
import { Button } from "@reactive-resume/ui";
import { Link } from "react-router-dom";
@ -14,12 +15,12 @@ export const HeroCTA = () => {
return (
<>
<Button asChild size="lg">
<Link to="/dashboard">Go to Dashboard</Link>
<Link to="/dashboard">{t`Go to Dashboard`}</Link>
</Button>
<Button size="lg" variant="link" onClick={() => logout()}>
<SignOut className="mr-3" />
Logout
{t`Logout`}
</Button>
</>
);
@ -29,13 +30,13 @@ export const HeroCTA = () => {
return (
<>
<Button asChild size="lg">
<Link to="/auth/login">Get started</Link>
<Link to="/auth/login">{t`Get Started`}</Link>
</Button>
<Button asChild size="lg" variant="link">
<a href="https://docs.rxresu.me" target="_blank" rel="noopener noreferrer nofollow">
<Book className="mr-3" />
Learn more
{t`Learn more`}
</a>
</Button>
</>

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { ArrowRight } from "@phosphor-icons/react";
import { Badge, Button } from "@reactive-resume/ui";
import { motion } from "framer-motion";
@ -20,24 +21,23 @@ export const HeroSection = () => (
whileInView={{ opacity: 1, x: 0 }}
>
<div className="mt-24 flex items-center gap-x-4 sm:mt-32 lg:mt-0">
<Badge>Version 4</Badge>
<Badge>{t`Version 4`}</Badge>
<Button variant="link" className="space-x-2 text-left">
<p>What's new in the latest version</p>
<p>{t`What's new in the latest version`}</p>
<ArrowRight />
</Button>
</div>
<div className="mt-10 space-y-2">
<h6 className="text-base font-bold tracking-wide">Finally,</h6>
<h6 className="text-base font-bold tracking-wide">{t`Finally,`}</h6>
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl">
A free and open-source resume builder.
{t`A free and open-source resume builder.`}
</h1>
</div>
<p className="prose prose-base prose-zinc mt-6 text-lg leading-8 dark:prose-invert">
A free and open-source resume builder that simplifies the process of creating, updating,
and sharing your resume.
{t`A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.`}
</p>
<div className="mt-10 flex items-center gap-x-8">
@ -53,7 +53,7 @@ export const HeroSection = () => (
width={3600}
height={2078}
src="/screenshots/builder.png"
alt="Reactive Resume - Screenshot - Builder Screen"
alt={t`Reactive Resume - Screenshot - Builder Screen`}
className="w-[76rem] rounded-lg bg-background/5 shadow-2xl ring-1 ring-foreground/10"
/>
</Tilt>

View File

@ -1,4 +1,5 @@
import { Button } from "@reactive-resume/ui";
import { t, Trans } from "@lingui/macro";
import { buttonVariants } from "@reactive-resume/ui";
import { cn } from "@reactive-resume/utils";
type LogoProps = { company: string };
@ -35,7 +36,7 @@ export const LogoCloudSection = () => (
<section className="relative py-24 sm:py-32">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<p className="text-center text-lg leading-relaxed">
Reactive Resume has helped people land jobs at these great companies:
{t`Reactive Resume has helped people land jobs at these great companies:`}
</p>
<div className="mx-auto mt-10 grid max-w-lg grid-cols-4 items-center gap-x-8 gap-y-10 sm:max-w-xl sm:grid-cols-6 sm:gap-x-10 lg:mx-0 lg:max-w-none lg:grid-cols-5">
{logoList.map((company) => (
@ -43,17 +44,18 @@ export const LogoCloudSection = () => (
))}
</div>
<p className="mx-auto mt-8 max-w-sm text-center leading-relaxed">
If this app has helped you with your job hunt, let me know by reaching out through{" "}
<Button asChild variant="link" className="p-0">
<Trans>
If this app has helped you with your job hunt, let me know by reaching out through{" "}
<a
href="https://www.amruthpillai.com/#contact"
target="_blank"
rel="noopener noreferrer nofollow"
href="https://www.amruthpillai.com/#contact"
className={cn(buttonVariants({ variant: "link" }), "p-0")}
>
this contact form
</a>
</Button>
.
.
</Trans>
</p>
</div>
</section>

View File

@ -1,3 +1,5 @@
import { t } from "@lingui/macro";
import { Counter } from "./counter";
type Statistic = {
@ -5,25 +7,27 @@ type Statistic = {
value: number;
};
const stats: Statistic[] = [
{ name: "GitHub Stars", value: 11800 },
{ name: "Users Signed Up", value: 300000 },
{ name: "Resumes Generated", value: 400000 },
];
export const StatisticsSection = () => {
const stats: Statistic[] = [
{ name: t`GitHub Stars`, value: 11800 },
{ name: t`Users Signed Up`, value: 300000 },
{ name: t`Resumes Generated`, value: 400000 },
];
export const StatisticsSection = () => (
<section className="relative py-24 sm:py-32">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<dl className="grid grid-cols-1 gap-x-8 gap-y-16 text-center lg:grid-cols-3">
{stats.map((stat, index) => (
<div key={index} className="mx-auto flex max-w-xs flex-col gap-y-4">
<dt className="text-base leading-7 opacity-60">{stat.name}</dt>
<dd className="order-first text-3xl font-semibold tracking-tight sm:text-5xl">
<Counter from={0} to={stat.value} />+
</dd>
</div>
))}
</dl>
</div>
</section>
);
return (
<section className="relative py-24 sm:py-32">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<dl className="grid grid-cols-1 gap-x-8 gap-y-16 text-center lg:grid-cols-3">
{stats.map((stat, index) => (
<div key={index} className="mx-auto flex max-w-xs flex-col gap-y-4">
<dt className="text-base leading-7 opacity-60">{stat.name}</dt>
<dd className="order-first text-3xl font-semibold tracking-tight sm:text-5xl">
<Counter from={0} to={stat.value} />+
</dd>
</div>
))}
</dl>
</div>
</section>
);
};

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { ResumeDto } from "@reactive-resume/dto";
import { Button } from "@reactive-resume/ui";
import { pageSizeMap } from "@reactive-resume/utils";
@ -52,7 +53,9 @@ export const PublicResumePage = () => {
return (
<div>
<Helmet>
<title>{title} - Reactive Resume</title>
<title>
{title} - {t`Reactive Resume`}
</title>
</Helmet>
<div
@ -70,9 +73,9 @@ export const PublicResumePage = () => {
<div className="flex justify-center py-10 opacity-50 print:hidden">
<Link to="/">
<Button size="sm" variant="ghost" className="space-x-1.5 text-xs font-normal">
<span>Built with</span>
<span>{t`Built with`}</span>
<Icon size={12} />
<span>Reactive Resume</span>
<span>{t`Reactive Resume`}</span>
</Button>
</Link>
</div>
@ -98,7 +101,7 @@ export const publicLoader: LoaderFunction<ResumeDto> = async ({ params }) => {
} catch (error) {
toast({
variant: "error",
title: "The resume you were looking for was nowhere to be found... or maybe never existed?",
title: t`The resume you were looking for doesn't seem to exist, please check the link and try again.`,
});
return redirect("/");

View File

@ -1,5 +1,3 @@
import "@/client/libs/dayjs";
import { TooltipProvider } from "@reactive-resume/ui";
import { QueryClientProvider } from "@tanstack/react-query";
import { HelmetProvider } from "react-helmet-async";
@ -7,21 +5,24 @@ import { Outlet } from "react-router-dom";
import { queryClient } from "../libs/query-client";
import { DialogProvider } from "./dialog";
import { LocaleProvider } from "./locale";
import { ThemeProvider } from "./theme";
import { Toaster } from "./toaster";
export const Providers = () => (
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<TooltipProvider>
<DialogProvider>
<HelmetProvider>
<Outlet />
<LocaleProvider>
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<TooltipProvider>
<DialogProvider>
<HelmetProvider>
<Outlet />
<Toaster />
</HelmetProvider>
</DialogProvider>
</TooltipProvider>
</ThemeProvider>
</QueryClientProvider>
<Toaster />
</HelmetProvider>
</DialogProvider>
</TooltipProvider>
</ThemeProvider>
</QueryClientProvider>
</LocaleProvider>
);

View File

@ -0,0 +1,27 @@
import "@/client/libs/dayjs";
import { i18n } from "@lingui/core";
import { detect, fromNavigator, fromStorage, fromUrl } from "@lingui/detect-locale";
import { I18nProvider } from "@lingui/react";
import { useEffect } from "react";
import { defaultLocale, dynamicActivate } from "../libs/lingui";
type Props = {
children: React.ReactNode;
};
export const LocaleProvider = ({ children }: Props) => {
useEffect(() => {
const detectedLocale = detect(
fromUrl("lang"),
fromStorage("lang"),
fromNavigator(),
defaultLocale,
)!;
dynamicActivate(detectedLocale);
}, []);
return <I18nProvider i18n={i18n}>{children}</I18nProvider>;
};

View File

@ -1,3 +1,7 @@
/* eslint-disable lingui/text-restrictions */
import { t } from "@lingui/macro";
import { openai } from "./client";
const PROMPT = `You are an AI writing assistant specialized in writing copy for resumes.
@ -23,7 +27,7 @@ export const changeTone = async (text: string, mood: Mood) => {
});
if (result.choices.length === 0) {
throw new Error("OpenAI did not return any choices for your text.");
throw new Error(t`OpenAI did not return any choices for your text.`);
}
return result.choices[0].text;

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { OpenAI } from "openai";
import { useOpenAiStore } from "@/client/stores/openai";
@ -7,7 +8,7 @@ export const openai = () => {
if (!apiKey) {
throw new Error(
"Your OpenAI API Key has not been set yet. Please go to your account settings to enable OpenAI Integration.",
t`Your OpenAI API Key has not been set yet. Please go to your account settings to enable OpenAI Integration.`,
);
}

View File

@ -1,3 +1,7 @@
/* eslint-disable lingui/text-restrictions */
import { t } from "@lingui/macro";
import { openai } from "./client";
const PROMPT = `You are an AI writing assistant specialized in writing copy for resumes.
@ -21,7 +25,7 @@ export const fixGrammar = async (text: string) => {
});
if (result.choices.length === 0) {
throw new Error("OpenAI did not return any choices for your text.");
throw new Error(t`OpenAI did not return any choices for your text.`);
}
return result.choices[0].text;

View File

@ -1,3 +1,7 @@
/* eslint-disable lingui/text-restrictions */
import { t } from "@lingui/macro";
import { openai } from "./client";
const PROMPT = `You are an AI writing assistant specialized in writing copy for resumes.
@ -21,7 +25,7 @@ export const improveWriting = async (text: string) => {
});
if (result.choices.length === 0) {
throw new Error("OpenAI did not return any choices for your text.");
throw new Error(t`OpenAI did not return any choices for your text.`);
}
return result.choices[0].text;

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { StatisticsDto, UrlDto } from "@reactive-resume/dto";
import { useMutation } from "@tanstack/react-query";
import { AxiosError } from "axios";
@ -25,11 +26,6 @@ export const usePrintResume = () => {
if (cache === undefined) return cache;
return { ...cache, downloads: cache.downloads + 1 } satisfies StatisticsDto;
});
toast({
variant: "success",
title: "A PDF of your resume has been successfully generated.",
});
},
onError: (error) => {
if (error instanceof AxiosError) {
@ -37,7 +33,7 @@ export const usePrintResume = () => {
toast({
variant: "error",
title: "An error occurred while trying to generate your resume.",
title: t`An error occurred while trying to print your resume.`,
description: message,
});
}

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { ResumeDto, UpdateResumeDto } from "@reactive-resume/dto";
import { useMutation } from "@tanstack/react-query";
import { AxiosError, AxiosResponse } from "axios";
@ -31,7 +32,7 @@ export const updateResume = async (data: UpdateResumeDto) => {
toast({
variant: "error",
title: "There was an error while updating your resume.",
title: t`There was an error while updating your resume.`,
description: message,
});
}

View File

@ -1,3 +1,4 @@
import { t } from "@lingui/macro";
import { createId } from "@paralleldrive/cuid2";
import { ResumeDto } from "@reactive-resume/dto";
import { CustomSectionGroup, defaultSection, SectionKey } from "@reactive-resume/schema";
@ -41,7 +42,7 @@ export const useResumeStore = create<ResumeStore>()(
const section: CustomSectionGroup = {
...defaultSection,
id: createId(),
name: "Custom Section",
name: t`Custom Section`,
items: [],
};

View File

@ -1,5 +1,6 @@
/// <reference types='vitest' />
import { lingui } from "@lingui/vite-plugin";
import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
import react from "@vitejs/plugin-react-swc";
import { defineConfig, searchForWorkspaceRoot, splitVendorChunkPlugin } from "vite";
@ -21,7 +22,14 @@ export default defineConfig({
fs: { allow: [searchForWorkspaceRoot(process.cwd())] },
},
plugins: [react(), nxViteTsPaths(), splitVendorChunkPlugin()],
plugins: [
react({
plugins: [["@lingui/swc-plugin", {}]],
}),
lingui(),
nxViteTsPaths(),
splitVendorChunkPlugin(),
],
test: {
globals: true,

View File

@ -100,7 +100,7 @@ export class AuthService {
name: registerDto.name,
email: registerDto.email,
username: registerDto.username,
language: registerDto.language,
locale: registerDto.locale,
provider: "email",
emailVerified: false, // Set to true if you don't want to verify user's email
secrets: { create: { password: hashedPassword } },

View File

@ -44,7 +44,7 @@ export class GitHubStrategy extends PassportStrategy(Strategy, "github") {
user = await this.userService.create({
email,
picture,
language: "en",
locale: "en-US",
name: displayName,
provider: "github",
emailVerified: true, // auto-verify emails

View File

@ -44,7 +44,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, "google") {
user = await this.userService.create({
email,
picture,
language: "en",
locale: "en-US",
name: displayName,
provider: "google",
emailVerified: true, // auto-verify emails

View File

@ -41,7 +41,7 @@ export class UserController {
name: updateUserDto.name,
picture: updateUserDto.picture,
username: updateUserDto.username,
language: updateUserDto.language,
locale: updateUserDto.locale,
});
}

11
crowdin.yml Normal file
View File

@ -0,0 +1,11 @@
base_path: .
project_id: "503410"
base_url: https://api.crowdin.com
api_token_env: CROWDIN_PERSONAL_TOKEN
preserve_hierarchy: false
files:
- type: po
source: /apps/client/src/locales/en-US.po
translation: /apps/client/src/locales/%locale%.po

View File

@ -4,7 +4,7 @@ import { z } from "nestjs-zod/z";
import { userSchema } from "../user";
export const registerSchema = userSchema
.pick({ name: true, email: true, username: true, language: true })
.pick({ name: true, email: true, username: true, locale: true })
.extend({ password: z.password().min(6) });
export class RegisterDto extends createZodDto(registerSchema) {}

Some files were not shown because too many files have changed in this diff Show More