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:
lleohao
2025-01-04 21:17:17 +08:00
committed by GitHub
parent 290b7d9d94
commit 670ee64179
119 changed files with 1672 additions and 649 deletions

View File

@ -1,4 +1,5 @@
import { Button, Group } from '@mantine/core';
import { Button, Group } from "@mantine/core";
import { useTranslation } from "react-i18next";
type CommentActionsProps = {
onSave: () => void;
@ -6,9 +7,13 @@ type CommentActionsProps = {
};
function CommentActions({ onSave, isLoading }: CommentActionsProps) {
const { t } = useTranslation();
return (
<Group justify="flex-end" pt={2} wrap="nowrap">
<Button size="compact-sm" loading={isLoading} onClick={onSave}>Save</Button>
<Button size="compact-sm" loading={isLoading} onClick={onSave}>
{t("Save")}
</Button>
</Group>
);
}

View File

@ -14,6 +14,7 @@ import { useCreateCommentMutation } from "@/features/comment/queries/comment-que
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
import { useEditor } from "@tiptap/react";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import { useTranslation } from "react-i18next";
interface CommentDialogProps {
editor: ReturnType<typeof useEditor>;
@ -21,6 +22,7 @@ interface CommentDialogProps {
}
function CommentDialog({ editor, pageId }: CommentDialogProps) {
const { t } = useTranslation();
const [comment, setComment] = useState("");
const [, setShowCommentPopup] = useAtom(showCommentPopupAtom);
const [, setActiveCommentId] = useAtom(activeCommentIdAtom);
@ -107,7 +109,7 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) {
<CommentEditor
onUpdate={handleCommentEditorChange}
placeholder="Write a comment"
placeholder={t("Write a comment")}
editable={true}
autofocus={true}
/>

View File

@ -7,6 +7,7 @@ import classes from "./comment.module.css";
import { useFocusWithin } from "@mantine/hooks";
import clsx from "clsx";
import { forwardRef, useEffect, useImperativeHandle } from "react";
import { useTranslation } from "react-i18next";
interface CommentEditorProps {
defaultContent?: any;
@ -27,6 +28,7 @@ const CommentEditor = forwardRef(
}: CommentEditorProps,
ref,
) => {
const { t } = useTranslation();
const { ref: focusRef, focused } = useFocusWithin();
const commentEditor = useEditor({
@ -36,7 +38,7 @@ const CommentEditor = forwardRef(
dropcursor: false,
}),
Placeholder.configure({
placeholder: placeholder || "Reply...",
placeholder: placeholder || t("Reply..."),
}),
Underline,
Link,

View File

@ -24,7 +24,6 @@ function CommentListItem({ comment }: CommentListItemProps) {
const { hovered, ref } = useHover();
const [isEditing, setIsEditing] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const editor = useAtomValue(pageEditorAtom);
const [content, setContent] = useState<string>(comment.content);
const updateCommentMutation = useUpdateCommentMutation();

View File

@ -6,7 +6,6 @@ import {
useCommentsQuery,
useCreateCommentMutation,
} from "@/features/comment/queries/comment-query";
import CommentEditor from "@/features/comment/components/comment-editor";
import CommentActions from "@/features/comment/components/comment-actions";
import { useFocusWithin } from "@mantine/hooks";
@ -14,8 +13,10 @@ import { IComment } from "@/features/comment/types/comment.types.ts";
import { usePageQuery } from "@/features/page/queries/page-query.ts";
import { IPagination } from "@/lib/types.ts";
import { extractPageSlugId } from "@/lib";
import { useTranslation } from "react-i18next";
function CommentList() {
const { t } = useTranslation();
const { pageSlug } = useParams();
const { data: page } = usePageQuery({ pageId: extractPageSlugId(pageSlug) });
const {
@ -79,11 +80,11 @@ function CommentList() {
}
if (isError) {
return <div>Error loading comments.</div>;
return <div>{t("Error loading comments.")}</div>;
}
if (!comments || comments.items.length === 0) {
return <>No comments yet.</>;
return <>{t("No comments yet.")}</>;
}
return (

View File

@ -1,6 +1,7 @@
import { ActionIcon, Menu } from '@mantine/core';
import { IconDots, IconEdit, IconTrash } from '@tabler/icons-react';
import { modals } from '@mantine/modals';
import { ActionIcon, Menu } from "@mantine/core";
import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react";
import { modals } from "@mantine/modals";
import { useTranslation } from "react-i18next";
type CommentMenuProps = {
onEditComment: () => void;
@ -8,34 +9,35 @@ type CommentMenuProps = {
};
function CommentMenu({ onEditComment, onDeleteComment }: CommentMenuProps) {
const { t } = useTranslation();
//@ts-ignore
const openDeleteModal = () =>
modals.openConfirmModal({
title: 'Are you sure you want to delete this comment?',
title: t("Are you sure you want to delete this comment?"),
centered: true,
labels: { confirm: 'Delete', cancel: 'Cancel' },
confirmProps: { color: 'red' },
labels: { confirm: t("Delete"), cancel: t("Cancel") },
confirmProps: { color: "red" },
onConfirm: onDeleteComment,
});
return (
<Menu shadow="md" width={200}>
<Menu.Target>
<ActionIcon variant="default" style={{ border: 'none' }}>
<ActionIcon variant="default" style={{ border: "none" }}>
<IconDots size={20} stroke={2} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item onClick={onEditComment}
leftSection={<IconEdit size={14} />}>
Edit comment
<Menu.Item onClick={onEditComment} leftSection={<IconEdit size={14} />}>
{t("Edit comment")}
</Menu.Item>
<Menu.Item leftSection={<IconTrash size={14} />}
onClick={openDeleteModal}
<Menu.Item
leftSection={<IconTrash size={14} />}
onClick={openDeleteModal}
>
Delete comment
{t("Delete comment")}
</Menu.Item>
</Menu.Dropdown>
</Menu>

View File

@ -1,34 +1,44 @@
import { ActionIcon } from '@mantine/core';
import { IconCircleCheck } from '@tabler/icons-react';
import { modals } from '@mantine/modals';
import { useResolveCommentMutation } from '@/features/comment/queries/comment-query';
import { ActionIcon } from "@mantine/core";
import { IconCircleCheck } from "@tabler/icons-react";
import { modals } from "@mantine/modals";
import { useResolveCommentMutation } from "@/features/comment/queries/comment-query";
import { useTranslation } from "react-i18next";
function ResolveComment({ commentId, pageId, resolvedAt }) {
const { t } = useTranslation();
const resolveCommentMutation = useResolveCommentMutation();
const isResolved = resolvedAt != null;
const iconColor = isResolved ? 'green' : 'gray';
const iconColor = isResolved ? "green" : "gray";
//@ts-ignore
const openConfirmModal = () =>
modals.openConfirmModal({
title: 'Are you sure you want to resolve this comment thread?',
title: t("Are you sure you want to resolve this comment thread?"),
centered: true,
labels: { confirm: 'Confirm', cancel: 'Cancel' },
labels: { confirm: t("Confirm"), cancel: t("Cancel") },
onConfirm: handleResolveToggle,
});
const handleResolveToggle = async () => {
try {
await resolveCommentMutation.mutateAsync({ commentId, resolved: !isResolved });
await resolveCommentMutation.mutateAsync({
commentId,
resolved: !isResolved,
});
//TODO: remove comment mark
// Remove comment thread from state on resolve
} catch (error) {
console.error('Failed to toggle resolved state:', error);
console.error("Failed to toggle resolved state:", error);
}
};
return (
<ActionIcon onClick={openConfirmModal} variant="default" style={{ border: 'none' }}>
<ActionIcon
onClick={openConfirmModal}
variant="default"
style={{ border: "none" }}
>
<IconCircleCheck size={20} stroke={2} color={iconColor} />
</ActionIcon>
);

View File

@ -18,6 +18,7 @@ import {
} from "@/features/comment/types/comment.types";
import { notifications } from "@mantine/notifications";
import { IPagination } from "@/lib/types.ts";
import { useTranslation } from "react-i18next";
export const RQ_KEY = (pageId: string) => ["comments", pageId];
@ -33,6 +34,7 @@ export function useCommentsQuery(
export function useCreateCommentMutation() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation<IComment, Error, Partial<IComment>>({
mutationFn: (data) => createComment(data),
@ -45,28 +47,37 @@ export function useCreateCommentMutation() {
//}
queryClient.refetchQueries({ queryKey: RQ_KEY(data.pageId) });
notifications.show({ message: "Comment created successfully" });
notifications.show({ message: t("Comment created successfully") });
},
onError: (error) => {
notifications.show({ message: "Error creating comment", color: "red" });
notifications.show({
message: t("Error creating comment"),
color: "red",
});
},
});
}
export function useUpdateCommentMutation() {
const { t } = useTranslation();
return useMutation<IComment, Error, Partial<IComment>>({
mutationFn: (data) => updateComment(data),
onSuccess: (data) => {
notifications.show({ message: "Comment updated successfully" });
notifications.show({ message: t("Comment updated successfully") });
},
onError: (error) => {
notifications.show({ message: "Failed to update comment", color: "red" });
notifications.show({
message: t("Failed to update comment"),
color: "red",
});
},
});
}
export function useDeleteCommentMutation(pageId?: string) {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: (commentId: string) => deleteComment(commentId),
@ -86,16 +97,20 @@ export function useDeleteCommentMutation(pageId?: string) {
});
}
notifications.show({ message: "Comment deleted successfully" });
notifications.show({ message: t("Comment deleted successfully") });
},
onError: (error) => {
notifications.show({ message: "Failed to delete comment", color: "red" });
notifications.show({
message: t("Failed to delete comment"),
color: "red",
});
},
});
}
export function useResolveCommentMutation() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation({
mutationFn: (data: IResolveComment) => resolveComment(data),
@ -114,11 +129,11 @@ export function useResolveCommentMutation() {
queryClient.setQueryData(RQ_KEY(data.pageId), updatedComments);
}*/
notifications.show({ message: "Comment resolved successfully" });
notifications.show({ message: t("Comment resolved successfully") });
},
onError: (error) => {
notifications.show({
message: "Failed to resolve comment",
message: t("Failed to resolve comment"),
color: "red",
});
},