mirror of
https://github.com/docmost/docmost.git
synced 2026-05-18 15:34:05 +08: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:
@@ -24,6 +24,7 @@ import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
|
||||
import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx";
|
||||
import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx";
|
||||
import PageExportModal from "@/features/page/components/page-export-modal.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ExportModal from "@/components/common/export-modal";
|
||||
|
||||
interface PageHeaderMenuProps {
|
||||
@@ -53,6 +54,7 @@ interface PageActionMenuProps {
|
||||
readOnly?: boolean;
|
||||
}
|
||||
function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const [, setHistoryModalOpen] = useAtom(historyAtoms);
|
||||
const clipboard = useClipboard({ timeout: 500 });
|
||||
const { pageSlug, spaceSlug } = useParams();
|
||||
@@ -69,7 +71,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
getAppUrl() + buildPageUrl(spaceSlug, page.slugId, page.title);
|
||||
|
||||
clipboard.copy(pageUrl);
|
||||
notifications.show({ message: "Link copied" });
|
||||
notifications.show({ message: t("Link copied") });
|
||||
};
|
||||
|
||||
const handlePrint = () => {
|
||||
@@ -107,13 +109,13 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
leftSection={<IconLink size={16} />}
|
||||
onClick={handleCopyLink}
|
||||
>
|
||||
Copy link
|
||||
{t("Copy link")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item leftSection={<IconArrowsHorizontal size={16} />}>
|
||||
<Group wrap="nowrap">
|
||||
<PageWidthToggle label="Full width" />
|
||||
<PageWidthToggle label={t("Full width")} />
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
|
||||
@@ -121,7 +123,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
leftSection={<IconHistory size={16} />}
|
||||
onClick={openHistoryModal}
|
||||
>
|
||||
Page history
|
||||
{t("Page history")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
@@ -130,14 +132,14 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
leftSection={<IconFileExport size={16} />}
|
||||
onClick={openExportModal}
|
||||
>
|
||||
Export
|
||||
{t("Export")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
leftSection={<IconPrinter size={16} />}
|
||||
onClick={handlePrint}
|
||||
>
|
||||
Print PDF
|
||||
{t("Print PDF")}
|
||||
</Menu.Item>
|
||||
|
||||
{!readOnly && (
|
||||
@@ -148,7 +150,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
leftSection={<IconTrash size={16} />}
|
||||
onClick={handleDeletePage}
|
||||
>
|
||||
Delete
|
||||
{t("Delete")}
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
import * as React from "react";
|
||||
import { ExportFormat } from "@/features/page/types/page.types.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface PageExportModalProps {
|
||||
pageId: string;
|
||||
@@ -16,6 +17,7 @@ export default function PageExportModal({
|
||||
open,
|
||||
onClose,
|
||||
}: PageExportModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
|
||||
|
||||
const handleExport = async () => {
|
||||
@@ -24,7 +26,7 @@ export default function PageExportModal({
|
||||
onClose();
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
message: "Export failed:" + err.response?.data.message,
|
||||
message: t("Export failed:") + err.response?.data.message,
|
||||
color: "red",
|
||||
});
|
||||
console.error("export error", err);
|
||||
@@ -48,32 +50,29 @@ export default function PageExportModal({
|
||||
<Modal.Overlay />
|
||||
<Modal.Content style={{ overflow: "hidden" }}>
|
||||
<Modal.Header py={0}>
|
||||
<Modal.Title fw={500}>Export page</Modal.Title>
|
||||
<Modal.Title fw={500}>{t("Export page")}</Modal.Title>
|
||||
<Modal.CloseButton />
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<div>
|
||||
<Text size="md">Format</Text>
|
||||
<Text size="md">{t("Format")}</Text>
|
||||
</div>
|
||||
<ExportFormatSelection format={format} onChange={handleChange} />
|
||||
|
||||
</Group>
|
||||
|
||||
<Group justify="space-between" wrap="nowrap" pt="md">
|
||||
<div>
|
||||
<Text size="md">Include subpages</Text>
|
||||
<Text size="md">{t("Include subpages")}</Text>
|
||||
</div>
|
||||
<Switch defaultChecked />
|
||||
|
||||
</Group>
|
||||
|
||||
|
||||
<Group justify="center" mt="md">
|
||||
<Button onClick={onClose} variant="default">
|
||||
Cancel
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleExport}>Export</Button>
|
||||
<Button onClick={handleExport}>{t("Export")}</Button>
|
||||
</Group>
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
@@ -86,6 +85,8 @@ interface ExportFormatSelection {
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Select
|
||||
data={[
|
||||
@@ -98,7 +99,7 @@ function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
||||
comboboxProps={{ width: "120" }}
|
||||
allowDeselect={false}
|
||||
withCheckIcon={false}
|
||||
aria-label="Select export format"
|
||||
aria-label={t("Select export format")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useAtom } from "jotai";
|
||||
import { buildTree } from "@/features/page/tree/utils";
|
||||
import { IPage } from "@/features/page/types/page.types.ts";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface PageImportModalProps {
|
||||
spaceId: string;
|
||||
@@ -24,6 +25,7 @@ export default function PageImportModal({
|
||||
open,
|
||||
onClose,
|
||||
}: PageImportModalProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<Modal.Root
|
||||
@@ -38,7 +40,7 @@ export default function PageImportModal({
|
||||
<Modal.Overlay />
|
||||
<Modal.Content style={{ overflow: "hidden" }}>
|
||||
<Modal.Header py={0}>
|
||||
<Modal.Title fw={500}>Import pages</Modal.Title>
|
||||
<Modal.Title fw={500}>{t("Import pages")}</Modal.Title>
|
||||
<Modal.CloseButton />
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
@@ -55,6 +57,7 @@ interface ImportFormatSelection {
|
||||
onClose: () => void;
|
||||
}
|
||||
function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
||||
const { t } = useTranslation();
|
||||
const [treeData, setTreeData] = useAtom(treeDataAtom);
|
||||
|
||||
const handleFileUpload = async (selectedFiles: File[]) => {
|
||||
@@ -65,8 +68,8 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
||||
onClose();
|
||||
|
||||
const alert = notifications.show({
|
||||
title: "Importing pages",
|
||||
message: "Page import is in progress. Please do not close this tab.",
|
||||
title: t("Importing pages"),
|
||||
message: t("Page import is in progress. Please do not close this tab."),
|
||||
loading: true,
|
||||
autoClose: false,
|
||||
});
|
||||
@@ -92,13 +95,14 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
||||
setTreeData(fullTree);
|
||||
}
|
||||
|
||||
const pageCountText = pageCount === 1 ? "1 page" : `${pageCount} pages`;
|
||||
const pageCountText =
|
||||
pageCount === 1 ? `1 ${t("page")}` : `${pageCount} ${t("pages")}`;
|
||||
|
||||
notifications.update({
|
||||
id: alert,
|
||||
color: "teal",
|
||||
title: `Successfully imported ${pageCountText}`,
|
||||
message: "Your import is complete.",
|
||||
title: `${t("Successfully imported")} ${pageCountText}`,
|
||||
message: t("Your import is complete."),
|
||||
icon: <IconCheck size={18} />,
|
||||
loading: false,
|
||||
autoClose: 5000,
|
||||
@@ -107,8 +111,8 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
|
||||
notifications.update({
|
||||
id: alert,
|
||||
color: "red",
|
||||
title: `Failed to import pages`,
|
||||
message: "Unable to import pages. Please try again.",
|
||||
title: t("Failed to import pages"),
|
||||
message: t("Unable to import pages. Please try again."),
|
||||
icon: <IconX size={18} />,
|
||||
loading: false,
|
||||
autoClose: 5000,
|
||||
|
||||
Reference in New Issue
Block a user