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 { 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>