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

@@ -14,18 +14,22 @@ import {
import { useForm, yupResolver } from "@mantine/form";
import { useModals } from "@mantine/modals";
import { Tb2Fa } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import * as yup from "yup";
import showEnableTotpModal from "../../components/account/showEnableTotpModal";
import ThemeSwitcher from "../../components/account/ThemeSwitcher";
import Meta from "../../components/Meta";
import ThemeSwitcher from "../../components/account/ThemeSwitcher";
import showEnableTotpModal from "../../components/account/showEnableTotpModal";
import useTranslate from "../../hooks/useTranslate.hook";
import useUser from "../../hooks/user.hook";
import authService from "../../services/auth.service";
import userService from "../../services/user.service";
import toast from "../../utils/toast.util";
import LanguagePicker from "../../components/account/LanguagePicker";
const Account = () => {
const { user, refreshUser } = useUser();
const modals = useModals();
const t = useTranslate();
const accountForm = useForm({
initialValues: {
@@ -34,8 +38,10 @@ const Account = () => {
},
validate: yupResolver(
yup.object().shape({
email: yup.string().email(),
username: yup.string().min(3),
email: yup.string().email(t("common.error.invalid-email")),
username: yup
.string()
.min(3, t("common.error.too-short", { length: 3 })),
})
),
});
@@ -47,8 +53,14 @@ const Account = () => {
},
validate: yupResolver(
yup.object().shape({
oldPassword: yup.string().min(8),
password: yup.string().min(8),
oldPassword: yup
.string()
.min(8, t("common.error.too-short", { length: 8 }))
.required(t("common.error.field-required")),
password: yup
.string()
.min(8, t("common.error.too-short", { length: 8 }))
.required(t("common.error.field-required")),
})
),
});
@@ -59,7 +71,10 @@ const Account = () => {
},
validate: yupResolver(
yup.object().shape({
password: yup.string().min(8),
password: yup
.string()
.min(8, t("common.error.too-short", { length: 8 }))
.required(t("common.error.field-required")),
})
),
});
@@ -74,23 +89,23 @@ const Account = () => {
password: yup.string().min(8),
code: yup
.string()
.min(6)
.max(6)
.matches(/^[0-9]+$/, { message: "Code must be a number" }),
.min(6, t("common.error.exact-length", { length: 6 }))
.max(6, t("common.error.exact-length", { length: 6 }))
.matches(/^[0-9]+$/, { message: t("common.error.invalid-number") }),
})
),
});
return (
<>
<Meta title="My account" />
<Meta title={t("account.title")} />
<Container size="sm">
<Title order={3} mb="xs">
My account
<FormattedMessage id="account.title" />
</Title>
<Paper withBorder p="xl">
<Title order={5} mb="xs">
Account Info
<FormattedMessage id="account.card.info.title" />
</Title>
<form
onSubmit={accountForm.onSubmit((values) =>
@@ -99,35 +114,37 @@ const Account = () => {
username: values.username,
email: values.email,
})
.then(() => toast.success("User updated successfully"))
.then(() => toast.success(t("account.notify.info.success")))
.catch(toast.axiosError)
)}
>
<Stack>
<TextInput
label="Username"
label={t("account.card.info.username")}
{...accountForm.getInputProps("username")}
/>
<TextInput
label="Email"
label={t("account.card.info.email")}
{...accountForm.getInputProps("email")}
/>
<Group position="right">
<Button type="submit">Save</Button>
<Button type="submit">
<FormattedMessage id="common.button.save" />
</Button>
</Group>
</Stack>
</form>
</Paper>
<Paper withBorder p="xl" mt="lg">
<Title order={5} mb="xs">
Password
<FormattedMessage id="account.card.password.title" />
</Title>
<form
onSubmit={passwordForm.onSubmit((values) =>
authService
.updatePassword(values.oldPassword, values.password)
.then(() => {
toast.success("Password updated successfully");
toast.success(t("account.notify.password.success"));
passwordForm.reset();
})
.catch(toast.axiosError)
@@ -135,15 +152,17 @@ const Account = () => {
>
<Stack>
<PasswordInput
label="Old password"
label={t("account.card.password.old")}
{...passwordForm.getInputProps("oldPassword")}
/>
<PasswordInput
label="New password"
label={t("account.card.password.new")}
{...passwordForm.getInputProps("password")}
/>
<Group position="right">
<Button type="submit">Save</Button>
<Button type="submit">
<FormattedMessage id="common.button.save" />
</Button>
</Group>
</Stack>
</form>
@@ -151,7 +170,7 @@ const Account = () => {
<Paper withBorder p="xl" mt="lg">
<Title order={5} mb="xs">
Security
<FormattedMessage id="account.card.security.title" />
</Title>
<Tabs defaultValue="totp">
@@ -169,7 +188,7 @@ const Account = () => {
authService
.disableTOTP(values.code, values.password)
.then(() => {
toast.success("Successfully disabled TOTP");
toast.success(t("account.notify.totp.disable"));
values.password = "";
values.code = "";
refreshUser();
@@ -179,21 +198,23 @@ const Account = () => {
>
<Stack>
<PasswordInput
description="Enter your current password to disable TOTP"
label="Password"
description={t(
"account.card.security.totp.disable.description"
)}
label={t("account.card.password.title")}
{...disableTotpForm.getInputProps("password")}
/>
<TextInput
variant="filled"
label="Code"
label={t("account.modal.totp.code")}
placeholder="******"
{...disableTotpForm.getInputProps("code")}
/>
<Group position="right">
<Button color="red" type="submit">
Disable
<FormattedMessage id="common.button.disable" />
</Button>
</Group>
</Stack>
@@ -218,12 +239,16 @@ const Account = () => {
>
<Stack>
<PasswordInput
label="Password"
description="Enter your current password to start enabling TOTP"
label={t("account.card.password.title")}
description={t(
"account.card.security.totp.enable.description"
)}
{...enableTotpForm.getInputProps("password")}
/>
<Group position="right">
<Button type="submit">Start</Button>
<Button type="submit">
<FormattedMessage id="account.card.security.totp.button.start" />
</Button>
</Group>
</Stack>
</form>
@@ -234,7 +259,13 @@ const Account = () => {
</Paper>
<Paper withBorder p="xl" mt="lg">
<Title order={5} mb="xs">
Color scheme
<FormattedMessage id="account.card.language.title" />
</Title>
<LanguagePicker />
</Paper>
<Paper withBorder p="xl" mt="lg">
<Title order={5} mb="xs">
<FormattedMessage id="account.card.color.title" />
</Title>
<ThemeSwitcher />
</Paper>
@@ -245,15 +276,17 @@ const Account = () => {
color="red"
onClick={() =>
modals.openConfirmModal({
title: "Account deletion",
title: t("account.modal.delete.title"),
children: (
<Text size="sm">
Do you really want to delete your account including all
your active shares?
<FormattedMessage id="account.modal.delete.description" />
</Text>
),
labels: { confirm: "Delete", cancel: "Cancel" },
labels: {
confirm: t("common.button.delete"),
cancel: t("common.button.cancel"),
},
confirmProps: { color: "red" },
onConfirm: async () => {
await userService.removeCurrentUser();
@@ -262,7 +295,7 @@ const Account = () => {
})
}
>
Delete Account
<FormattedMessage id="account.button.delete" />
</Button>
</Stack>
</Center>

View File

@@ -17,12 +17,14 @@ import { useModals } from "@mantine/modals";
import moment from "moment";
import { useEffect, useState } from "react";
import { TbInfoCircle, TbLink, TbPlus, TbTrash } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import Meta from "../../components/Meta";
import showReverseShareLinkModal from "../../components/account/showReverseShareLinkModal";
import showShareLinkModal from "../../components/account/showShareLinkModal";
import CenterLoader from "../../components/core/CenterLoader";
import showCreateReverseShareModal from "../../components/share/modals/showCreateReverseShareModal";
import useConfig from "../../hooks/config.hook";
import useTranslate from "../../hooks/useTranslate.hook";
import shareService from "../../services/share.service";
import { MyReverseShare } from "../../types/share.type";
import { byteToHumanSizeString } from "../../utils/fileSize.util";
@@ -31,13 +33,14 @@ import toast from "../../utils/toast.util";
const MyShares = () => {
const modals = useModals();
const clipboard = useClipboard();
const t = useTranslate();
const config = useConfig();
const [reverseShares, setReverseShares] = useState<MyReverseShare[]>();
const appUrl = config.get("general.appUrl");
const [reverseShares, setReverseShares] = useState<MyReverseShare[]>();
const getReverseShares = () => {
shareService
.getMyReverseShares()
@@ -51,15 +54,17 @@ const MyShares = () => {
if (!reverseShares) return <CenterLoader />;
return (
<>
<Meta title="My shares" />
<Meta title={t("account.reverseShares.title")} />
<Group position="apart" align="baseline" mb={20}>
<Group align="center" spacing={3} mb={30}>
<Title order={3}>My reverse shares</Title>
<Title order={3}>
<FormattedMessage id="account.reverseShares.title" />
</Title>
<Tooltip
position="bottom"
multiline
width={220}
label="A reverse share allows you to generate a unique URL that allows external users to create a share."
label={t("account.reverseShares.description")}
events={{ hover: true, focus: false, touch: true }}
>
<ActionIcon>
@@ -77,14 +82,18 @@ const MyShares = () => {
}
leftIcon={<TbPlus size={20} />}
>
Create
<FormattedMessage id="common.button.create" />
</Button>
</Group>
{reverseShares.length == 0 ? (
<Center style={{ height: "70vh" }}>
<Stack align="center" spacing={10}>
<Title order={3}>It's empty here 👀</Title>
<Text>You don't have any reverse shares.</Text>
<Title order={3}>
<FormattedMessage id="account.reverseShares.title.empty" />
</Title>
<Text>
<FormattedMessage id="account.reverseShares.description.empty" />
</Text>
</Stack>
</Center>
) : (
@@ -92,10 +101,18 @@ const MyShares = () => {
<Table>
<thead>
<tr>
<th>Shares</th>
<th>Remaining uses</th>
<th>Max share size</th>
<th>Expires at</th>
<th>
<FormattedMessage id="account.reverseShares.table.shares" />
</th>
<th>
<FormattedMessage id="account.reverseShares.table.remaining" />
</th>
<th>
<FormattedMessage id="account.reverseShares.table.max-size" />
</th>
<th>
<FormattedMessage id="account.reverseShares.table.expires" />
</th>
<th></th>
</tr>
</thead>
@@ -105,7 +122,7 @@ const MyShares = () => {
<td style={{ width: 220 }}>
{reverseShare.shares.length == 0 ? (
<Text color="dimmed" size="sm">
No shares created yet
<FormattedMessage id="account.reverseShares.table.no-shares" />
</Text>
) : (
<Accordion>
@@ -115,9 +132,13 @@ const MyShares = () => {
>
<Accordion.Control p={0}>
<Text size="sm">
{`${reverseShare.shares.length} share${
reverseShare.shares.length > 1 ? "s" : ""
}`}
{reverseShare.shares.length == 1
? `1 ${t(
"account.reverseShares.table.count.singular"
)}`
: `${reverseShare.shares.length} ${t(
"account.reverseShares.table.count.plural"
)}`}
</Text>
</Accordion.Control>
<Accordion.Panel>
@@ -140,9 +161,7 @@ const MyShares = () => {
clipboard.copy(
`${appUrl}/share/${share.id}`
);
toast.success(
"The share link was copied to the keyboard."
);
toast.success(t("common.notify.copied"));
} else {
showShareLinkModal(
modals,
@@ -183,9 +202,7 @@ const MyShares = () => {
reverseShare.token
}`
);
toast.success(
"The link was copied to your clipboard."
);
toast.success(t("common.notify.copied"));
} else {
showReverseShareLinkModal(
modals,
@@ -203,18 +220,21 @@ const MyShares = () => {
size={25}
onClick={() => {
modals.openConfirmModal({
title: `Delete reverse share`,
title: t(
"account.reverseShares.modal.delete.title"
),
children: (
<Text size="sm">
Do you really want to delete this reverse share?
If you do, the associated shares will be deleted
as well.
<FormattedMessage id="account.reverseShares.modal.delete.description" />
</Text>
),
confirmProps: {
color: "red",
},
labels: { confirm: "Delete", cancel: "Cancel" },
labels: {
confirm: t("common.button.delete"),
cancel: t("common.button.cancel"),
},
onConfirm: () => {
shareService.removeReverseShare(reverseShare.id);
setReverseShares(

View File

@@ -17,11 +17,13 @@ import moment from "moment";
import Link from "next/link";
import { useEffect, useState } from "react";
import { TbInfoCircle, TbLink, TbTrash } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import Meta from "../../components/Meta";
import showShareInformationsModal from "../../components/account/showShareInformationsModal";
import showShareLinkModal from "../../components/account/showShareLinkModal";
import CenterLoader from "../../components/core/CenterLoader";
import useConfig from "../../hooks/config.hook";
import useTranslate from "../../hooks/useTranslate.hook";
import shareService from "../../services/share.service";
import { MyShare } from "../../types/share.type";
import toast from "../../utils/toast.util";
@@ -30,6 +32,7 @@ const MyShares = () => {
const modals = useModals();
const clipboard = useClipboard();
const config = useConfig();
const t = useTranslate();
const [shares, setShares] = useState<MyShare[]>();
@@ -41,18 +44,22 @@ const MyShares = () => {
return (
<>
<Meta title="My shares" />
<Meta title={t("account.shares.title")} />
<Title mb={30} order={3}>
My shares
<FormattedMessage id="account.shares.title" />
</Title>
{shares.length == 0 ? (
<Center style={{ height: "70vh" }}>
<Stack align="center" spacing={10}>
<Title order={3}>It's empty here 👀</Title>
<Text>You don't have any shares.</Text>
<Title order={3}>
<FormattedMessage id="account.shares.title.empty" />
</Title>
<Text>
<FormattedMessage id="account.shares.description.empty" />
</Text>
<Space h={5} />
<Button component={Link} href="/upload" variant="light">
Create one
<FormattedMessage id="account.shares.button.create" />
</Button>
</Stack>
</Center>
@@ -61,13 +68,21 @@ const MyShares = () => {
<Table>
<thead>
<tr>
<th>Name</th>
<th>
<FormattedMessage id="account.shares.table.name" />
</th>
<MediaQuery smallerThan="md" styles={{ display: "none" }}>
<th>Description</th>
<th>
<FormattedMessage id="account.shares.table.description" />
</th>
</MediaQuery>
<th>Visitors</th>
<th>Expires at</th>
<th>
<FormattedMessage id="account.shares.table.visitors" />
</th>
<th>
<FormattedMessage id="account.shares.table.expiresAt" />
</th>
<th></th>
</tr>
</thead>
@@ -121,9 +136,7 @@ const MyShares = () => {
share.id
}`
);
toast.success(
"The link was copied to your clipboard."
);
toast.success(t("common.notify.copied"));
} else {
showShareLinkModal(
modals,
@@ -141,16 +154,21 @@ const MyShares = () => {
size={25}
onClick={() => {
modals.openConfirmModal({
title: `Delete share ${share.id}`,
title: t("account.shares.modal.delete.title", {
share: share.id,
}),
children: (
<Text size="sm">
Do you really want to delete this share?
<FormattedMessage id="account.shares.modal.delete.description" />
</Text>
),
confirmProps: {
color: "red",
},
labels: { confirm: "Confirm", cancel: "Cancel" },
labels: {
confirm: t("common.button.delete"),
cancel: t("common.button.cancel"),
},
onConfirm: () => {
shareService.remove(share.id);
setShares(