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

@@ -1,11 +1,15 @@
import { Button } from "@mantine/core";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import useTranslate from "../../hooks/useTranslate.hook";
import shareService from "../../services/share.service";
import toast from "../../utils/toast.util";
const DownloadAllButton = ({ shareId }: { shareId: string }) => {
const [isZipReady, setIsZipReady] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const t = useTranslate();
const downloadAll = async () => {
setIsLoading(true);
await shareService
@@ -39,13 +43,13 @@ const DownloadAllButton = ({ shareId }: { shareId: string }) => {
loading={isLoading}
onClick={() => {
if (!isZipReady) {
toast.error("The share is preparing. Try again in a few minutes.");
toast.error(t("share.notify.download-all-preparing"));
} else {
downloadAll();
}
}}
>
Download all
<FormattedMessage id="share.download-all" />
</Button>
);
};

View File

@@ -19,6 +19,8 @@ import { byteToHumanSizeString } from "../../utils/fileSize.util";
import toast from "../../utils/toast.util";
import TableSortIcon, { TableSort } from "../core/SortIcon";
import showFilePreviewModal from "./modals/showFilePreviewModal";
import useTranslate from "../../hooks/useTranslate.hook";
import { FormattedMessage } from "react-intl";
const FileList = ({
files,
@@ -34,6 +36,7 @@ const FileList = ({
const clipboard = useClipboard();
const config = useConfig();
const modals = useModals();
const t = useTranslate();
const [sort, setSort] = useState<TableSort>({
property: undefined,
@@ -68,10 +71,10 @@ const FileList = ({
if (window.isSecureContext) {
clipboard.copy(link);
toast.success("Your file link was copied to the keyboard.");
toast.success(t("common.notify.copied"));
} else {
modals.openModal({
title: "File link",
title: t("share.modal.file-link"),
children: (
<Stack align="stretch">
<TextInput variant="filled" value={link} />
@@ -90,13 +93,13 @@ const FileList = ({
<tr>
<th>
<Group spacing="xs">
Name
<FormattedMessage id="share.table.name" />
<TableSortIcon sort={sort} setSort={setSort} property="name" />
</Group>
</th>
<th>
<Group spacing="xs">
Size
<FormattedMessage id="share.table.size" />
<TableSortIcon sort={sort} setSort={setSort} property="size" />
</Group>
</th>

View File

@@ -2,6 +2,7 @@ import { Button, Center, Stack, Text, Title } from "@mantine/core";
import { modals } from "@mantine/modals";
import Link from "next/link";
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import api from "../../services/api.service";
const FilePreviewContext = React.createContext<{
@@ -144,10 +145,11 @@ const UnSupportedFile = () => {
return (
<Center style={{ minHeight: 200 }}>
<Stack align="center" spacing={10}>
<Title order={3}>Preview not supported</Title>
<Title order={3}>
<FormattedMessage id="share.modal.file-preview.error.not-supported.title" />
</Title>
<Text>
A preview for thise file type is unsupported. Please download the file
to view it.
<FormattedMessage id="share.modal.file-preview.error.not-supported.description" />
</Text>
</Stack>
</Center>

View File

@@ -1,6 +1,8 @@
import { Button, Stack } from "@mantine/core";
import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { FormattedMessage } from "react-intl";
import { translateOutsideContext } from "../../../hooks/useTranslate.hook";
import CopyTextField from "../../upload/CopyTextField";
const showCompletedReverseShareModal = (
@@ -8,11 +10,12 @@ const showCompletedReverseShareModal = (
link: string,
getReverseShares: () => void
) => {
const t = translateOutsideContext();
return modals.openModal({
closeOnClickOutside: false,
withCloseButton: false,
closeOnEscape: false,
title: "Reverse share link",
title: t("account.reverseShares.modal.reverse-share-link"),
children: <Body link={link} getReverseShares={getReverseShares} />,
});
};
@@ -36,7 +39,7 @@ const Body = ({
getReverseShares();
}}
>
Done
<FormattedMessage id="common.button.done" />
</Button>
</Stack>
);

View File

@@ -12,6 +12,8 @@ import {
import { useForm } from "@mantine/form";
import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { FormattedMessage } from "react-intl";
import useTranslate from "../../../hooks/useTranslate.hook";
import shareService from "../../../services/share.service";
import { getExpirationPreview } from "../../../utils/date.util";
import toast from "../../../utils/toast.util";
@@ -42,6 +44,7 @@ const Body = ({
showSendEmailNotificationOption: boolean;
}) => {
const modals = useModals();
const t = useTranslate();
const form = useForm({
initialValues: {
@@ -79,7 +82,7 @@ const Body = ({
max={99999}
precision={0}
variant="filled"
label="Share expiration"
label={t("account.reverseShares.modal.expiration.label")}
{...form.getInputProps("expiration_num")}
/>
</Col>
@@ -91,27 +94,44 @@ const Body = ({
{
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:
form.values.expiration_num == 1
? t("upload.modal.expires.year-singular")
: t("upload.modal.expires.year-plural"),
},
]}
/>
@@ -125,11 +145,17 @@ const Body = ({
color: theme.colors.gray[6],
})}
>
{getExpirationPreview("reverse share", form)}
{getExpirationPreview(
{
expiresOn: t("account.reverseShare.expires-on"),
neverExpires: t("account.reverseShare.never-expires"),
},
form
)}
</Text>
</div>
<FileSizeInput
label="Max share size"
label={t("account.reverseShares.modal.max-size.label")}
value={form.values.maxShareSize}
onChange={(number) => form.setFieldValue("maxShareSize", number)}
/>
@@ -138,16 +164,18 @@ const Body = ({
max={1000}
precision={0}
variant="filled"
label="Max use count"
description="The maximum number of times this reverse share link can be used"
label={t("account.reverseShares.modal.max-use.label")}
description={t("account.reverseShares.modal.max-use.description")}
{...form.getInputProps("maxUseCount")}
/>
{showSendEmailNotificationOption && (
<Switch
mt="xs"
labelPosition="left"
label="Send email notification"
description="Send an email notification when a share is created with this reverse share link"
label={t("account.reverseShares.modal.send-email")}
description={t(
"account.reverseShares.modal.send-email.description"
)}
{...form.getInputProps("sendEmailNotification", {
type: "checkbox",
})}
@@ -155,7 +183,7 @@ const Body = ({
)}
<Button mt="md" type="submit">
Create
<FormattedMessage id="common.button.create" />
</Button>
</Stack>
</form>

View File

@@ -1,16 +1,21 @@
import { Button, PasswordInput, Stack, Text } from "@mantine/core";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import useTranslate, {
translateOutsideContext,
} from "../../hooks/useTranslate.hook";
const showEnterPasswordModal = (
modals: ModalsContextProps,
submitCallback: (password: string) => Promise<void>
) => {
const t = translateOutsideContext();
return modals.openModal({
closeOnClickOutside: false,
withCloseButton: false,
closeOnEscape: false,
title: "Password required",
title: t("share.modal.password.title"),
children: <Body submitCallback={submitCallback} />,
});
};
@@ -22,10 +27,11 @@ const Body = ({
}) => {
const [password, setPassword] = useState("");
const [passwordWrong, setPasswordWrong] = useState(false);
const t = useTranslate();
return (
<Stack align="stretch">
<Text size="sm">
This access this share please enter the password for the share.
<FormattedMessage id="share.modal.password.description" />
</Text>
<form
@@ -37,13 +43,15 @@ const Body = ({
<Stack>
<PasswordInput
variant="filled"
placeholder="Password"
error={passwordWrong && "Wrong password"}
placeholder={t("share.modal.password")}
error={passwordWrong && t("share.modal.error.invalid-password")}
onFocus={() => setPasswordWrong(false)}
onChange={(e) => setPassword(e.target.value)}
value={password}
/>
<Button type="submit">Submit</Button>
<Button type="submit">
<FormattedMessage id="common.button.submit" />
</Button>
</Stack>
</form>
</Stack>

View File

@@ -2,6 +2,7 @@ import { Button, Stack, Text } from "@mantine/core";
import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { useRouter } from "next/router";
import { FormattedMessage } from "react-intl";
const showErrorModal = (
modals: ModalsContextProps,
@@ -31,7 +32,7 @@ const Body = ({ text }: { text: string }) => {
router.back();
}}
>
Go back
<FormattedMessage id="common.button.go-back" />
</Button>
</Stack>
</>