mirror of
https://github.com/docmost/docmost.git
synced 2025-11-23 03:01:11 +10:00
Support I18n (#243)
* feat: support i18n * feat: wip support i18n * feat: complete space translation * feat: complete page translation * feat: update space translation * feat: update workspace translation * feat: update group translation * feat: update workspace translation * feat: update page translation * feat: update user translation * chore: update pnpm-lock * feat: add query translation * refactor: merge to single file * chore: remove necessary code * feat: save language to BE * fix: only load current language * feat: save language to locale column * fix: cleanups * add language menu to preferences page * new translations * translate editor * Translate editor placeholders * translate space selection component --------- Co-authored-by: Philip Okugbe <phil@docmost.com> Co-authored-by: Philip Okugbe <16838612+Philipinho@users.noreply.github.com>
This commit is contained in:
@ -5,10 +5,12 @@ import { useAtom } from "jotai";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import { FileButton, Tooltip } from "@mantine/core";
|
||||
import { uploadAvatar } from "@/features/user/services/user-service.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const userAtom = focusAtom(currentUserAtom, (optic) => optic.prop("user"));
|
||||
|
||||
export default function AccountAvatar() {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const [, setUser] = useAtom(userAtom);
|
||||
@ -36,7 +38,7 @@ export default function AccountAvatar() {
|
||||
<>
|
||||
<FileButton onChange={handleFileChange} accept="image/png,image/jpeg">
|
||||
{(props) => (
|
||||
<Tooltip label="Change photo" position="bottom">
|
||||
<Tooltip label={t("Change photo")} position="bottom">
|
||||
<CustomAvatar
|
||||
{...props}
|
||||
component="button"
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
import { Group, Text, Select } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateUser } from "../services/user-service";
|
||||
import { useAtom } from "jotai";
|
||||
import { userAtom } from "../atoms/current-user-atom";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function AccountLanguage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">{t("Language")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("Choose your preferred interface language.")}
|
||||
</Text>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
function LanguageSwitcher() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [user, setUser] = useAtom(userAtom);
|
||||
const [language, setLanguage] = useState(
|
||||
user?.locale === "en" ? "en-US" : user.locale,
|
||||
);
|
||||
|
||||
const handleChange = async (value: string) => {
|
||||
const updatedUser = await updateUser({ locale: value });
|
||||
|
||||
setLanguage(value);
|
||||
setUser(updatedUser);
|
||||
|
||||
i18n.changeLanguage(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
label={t("Select language")}
|
||||
data={[
|
||||
{ value: "en-US", label: "English (United States)" },
|
||||
{ value: "zh-CN", label: "中文 (简体)" },
|
||||
]}
|
||||
value={language}
|
||||
onChange={handleChange}
|
||||
allowDeselect={false}
|
||||
checkIconPosition="right"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -8,9 +8,10 @@ import { IUser } from "@/features/user/types/user.types.ts";
|
||||
import { useState } from "react";
|
||||
import { TextInput, Button } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(40).nonempty("Your name cannot be blank"),
|
||||
name: z.string().min(2).max(40),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
@ -18,6 +19,7 @@ type FormValues = z.infer<typeof formSchema>;
|
||||
const userAtom = focusAtom(currentUserAtom, (optic) => optic.prop("user"));
|
||||
|
||||
export default function AccountNameForm() {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const [, setUser] = useAtom(userAtom);
|
||||
@ -36,12 +38,12 @@ export default function AccountNameForm() {
|
||||
const updatedUser = await updateUser(data);
|
||||
setUser(updatedUser);
|
||||
notifications.show({
|
||||
message: "Updated successfully",
|
||||
message: t("Updated successfully"),
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
notifications.show({
|
||||
message: "Failed to update data",
|
||||
message: t("Failed to update data"),
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
@ -53,13 +55,13 @@ export default function AccountNameForm() {
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<TextInput
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Your name"
|
||||
label={t("Name")}
|
||||
placeholder={t("Your name")}
|
||||
variant="filled"
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<Button type="submit" mt="sm" disabled={isLoading} loading={isLoading}>
|
||||
Save
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@ -5,14 +5,17 @@ import {
|
||||
Select,
|
||||
MantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function AccountTheme() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">Theme</Text>
|
||||
<Text size="md">{t("Theme")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Choose your preferred color scheme.
|
||||
{t("Choose your preferred color scheme.")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@ -22,6 +25,7 @@ export default function AccountTheme() {
|
||||
}
|
||||
|
||||
function ThemeSwitcher() {
|
||||
const { t } = useTranslation();
|
||||
const { colorScheme, setColorScheme } = useMantineColorScheme();
|
||||
|
||||
const handleChange = (value: MantineColorScheme) => {
|
||||
@ -30,11 +34,11 @@ function ThemeSwitcher() {
|
||||
|
||||
return (
|
||||
<Select
|
||||
label="Select theme"
|
||||
label={t("Select theme")}
|
||||
data={[
|
||||
{ value: "light", label: "Light" },
|
||||
{ value: "dark", label: "Dark" },
|
||||
{ value: "auto", label: "System settings" },
|
||||
{ value: "light", label: t("Light") },
|
||||
{ value: "dark", label: t("Dark") },
|
||||
{ value: "auto", label: t("System settings") },
|
||||
]}
|
||||
value={colorScheme}
|
||||
onChange={handleChange}
|
||||
|
||||
@ -13,15 +13,17 @@ import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import * as React from "react";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ChangeEmail() {
|
||||
const { t } = useTranslation();
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">Email</Text>
|
||||
<Text size="md">{t("Email")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{currentUser?.user.email}
|
||||
</Text>
|
||||
@ -29,13 +31,15 @@ export default function ChangeEmail() {
|
||||
|
||||
{/*
|
||||
<Button onClick={open} variant="default">
|
||||
Change email
|
||||
{t("Change email")}
|
||||
</Button>
|
||||
*/}
|
||||
|
||||
<Modal opened={opened} onClose={close} title="Change email" centered>
|
||||
<Modal opened={opened} onClose={close} title={t("Change email")} centered>
|
||||
<Text mb="md">
|
||||
To change your email, you have to enter your password and new email.
|
||||
{t(
|
||||
"To change your email, you have to enter your password and new email.",
|
||||
)}
|
||||
</Text>
|
||||
<ChangeEmailForm />
|
||||
</Modal>
|
||||
@ -53,6 +57,7 @@ const formSchema = z.object({
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
function ChangeEmailForm() {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
@ -71,8 +76,8 @@ function ChangeEmailForm() {
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Enter your password"
|
||||
label={t("Password")}
|
||||
placeholder={t("Enter your password")}
|
||||
variant="filled"
|
||||
mb="md"
|
||||
{...form.getInputProps("password")}
|
||||
@ -80,16 +85,16 @@ function ChangeEmailForm() {
|
||||
|
||||
<TextInput
|
||||
id="email"
|
||||
label="Email"
|
||||
description="Enter your new preferred email"
|
||||
placeholder="New email"
|
||||
label={t("Email")}
|
||||
description={t("Enter your new preferred email")}
|
||||
placeholder={t("New email")}
|
||||
variant="filled"
|
||||
mb="md"
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={isLoading} loading={isLoading}>
|
||||
Change email
|
||||
{t("Change email")}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@ -6,25 +6,34 @@ import * as React from "react";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { changePassword } from "@/features/auth/services/auth-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ChangePassword() {
|
||||
const { t } = useTranslation();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">Password</Text>
|
||||
<Text size="md">{t("Password")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
You can change your password here.
|
||||
{t("You can change your password here.")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Button onClick={open} variant="default">
|
||||
Change password
|
||||
{t("Change password")}
|
||||
</Button>
|
||||
|
||||
<Modal opened={opened} onClose={close} title="Change password" centered>
|
||||
<Text mb="md">Your password must be a minimum of 8 characters.</Text>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={t("Change password")}
|
||||
centered
|
||||
>
|
||||
<Text mb="md">
|
||||
{t("Your password must be a minimum of 8 characters.")}
|
||||
</Text>
|
||||
<ChangePasswordForm onClose={close} />
|
||||
</Modal>
|
||||
</Group>
|
||||
@ -44,6 +53,7 @@ interface ChangePasswordFormProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
function ChangePasswordForm({ onClose }: ChangePasswordFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
@ -62,7 +72,7 @@ function ChangePasswordForm({ onClose }: ChangePasswordFormProps) {
|
||||
newPassword: data.newPassword,
|
||||
});
|
||||
notifications.show({
|
||||
message: "Password changed successfully",
|
||||
message: t("Password changed successfully"),
|
||||
});
|
||||
|
||||
onClose();
|
||||
@ -78,9 +88,9 @@ function ChangePasswordForm({ onClose }: ChangePasswordFormProps) {
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<PasswordInput
|
||||
label="Current password"
|
||||
label={t("Current password")}
|
||||
name="oldPassword"
|
||||
placeholder="Enter your current password"
|
||||
placeholder={t("Enter your current password")}
|
||||
variant="filled"
|
||||
mb="md"
|
||||
data-autofocus
|
||||
@ -88,8 +98,8 @@ function ChangePasswordForm({ onClose }: ChangePasswordFormProps) {
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="New password"
|
||||
placeholder="Enter your new password"
|
||||
label={t("New password")}
|
||||
placeholder={t("Enter your new password")}
|
||||
variant="filled"
|
||||
mb="md"
|
||||
{...form.getInputProps("newPassword")}
|
||||
@ -97,7 +107,7 @@ function ChangePasswordForm({ onClose }: ChangePasswordFormProps) {
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button type="submit" disabled={isLoading} loading={isLoading}>
|
||||
Change password
|
||||
{t("Change password")}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
|
||||
@ -3,14 +3,17 @@ import { useAtom } from "jotai/index";
|
||||
import { userAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { updateUser } from "@/features/user/services/user-service.ts";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function PageWidthPref() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">Full page width</Text>
|
||||
<Text size="md">{t("Full page width")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Choose your preferred page width.
|
||||
{t("Choose your preferred page width.")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@ -24,6 +27,7 @@ interface PageWidthToggleProps {
|
||||
label?: string;
|
||||
}
|
||||
export function PageWidthToggle({ size, label }: PageWidthToggleProps) {
|
||||
const { t } = useTranslation();
|
||||
const [user, setUser] = useAtom(userAtom);
|
||||
const [checked, setChecked] = useState(
|
||||
user.settings?.preferences?.fullPageWidth,
|
||||
@ -43,7 +47,7 @@ export function PageWidthToggle({ size, label }: PageWidthToggleProps) {
|
||||
labelPosition="left"
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
aria-label="Toggle full page width"
|
||||
aria-label={t("Toggle full page width")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ export interface IUser {
|
||||
invitedById: string;
|
||||
lastLoginAt: string;
|
||||
lastActiveAt: Date;
|
||||
locale: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
role: string;
|
||||
|
||||
@ -2,14 +2,19 @@ import { useAtom } from "jotai";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
|
||||
import React, { useEffect } from "react";
|
||||
import useCurrentUser from "@/features/user/hooks/use-current-user";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function UserProvider({ children }: React.PropsWithChildren) {
|
||||
const [, setCurrentUser] = useAtom(currentUserAtom);
|
||||
const { data, isLoading, error } = useCurrentUser();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data.user && data.workspace) {
|
||||
setCurrentUser(data);
|
||||
i18n.changeLanguage(
|
||||
data.user.locale === "en" ? "en-US" : data.user.locale,
|
||||
);
|
||||
}
|
||||
}, [data, isLoading]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user