feat: localization (#196)

* Started adding locale translations :)

* Added some more translations

* Working on translating even more pages

* More translations

* Added test default locale retrieval

* replace `intl.formatMessage` with custom `t` hook

* add more translations

* improve title syntax

* add more translations

* translate admin config page

* translated error messages

* add language selecter

* minor fixes

* improve language handling

* add upcoming languages

* add `crowdin.yml`

* run formatter

---------

Co-authored-by: Steve Tautonico <stautonico@gmail.com>
This commit is contained in:
Elias Schneider
2023-07-20 15:32:07 +02:00
committed by GitHub
parent 7c5ec8d0ea
commit b9f6e3bd08
68 changed files with 4712 additions and 461 deletions

View File

@@ -13,14 +13,17 @@ import {
Text,
Textarea,
TextInput,
Title,
} from "@mantine/core";
import { useForm, yupResolver } from "@mantine/form";
import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { useState } from "react";
import { TbAlertCircle } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import * as yup from "yup";
import useTranslate, {
translateOutsideContext,
} from "../../../hooks/useTranslate.hook";
import shareService from "../../../services/share.service";
import { CreateShare } from "../../../types/share.type";
import { getExpirationPreview } from "../../../utils/date.util";
@@ -36,8 +39,10 @@ const showCreateUploadModal = (
},
uploadCallback: (createShare: CreateShare) => void
) => {
const t = translateOutsideContext();
return modals.openModal({
title: "Share",
title: t("upload.modal.title"),
children: (
<CreateUploadModalBody
options={options}
@@ -61,6 +66,7 @@ const CreateUploadModalBody = ({
};
}) => {
const modals = useModals();
const t = useTranslate();
const generatedLink = Buffer.from(Math.random().toString(), "utf8")
.toString("base64")
@@ -71,11 +77,11 @@ const CreateUploadModalBody = ({
const validationSchema = yup.object().shape({
link: yup
.string()
.required()
.min(3)
.max(50)
.required(t("common.error.field-required"))
.min(3, t("common.error.too-short", { length: 3 }))
.max(50, t("common.error.too-long", { length: 50 }))
.matches(new RegExp("^[a-zA-Z0-9_-]*$"), {
message: "Can only contain letters, numbers, underscores and hyphens",
message: t("upload.modal.link.error.invalid"),
}),
password: yup.string().min(3).max(30),
maxViews: yup.number().min(1),
@@ -100,20 +106,19 @@ const CreateUploadModalBody = ({
withCloseButton
onClose={() => setShowNotSignedInAlert(false)}
icon={<TbAlertCircle size={16} />}
title="You're not signed in"
title={t("upload.modal.not-signed-in")}
color="yellow"
>
You will be unable to delete your share manually and view the visitor
count.
<FormattedMessage id="upload.modal.not-signed-in-description" />
</Alert>
)}
<form
onSubmit={form.onSubmit(async (values) => {
if (!(await shareService.isShareIdAvailable(values.link))) {
form.setFieldError("link", "This link is already in use");
form.setFieldError("link", t("upload.modal.link.error.taken"));
} else {
const expiration = form.values.never_expires
? "never"
? t("upload.modal.expires.never")
: form.values.expiration_num + form.values.expiration_unit;
uploadCallback({
id: values.link,
@@ -151,7 +156,7 @@ const CreateUploadModalBody = ({
)
}
>
Generate
<FormattedMessage id="common.button.generate" />
</Button>
</Col>
</Grid>
@@ -169,18 +174,6 @@ const CreateUploadModalBody = ({
{!options.isReverseShare && (
<>
<Grid align={form.errors.link ? "center" : "flex-end"}>
<Col xs={6}>
<NumberInput
min={1}
max={99999}
precision={0}
variant="filled"
label="Expiration"
placeholder="n"
disabled={form.values.never_expires}
{...form.getInputProps("expiration_num")}
/>
</Col>
<Col xs={6}>
<Select
disabled={form.values.never_expires}
@@ -190,41 +183,51 @@ const CreateUploadModalBody = ({
{
value: "-minutes",
label:
"Minute" +
(form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.minute-singular")
: t("upload.modal.expires.minute-plural"),
},
{
value: "-hours",
label:
"Hour" + (form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.hour-singular")
: t("upload.modal.expires.hour-plural"),
},
{
value: "-days",
label:
"Day" + (form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.day-singular")
: t("upload.modal.expires.day-plural"),
},
{
value: "-weeks",
label:
"Week" + (form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.week-singular")
: t("upload.modal.expires.week-plural"),
},
{
value: "-months",
label:
"Month" +
(form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.month-singular")
: t("upload.modal.expires.month-plural"),
},
{
value: "-years",
label:
"Year" + (form.values.expiration_num == 1 ? "" : "s"),
form.values.expiration_num == 1
? t("upload.modal.expires.year-singular")
: t("upload.modal.expires.year-plural"),
},
]}
/>
</Col>
</Grid>
<Checkbox
label="Never Expires"
label={t("upload.modal.expires.never-long")}
{...form.getInputProps("never_expires")}
/>
<Text
@@ -234,18 +237,28 @@ const CreateUploadModalBody = ({
color: theme.colors.gray[6],
})}
>
{getExpirationPreview("share", form)}
{getExpirationPreview(
{
neverExpires: t("upload.modal.completed.never-expires"),
expiresOn: t("upload.modal.completed.expires-on"),
},
form
)}
</Text>
</>
)}
<Accordion>
<Accordion.Item value="description" sx={{ borderBottom: "none" }}>
<Accordion.Control>Description</Accordion.Control>
<Accordion.Control>
<FormattedMessage id="upload.modal.accordion.description.title" />
</Accordion.Control>
<Accordion.Panel>
<Stack align="stretch">
<Textarea
variant="filled"
placeholder="Note for the recepients"
placeholder={t(
"upload.modal.accordion.description.placeholder"
)}
{...form.getInputProps("description")}
/>
</Stack>
@@ -253,11 +266,13 @@ const CreateUploadModalBody = ({
</Accordion.Item>
{options.enableEmailRecepients && (
<Accordion.Item value="recipients" sx={{ borderBottom: "none" }}>
<Accordion.Control>Email recipients</Accordion.Control>
<Accordion.Control>
<FormattedMessage id="upload.modal.accordion.email.tile" />
</Accordion.Control>
<Accordion.Panel>
<MultiSelect
data={form.values.recipients}
placeholder="Enter email recipients"
placeholder={t("upload.modal.accordion.email.placeholder")}
searchable
{...form.getInputProps("recipients")}
creatable
@@ -266,7 +281,7 @@ const CreateUploadModalBody = ({
if (!query.match(/^\S+@\S+\.\S+$/)) {
form.setFieldError(
"recipients",
"Invalid email address"
t("upload.modal.accordion.email.invalid-email")
);
} else {
form.setFieldError("recipients", null);
@@ -283,28 +298,36 @@ const CreateUploadModalBody = ({
)}
<Accordion.Item value="security" sx={{ borderBottom: "none" }}>
<Accordion.Control>Security options</Accordion.Control>
<Accordion.Control>
<FormattedMessage id="upload.modal.accordion.security.title" />
</Accordion.Control>
<Accordion.Panel>
<Stack align="stretch">
<PasswordInput
variant="filled"
placeholder="No password"
label="Password protection"
placeholder={t(
"upload.modal.accordion.security.password.placeholder"
)}
label={t("upload.modal.accordion.security.password.label")}
{...form.getInputProps("password")}
/>
<NumberInput
min={1}
type="number"
variant="filled"
placeholder="No limit"
label="Maximal views"
placeholder={t(
"upload.modal.accordion.security.max-views.placeholder"
)}
label={t("upload.modal.accordion.security.max-views.label")}
{...form.getInputProps("maxViews")}
/>
</Stack>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
<Button type="submit">Share</Button>
<Button type="submit">
<FormattedMessage id="common.button.share" />
</Button>
</Stack>
</form>
</>