mirror of
https://github.com/docmost/docmost.git
synced 2025-11-22 19:11:10 +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:
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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",
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user