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

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>