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:
@@ -9,6 +9,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import useConfig from "../../../hooks/config.hook";
|
||||
import Logo from "../../Logo";
|
||||
|
||||
@@ -42,7 +43,7 @@ const ConfigurationHeader = ({
|
||||
</Link>
|
||||
<MediaQuery smallerThan="sm" styles={{ display: "none" }}>
|
||||
<Button variant="light" component={Link} href="/admin">
|
||||
Go back
|
||||
<FormattedMessage id="common.button.go-back" />
|
||||
</Button>
|
||||
</MediaQuery>
|
||||
</Group>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import Link from "next/link";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { TbAt, TbMail, TbShare, TbSquare } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const categories = [
|
||||
{ name: "General", icon: <TbSquare /> },
|
||||
@@ -53,7 +54,7 @@ const ConfigurationNavBar = ({
|
||||
>
|
||||
<Navbar.Section>
|
||||
<Text size="xs" color="dimmed" mb="sm">
|
||||
Configuration
|
||||
<FormattedMessage id="admin.config.title" />
|
||||
</Text>
|
||||
<Stack spacing="xs">
|
||||
{categories.map((category) => (
|
||||
@@ -79,7 +80,11 @@ const ConfigurationNavBar = ({
|
||||
>
|
||||
{category.icon}
|
||||
</ThemeIcon>
|
||||
<Text size="sm">{category.name}</Text>
|
||||
<Text size="sm">
|
||||
<FormattedMessage
|
||||
id={`admin.config.category.${category.name.toLowerCase()}`}
|
||||
/>
|
||||
</Text>
|
||||
</Group>
|
||||
</Box>
|
||||
))}
|
||||
@@ -87,7 +92,7 @@ const ConfigurationNavBar = ({
|
||||
</Navbar.Section>
|
||||
<MediaQuery largerThan="sm" styles={{ display: "none" }}>
|
||||
<Button mt="xl" variant="light" component={Link} href="/admin">
|
||||
Go back
|
||||
<FormattedMessage id="common.button.go-back" />
|
||||
</Button>
|
||||
</MediaQuery>
|
||||
</Navbar>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Box, FileInput, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { TbUpload } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
|
||||
const LogoConfigInput = ({
|
||||
logo,
|
||||
@@ -11,14 +13,16 @@ const LogoConfigInput = ({
|
||||
setLogo: Dispatch<SetStateAction<File | null>>;
|
||||
}) => {
|
||||
const isMobile = useMediaQuery("(max-width: 560px)");
|
||||
const t = useTranslate();
|
||||
|
||||
return (
|
||||
<Group position="apart">
|
||||
<Stack style={{ maxWidth: isMobile ? "100%" : "40%" }} spacing={0}>
|
||||
<Title order={6}>Logo</Title>
|
||||
<Title order={6}>
|
||||
<FormattedMessage id="admin.config.general.logo" />
|
||||
</Title>
|
||||
<Text color="dimmed" size="sm" mb="xs">
|
||||
Change your logo by uploading a new image. The image must be a PNG and
|
||||
should have the format 1:1.
|
||||
<FormattedMessage id="admin.config.general.logo.description" />
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack></Stack>
|
||||
@@ -29,7 +33,7 @@ const LogoConfigInput = ({
|
||||
value={logo}
|
||||
onChange={(v) => setLogo(v)}
|
||||
accept=".png"
|
||||
placeholder="Pick image"
|
||||
placeholder={t("admin.config.general.logo.placeholder")}
|
||||
/>
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button, Stack, Text, Textarea } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import useUser from "../../../hooks/user.hook";
|
||||
import configService from "../../../services/config.service";
|
||||
import toast from "../../../utils/toast.util";
|
||||
@@ -65,7 +66,7 @@ const TestEmailButton = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send test email
|
||||
<FormattedMessage id="admin.config.smtp.button.test" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useModals } from "@mantine/modals";
|
||||
import { TbCheck, TbEdit, TbTrash } from "react-icons/tb";
|
||||
import User from "../../../types/user.type";
|
||||
import showUpdateUserModal from "./showUpdateUserModal";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
const ManageUserTable = ({
|
||||
users,
|
||||
@@ -22,9 +23,15 @@ const ManageUserTable = ({
|
||||
<Table verticalSpacing="sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Admin</th>
|
||||
<th>
|
||||
<FormattedMessage id="admin.users.table.username" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="admin.users.table.email" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="admin.users.table.admin" />
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
import userService from "../../../services/user.service";
|
||||
import toast from "../../../utils/toast.util";
|
||||
|
||||
@@ -34,6 +36,7 @@ const Body = ({
|
||||
smtpEnabled: boolean;
|
||||
getUsers: () => void;
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
username: "",
|
||||
@@ -44,9 +47,14 @@ const Body = ({
|
||||
},
|
||||
validate: yupResolver(
|
||||
yup.object().shape({
|
||||
email: yup.string().email(),
|
||||
username: yup.string().min(3),
|
||||
password: yup.string().min(8).optional(),
|
||||
email: yup.string().email(t("common.error.invalid-email")),
|
||||
username: yup
|
||||
.string()
|
||||
.min(3, t("common.error.too-short", { length: 3 })),
|
||||
password: yup
|
||||
.string()
|
||||
.min(8, t("common.error.too-short", { length: 8 }))
|
||||
.optional(),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -65,14 +73,22 @@ const Body = ({
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput label="Username" {...form.getInputProps("username")} />
|
||||
<TextInput label="Email" {...form.getInputProps("email")} />
|
||||
<TextInput
|
||||
label={t("admin.users.modal.create.username")}
|
||||
{...form.getInputProps("username")}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("admin.users.modal.create.email")}
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
{smtpEnabled && (
|
||||
<Switch
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
label="Set password manually"
|
||||
description="If not checked, the user will receive an email with a link to set their password."
|
||||
label={t("admin.users.modal.create.manual-password")}
|
||||
description={t(
|
||||
"admin.users.modal.create.manual-password.description"
|
||||
)}
|
||||
{...form.getInputProps("setPasswordManually", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
@@ -80,7 +96,7 @@ const Body = ({
|
||||
)}
|
||||
{(form.values.setPasswordManually || !smtpEnabled) && (
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
label={t("admin.users.modal.create.password")}
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
)}
|
||||
@@ -93,12 +109,14 @@ const Body = ({
|
||||
}}
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
label="Admin privileges"
|
||||
description="If checked, the user will be able to access the admin panel."
|
||||
label={t("admin.users.modal.create.admin")}
|
||||
description={t("admin.users.modal.create.admin.description")}
|
||||
{...form.getInputProps("isAdmin", { type: "checkbox" })}
|
||||
/>
|
||||
<Group position="right">
|
||||
<Button type="submit">Create</Button>
|
||||
<Button type="submit">
|
||||
<FormattedMessage id="common.button.create" />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import useTranslate, {
|
||||
translateOutsideContext,
|
||||
} from "../../../hooks/useTranslate.hook";
|
||||
import userService from "../../../services/user.service";
|
||||
import User from "../../../types/user.type";
|
||||
import toast from "../../../utils/toast.util";
|
||||
@@ -19,8 +23,9 @@ const showUpdateUserModal = (
|
||||
user: User,
|
||||
getUsers: () => void
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
return modals.openModal({
|
||||
title: `Update ${user.username}`,
|
||||
title: t("admin.users.edit.update.title", { username: user.username }),
|
||||
children: <Body user={user} modals={modals} getUsers={getUsers} />,
|
||||
});
|
||||
};
|
||||
@@ -34,6 +39,8 @@ const Body = ({
|
||||
user: User;
|
||||
getUsers: () => void;
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
|
||||
const accountForm = useForm({
|
||||
initialValues: {
|
||||
username: user.username,
|
||||
@@ -42,8 +49,10 @@ const Body = ({
|
||||
},
|
||||
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 })),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -54,7 +63,9 @@ const Body = ({
|
||||
},
|
||||
validate: yupResolver(
|
||||
yup.object().shape({
|
||||
password: yup.string().min(8),
|
||||
password: yup
|
||||
.string()
|
||||
.min(8, t("common.error.too-short", { length: 8 })),
|
||||
})
|
||||
),
|
||||
});
|
||||
@@ -75,21 +86,26 @@ const Body = ({
|
||||
>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label="Username"
|
||||
label={t("admin.users.table.username")}
|
||||
{...accountForm.getInputProps("username")}
|
||||
/>
|
||||
<TextInput label="Email" {...accountForm.getInputProps("email")} />
|
||||
<TextInput
|
||||
label={t("admin.users.table.email")}
|
||||
{...accountForm.getInputProps("email")}
|
||||
/>
|
||||
<Switch
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
label="Admin privileges"
|
||||
label={t("admin.users.edit.update.admin-privileges")}
|
||||
{...accountForm.getInputProps("isAdmin", { type: "checkbox" })}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
<Accordion>
|
||||
<Accordion.Item sx={{ borderBottom: "none" }} value="changePassword">
|
||||
<Accordion.Control px={0}>Change password</Accordion.Control>
|
||||
<Accordion.Control px={0}>
|
||||
<FormattedMessage id="admin.users.edit.update.change-password.title" />
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<form
|
||||
onSubmit={passwordForm.onSubmit(async (values) => {
|
||||
@@ -97,17 +113,21 @@ const Body = ({
|
||||
.update(user.id, {
|
||||
password: values.password,
|
||||
})
|
||||
.then(() => toast.success("Password changed successfully"))
|
||||
.then(() =>
|
||||
toast.success(
|
||||
t("admin.users.edit.update.notify.password.success")
|
||||
)
|
||||
)
|
||||
.catch(toast.axiosError);
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<PasswordInput
|
||||
label="New password"
|
||||
label={t("admin.users.edit.update.change-password.field")}
|
||||
{...passwordForm.getInputProps("password")}
|
||||
/>
|
||||
<Button variant="light" type="submit">
|
||||
Save new password
|
||||
<FormattedMessage id="admin.users.edit.update.change-password.button" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
@@ -116,7 +136,7 @@ const Body = ({
|
||||
</Accordion>
|
||||
<Group position="right">
|
||||
<Button type="submit" form="accountForm">
|
||||
Save
|
||||
<FormattedMessage id="common.button.save" />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
Reference in New Issue
Block a user