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