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
@@ -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,