Merge pull request #110 from drawdb-io/i18n

Configure i18n and add simplified Chinese (#99)
This commit is contained in:
lilit 2024-05-17 04:00:43 +03:00 committed by GitHub
commit 002e240b7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 1835 additions and 594 deletions

76
package-lock.json generated
View File

@ -22,6 +22,8 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"framer-motion": "^10.18.0", "framer-motion": "^10.18.0",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"i18next": "^23.11.4",
"i18next-browser-languagedetector": "^8.0.0",
"jsonschema": "^1.4.1", "jsonschema": "^1.4.1",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"lexical": "^0.12.5", "lexical": "^0.12.5",
@ -29,6 +31,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hotkeys-hook": "^4.4.1", "react-hotkeys-hook": "^4.4.1",
"react-i18next": "^14.1.1",
"react-router-dom": "^6.21.0", "react-router-dom": "^6.21.0",
"url": "^0.11.1" "url": "^0.11.1"
}, },
@ -361,9 +364,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.23.6", "version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
"integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@ -3611,6 +3614,14 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/html-to-image": { "node_modules/html-to-image": {
"version": "1.11.11", "version": "1.11.11",
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz", "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz",
@ -3629,6 +3640,36 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/i18next": {
"version": "23.11.4",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.4.tgz",
"integrity": "sha512-CCUjtd5TfaCl+mLUzAA0uPSN+AVn4fP/kWCYt/hocPUwusTpMVczdrRyOBUwk6N05iH40qiKx6q1DoNJtBIwdg==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz",
"integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
@ -4978,6 +5019,27 @@
"react-dom": ">=16.8.1" "react-dom": ">=16.8.1"
} }
}, },
"node_modules/react-i18next": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.1.tgz",
"integrity": "sha512-QSiKw+ihzJ/CIeIYWrarCmXJUySHDwQr5y8uaNIkbxoGRm/5DukkxZs+RPla79IKyyDPzC/DRlgQCABHtrQuQQ==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 23.2.3",
"react": ">= 16.8.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -5898,6 +5960,14 @@
} }
} }
}, },
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/w3c-keyname": { "node_modules/w3c-keyname": {
"version": "2.2.8", "version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",

View File

@ -24,6 +24,8 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"framer-motion": "^10.18.0", "framer-motion": "^10.18.0",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"i18next": "^23.11.4",
"i18next-browser-languagedetector": "^8.0.0",
"jsonschema": "^1.4.1", "jsonschema": "^1.4.1",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"lexical": "^0.12.5", "lexical": "^0.12.5",
@ -31,6 +33,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hotkeys-hook": "^4.4.1", "react-hotkeys-hook": "^4.4.1",
"react-i18next": "^14.1.1",
"react-router-dom": "^6.21.0", "react-router-dom": "^6.21.0",
"url": "^0.11.1" "url": "^0.11.1"
}, },

View File

@ -1,18 +1,20 @@
import { Button } from "@douyinfe/semi-ui"; import { Button } from "@douyinfe/semi-ui";
import { IconCheckboxTick } from "@douyinfe/semi-icons"; import { IconCheckboxTick } from "@douyinfe/semi-icons";
import { tableThemes } from "../data/constants"; import { tableThemes } from "../data/constants";
import { useTranslation } from "react-i18next";
export default function ColorPalette({ export default function ColorPalette({
currentColor, currentColor,
onClearColor, onClearColor,
onPickColor, onPickColor,
}) { }) {
const { t } = useTranslation();
return ( return (
<div> <div>
<div className="flex justify-between items-center p-2"> <div className="flex justify-between items-center p-2">
<div className="font-medium">Theme</div> <div className="font-medium">{t("theme")}</div>
<Button type="tertiary" size="small" onClick={onClearColor}> <Button type="tertiary" size="small" onClick={onClearColor}>
Clear {t("clear")}
</Button> </Button>
</div> </div>
<hr /> <hr />

View File

@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { Button, Popover, Input, Toast } from "@douyinfe/semi-ui"; import { Button, Popover, Input } from "@douyinfe/semi-ui";
import { IconEdit, IconDeleteStroked } from "@douyinfe/semi-icons"; import { IconEdit, IconDeleteStroked } from "@douyinfe/semi-icons";
import { import {
Tab, Tab,
@ -17,7 +17,8 @@ import {
useSaveState, useSaveState,
useTransform, useTransform,
} from "../../hooks"; } from "../../hooks";
import ColorPalette from "../ColorPalette"; import ColorPalette from "../ColorPicker";
import { useTranslation } from "react-i18next";
export default function Area({ data, onMouseDown, setResize, setInitCoords }) { export default function Area({ data, onMouseDown, setResize, setInitCoords }) {
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
@ -191,14 +192,15 @@ function EditPopoverContent({ data }) {
const { setSaveState } = useSaveState(); const { setSaveState } = useSaveState();
const { updateArea, deleteArea } = useAreas(); const { updateArea, deleteArea } = useAreas();
const { setUndoStack, setRedoStack } = useUndoRedo(); const { setUndoStack, setRedoStack } = useUndoRedo();
const { t } = useTranslation();
return ( return (
<div className="popover-theme"> <div className="popover-theme">
<div className="font-semibold mb-2 ms-1">Edit subject area</div> <div className="font-semibold mb-2 ms-1">{t("edit")}</div>
<div className="w-[280px] flex items-center mb-2"> <div className="w-[280px] flex items-center mb-2">
<Input <Input
value={data.name} value={data.name}
placeholder="Name" placeholder={t("name")}
className="me-2" className="me-2"
onChange={(value) => updateArea(data.id, { name: value })} onChange={(value) => updateArea(data.id, { name: value })}
onFocus={(e) => setEditField({ name: e.target.value })} onFocus={(e) => setEditField({ name: e.target.value })}
@ -212,7 +214,10 @@ function EditPopoverContent({ data }) {
aid: data.id, aid: data.id,
undo: editField, undo: editField,
redo: { name: e.target.value }, redo: { name: e.target.value },
message: `Edit area name to ${e.target.value}`, message: t("edit_area", {
areaName: e.target.value,
extra: "[name]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -232,7 +237,10 @@ function EditPopoverContent({ data }) {
aid: data.id, aid: data.id,
undo: { color: data.color }, undo: { color: data.color },
redo: { color: c }, redo: { color: c },
message: `Edit area color to ${c}`, message: t("edit_area", {
areaName: data.name,
extra: "[color]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -263,12 +271,9 @@ function EditPopoverContent({ data }) {
icon={<IconDeleteStroked />} icon={<IconDeleteStroked />}
type="danger" type="danger"
block block
onClick={() => { onClick={() => deleteArea(data.id, true)}
Toast.success(`Area deleted!`);
deleteArea(data.id);
}}
> >
Delete {t("delete")}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -20,8 +20,11 @@ import {
useNotes, useNotes,
useLayout, useLayout,
} from "../../hooks"; } from "../../hooks";
import { useTranslation } from "react-i18next";
import { diagram } from "../../data/heroDiagram";
export default function Canvas() { export default function Canvas() {
const { t } = useTranslation();
const { tables, updateTable, relationships, addRelationship } = useTables(); const { tables, updateTable, relationships, addRelationship } = useTables();
const { areas, updateArea } = useAreas(); const { areas, updateArea } = useAreas();
const { notes, updateNote } = useNotes(); const { notes, updateNote } = useNotes();
@ -278,7 +281,10 @@ export default function Canvas() {
toX: info.x, toX: info.x,
toY: info.y, toY: info.y,
id: dragging.id, id: dragging.id,
message: `Move ${info.name} to (${info.x}, ${info.y})`, message: t("move_element", {
coords: `(${info.x}, ${info.y})`,
name: info.name,
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -291,7 +297,10 @@ export default function Canvas() {
action: Action.PAN, action: Action.PAN,
undo: { x: panning.x, y: panning.y }, undo: { x: panning.x, y: panning.y },
redo: transform.pan, redo: transform.pan,
message: `Move diagram to (${transform.pan?.x}, ${transform.pan?.y})`, message: t("move_element", {
coords: `(${transform?.pan.x}, ${transform?.pan.y})`,
name: diagram,
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -321,7 +330,10 @@ export default function Canvas() {
height: initCoords.height, height: initCoords.height,
}, },
redo: areas[areaResize.id], redo: areas[areaResize.id],
message: `Resize area`, message: t("edit_area", {
areaName: areas[areaResize.id].name,
extra: "[resize]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -350,7 +362,7 @@ export default function Canvas() {
tables[linkingLine.startTableId].fields[linkingLine.startFieldId].type !== tables[linkingLine.startTableId].fields[linkingLine.startFieldId].type !==
tables[hoveredTable.tableId].fields[hoveredTable.field].type tables[hoveredTable.tableId].fields[hoveredTable.field].type
) { ) {
Toast.info("Cannot connect"); Toast.info(t("connot_connect"));
return; return;
} }
if ( if (

View File

@ -6,7 +6,7 @@ import {
State, State,
noteThemes, noteThemes,
} from "../../data/constants"; } from "../../data/constants";
import { Input, Button, Popover, Toast } from "@douyinfe/semi-ui"; import { Input, Button, Popover } from "@douyinfe/semi-ui";
import { import {
IconEdit, IconEdit,
IconDeleteStroked, IconDeleteStroked,
@ -19,6 +19,7 @@ import {
useNotes, useNotes,
useSaveState, useSaveState,
} from "../../hooks"; } from "../../hooks";
import { useTranslation } from "react-i18next";
export default function Note({ data, onMouseDown }) { export default function Note({ data, onMouseDown }) {
const w = 180; const w = 180;
@ -27,6 +28,7 @@ export default function Note({ data, onMouseDown }) {
const [editField, setEditField] = useState({}); const [editField, setEditField] = useState({});
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const { layout } = useLayout(); const { layout } = useLayout();
const { t } = useTranslation();
const { setSaveState } = useSaveState(); const { setSaveState } = useSaveState();
const { updateNote, deleteNote } = useNotes(); const { updateNote, deleteNote } = useNotes();
const { setUndoStack, setRedoStack } = useUndoRedo(); const { setUndoStack, setRedoStack } = useUndoRedo();
@ -54,7 +56,10 @@ export default function Note({ data, onMouseDown }) {
nid: data.id, nid: data.id,
undo: editField, undo: editField,
redo: { content: e.target.value, height: newHeight }, redo: { content: e.target.value, height: newHeight },
message: `Edit note content to "${e.target.value}"`, message: t("edit_note", {
noteTitle: e.target.value,
extra: "[content]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -168,11 +173,11 @@ export default function Note({ data, onMouseDown }) {
stopPropagation stopPropagation
content={ content={
<div className="popover-theme"> <div className="popover-theme">
<div className="font-semibold mb-2 ms-1">Edit note</div> <div className="font-semibold mb-2 ms-1">{t("edit")}</div>
<div className="w-[280px] flex items-center mb-2"> <div className="w-[280px] flex items-center mb-2">
<Input <Input
value={data.title} value={data.title}
placeholder="Title" placeholder={t("title")}
className="me-2" className="me-2"
onChange={(value) => onChange={(value) =>
updateNote(data.id, { title: value }) updateNote(data.id, { title: value })
@ -190,7 +195,10 @@ export default function Note({ data, onMouseDown }) {
nid: data.id, nid: data.id,
undo: editField, undo: editField,
redo: { title: e.target.value }, redo: { title: e.target.value },
message: `Edit note title to "${e.target.value}"`, message: t("edit_note", {
noteTitle: e.target.value,
extra: "[title]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -199,7 +207,9 @@ export default function Note({ data, onMouseDown }) {
<Popover <Popover
content={ content={
<div className="popover-theme"> <div className="popover-theme">
<div className="font-medium mb-1">Theme</div> <div className="font-medium mb-1">
{t("theme")}
</div>
<hr /> <hr />
<div className="py-3"> <div className="py-3">
{noteThemes.map((c) => ( {noteThemes.map((c) => (
@ -216,7 +226,10 @@ export default function Note({ data, onMouseDown }) {
nid: data.id, nid: data.id,
undo: { color: data.color }, undo: { color: data.color },
redo: { color: c }, redo: { color: c },
message: `Edit note color to ${c}`, message: t("edit_note", {
noteTitle: data.title,
extra: "[color]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -249,12 +262,9 @@ export default function Note({ data, onMouseDown }) {
icon={<IconDeleteStroked />} icon={<IconDeleteStroked />}
type="danger" type="danger"
block block
onClick={() => { onClick={() => deleteNote(data.id, true)}
Toast.success(`Note deleted!`);
deleteNote(data.id);
}}
> >
Delete {t("delete")}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -13,9 +13,10 @@ import {
IconDeleteStroked, IconDeleteStroked,
IconKeyStroked, IconKeyStroked,
} from "@douyinfe/semi-icons"; } from "@douyinfe/semi-icons";
import { Popover, Tag, Button, Toast, SideSheet } from "@douyinfe/semi-ui"; import { Popover, Tag, Button, SideSheet } from "@douyinfe/semi-ui";
import { useLayout, useSettings, useTables, useSelect } from "../../hooks"; import { useLayout, useSettings, useTables, useSelect } from "../../hooks";
import TableInfo from "../EditorSidePanel/TablesTab/TableInfo"; import TableInfo from "../EditorSidePanel/TablesTab/TableInfo";
import { useTranslation } from "react-i18next";
export default function Table(props) { export default function Table(props) {
const [hoveredField, setHoveredField] = useState(-1); const [hoveredField, setHoveredField] = useState(-1);
@ -29,6 +30,7 @@ export default function Table(props) {
const { layout } = useLayout(); const { layout } = useLayout();
const { deleteTable, deleteField } = useTables(); const { deleteTable, deleteField } = useTables();
const { settings } = useSettings(); const { settings } = useSettings();
const { t } = useTranslation();
const { selectedElement, setSelectedElement } = useSelect(); const { selectedElement, setSelectedElement } = useSelect();
const height = const height =
@ -110,9 +112,9 @@ export default function Table(props) {
content={ content={
<div className="popover-theme"> <div className="popover-theme">
<div className="mb-2"> <div className="mb-2">
<strong>Comment :</strong>{" "} <strong>{t("comment")}:</strong>{" "}
{tableData.comment === "" ? ( {tableData.comment === "" ? (
"No comment" t("not_set")
) : ( ) : (
<div>{tableData.comment}</div> <div>{tableData.comment}</div>
)} )}
@ -123,10 +125,10 @@ export default function Table(props) {
tableData.indices.length === 0 ? "" : "block" tableData.indices.length === 0 ? "" : "block"
}`} }`}
> >
Indices : {t("indices")}:
</strong>{" "} </strong>{" "}
{tableData.indices.length === 0 ? ( {tableData.indices.length === 0 ? (
"No indices" t("not_set")
) : ( ) : (
<div> <div>
{tableData.indices.map((index, k) => ( {tableData.indices.map((index, k) => (
@ -156,12 +158,9 @@ export default function Table(props) {
type="danger" type="danger"
block block
style={{ marginTop: "8px" }} style={{ marginTop: "8px" }}
onClick={() => { onClick={() => deleteTable(tableData.id)}
Toast.success(`Table deleted!`);
deleteTable(tableData.id);
}}
> >
Delete table {t("delete")}
</Button> </Button>
</div> </div>
} }
@ -196,37 +195,31 @@ export default function Table(props) {
<hr /> <hr />
{e.primary && ( {e.primary && (
<Tag color="blue" className="me-2 my-2"> <Tag color="blue" className="me-2 my-2">
Primary {t("primary")}
</Tag> </Tag>
)} )}
{e.unique && ( {e.unique && (
<Tag color="amber" className="me-2 my-2"> <Tag color="amber" className="me-2 my-2">
Unique {t("unique")}
</Tag> </Tag>
)} )}
{e.notNull && ( {e.notNull && (
<Tag color="purple" className="me-2 my-2"> <Tag color="purple" className="me-2 my-2">
Not null {t("not_null")}
</Tag> </Tag>
)} )}
{e.increment && ( {e.increment && (
<Tag color="green" className="me-2 my-2"> <Tag color="green" className="me-2 my-2">
Increment {t("autoincrement")}
</Tag> </Tag>
)} )}
<p> <p>
<strong>Default: </strong> <strong>{t("default_value")}: </strong>
{e.default === "" ? "Not set" : e.default} {e.default === "" ? t("not_set") : e.default}
</p> </p>
<p> <p>
<strong>Comment: </strong> <strong>{t("comment")}: </strong>
{e.comment === "" ? ( {e.comment === "" ? t("not_set") : e.comment}
"No comment"
) : (
<div className="max-w-[260px] break-words">
{e.comment}
</div>
)}
</p> </p>
</div> </div>
} }
@ -242,7 +235,7 @@ export default function Table(props) {
</div> </div>
</foreignObject> </foreignObject>
<SideSheet <SideSheet
title="Edit table" title={t("edit")}
size="small" size="small"
visible={ visible={
selectedElement.element === ObjectType.TABLE && selectedElement.element === ObjectType.TABLE &&

View File

@ -20,7 +20,6 @@ import {
Spin, Spin,
Toast, Toast,
Popconfirm, Popconfirm,
Tag,
} from "@douyinfe/semi-ui"; } from "@douyinfe/semi-ui";
import { toPng, toJpeg, toSvg } from "html-to-image"; import { toPng, toJpeg, toSvg } from "html-to-image";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
@ -62,6 +61,7 @@ import { IconAddArea, IconAddNote, IconAddTable } from "../../icons";
import LayoutDropdown from "./LayoutDropdown"; import LayoutDropdown from "./LayoutDropdown";
import Sidesheet from "./SideSheet/Sidesheet"; import Sidesheet from "./SideSheet/Sidesheet";
import Modal from "./Modal/Modal"; import Modal from "./Modal/Modal";
import { useTranslation } from "react-i18next";
export default function ControlPanel({ export default function ControlPanel({
diagramId, diagramId,
@ -100,6 +100,7 @@ export default function ControlPanel({
const { undoStack, redoStack, setUndoStack, setRedoStack } = useUndoRedo(); const { undoStack, redoStack, setUndoStack, setRedoStack } = useUndoRedo();
const { selectedElement, setSelectedElement } = useSelect(); const { selectedElement, setSelectedElement } = useSelect();
const { transform, setTransform } = useTransform(); const { transform, setTransform } = useTransform();
const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const invertLayout = (component) => const invertLayout = (component) =>
@ -460,16 +461,12 @@ export default function ControlPanel({
setTransform((prev) => ({ ...prev, zoom: prev.zoom / 1.2 })); setTransform((prev) => ({ ...prev, zoom: prev.zoom / 1.2 }));
const viewStrictMode = () => { const viewStrictMode = () => {
setSettings((prev) => ({ ...prev, strictMode: !prev.strictMode })); setSettings((prev) => ({ ...prev, strictMode: !prev.strictMode }));
Toast.success(`Stict mode is ${settings.strictMode ? "on" : "off"}.`);
}; };
const viewFieldSummary = () => { const viewFieldSummary = () => {
setSettings((prev) => ({ setSettings((prev) => ({
...prev, ...prev,
showFieldSummary: !prev.showFieldSummary, showFieldSummary: !prev.showFieldSummary,
})); }));
Toast.success(
`Field summary is ${settings.showFieldSummary ? "off" : "on"}.`,
);
}; };
const copyAsImage = () => { const copyAsImage = () => {
toPng(document.getElementById("canvas")).then(function (dataUrl) { toPng(document.getElementById("canvas")).then(function (dataUrl) {
@ -477,10 +474,10 @@ export default function ControlPanel({
navigator.clipboard navigator.clipboard
.write([new ClipboardItem({ "image/png": blob })]) .write([new ClipboardItem({ "image/png": blob })])
.then(() => { .then(() => {
Toast.success("Copied to clipboard."); Toast.success(t("copied_to_clipboard"));
}) })
.catch(() => { .catch(() => {
Toast.error("Could not copy to clipboard."); Toast.error(t("oops_smth_went_wrong"));
}); });
}); });
}; };
@ -607,23 +604,17 @@ export default function ControlPanel({
case ObjectType.TABLE: case ObjectType.TABLE:
navigator.clipboard navigator.clipboard
.writeText(JSON.stringify({ ...tables[selectedElement.id] })) .writeText(JSON.stringify({ ...tables[selectedElement.id] }))
.catch(() => { .catch(() => Toast.error(t("oops_smth_went_wrong")));
Toast.error("Could not copy");
});
break; break;
case ObjectType.NOTE: case ObjectType.NOTE:
navigator.clipboard navigator.clipboard
.writeText(JSON.stringify({ ...notes[selectedElement.id] })) .writeText(JSON.stringify({ ...notes[selectedElement.id] }))
.catch(() => { .catch(() => Toast.error(t("oops_smth_went_wrong")));
Toast.error("Could not copy");
});
break; break;
case ObjectType.AREA: case ObjectType.AREA:
navigator.clipboard navigator.clipboard
.writeText(JSON.stringify({ ...areas[selectedElement.id] })) .writeText(JSON.stringify({ ...areas[selectedElement.id] }))
.catch(() => { .catch(() => Toast.error(t("oops_smth_went_wrong")));
Toast.error("Could not copy");
});
break; break;
default: default:
break; break;
@ -671,29 +662,29 @@ export default function ControlPanel({
const saveDiagramAs = () => setModal(MODAL.SAVEAS); const saveDiagramAs = () => setModal(MODAL.SAVEAS);
const menu = { const menu = {
File: { file: {
New: { new: {
function: () => setModal(MODAL.NEW), function: () => setModal(MODAL.NEW),
}, },
"New window": { new_window: {
function: () => { function: () => {
const newWindow = window.open("/editor", "_blank"); const newWindow = window.open("/editor", "_blank");
newWindow.name = window.name; newWindow.name = window.name;
}, },
}, },
Open: { open: {
function: open, function: open,
shortcut: "Ctrl+O", shortcut: "Ctrl+O",
}, },
Save: { save: {
function: save, function: save,
shortcut: "Ctrl+S", shortcut: "Ctrl+S",
}, },
"Save as": { save_as: {
function: saveDiagramAs, function: saveDiagramAs,
shortcut: "Ctrl+Shift+S", shortcut: "Ctrl+Shift+S",
}, },
"Save as template": { save_as_template: {
function: () => { function: () => {
db.templates db.templates
.add({ .add({
@ -706,21 +697,20 @@ export default function ControlPanel({
custom: 1, custom: 1,
}) })
.then(() => { .then(() => {
Toast.success("Template saved!"); Toast.success(t("template_saved"));
}); });
}, },
}, },
Rename: { rename: {
function: () => { function: () => {
setModal(MODAL.RENAME); setModal(MODAL.RENAME);
setPrevTitle(title); setPrevTitle(title);
}, },
}, },
"Delete diagram": { delete_diagram: {
warning: { warning: {
title: "Delete diagram", title: t("delete_diagram"),
message: message: t("are_you_sure_delete_diagram"),
"Are you sure you want to delete this diagram? This operation is irreversible.",
}, },
function: async () => { function: async () => {
await db.diagrams await db.diagrams
@ -736,17 +726,17 @@ export default function ControlPanel({
setUndoStack([]); setUndoStack([]);
setRedoStack([]); setRedoStack([]);
}) })
.catch(() => Toast.error("Oops! Something went wrong.")); .catch(() => Toast.error(t("oops_smth_went_wrong")));
}, },
}, },
"Import diagram": { import_diagram: {
function: fileImport, function: fileImport,
shortcut: "Ctrl+I", shortcut: "Ctrl+I",
}, },
"Import from source": { import_from_source: {
function: () => setModal(MODAL.IMPORT_SRC), function: () => setModal(MODAL.IMPORT_SRC),
}, },
"Export as": { export_as: {
children: [ children: [
{ {
PNG: () => { PNG: () => {
@ -856,7 +846,7 @@ export default function ControlPanel({
], ],
function: () => {}, function: () => {},
}, },
"Export source": { export_source: {
children: [ children: [
{ {
MySQL: () => { MySQL: () => {
@ -936,23 +926,27 @@ export default function ControlPanel({
], ],
function: () => {}, function: () => {},
}, },
Exit: { exit: {
function: () => { function: () => {
save(); save();
if (saveState === State.SAVED) navigate("/"); if (saveState === State.SAVED) navigate("/");
}, },
}, },
}, },
Edit: { edit: {
Undo: { undo: {
function: undo, function: undo,
shortcut: "Ctrl+Z", shortcut: "Ctrl+Z",
}, },
Redo: { redo: {
function: redo, function: redo,
shortcut: "Ctrl+Y", shortcut: "Ctrl+Y",
}, },
Clear: { clear: {
warning: {
title: t("clear"),
message: t("are_you_sure_clear"),
},
function: () => { function: () => {
setTables([]); setTables([]);
setRelationships([]); setRelationships([]);
@ -962,57 +956,73 @@ export default function ControlPanel({
setRedoStack([]); setRedoStack([]);
}, },
}, },
Edit: { edit: {
function: edit, function: edit,
shortcut: "Ctrl+E", shortcut: "Ctrl+E",
}, },
Cut: { cut: {
function: cut, function: cut,
shortcut: "Ctrl+X", shortcut: "Ctrl+X",
}, },
Copy: { copy: {
function: copy, function: copy,
shortcut: "Ctrl+C", shortcut: "Ctrl+C",
}, },
Paste: { paste: {
function: paste, function: paste,
shortcut: "Ctrl+V", shortcut: "Ctrl+V",
}, },
Duplicate: { duplicate: {
function: duplicate, function: duplicate,
shortcut: "Ctrl+D", shortcut: "Ctrl+D",
}, },
Delete: { delete: {
function: del, function: del,
shortcut: "Del", shortcut: "Del",
}, },
"Copy as image": { copy_as_image: {
function: copyAsImage, function: copyAsImage,
shortcut: "Ctrl+Alt+C", shortcut: "Ctrl+Alt+C",
}, },
}, },
View: { view: {
Header: { header: {
state: layout.header ? "on" : "off", state: layout.header ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: () => function: () =>
setLayout((prev) => ({ ...prev, header: !prev.header })), setLayout((prev) => ({ ...prev, header: !prev.header })),
}, },
Sidebar: { sidebar: {
state: layout.sidebar ? "on" : "off", state: layout.sidebar ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: () => function: () =>
setLayout((prev) => ({ ...prev, sidebar: !prev.sidebar })), setLayout((prev) => ({ ...prev, sidebar: !prev.sidebar })),
}, },
Issues: { issues: {
state: layout.issues ? "on" : "off", state: layout.issues ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: () => function: () =>
setLayout((prev) => ({ ...prev, issues: !prev.issues })), setLayout((prev) => ({ ...prev, issues: !prev.issues })),
}, },
"Strict mode": { strict_mode: {
state: settings.strictMode ? "off" : "on", state: settings.strictMode ? (
<i className="bi bi-toggle-off" />
) : (
<i className="bi bi-toggle-on" />
),
function: viewStrictMode, function: viewStrictMode,
shortcut: "Ctrl+Shift+M", shortcut: "Ctrl+Shift+M",
}, },
"Presentation mode": { presentation_mode: {
function: () => { function: () => {
setLayout((prev) => ({ setLayout((prev) => ({
...prev, ...prev,
@ -1023,32 +1033,44 @@ export default function ControlPanel({
enterFullscreen(); enterFullscreen();
}, },
}, },
"Field details": { field_details: {
state: settings.showFieldSummary ? "on" : "off", state: settings.showFieldSummary ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: viewFieldSummary, function: viewFieldSummary,
shortcut: "Ctrl+Shift+F", shortcut: "Ctrl+Shift+F",
}, },
"Reset view": { reset_view: {
function: resetView, function: resetView,
shortcut: "Ctrl+R", shortcut: "Ctrl+R",
}, },
"Show grid": { show_grid: {
state: settings.showGrid ? "on" : "off", state: settings.showGrid ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: viewGrid, function: viewGrid,
shortcut: "Ctrl+Shift+G", shortcut: "Ctrl+Shift+G",
}, },
"Show cardinality": { show_cardinality: {
state: settings.showCardinality ? "on" : "off", state: settings.showCardinality ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: () => function: () =>
setSettings((prev) => ({ setSettings((prev) => ({
...prev, ...prev,
showCardinality: !prev.showCardinality, showCardinality: !prev.showCardinality,
})), })),
}, },
Theme: { theme: {
children: [ children: [
{ {
Light: () => { light: () => {
const body = document.body; const body = document.body;
if (body.hasAttribute("theme-mode")) { if (body.hasAttribute("theme-mode")) {
body.setAttribute("theme-mode", "light"); body.setAttribute("theme-mode", "light");
@ -1058,7 +1080,7 @@ export default function ControlPanel({
}, },
}, },
{ {
Dark: () => { dark: () => {
const body = document.body; const body = document.body;
if (body.hasAttribute("theme-mode")) { if (body.hasAttribute("theme-mode")) {
body.setAttribute("theme-mode", "dark"); body.setAttribute("theme-mode", "dark");
@ -1070,71 +1092,75 @@ export default function ControlPanel({
], ],
function: () => {}, function: () => {},
}, },
"Zoom in": { zoom_in: {
function: zoomIn, function: zoomIn,
shortcut: "Ctrl+Up/Wheel", shortcut: "Ctrl+Up/Wheel",
}, },
"Zoom out": { zoom_out: {
function: zoomOut, function: zoomOut,
shortcut: "Ctrl+Down/Wheel", shortcut: "Ctrl+Down/Wheel",
}, },
Fullscreen: { fullscreen: {
function: enterFullscreen, function: enterFullscreen,
}, },
}, },
Settings: { settings: {
"Show timeline": { show_timeline: {
function: () => setSidesheet(SIDESHEET.TIMELINE), function: () => setSidesheet(SIDESHEET.TIMELINE),
}, },
Autosave: { autosave: {
state: settings.autosave ? "on" : "off", state: settings.autosave ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: () => function: () =>
setSettings((prev) => { setSettings((prev) => ({ ...prev, autosave: !prev.autosave })),
Toast.success(`Autosave is ${settings.autosave ? "off" : "on"}`);
return { ...prev, autosave: !prev.autosave };
}),
}, },
Panning: { panning: {
state: settings.panning ? "on" : "off", state: settings.panning ? (
<i className="bi bi-toggle-on" />
) : (
<i className="bi bi-toggle-off" />
),
function: () => function: () =>
setSettings((prev) => { setSettings((prev) => ({ ...prev, panning: !prev.panning })),
Toast.success(`Panning is ${settings.panning ? "off" : "on"}`);
return { ...prev, panning: !prev.panning };
}),
}, },
"Table width": { table_width: {
function: () => setModal(MODAL.TABLE_WIDTH), function: () => setModal(MODAL.TABLE_WIDTH),
}, },
"Flush storage": { language: {
function: () => setModal(MODAL.LANGUAGE),
},
flush_storage: {
warning: { warning: {
title: "Flush storage", title: t("flush_storage"),
message: message: t("are_you_sure_flush_storage"),
"Are you sure you want to flush the storage? This will irreversibly delete all your diagrams and custom templates.",
}, },
function: async () => { function: async () => {
db.delete() db.delete()
.then(() => { .then(() => {
Toast.success("Storage flushed"); Toast.success(t("storage_flushed"));
window.location.reload(false); window.location.reload(false);
}) })
.catch(() => { .catch(() => {
Toast.error("Oops! Something went wrong."); Toast.error(t("oops_smth_went_wrong"));
}); });
}, },
}, },
}, },
Help: { help: {
Shortcuts: { shortcuts: {
function: () => window.open("/shortcuts", "_blank"), function: () => window.open("/shortcuts", "_blank"),
shortcut: "Ctrl+H", shortcut: "Ctrl+H",
}, },
"Ask us on discord": { ask_on_discord: {
function: () => window.open("https://discord.gg/BrjZgNrmR6", "_blank"), function: () => window.open("https://discord.gg/BrjZgNrmR6", "_blank"),
}, },
"Report a bug": { report_bug: {
function: () => window.open("/bug-report", "_blank"), function: () => window.open("/bug-report", "_blank"),
}, },
"Give feedback": { feedback: {
function: () => window.open("/survey", "_blank"), function: () => window.open("/survey", "_blank"),
}, },
}, },
@ -1207,7 +1233,7 @@ export default function ControlPanel({
onClick={fitWindow} onClick={fitWindow}
style={{ display: "flex", justifyContent: "space-between" }} style={{ display: "flex", justifyContent: "space-between" }}
> >
<div>Fit window / Reset</div> <div>{t("fit_window_reset")}</div>
<div className="text-gray-400">Ctrl+Alt+W</div> <div className="text-gray-400">Ctrl+Alt+W</div>
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Divider /> <Dropdown.Divider />
@ -1225,8 +1251,8 @@ export default function ControlPanel({
<Dropdown.Item> <Dropdown.Item>
<InputNumber <InputNumber
field="zoom" field="zoom"
label="Custom zoom" label={t("zoom")}
placeholder="Zoom" placeholder={t("zoom")}
suffix={<div className="p-1">%</div>} suffix={<div className="p-1">%</div>}
onChange={(v) => onChange={(v) =>
setTransform((prev) => ({ setTransform((prev) => ({
@ -1249,7 +1275,7 @@ export default function ControlPanel({
</div> </div>
</div> </div>
</Dropdown> </Dropdown>
<Tooltip content="Zoom in" position="bottom"> <Tooltip content={t("zoom_in")} position="bottom">
<button <button
className="py-1 px-2 hover-2 rounded text-lg" className="py-1 px-2 hover-2 rounded text-lg"
onClick={() => onClick={() =>
@ -1259,7 +1285,7 @@ export default function ControlPanel({
<i className="fa-solid fa-magnifying-glass-plus" /> <i className="fa-solid fa-magnifying-glass-plus" />
</button> </button>
</Tooltip> </Tooltip>
<Tooltip content="Zoom out" position="bottom"> <Tooltip content={t("zoom_out")} position="bottom">
<button <button
className="py-1 px-2 hover-2 rounded text-lg" className="py-1 px-2 hover-2 rounded text-lg"
onClick={() => onClick={() =>
@ -1270,7 +1296,7 @@ export default function ControlPanel({
</button> </button>
</Tooltip> </Tooltip>
<Divider layout="vertical" margin="8px" /> <Divider layout="vertical" margin="8px" />
<Tooltip content="Undo" position="bottom"> <Tooltip content={t("undo")} position="bottom">
<button <button
className="py-1 px-2 hover-2 rounded flex items-center" className="py-1 px-2 hover-2 rounded flex items-center"
onClick={undo} onClick={undo}
@ -1281,7 +1307,7 @@ export default function ControlPanel({
/> />
</button> </button>
</Tooltip> </Tooltip>
<Tooltip content="Redo" position="bottom"> <Tooltip content={t("redo")} position="bottom">
<button <button
className="py-1 px-2 hover-2 rounded flex items-center" className="py-1 px-2 hover-2 rounded flex items-center"
onClick={redo} onClick={redo}
@ -1293,7 +1319,7 @@ export default function ControlPanel({
</button> </button>
</Tooltip> </Tooltip>
<Divider layout="vertical" margin="8px" /> <Divider layout="vertical" margin="8px" />
<Tooltip content="Add table" position="bottom"> <Tooltip content={t("add_table")} position="bottom">
<button <button
className="flex items-center py-1 px-2 hover-2 rounded" className="flex items-center py-1 px-2 hover-2 rounded"
onClick={() => addTable()} onClick={() => addTable()}
@ -1301,7 +1327,7 @@ export default function ControlPanel({
<IconAddTable /> <IconAddTable />
</button> </button>
</Tooltip> </Tooltip>
<Tooltip content="Add subject area" position="bottom"> <Tooltip content={t("add_area")} position="bottom">
<button <button
className="py-1 px-2 hover-2 rounded flex items-center" className="py-1 px-2 hover-2 rounded flex items-center"
onClick={() => addArea()} onClick={() => addArea()}
@ -1309,7 +1335,7 @@ export default function ControlPanel({
<IconAddArea /> <IconAddArea />
</button> </button>
</Tooltip> </Tooltip>
<Tooltip content="Add note" position="bottom"> <Tooltip content={t("add_note")} position="bottom">
<button <button
className="py-1 px-2 hover-2 rounded flex items-center" className="py-1 px-2 hover-2 rounded flex items-center"
onClick={() => addNote()} onClick={() => addNote()}
@ -1318,7 +1344,7 @@ export default function ControlPanel({
</button> </button>
</Tooltip> </Tooltip>
<Divider layout="vertical" margin="8px" /> <Divider layout="vertical" margin="8px" />
<Tooltip content="Save" position="bottom"> <Tooltip content={t("save")} position="bottom">
<button <button
className="py-1 px-2 hover-2 rounded flex items-center" className="py-1 px-2 hover-2 rounded flex items-center"
onClick={save} onClick={save}
@ -1326,7 +1352,7 @@ export default function ControlPanel({
<IconSaveStroked size="extra-large" /> <IconSaveStroked size="extra-large" />
</button> </button>
</Tooltip> </Tooltip>
<Tooltip content="To-do" position="bottom"> <Tooltip content={t("to_do")} position="bottom">
<button <button
className="py-1 px-2 hover-2 rounded text-xl -mt-0.5" className="py-1 px-2 hover-2 rounded text-xl -mt-0.5"
onClick={() => setSidesheet(SIDESHEET.TODO)} onClick={() => setSidesheet(SIDESHEET.TODO)}
@ -1335,16 +1361,16 @@ export default function ControlPanel({
</button> </button>
</Tooltip> </Tooltip>
<Divider layout="vertical" margin="8px" /> <Divider layout="vertical" margin="8px" />
<Tooltip content="Change theme" position="bottom"> <Tooltip content={t("theme")} position="bottom">
<button <button
className="py-1 px-2 hover-2 rounded text-xl -mt-0.5" className="py-1 px-2 hover-2 rounded text-xl -mt-0.5"
onClick={() => { onClick={() => {
const body = document.body; const body = document.body;
if (body.hasAttribute("theme-mode")) { if (body.hasAttribute("theme-mode")) {
if (body.getAttribute("theme-mode") === "light") { if (body.getAttribute("theme-mode") === "light") {
menu["View"]["Theme"].children[1]["Dark"](); menu["view"]["theme"].children[1]["dark"]();
} else { } else {
menu["View"]["Theme"].children[0]["Light"](); menu["view"]["theme"].children[0]["light"]();
} }
} }
}} }}
@ -1366,15 +1392,15 @@ export default function ControlPanel({
function getState() { function getState() {
switch (saveState) { switch (saveState) {
case State.NONE: case State.NONE:
return "No changes"; return t("no_changes");
case State.LOADING: case State.LOADING:
return "Loading . . ."; return t("loading");
case State.SAVED: case State.SAVED:
return `Last saved ${lastSaved}`; return `${t("last_saved")} ${lastSaved}`;
case State.SAVING: case State.SAVING:
return "Saving . . ."; return t("saving");
case State.ERROR: case State.ERROR:
return "Failed to save"; return t("failed_to_save");
default: default:
return ""; return "";
} }
@ -1429,7 +1455,7 @@ export default function ControlPanel({
key={i} key={i}
onClick={Object.values(e)[0]} onClick={Object.values(e)[0]}
> >
{Object.keys(e)[0]} {t(Object.keys(e)[0])}
</Dropdown.Item> </Dropdown.Item>
), ),
)} )}
@ -1444,7 +1470,7 @@ export default function ControlPanel({
}} }}
onClick={menu[category][item].function} onClick={menu[category][item].function}
> >
{item} {t(item)}
<IconChevronRight /> <IconChevronRight />
</Dropdown.Item> </Dropdown.Item>
</Dropdown> </Dropdown>
@ -1458,8 +1484,10 @@ export default function ControlPanel({
content={menu[category][item].warning.message} content={menu[category][item].warning.message}
onConfirm={menu[category][item].function} onConfirm={menu[category][item].function}
position="right" position="right"
okText={t("confirm")}
cancelText={t("cancel")}
> >
<Dropdown.Item>{item}</Dropdown.Item> <Dropdown.Item>{t(item)}</Dropdown.Item>
</Popconfirm> </Popconfirm>
); );
} }
@ -1476,18 +1504,15 @@ export default function ControlPanel({
} }
> >
<div className="w-full flex items-center justify-between"> <div className="w-full flex items-center justify-between">
<div>{item}</div> <div>{t(item)}</div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{menu[category][item].shortcut && ( {menu[category][item].shortcut && (
<div className="text-gray-400"> <div className="text-gray-400">
{menu[category][item].shortcut} {menu[category][item].shortcut}
</div> </div>
)} )}
{menu[category][item].state && ( {menu[category][item].state &&
<Tag color="blue"> menu[category][item].state}
{menu[category][item].state}
</Tag>
)}
</div> </div>
</div> </div>
</Dropdown.Item> </Dropdown.Item>
@ -1496,7 +1521,9 @@ export default function ControlPanel({
</Dropdown.Menu> </Dropdown.Menu>
} }
> >
<div className="px-3 py-1 hover-2 rounded">{category}</div> <div className="px-3 py-1 hover-2 rounded">
{t(category)}
</div>
</Dropdown> </Dropdown>
))} ))}
</div> </div>

View File

@ -6,9 +6,12 @@ import {
import { Dropdown } from "@douyinfe/semi-ui"; import { Dropdown } from "@douyinfe/semi-ui";
import { useLayout } from "../../hooks"; import { useLayout } from "../../hooks";
import { enterFullscreen, exitFullscreen } from "../../utils/fullscreen"; import { enterFullscreen, exitFullscreen } from "../../utils/fullscreen";
import { useTranslation } from "react-i18next";
export default function LayoutDropdown() { export default function LayoutDropdown() {
const { layout, setLayout } = useLayout(); const { layout, setLayout } = useLayout();
const { t } = useTranslation();
const invertLayout = (component) => const invertLayout = (component) =>
setLayout((prev) => ({ ...prev, [component]: !prev[component] })); setLayout((prev) => ({ ...prev, [component]: !prev[component] }));
@ -24,7 +27,7 @@ export default function LayoutDropdown() {
} }
onClick={() => invertLayout("header")} onClick={() => invertLayout("header")}
> >
Header {t("header")}
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item <Dropdown.Item
icon={ icon={
@ -32,7 +35,7 @@ export default function LayoutDropdown() {
} }
onClick={() => invertLayout("sidebar")} onClick={() => invertLayout("sidebar")}
> >
Sidebar {t("sidebar")}
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Item <Dropdown.Item
icon={ icon={
@ -40,7 +43,7 @@ export default function LayoutDropdown() {
} }
onClick={() => invertLayout("issues")} onClick={() => invertLayout("issues")}
> >
Issues {t("issues")}
</Dropdown.Item> </Dropdown.Item>
<Dropdown.Divider /> <Dropdown.Divider />
<Dropdown.Item <Dropdown.Item
@ -54,7 +57,7 @@ export default function LayoutDropdown() {
invertLayout("fullscreen"); invertLayout("fullscreen");
}} }}
> >
Fullscreen {t("fullscreen")}
</Dropdown.Item> </Dropdown.Item>
</Dropdown.Menu> </Dropdown.Menu>
} }

View File

@ -5,11 +5,13 @@ import {
import { Upload, Banner } from "@douyinfe/semi-ui"; import { Upload, Banner } from "@douyinfe/semi-ui";
import { STATUS } from "../../../data/constants"; import { STATUS } from "../../../data/constants";
import { useAreas, useNotes, useTables } from "../../../hooks"; import { useAreas, useNotes, useTables } from "../../../hooks";
import { useTranslation } from "react-i18next";
export default function ImportDiagram({ setImportData, error, setError }) { export default function ImportDiagram({ setImportData, error, setError }) {
const { areas } = useAreas(); const { areas } = useAreas();
const { notes } = useNotes(); const { notes } = useNotes();
const { tables, relationships } = useTables(); const { tables, relationships } = useTables();
const { t } = useTranslation();
const diagramIsEmpty = () => { const diagramIsEmpty = () => {
return ( return (
@ -84,8 +86,8 @@ export default function ImportDiagram({ setImportData, error, setError }) {
}; };
}} }}
draggable={true} draggable={true}
dragMainText="Drag and drop the file here or click to upload." dragMainText={t("drag_and_drop_files")}
dragSubText="Support json and ddb" dragSubText={t("support_json_and_ddb")}
accept="application/json,.ddb" accept="application/json,.ddb"
onRemove={() => onRemove={() =>
setError({ setError({

View File

@ -1,5 +1,6 @@
import { Upload, Checkbox, Banner } from "@douyinfe/semi-ui"; import { Upload, Checkbox, Banner } from "@douyinfe/semi-ui";
import { STATUS } from "../../../data/constants"; import { STATUS } from "../../../data/constants";
import { useTranslation } from "react-i18next";
export default function ImportSource({ export default function ImportSource({
importData, importData,
@ -7,6 +8,8 @@ export default function ImportSource({
error, error,
setError, setError,
}) { }) {
const { t } = useTranslation();
return ( return (
<div> <div>
<Upload <Upload
@ -30,8 +33,8 @@ export default function ImportSource({
}; };
}} }}
draggable={true} draggable={true}
dragMainText="Drag and drop the file here or click to upload." dragMainText={t("drag_and_drop_files")}
dragSubText="Upload an sql file to autogenerate your tables and columns." dragSubText={t("upload_sql_to_generate_diagrams")}
accept=".sql" accept=".sql"
onRemove={() => { onRemove={() => {
setError({ setError({
@ -50,7 +53,7 @@ export default function ImportSource({
/> />
<div> <div>
<div className="text-xs mb-3 mt-1 opacity-80"> <div className="text-xs mb-3 mt-1 opacity-80">
* For the time being loading only MySQL scripts is supported. {t("only_mysql_supported")}
</div> </div>
<Checkbox <Checkbox
aria-label="overwrite checkbox" aria-label="overwrite checkbox"
@ -63,7 +66,7 @@ export default function ImportSource({
})) }))
} }
> >
Overwrite existing diagram {t("overwrite_existing_diagram")}
</Checkbox> </Checkbox>
<div className="mt-2"> <div className="mt-2">
{error.type === STATUS.ERROR ? ( {error.type === STATUS.ERROR ? (

View File

@ -0,0 +1,30 @@
import { useTranslation } from "react-i18next";
import { useSettings } from "../../../hooks";
import { languages } from "../../../i18n/i18n";
export default function Language() {
const { settings } = useSettings();
const { i18n } = useTranslation();
return (
<div className="grid grid-cols-3 gap-4">
{languages.map((l) => (
<button
key={l.code}
onClick={() => i18n.changeLanguage(l.code)}
className={`space-y-1 py-3 px-4 rounded-md border-2 ${
settings.mode === "dark"
? "bg-zinc-700 hover:bg-zinc-600"
: "bg-zinc-100 hover:bg-zinc-200"
} ${i18n.resolvedLanguage === l.code ? "border-zinc-400" : "border-transparent"}`}
>
<div className="flex justify-between items-center">
<div className="font-semibold">{l.native_name}</div>
<div className="opacity-60">{l.code}</div>
</div>
<div className="text-start">{l.name}</div>
</button>
))}
</div>
);
}

View File

@ -27,11 +27,13 @@ import New from "./New";
import ImportDiagram from "./ImportDiagram"; import ImportDiagram from "./ImportDiagram";
import ImportSource from "./ImportSource"; import ImportSource from "./ImportSource";
import SetTableWidth from "./SetTableWidth"; import SetTableWidth from "./SetTableWidth";
import Language from "./Language";
import CodeMirror from "@uiw/react-codemirror"; import CodeMirror from "@uiw/react-codemirror";
import { sql } from "@codemirror/lang-sql"; import { sql } from "@codemirror/lang-sql";
import { vscodeDark } from "@uiw/codemirror-theme-vscode"; import { vscodeDark } from "@uiw/codemirror-theme-vscode";
import { json } from "@codemirror/lang-json"; import { json } from "@codemirror/lang-json";
import { githubLight } from "@uiw/codemirror-theme-github"; import { githubLight } from "@uiw/codemirror-theme-github";
import { useTranslation } from "react-i18next";
const languageExtension = { const languageExtension = {
sql: [sql()], sql: [sql()],
@ -49,6 +51,7 @@ export default function Modal({
exportData, exportData,
setExportData, setExportData,
}) { }) {
const { t } = useTranslation();
const { setTables, setRelationships } = useTables(); const { setTables, setRelationships } = useTables();
const { setNotes } = useNotes(); const { setNotes } = useNotes();
const { setAreas } = useAreas(); const { setAreas } = useAreas();
@ -239,7 +242,7 @@ export default function Modal({
case MODAL.SAVEAS: case MODAL.SAVEAS:
return ( return (
<Input <Input
placeholder="Diagram name" placeholder={t("name")}
value={saveAsTitle} value={saveAsTitle}
onChange={(v) => setSaveAsTitle(v)} onChange={(v) => setSaveAsTitle(v)}
/> />
@ -261,10 +264,10 @@ export default function Modal({
theme={settings.mode === "dark" ? vscodeDark : githubLight} theme={settings.mode === "dark" ? vscodeDark : githubLight}
/> />
)} )}
<div className="text-sm font-semibold mt-2">Filename:</div> <div className="text-sm font-semibold mt-2">{t("filename")}:</div>
<Input <Input
value={exportData.filename} value={exportData.filename}
placeholder="Filename" placeholder={t("filename")}
suffix={<div className="p-2">{`.${exportData.extension}`}</div>} suffix={<div className="p-2">{`.${exportData.extension}`}</div>}
onChange={(value) => onChange={(value) =>
setExportData((prev) => ({ ...prev, filename: value })) setExportData((prev) => ({ ...prev, filename: value }))
@ -276,12 +279,14 @@ export default function Modal({
} else { } else {
return ( return (
<div className="text-center my-3"> <div className="text-center my-3">
<Spin tip="Loading..." size="large" /> <Spin tip={t("loading")} size="large" />
</div> </div>
); );
} }
case MODAL.TABLE_WIDTH: case MODAL.TABLE_WIDTH:
return <SetTableWidth />; return <SetTableWidth />;
case MODAL.LANGUAGE:
return <Language />;
default: default:
return <></>; return <></>;
} }
@ -326,8 +331,9 @@ export default function Modal({
(modal === MODAL.SAVEAS && saveAsTitle === "") || (modal === MODAL.SAVEAS && saveAsTitle === "") ||
(modal === MODAL.IMPORT_SRC && importSource.src === ""), (modal === MODAL.IMPORT_SRC && importSource.src === ""),
}} }}
cancelText="Cancel" cancelText={t("cancel")}
width={modal === MODAL.NEW ? 740 : 600} width={modal === MODAL.NEW ? 740 : 600}
bodyStyle={{ maxHeight: window.innerHeight - 280, overflow: "auto" }}
> >
{getModalBody()} {getModalBody()}
</SemiUIModal> </SemiUIModal>

View File

@ -2,13 +2,15 @@ import { db } from "../../../data/db";
import { useSettings } from "../../../hooks"; import { useSettings } from "../../../hooks";
import { useLiveQuery } from "dexie-react-hooks"; import { useLiveQuery } from "dexie-react-hooks";
import Thumbnail from "../../Thumbnail"; import Thumbnail from "../../Thumbnail";
import { useTranslation } from "react-i18next";
export default function New({ selectedTemplateId, setSelectedTemplateId }) { export default function New({ selectedTemplateId, setSelectedTemplateId }) {
const { settings } = useSettings(); const { settings } = useSettings();
const { t } = useTranslation();
const templates = useLiveQuery(() => db.templates.toArray()); const templates = useLiveQuery(() => db.templates.toArray());
return ( return (
<div className="h-[360px] grid grid-cols-3 gap-2 overflow-auto px-1"> <div className="grid grid-cols-3 gap-2 overflow-auto px-1">
<div onClick={() => setSelectedTemplateId(0)}> <div onClick={() => setSelectedTemplateId(0)}>
<div <div
className={`rounded-md h-[180px] border-2 hover:border-dashed ${ className={`rounded-md h-[180px] border-2 hover:border-dashed ${
@ -17,7 +19,7 @@ export default function New({ selectedTemplateId, setSelectedTemplateId }) {
> >
<Thumbnail i={0} diagram={{}} zoom={0.24} theme={settings.mode} /> <Thumbnail i={0} diagram={{}} zoom={0.24} theme={settings.mode} />
</div> </div>
<div className="text-center mt-1">Blank</div> <div className="text-center mt-1">{t("blank")}</div>
</div> </div>
{templates?.map((temp, i) => ( {templates?.map((temp, i) => (
<div key={i} onClick={() => setSelectedTemplateId(temp.id)}> <div key={i} onClick={() => setSelectedTemplateId(temp.id)}>

View File

@ -1,9 +1,11 @@
import { db } from "../../../data/db"; import { db } from "../../../data/db";
import { Banner } from "@douyinfe/semi-ui"; import { Banner } from "@douyinfe/semi-ui";
import { useLiveQuery } from "dexie-react-hooks"; import { useLiveQuery } from "dexie-react-hooks";
import { useTranslation } from "react-i18next";
export default function Open({ selectedDiagramId, setSelectedDiagramId }) { export default function Open({ selectedDiagramId, setSelectedDiagramId }) {
const diagrams = useLiveQuery(() => db.diagrams.toArray()); const diagrams = useLiveQuery(() => db.diagrams.toArray());
const { t } = useTranslation();
const getDiagramSize = (d) => { const getDiagramSize = (d) => {
const size = JSON.stringify(d).length; const size = JSON.stringify(d).length;
@ -32,9 +34,9 @@ export default function Open({ selectedDiagramId, setSelectedDiagramId }) {
<table className="w-full text-left border-separate border-spacing-x-0"> <table className="w-full text-left border-separate border-spacing-x-0">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>{t("name")}</th>
<th>Last Modified</th> <th>{t("last_modified")}</th>
<th>Size</th> <th>{t("size")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@ -1,9 +1,12 @@
import { Input } from "@douyinfe/semi-ui"; import { Input } from "@douyinfe/semi-ui";
import { useTranslation } from "react-i18next";
export default function Rename({ title, setTitle }) { export default function Rename({ title, setTitle }) {
const { t } = useTranslation();
return ( return (
<Input <Input
placeholder="Diagram name" placeholder={t("name")}
value={title} value={title}
onChange={(v) => setTitle(v)} onChange={(v) => setTitle(v)}
/> />

View File

@ -6,8 +6,10 @@ import timeLineDark from "../../../assets/process_dark.png";
import todo from "../../../assets/calendar.png"; import todo from "../../../assets/calendar.png";
import Timeline from "./Timeline"; import Timeline from "./Timeline";
import Todo from "./Todo"; import Todo from "./Todo";
import { useTranslation } from "react-i18next";
export default function Sidesheet({ type, onClose }) { export default function Sidesheet({ type, onClose }) {
const { t } = useTranslation();
const { settings } = useSettings(); const { settings } = useSettings();
function getTitle(type) { function getTitle(type) {
@ -20,14 +22,14 @@ export default function Sidesheet({ type, onClose }) {
className="w-7" className="w-7"
alt="chat icon" alt="chat icon"
/> />
<div className="ms-3 text-lg">Timeline</div> <div className="ms-3 text-lg">{t("timeline")}</div>
</div> </div>
); );
case SIDESHEET.TODO: case SIDESHEET.TODO:
return ( return (
<div className="flex items-center"> <div className="flex items-center">
<img src={todo} className="w-7" alt="todo icon" /> <img src={todo} className="w-7" alt="todo icon" />
<div className="ms-3 text-lg">To-do list</div> <div className="ms-3 text-lg">{t("to_do")}</div>
</div> </div>
); );
default: default:

View File

@ -1,8 +1,10 @@
import { useTranslation } from "react-i18next";
import { useUndoRedo } from "../../../hooks"; import { useUndoRedo } from "../../../hooks";
import { List } from "@douyinfe/semi-ui"; import { List } from "@douyinfe/semi-ui";
export default function Timeline() { export default function Timeline() {
const { undoStack } = useUndoRedo(); const { undoStack } = useUndoRedo();
const { t } = useTranslation();
if (undoStack.length > 0) { if (undoStack.length > 0) {
return ( return (
@ -22,11 +24,6 @@ export default function Timeline() {
</List> </List>
); );
} else { } else {
return ( return <div className="m-5 sidesheet-theme">{t("no_activity")}</div>;
<div className="m-5 sidesheet-theme">
No activity was recorded. You have not added anything to your diagram
yet.
</div>
);
} }
} }

View File

@ -21,6 +21,7 @@ import {
} from "@douyinfe/semi-icons"; } from "@douyinfe/semi-icons";
import { State } from "../../../data/constants"; import { State } from "../../../data/constants";
import { useTasks, useSaveState } from "../../../hooks"; import { useTasks, useSaveState } from "../../../hooks";
import { useTranslation } from "react-i18next";
const Priority = { const Priority = {
NONE: 0, NONE: 0,
@ -30,10 +31,10 @@ const Priority = {
}; };
const SortOrder = { const SortOrder = {
ORIGINAL: "My order", ORIGINAL: "my_order",
PRIORITY: "Priority", PRIORITY: "priority",
COMPLETED: "Completed", COMPLETED: "completed",
ALPHABETICALLY: "Alphabetically", ALPHABETICALLY: "alphabetically",
}; };
export default function Todo() { export default function Todo() {
@ -41,17 +42,18 @@ export default function Todo() {
const [, setSortOrder] = useState(SortOrder.ORIGINAL); const [, setSortOrder] = useState(SortOrder.ORIGINAL);
const { tasks, setTasks, updateTask } = useTasks(); const { tasks, setTasks, updateTask } = useTasks();
const { setSaveState } = useSaveState(); const { setSaveState } = useSaveState();
const { t } = useTranslation();
const priorityLabel = (p) => { const priorityLabel = (p) => {
switch (p) { switch (p) {
case Priority.NONE: case Priority.NONE:
return "None"; return t("none");
case Priority.LOW: case Priority.LOW:
return "Low"; return t("low");
case Priority.MEDIUM: case Priority.MEDIUM:
return "Medium"; return t("medium");
case Priority.HIGH: case Priority.HIGH:
return "High"; return t("high");
default: default:
return ""; return "";
} }
@ -91,7 +93,7 @@ export default function Todo() {
} else { } else {
return 0; return 0;
} }
}) }),
); );
break; break;
case SortOrder.ALPHABETICALLY: case SortOrder.ALPHABETICALLY:
@ -116,7 +118,7 @@ export default function Todo() {
sort(order); sort(order);
}} }}
> >
{order} {t(order)}
</Dropdown.Item> </Dropdown.Item>
))} ))}
</Dropdown.Menu> </Dropdown.Menu>
@ -128,7 +130,7 @@ export default function Todo() {
theme="borderless" theme="borderless"
type="tertiary" type="tertiary"
> >
Sort by <IconCaretdown /> {t("sort_by")} <IconCaretdown />
</Button> </Button>
</Dropdown> </Dropdown>
<Button <Button
@ -147,12 +149,12 @@ export default function Todo() {
]); ]);
}} }}
> >
Add task {t("add_task")}
</Button> </Button>
</div> </div>
{tasks.length > 0 ? ( {tasks.length > 0 ? (
<List className="sidesheet-theme"> <List className="sidesheet-theme">
{tasks.map((t, i) => ( {tasks.map((task, i) => (
<List.Item <List.Item
key={i} key={i}
style={{ paddingLeft: "18px", paddingRight: "18px" }} style={{ paddingLeft: "18px", paddingRight: "18px" }}
@ -163,7 +165,7 @@ export default function Todo() {
<Row gutter={6} align="middle" type="flex" className="mb-2"> <Row gutter={6} align="middle" type="flex" className="mb-2">
<Col span={2}> <Col span={2}>
<Checkbox <Checkbox
checked={t.complete} checked={task.complete}
onChange={(e) => { onChange={(e) => {
updateTask(i, { complete: e.target.checked }); updateTask(i, { complete: e.target.checked });
setSaveState(State.SAVING); setSaveState(State.SAVING);
@ -172,25 +174,25 @@ export default function Todo() {
</Col> </Col>
<Col span={19}> <Col span={19}>
<Input <Input
placeholder="Title" placeholder={t("title")}
onChange={(v) => updateTask(i, { title: v })} onChange={(v) => updateTask(i, { title: v })}
value={t.title} value={task.title}
onBlur={() => setSaveState(State.SAVING)} onBlur={() => setSaveState(State.SAVING)}
></Input> />
</Col> </Col>
<Col span={3}> <Col span={3}>
<Popover <Popover
content={ content={
<div className="p-2 popover-theme"> <div className="p-2 popover-theme">
<div className="mb-2 font-semibold"> <div className="mb-2 font-semibold">
Set priority: {t("priority")}:
</div> </div>
<RadioGroup <RadioGroup
onChange={(e) => { onChange={(e) => {
updateTask(i, { priority: e.target.value }); updateTask(i, { priority: e.target.value });
setSaveState(State.SAVING); setSaveState(State.SAVING);
}} }}
value={t.priority} value={task.priority}
direction="vertical" direction="vertical"
> >
<Radio value={Priority.NONE}> <Radio value={Priority.NONE}>
@ -221,12 +223,12 @@ export default function Todo() {
style={{ marginTop: "12px" }} style={{ marginTop: "12px" }}
onClick={() => { onClick={() => {
setTasks((prev) => setTasks((prev) =>
prev.filter((task, j) => i !== j) prev.filter((_, j) => i !== j),
); );
setSaveState(State.SAVING); setSaveState(State.SAVING);
}} }}
> >
Delete {t("delete")}
</Button> </Button>
</div> </div>
} }
@ -243,7 +245,7 @@ export default function Todo() {
<Col span={2}></Col> <Col span={2}></Col>
<Col span={22}> <Col span={22}>
<TextArea <TextArea
placeholder="Details" placeholder={t("details")}
onChange={(v) => updateTask(i, { details: v })} onChange={(v) => updateTask(i, { details: v })}
value={t.details} value={t.details}
onBlur={() => setSaveState(State.SAVING)} onBlur={() => setSaveState(State.SAVING)}
@ -254,9 +256,9 @@ export default function Todo() {
<Row> <Row>
<Col span={2}></Col> <Col span={2}></Col>
<Col span={22}> <Col span={22}>
Priority:{" "} {t("priority")}:{" "}
<Tag color={priorityColor(t.priority)}> <Tag color={priorityColor(task.priority)}>
{priorityLabel(t.priority)} {priorityLabel(task.priority)}
</Tag> </Tag>
</Col> </Col>
</Row> </Row>
@ -265,10 +267,7 @@ export default function Todo() {
))} ))}
</List> </List>
) : ( ) : (
<div className="m-5 sidesheet-theme"> <div className="m-5 sidesheet-theme">{t("no_tasks")}</div>
You have no tasks yet. Add your to-dos and keep track of your
progress.
</div>
)} )}
</> </>
); );

View File

@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { Row, Col, Button, Input, Popover, Toast } from "@douyinfe/semi-ui"; import { Row, Col, Button, Input, Popover } from "@douyinfe/semi-ui";
import { IconDeleteStroked } from "@douyinfe/semi-icons"; import { IconDeleteStroked } from "@douyinfe/semi-icons";
import { useAreas, useSaveState, useUndoRedo } from "../../../hooks"; import { useAreas, useSaveState, useUndoRedo } from "../../../hooks";
import { import {
@ -8,9 +8,11 @@ import {
State, State,
defaultBlue, defaultBlue,
} from "../../../data/constants"; } from "../../../data/constants";
import ColorPalette from "../../ColorPalette"; import ColorPalette from "../../ColorPicker";
import { useTranslation } from "react-i18next";
export default function AreaInfo({ data, i }) { export default function AreaInfo({ data, i }) {
const { t } = useTranslation();
const { setSaveState } = useSaveState(); const { setSaveState } = useSaveState();
const { deleteArea, updateArea } = useAreas(); const { deleteArea, updateArea } = useAreas();
const { setUndoStack, setRedoStack } = useUndoRedo(); const { setUndoStack, setRedoStack } = useUndoRedo();
@ -28,7 +30,7 @@ export default function AreaInfo({ data, i }) {
<Col span={18}> <Col span={18}>
<Input <Input
value={data.name} value={data.name}
placeholder="Name" placeholder={t("name")}
onChange={(value) => updateArea(data.id, { name: value })} onChange={(value) => updateArea(data.id, { name: value })}
onFocus={(e) => setEditField({ name: e.target.value })} onFocus={(e) => setEditField({ name: e.target.value })}
onBlur={(e) => { onBlur={(e) => {
@ -41,7 +43,10 @@ export default function AreaInfo({ data, i }) {
aid: i, aid: i,
undo: editField, undo: editField,
redo: { name: e.target.value }, redo: { name: e.target.value },
message: `Edit area name to ${e.target.value}`, message: t("edit_area", {
areaName: e.target.value,
extra: "[name]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -67,7 +72,10 @@ export default function AreaInfo({ data, i }) {
aid: i, aid: i,
undo: { color: data.color }, undo: { color: data.color },
redo: { color: c }, redo: { color: c },
message: `Edit area color to ${c}`, message: t("edit_area", {
areaName: data.name,
extra: "[color]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -90,10 +98,7 @@ export default function AreaInfo({ data, i }) {
<Button <Button
icon={<IconDeleteStroked />} icon={<IconDeleteStroked />}
type="danger" type="danger"
onClick={() => { onClick={() => deleteArea(i, true)}
Toast.success(`Area deleted!`);
deleteArea(i);
}}
/> />
</Col> </Col>
</Row> </Row>

View File

@ -1,29 +1,29 @@
import { Row, Col, Button } from "@douyinfe/semi-ui"; import { Button } from "@douyinfe/semi-ui";
import { IconPlus } from "@douyinfe/semi-icons"; import { IconPlus } from "@douyinfe/semi-icons";
import Empty from "../Empty"; import Empty from "../Empty";
import { useAreas } from "../../../hooks"; import { useAreas } from "../../../hooks";
import SearchBar from "./SearchBar"; import SearchBar from "./SearchBar";
import AreaInfo from "./AreaDetails"; import AreaInfo from "./AreaDetails";
import { useTranslation } from "react-i18next";
export default function AreasTab() { export default function AreasTab() {
const { areas, addArea } = useAreas(); const { areas, addArea } = useAreas();
const { t } = useTranslation();
return ( return (
<div> <div>
<Row gutter={6}> <div className="flex gap-2">
<Col span={16}>
<SearchBar /> <SearchBar />
</Col> <div>
<Col span={8}> <Button icon={<IconPlus />} block onClick={addArea}>
<Button icon={<IconPlus />} block onClick={() => addArea()}> {t("add_area")}
Add area
</Button> </Button>
</Col> </div>
</Row> </div>
{areas.length <= 0 ? ( {areas.length <= 0 ? (
<Empty <Empty
title="No subject areas" title={t("no_subject_areas")}
text="Add subject areas to organize tables!" text={t("no_subject_areas_text")}
/> />
) : ( ) : (
<div className="p-2"> <div className="p-2">

View File

@ -2,18 +2,20 @@ import { useState } from "react";
import { useAreas } from "../../../hooks"; import { useAreas } from "../../../hooks";
import { AutoComplete } from "@douyinfe/semi-ui"; import { AutoComplete } from "@douyinfe/semi-ui";
import { IconSearch } from "@douyinfe/semi-icons"; import { IconSearch } from "@douyinfe/semi-icons";
import { useTranslation } from "react-i18next";
export default function SearchBar() { export default function SearchBar() {
const { areas } = useAreas(); const { areas } = useAreas();
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const { t } = useTranslation();
const [filteredResult, setFilteredResult] = useState( const [filteredResult, setFilteredResult] = useState(
areas.map((t) => t.name) areas.map((t) => t.name),
); );
const handleStringSearch = (value) => { const handleStringSearch = (value) => {
setFilteredResult( setFilteredResult(
areas.map((t) => t.name).filter((i) => i.includes(value)) areas.map((t) => t.name).filter((i) => i.includes(value)),
); );
}; };
@ -23,8 +25,8 @@ export default function SearchBar() {
value={searchText} value={searchText}
showClear showClear
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder="Search..." placeholder={t("search")}
emptyContent={<div className="p-3 popover-theme">No areas found</div>} emptyContent={<div className="p-3 popover-theme">{t("not_found")}</div>}
onSearch={(v) => handleStringSearch(v)} onSearch={(v) => handleStringSearch(v)}
onChange={(v) => setSearchText(v)} onChange={(v) => setSearchText(v)}
onSelect={(v) => { onSelect={(v) => {

View File

@ -3,10 +3,12 @@ import { Collapse, Badge } from "@douyinfe/semi-ui";
import { arrayIsEqual } from "../../utils/utils"; import { arrayIsEqual } from "../../utils/utils";
import { getIssues } from "../../utils/issues"; import { getIssues } from "../../utils/issues";
import { useSettings, useTables, useTypes } from "../../hooks"; import { useSettings, useTables, useTypes } from "../../hooks";
import { useTranslation } from "react-i18next";
export default function Issues() { export default function Issues() {
const { settings } = useSettings();
const { types } = useTypes(); const { types } = useTypes();
const { t } = useTranslation();
const { settings } = useSettings();
const { tables, relationships } = useTables(); const { tables, relationships } = useTables();
const [issues, setIssues] = useState([]); const [issues, setIssues] = useState([]);
@ -38,7 +40,7 @@ export default function Issues() {
> >
<div className="pe-3 select-none"> <div className="pe-3 select-none">
<i className="fa-solid fa-triangle-exclamation me-2 text-yellow-500" /> <i className="fa-solid fa-triangle-exclamation me-2 text-yellow-500" />
Issues {t("issues")}
</div> </div>
</Badge> </Badge>
} }
@ -46,9 +48,7 @@ export default function Issues() {
> >
<div className="max-h-[160px] overflow-y-auto"> <div className="max-h-[160px] overflow-y-auto">
{settings.strictMode ? ( {settings.strictMode ? (
<div className="mb-1"> <div className="mb-1">{t("strict_mode_is_on_no_issues")}</div>
Strict mode is off so no issues will be displayed.
</div>
) : issues.length > 0 ? ( ) : issues.length > 0 ? (
<> <>
{issues.map((e, i) => ( {issues.map((e, i) => (
@ -58,7 +58,7 @@ export default function Issues() {
))} ))}
</> </>
) : ( ) : (
<div>No issues were detected.</div> <div>{t("no_issues")}</div>
)} )}
</div> </div>
</Collapse.Panel> </Collapse.Panel>

View File

@ -1,20 +1,15 @@
import { useState } from "react"; import { useState } from "react";
import { import { Button, Collapse, TextArea, Popover, Input } from "@douyinfe/semi-ui";
Button,
Collapse,
TextArea,
Popover,
Input,
Toast,
} from "@douyinfe/semi-ui";
import { IconDeleteStroked, IconCheckboxTick } from "@douyinfe/semi-icons"; import { IconDeleteStroked, IconCheckboxTick } from "@douyinfe/semi-icons";
import { noteThemes, Action, ObjectType } from "../../../data/constants"; import { noteThemes, Action, ObjectType } from "../../../data/constants";
import { useNotes, useUndoRedo } from "../../../hooks"; import { useNotes, useUndoRedo } from "../../../hooks";
import { useTranslation } from "react-i18next";
export default function NoteInfo({ data, nid }) { export default function NoteInfo({ data, nid }) {
const { updateNote, deleteNote } = useNotes(); const { updateNote, deleteNote } = useNotes();
const { setUndoStack, setRedoStack } = useUndoRedo(); const { setUndoStack, setRedoStack } = useUndoRedo();
const [editField, setEditField] = useState({}); const [editField, setEditField] = useState({});
const { t } = useTranslation();
return ( return (
<Collapse.Panel <Collapse.Panel
@ -27,10 +22,10 @@ export default function NoteInfo({ data, nid }) {
id={`scroll_note_${data.id}`} id={`scroll_note_${data.id}`}
> >
<div className="flex items-center mb-2"> <div className="flex items-center mb-2">
<div className="font-semibold me-2">Title:</div> <div className="font-semibold me-2 break-keep">{t("title")}:</div>
<Input <Input
value={data.title} value={data.title}
placeholder="Title" placeholder={t("title")}
onChange={(value) => updateNote(data.id, { title: value })} onChange={(value) => updateNote(data.id, { title: value })}
onFocus={(e) => setEditField({ title: e.target.value })} onFocus={(e) => setEditField({ title: e.target.value })}
onBlur={(e) => { onBlur={(e) => {
@ -43,7 +38,10 @@ export default function NoteInfo({ data, nid }) {
nid: data.id, nid: data.id,
undo: editField, undo: editField,
redo: { title: e.target.value }, redo: { title: e.target.value },
message: `Edit note title to "${e.target.name}"`, message: t("edit_note", {
noteTitle: e.target.value,
extra: "[title]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -52,7 +50,7 @@ export default function NoteInfo({ data, nid }) {
</div> </div>
<div className="flex justify-between align-top"> <div className="flex justify-between align-top">
<TextArea <TextArea
placeholder="Add content" placeholder={t("content")}
value={data.content} value={data.content}
autosize autosize
onChange={(value) => { onChange={(value) => {
@ -79,7 +77,10 @@ export default function NoteInfo({ data, nid }) {
nid: nid, nid: nid,
undo: editField, undo: editField,
redo: { content: e.target.value, height: newHeight }, redo: { content: e.target.value, height: newHeight },
message: `Edit note content to "${e.target.value}"`, message: t("edit_note", {
noteTitle: e.target.value,
extra: "[content]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -90,7 +91,7 @@ export default function NoteInfo({ data, nid }) {
<Popover <Popover
content={ content={
<div className="popover-theme"> <div className="popover-theme">
<div className="font-medium mb-1">Theme</div> <div className="font-medium mb-1">{t("theme")}</div>
<hr /> <hr />
<div className="py-3"> <div className="py-3">
{noteThemes.map((c) => ( {noteThemes.map((c) => (
@ -107,7 +108,10 @@ export default function NoteInfo({ data, nid }) {
nid: nid, nid: nid,
undo: { color: data.color }, undo: { color: data.color },
redo: { color: c }, redo: { color: c },
message: `Edit note color to ${c}`, message: t("edit_note", {
noteTitle: data.title,
extra: "[color]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -136,10 +140,7 @@ export default function NoteInfo({ data, nid }) {
<Button <Button
icon={<IconDeleteStroked />} icon={<IconDeleteStroked />}
type="danger" type="danger"
onClick={() => { onClick={() => deleteNote(nid, true)}
Toast.success(`Note deleted!`);
deleteNote(nid);
}}
/> />
</div> </div>
</div> </div>

View File

@ -1,18 +1,19 @@
import { Row, Col, Button, Collapse } from "@douyinfe/semi-ui"; import { Button, Collapse } from "@douyinfe/semi-ui";
import { IconPlus } from "@douyinfe/semi-icons"; import { IconPlus } from "@douyinfe/semi-icons";
import { useNotes, useSelect } from "../../../hooks"; import { useNotes, useSelect } from "../../../hooks";
import Empty from "../Empty"; import Empty from "../Empty";
import SearchBar from "./SearchBar"; import SearchBar from "./SearchBar";
import NoteInfo from "./NoteInfo"; import NoteInfo from "./NoteInfo";
import { useTranslation } from "react-i18next";
export default function NotesTab() { export default function NotesTab() {
const { notes, addNote } = useNotes(); const { notes, addNote } = useNotes();
const { selectedElement, setSelectedElement } = useSelect(); const { selectedElement, setSelectedElement } = useSelect();
const { t } = useTranslation();
return ( return (
<> <>
<Row gutter={6}> <div className="flex gap-2">
<Col span={16}>
<SearchBar <SearchBar
setActiveKey={(activeKey) => setActiveKey={(activeKey) =>
setSelectedElement((prev) => ({ setSelectedElement((prev) => ({
@ -21,15 +22,14 @@ export default function NotesTab() {
})) }))
} }
/> />
</Col> <div>
<Col span={8}>
<Button icon={<IconPlus />} block onClick={() => addNote()}> <Button icon={<IconPlus />} block onClick={() => addNote()}>
Add note {t("add_note")}
</Button> </Button>
</Col> </div>
</Row> </div>
{notes.length <= 0 ? ( {notes.length <= 0 ? (
<Empty title="No text notes" text="Add notes cuz why not!" /> <Empty title={t("no_notes")} text={t("no_notes_text")} />
) : ( ) : (
<Collapse <Collapse
activeKey={selectedElement.open ? `${selectedElement.id}` : ""} activeKey={selectedElement.open ? `${selectedElement.id}` : ""}

View File

@ -2,17 +2,20 @@ import { useState } from "react";
import { AutoComplete } from "@douyinfe/semi-ui"; import { AutoComplete } from "@douyinfe/semi-ui";
import { IconSearch } from "@douyinfe/semi-icons"; import { IconSearch } from "@douyinfe/semi-icons";
import { useNotes } from "../../../hooks"; import { useNotes } from "../../../hooks";
import { useTranslation } from "react-i18next";
export default function SearchBar({ setActiveKey }) { export default function SearchBar({ setActiveKey }) {
const { notes } = useNotes(); const { notes } = useNotes();
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const { t } = useTranslation();
const [filteredResult, setFilteredResult] = useState( const [filteredResult, setFilteredResult] = useState(
notes.map((t) => t.title) notes.map((t) => t.title),
); );
const handleStringSearch = (value) => { const handleStringSearch = (value) => {
setFilteredResult( setFilteredResult(
notes.map((t) => t.title).filter((i) => i.includes(value)) notes.map((t) => t.title).filter((i) => i.includes(value)),
); );
}; };
@ -22,8 +25,8 @@ export default function SearchBar({ setActiveKey }) {
value={searchText} value={searchText}
showClear showClear
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder="Search..." placeholder={t("search")}
emptyContent={<div className="p-3 popover-theme">No notes found</div>} emptyContent={<div className="p-3 popover-theme">{t("not_found")}</div>}
onSearch={(v) => handleStringSearch(v)} onSearch={(v) => handleStringSearch(v)}
onChange={(v) => setSearchText(v)} onChange={(v) => setSearchText(v)}
onSelect={(v) => { onSelect={(v) => {

View File

@ -19,14 +19,16 @@ import {
ObjectType, ObjectType,
} from "../../../data/constants"; } from "../../../data/constants";
import { useTables, useUndoRedo } from "../../../hooks"; import { useTables, useUndoRedo } from "../../../hooks";
import i18n from "../../../i18n/i18n";
import { useTranslation } from "react-i18next";
const columns = [ const columns = [
{ {
title: "Primary", title: i18n.t("primary"),
dataIndex: "primary", dataIndex: "primary",
}, },
{ {
title: "Foreign", title: i18n.t("foreign"),
dataIndex: "foreign", dataIndex: "foreign",
}, },
]; ];
@ -34,6 +36,7 @@ const columns = [
export default function RelationshipInfo({ data }) { export default function RelationshipInfo({ data }) {
const { setUndoStack, setRedoStack } = useUndoRedo(); const { setUndoStack, setRedoStack } = useUndoRedo();
const { tables, setRelationships, deleteRelationship } = useTables(); const { tables, setRelationships, deleteRelationship } = useTables();
const { t } = useTranslation();
const swapKeys = () => { const swapKeys = () => {
setUndoStack((prev) => [ setUndoStack((prev) => [
@ -54,7 +57,10 @@ export default function RelationshipInfo({ data }) {
endTableId: data.startTableId, endTableId: data.startTableId,
endFieldId: data.startFieldId, endFieldId: data.startFieldId,
}, },
message: `Swap primary and foreign tables`, message: t("edit_relationship", {
refName: data.name,
extra: "[swap keys]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -71,8 +77,8 @@ export default function RelationshipInfo({ data }) {
endTableId: e.startTableId, endTableId: e.startTableId,
endFieldId: e.startFieldId, endFieldId: e.startFieldId,
} }
: e : e,
) ),
); );
}; };
@ -85,12 +91,17 @@ export default function RelationshipInfo({ data }) {
rid: data.id, rid: data.id,
undo: { cardinality: data.cardinality }, undo: { cardinality: data.cardinality },
redo: { cardinality: value }, redo: { cardinality: value },
message: `Edit relationship cardinality`, message: t("edit_relationship", {
refName: data.name,
extra: "[cardinality]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
setRelationships((prev) => setRelationships((prev) =>
prev.map((e, idx) => (idx === data.id ? { ...e, cardinality: value } : e)) prev.map((e, idx) =>
idx === data.id ? { ...e, cardinality: value } : e,
),
); );
}; };
@ -102,7 +113,10 @@ export default function RelationshipInfo({ data }) {
rid: data.id, rid: data.id,
undo: { [undoKey]: data[undoKey] }, undo: { [undoKey]: data[undoKey] },
redo: { [undoKey]: value }, redo: { [undoKey]: value },
message: `Edit relationship ${key} constraint`, message: t("edit_relationship", {
refName: data.name,
extra: "[constraint]",
}),
}); });
setUndoStack((prev) => [ setUndoStack((prev) => [
...prev, ...prev,
@ -112,12 +126,15 @@ export default function RelationshipInfo({ data }) {
rid: data.id, rid: data.id,
undo: { [undoKey]: data[undoKey] }, undo: { [undoKey]: data[undoKey] },
redo: { [undoKey]: value }, redo: { [undoKey]: value },
message: `Edit relationship ${key} constraint`, message: t("edit_relationship", {
refName: data.name,
extra: "[constraint]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
setRelationships((prev) => setRelationships((prev) =>
prev.map((e, idx) => (idx === data.id ? { ...e, [undoKey]: value } : e)) prev.map((e, idx) => (idx === data.id ? { ...e, [undoKey]: value } : e)),
); );
}; };
@ -133,11 +150,11 @@ export default function RelationshipInfo({ data }) {
> >
<div className="flex justify-between items-center mb-3"> <div className="flex justify-between items-center mb-3">
<div className="me-3"> <div className="me-3">
<span className="font-semibold">Primary: </span> <span className="font-semibold">{t("primary")}: </span>
{tables[data.endTableId].name} {tables[data.endTableId].name}
</div> </div>
<div className="mx-1"> <div className="mx-1">
<span className="font-semibold">Foreign: </span> <span className="font-semibold">{t("foreign")}: </span>
{tables[data.startTableId].name} {tables[data.startTableId].name}
</div> </div>
<div className="ms-1"> <div className="ms-1">
@ -168,7 +185,7 @@ export default function RelationshipInfo({ data }) {
block block
onClick={swapKeys} onClick={swapKeys}
> >
Swap {t("swap")}
</Button> </Button>
</div> </div>
</div> </div>
@ -181,7 +198,7 @@ export default function RelationshipInfo({ data }) {
</Popover> </Popover>
</div> </div>
</div> </div>
<div className="font-semibold my-1">Cardinality</div> <div className="font-semibold my-1">{t("cardinality")}:</div>
<Select <Select
optionList={Object.values(Cardinality).map((v) => ({ optionList={Object.values(Cardinality).map((v) => ({
label: v, label: v,
@ -193,7 +210,7 @@ export default function RelationshipInfo({ data }) {
/> />
<Row gutter={6} className="my-3"> <Row gutter={6} className="my-3">
<Col span={12}> <Col span={12}>
<div className="font-semibold">On update: </div> <div className="font-semibold">{t("on_update")}: </div>
<Select <Select
optionList={Object.values(Constraint).map((v) => ({ optionList={Object.values(Constraint).map((v) => ({
label: v, label: v,
@ -205,7 +222,7 @@ export default function RelationshipInfo({ data }) {
/> />
</Col> </Col>
<Col span={12}> <Col span={12}>
<div className="font-semibold">On delete: </div> <div className="font-semibold">{t("on_delete")}: </div>
<Select <Select
optionList={Object.values(Constraint).map((v) => ({ optionList={Object.values(Constraint).map((v) => ({
label: v, label: v,
@ -223,7 +240,7 @@ export default function RelationshipInfo({ data }) {
type="danger" type="danger"
onClick={() => deleteRelationship(data.id)} onClick={() => deleteRelationship(data.id)}
> >
Delete {t("delete")}
</Button> </Button>
</Collapse.Panel> </Collapse.Panel>
</div> </div>

View File

@ -4,10 +4,12 @@ import Empty from "../Empty";
import SearchBar from "./SearchBar"; import SearchBar from "./SearchBar";
import RelationshipInfo from "./RelationshipInfo"; import RelationshipInfo from "./RelationshipInfo";
import { ObjectType } from "../../../data/constants"; import { ObjectType } from "../../../data/constants";
import { useTranslation } from "react-i18next";
export default function RelationshipsTab() { export default function RelationshipsTab() {
const { relationships } = useTables(); const { relationships } = useTables();
const { selectedElement, setSelectedElement } = useSelect(); const { selectedElement, setSelectedElement } = useSelect();
const { t } = useTranslation();
return ( return (
<> <>
@ -33,8 +35,8 @@ export default function RelationshipsTab() {
> >
{relationships.length <= 0 ? ( {relationships.length <= 0 ? (
<Empty <Empty
title="No relationships" title={t("no_relationships")}
text="Drag to connect fields and form relationships!" text={t("no_relationships_text")}
/> />
) : ( ) : (
relationships.map((r) => <RelationshipInfo key={r.id} data={r} />) relationships.map((r) => <RelationshipInfo key={r.id} data={r} />)

View File

@ -3,11 +3,14 @@ import { useSelect, useTables } from "../../../hooks";
import { AutoComplete } from "@douyinfe/semi-ui"; import { AutoComplete } from "@douyinfe/semi-ui";
import { IconSearch } from "@douyinfe/semi-icons"; import { IconSearch } from "@douyinfe/semi-icons";
import { ObjectType } from "../../../data/constants"; import { ObjectType } from "../../../data/constants";
import { useTranslation } from "react-i18next";
export default function SearchBar() { export default function SearchBar() {
const { relationships } = useTables(); const { relationships } = useTables();
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const { setSelectedElement } = useSelect(); const { setSelectedElement } = useSelect();
const { t } = useTranslation();
const [filteredResult, setFilteredResult] = useState( const [filteredResult, setFilteredResult] = useState(
relationships.map((t) => t.name), relationships.map((t) => t.name),
); );
@ -24,10 +27,8 @@ export default function SearchBar() {
value={searchText} value={searchText}
showClear showClear
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder="Search..." placeholder={t("search")}
emptyContent={ emptyContent={<div className="p-3 popover-theme">{t("not_found")}</div>}
<div className="p-3 popover-theme">No relationships found</div>
}
onSearch={(v) => handleStringSearch(v)} onSearch={(v) => handleStringSearch(v)}
onChange={(v) => setSearchText(v)} onChange={(v) => setSearchText(v)}
onSelect={(v) => { onSelect={(v) => {

View File

@ -7,17 +7,23 @@ import Issues from "./Issues";
import AreasTab from "./AreasTab/AreasTab"; import AreasTab from "./AreasTab/AreasTab";
import NotesTab from "./NotesTab/NotesTab"; import NotesTab from "./NotesTab/NotesTab";
import TablesTab from "./TablesTab/TablesTab"; import TablesTab from "./TablesTab/TablesTab";
import { useTranslation } from "react-i18next";
export default function SidePanel({ width, resize, setResize }) { export default function SidePanel({ width, resize, setResize }) {
const { layout } = useLayout(); const { layout } = useLayout();
const { selectedElement, setSelectedElement } = useSelect(); const { selectedElement, setSelectedElement } = useSelect();
const { t } = useTranslation();
const tabList = [ const tabList = [
{ tab: "Tables", itemKey: Tab.TABLES, component: <TablesTab /> }, { tab: t("tables"), itemKey: Tab.TABLES, component: <TablesTab /> },
{ tab: "Relationships", itemKey: Tab.RELATIONSHIPS, component: <RelationshipsTab /> }, {
{ tab: "Subject Areas", itemKey: Tab.AREAS, component: <AreasTab /> }, tab: t("relationships"),
{ tab: "Notes", itemKey: Tab.NOTES, component: <NotesTab /> }, itemKey: Tab.RELATIONSHIPS,
{ tab: "Types", itemKey: Tab.TYPES, component: <TypesTab /> }, component: <RelationshipsTab />,
},
{ tab: t("subject_areas"), itemKey: Tab.AREAS, component: <AreasTab /> },
{ tab: t("notes"), itemKey: Tab.NOTES, component: <NotesTab /> },
{ tab: t("types"), itemKey: Tab.TYPES, component: <TypesTab /> },
]; ];
return ( return (
@ -36,13 +42,12 @@ export default function SidePanel({ width, resize, setResize }) {
} }
collapsible collapsible
> >
{tabList.length && tabList.map(tab => {tabList.length &&
tabList.map((tab) => (
<TabPane tab={tab.tab} itemKey={tab.itemKey} key={tab.itemKey}> <TabPane tab={tab.tab} itemKey={tab.itemKey} key={tab.itemKey}>
<div className="p-2"> <div className="p-2">{tab.component}</div>
{tab.component}
</div>
</TabPane> </TabPane>
)} ))}
</Tabs> </Tabs>
</div> </div>
{layout.issues && ( {layout.issues && (

View File

@ -11,18 +11,21 @@ import { Action, ObjectType } from "../../../data/constants";
import { IconDeleteStroked } from "@douyinfe/semi-icons"; import { IconDeleteStroked } from "@douyinfe/semi-icons";
import { hasCheck, hasPrecision, isSized } from "../../../utils/toSQL"; import { hasCheck, hasPrecision, isSized } from "../../../utils/toSQL";
import { useTables, useUndoRedo } from "../../../hooks"; import { useTables, useUndoRedo } from "../../../hooks";
import { useTranslation } from "react-i18next";
export default function FieldDetails({ data, tid, index }) { export default function FieldDetails({ data, tid, index }) {
const { t } = useTranslation();
const { tables } = useTables();
const { setUndoStack, setRedoStack } = useUndoRedo(); const { setUndoStack, setRedoStack } = useUndoRedo();
const { updateField, deleteField } = useTables(); const { updateField, deleteField } = useTables();
const [editField, setEditField] = useState({}); const [editField, setEditField] = useState({});
return ( return (
<div> <div>
<div className="font-semibold">Default value</div> <div className="font-semibold">{t("default_value")}</div>
<Input <Input
className="my-2" className="my-2"
placeholder="Set default" placeholder={t("default_value")}
value={data.default} value={data.default}
disabled={ disabled={
data.type === "BLOB" || data.type === "BLOB" ||
@ -45,7 +48,10 @@ export default function FieldDetails({ data, tid, index }) {
fid: index, fid: index,
undo: editField, undo: editField,
redo: { default: e.target.value }, redo: { default: e.target.value },
message: `Edit table field default to ${e.target.value}`, message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -53,7 +59,9 @@ export default function FieldDetails({ data, tid, index }) {
/> />
{(data.type === "ENUM" || data.type === "SET") && ( {(data.type === "ENUM" || data.type === "SET") && (
<> <>
<div className="font-semibold mb-1">{data.type} values</div> <div className="font-semibold mb-1">
{data.type} {t("values")}
</div>
<TagInput <TagInput
separator={[",", ", ", " ,"]} separator={[",", ", ", " ,"]}
value={data.values} value={data.values}
@ -62,7 +70,7 @@ export default function FieldDetails({ data, tid, index }) {
} }
addOnBlur addOnBlur
className="my-2" className="my-2"
placeholder="Use ',' for batch input" placeholder={t("use_for_batch_input")}
onChange={(v) => updateField(tid, index, { values: v })} onChange={(v) => updateField(tid, index, { values: v })}
onFocus={() => setEditField({ values: data.values })} onFocus={() => setEditField({ values: data.values })}
onBlur={() => { onBlur={() => {
@ -80,9 +88,10 @@ export default function FieldDetails({ data, tid, index }) {
fid: index, fid: index,
undo: editField, undo: editField,
redo: { values: data.values }, redo: { values: data.values },
message: `Edit table field values to "${JSON.stringify( message: t("edit_table", {
data.values, tableName: tables[tid].name,
)}"`, extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -92,7 +101,7 @@ export default function FieldDetails({ data, tid, index }) {
)} )}
{isSized(data.type) && ( {isSized(data.type) && (
<> <>
<div className="font-semibold">Size</div> <div className="font-semibold">{t("size")}</div>
<InputNumber <InputNumber
className="my-2 w-full" className="my-2 w-full"
placeholder="Set length" placeholder="Set length"
@ -111,7 +120,10 @@ export default function FieldDetails({ data, tid, index }) {
fid: index, fid: index,
undo: editField, undo: editField,
redo: { size: e.target.value }, redo: { size: e.target.value },
message: `Edit table field size to ${e.target.value}`, message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -121,10 +133,10 @@ export default function FieldDetails({ data, tid, index }) {
)} )}
{hasPrecision(data.type) && ( {hasPrecision(data.type) && (
<> <>
<div className="font-semibold">Precision</div> <div className="font-semibold">{t("precision")}</div>
<Input <Input
className="my-2 w-full" className="my-2 w-full"
placeholder="Set precision: size, d" placeholder={t("set_precision")}
validateStatus={ validateStatus={
!data.size || /^\d+,\s*\d+$|^$/.test(data.size) !data.size || /^\d+,\s*\d+$|^$/.test(data.size)
? "default" ? "default"
@ -145,7 +157,10 @@ export default function FieldDetails({ data, tid, index }) {
fid: index, fid: index,
undo: editField, undo: editField,
redo: { size: e.target.value }, redo: { size: e.target.value },
message: `Edit table field precision to ${e.target.value}`, message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -155,10 +170,10 @@ export default function FieldDetails({ data, tid, index }) {
)} )}
{hasCheck(data.type) && ( {hasCheck(data.type) && (
<> <>
<div className="font-semibold">Check Expression</div> <div className="font-semibold">{t("check")}</div>
<Input <Input
className="mt-2" className="mt-2"
placeholder="Set constraint" placeholder={t("check")}
value={data.check} value={data.check}
disabled={data.increment} disabled={data.increment}
onChange={(value) => updateField(tid, index, { check: value })} onChange={(value) => updateField(tid, index, { check: value })}
@ -175,19 +190,20 @@ export default function FieldDetails({ data, tid, index }) {
fid: index, fid: index,
undo: editField, undo: editField,
redo: { check: e.target.value }, redo: { check: e.target.value },
message: `Edit table field check expression to ${e.target.value}`, message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
}} }}
/> />
<div className="text-xs mt-1"> <div className="text-xs mt-1">{t("this_will_appear_as_is")}</div>
*This will appear in the script as is.
</div>
</> </>
)} )}
<div className="flex justify-between items-center my-3"> <div className="flex justify-between items-center my-3">
<div className="font-medium">Unique</div> <div className="font-medium">{t("unique")}</div>
<Checkbox <Checkbox
value="unique" value="unique"
checked={data.unique} checked={data.unique}
@ -216,7 +232,7 @@ export default function FieldDetails({ data, tid, index }) {
/> />
</div> </div>
<div className="flex justify-between items-center my-3"> <div className="flex justify-between items-center my-3">
<div className="font-medium">Autoincrement</div> <div className="font-medium">{t("autoincrement")}</div>
<Checkbox <Checkbox
value="increment" value="increment"
checked={data.increment} checked={data.increment}
@ -242,9 +258,10 @@ export default function FieldDetails({ data, tid, index }) {
redo: { redo: {
[checkedValues.target.value]: checkedValues.target.checked, [checkedValues.target.value]: checkedValues.target.checked,
}, },
message: `Edit table field to${ message: t("edit_table", {
data.increment ? " not" : "" tableName: tables[tid].name,
} auto increment`, extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -255,10 +272,10 @@ export default function FieldDetails({ data, tid, index }) {
}} }}
/> />
</div> </div>
<div className="font-semibold">Comment</div> <div className="font-semibold">{t("comment")}</div>
<TextArea <TextArea
className="my-2" className="my-2"
placeholder="Add comment" placeholder={t("comment")}
value={data.comment} value={data.comment}
autosize autosize
rows={2} rows={2}
@ -276,7 +293,10 @@ export default function FieldDetails({ data, tid, index }) {
fid: index, fid: index,
undo: editField, undo: editField,
redo: { comment: e.target.value }, redo: { comment: e.target.value },
message: `Edit field comment to "${e.target.value}"`, message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -288,7 +308,7 @@ export default function FieldDetails({ data, tid, index }) {
block block
onClick={() => deleteField(data, tid)} onClick={() => deleteField(data, tid)}
> >
Delete field {t("delete")}
</Button> </Button>
</div> </div>
); );

View File

@ -2,15 +2,17 @@ import { Action, ObjectType } from "../../../data/constants";
import { Input, Button, Popover, Checkbox, Select } from "@douyinfe/semi-ui"; import { Input, Button, Popover, Checkbox, Select } from "@douyinfe/semi-ui";
import { IconMore, IconDeleteStroked } from "@douyinfe/semi-icons"; import { IconMore, IconDeleteStroked } from "@douyinfe/semi-icons";
import { useTables, useUndoRedo } from "../../../hooks"; import { useTables, useUndoRedo } from "../../../hooks";
import { useTranslation } from "react-i18next";
export default function IndexDetails({ data, fields, iid, tid }) { export default function IndexDetails({ data, fields, iid, tid }) {
const { t } = useTranslation();
const { tables, updateTable } = useTables(); const { tables, updateTable } = useTables();
const { setUndoStack, setRedoStack } = useUndoRedo(); const { setUndoStack, setRedoStack } = useUndoRedo();
return ( return (
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<Select <Select
placeholder="Select fields" placeholder={t("select_fields")}
multiple multiple
validateStatus={data.fields.length === 0 ? "error" : "default"} validateStatus={data.fields.length === 0 ? "error" : "default"}
optionList={fields} optionList={fields}
@ -33,7 +35,10 @@ export default function IndexDetails({ data, fields, iid, tid }) {
fields: [...value], fields: [...value],
name: `${value.join("_")}_index`, name: `${value.join("_")}_index`,
}, },
message: `Edit index fields to "${JSON.stringify(value)}"`, message: t("edit_table", {
tableName: tables[tid].name,
extra: "[index field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -45,7 +50,7 @@ export default function IndexDetails({ data, fields, iid, tid }) {
fields: [...value], fields: [...value],
name: `${value.join("_")}_index`, name: `${value.join("_")}_index`,
} }
: index : index,
), ),
}); });
}} }}
@ -53,10 +58,10 @@ export default function IndexDetails({ data, fields, iid, tid }) {
<Popover <Popover
content={ content={
<div className="px-1 popover-theme"> <div className="px-1 popover-theme">
<div className="font-semibold mb-1">Index name: </div> <div className="font-semibold mb-1">{t("name")}: </div>
<Input value={data.name} placeholder="Index name" disabled /> <Input value={data.name} placeholder={t("name")} disabled />
<div className="flex justify-between items-center my-3"> <div className="flex justify-between items-center my-3">
<div className="font-medium">Unique</div> <div className="font-medium">{t("unique")}</div>
<Checkbox <Checkbox
value="unique" value="unique"
checked={data.unique} checked={data.unique}
@ -77,9 +82,10 @@ export default function IndexDetails({ data, fields, iid, tid }) {
[checkedValues.target.value]: [checkedValues.target.value]:
checkedValues.target.checked, checkedValues.target.checked,
}, },
message: `Edit table field to${ message: t("edit_table", {
data.unique ? " not" : "" tableName: tables[tid].name,
} unique`, extra: "[index field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -91,7 +97,7 @@ export default function IndexDetails({ data, fields, iid, tid }) {
[checkedValues.target.value]: [checkedValues.target.value]:
checkedValues.target.checked, checkedValues.target.checked,
} }
: index : index,
), ),
}); });
}} }}
@ -110,7 +116,10 @@ export default function IndexDetails({ data, fields, iid, tid }) {
component: "index_delete", component: "index_delete",
tid: tid, tid: tid,
data: data, data: data,
message: `Delete index`, message: t("edit_table", {
tableName: tables[tid].name,
extra: "[delete index]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);

View File

@ -3,10 +3,12 @@ import { useSelect } from "../../../hooks";
import { AutoComplete } from "@douyinfe/semi-ui"; import { AutoComplete } from "@douyinfe/semi-ui";
import { IconSearch } from "@douyinfe/semi-icons"; import { IconSearch } from "@douyinfe/semi-icons";
import { ObjectType } from "../../../data/constants"; import { ObjectType } from "../../../data/constants";
import { useTranslation } from "react-i18next";
export default function SearchBar({ tables }) { export default function SearchBar({ tables }) {
const { setSelectedElement } = useSelect(); const { setSelectedElement } = useSelect();
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const { t } = useTranslation();
const filteredTable = useMemo( const filteredTable = useMemo(
() => tables.map((t) => t.name).filter((i) => i.includes(searchText)), () => tables.map((t) => t.name).filter((i) => i.includes(searchText)),
[tables, searchText], [tables, searchText],
@ -18,8 +20,8 @@ export default function SearchBar({ tables }) {
value={searchText} value={searchText}
showClear showClear
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder="Search..." placeholder={t("search")}
emptyContent={<div className="p-3 popover-theme">No tables found</div>} emptyContent={<div className="p-3 popover-theme">{t("not_found")}</div>}
onChange={(v) => setSearchText(v)} onChange={(v) => setSearchText(v)}
onSelect={(v) => { onSelect={(v) => {
const { id } = tables.find((t) => t.name === v); const { id } = tables.find((t) => t.name === v);

View File

@ -5,10 +5,13 @@ import { getSize, hasCheck, hasPrecision, isSized } from "../../../utils/toSQL";
import { useTables, useTypes, useUndoRedo } from "../../../hooks"; import { useTables, useTypes, useUndoRedo } from "../../../hooks";
import { useState } from "react"; import { useState } from "react";
import FieldDetails from "./FieldDetails"; import FieldDetails from "./FieldDetails";
import { useTranslation } from "react-i18next";
export default function TableField({ data, tid, index }) { export default function TableField({ data, tid, index }) {
const { updateField } = useTables(); const { updateField } = useTables();
const { types } = useTypes(); const { types } = useTypes();
const { tables } = useTables();
const { t } = useTranslation();
const { setUndoStack, setRedoStack } = useUndoRedo(); const { setUndoStack, setRedoStack } = useUndoRedo();
const [editField, setEditField] = useState({}); const [editField, setEditField] = useState({});
@ -33,7 +36,10 @@ export default function TableField({ data, tid, index }) {
fid: index, fid: index,
undo: editField, undo: editField,
redo: { name: e.target.value }, redo: { name: e.target.value },
message: `Edit table field name to ${e.target.value}`, message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -69,7 +75,10 @@ export default function TableField({ data, tid, index }) {
fid: index, fid: index,
undo: { type: data.type }, undo: { type: data.type },
redo: { type: value }, redo: { type: value },
message: `Edit table field type to ${value}`, message: t("edit_table", {
tableName: tables[tid].name,
extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -123,7 +132,7 @@ export default function TableField({ data, tid, index }) {
<Col span={3}> <Col span={3}>
<Button <Button
type={data.notNull ? "primary" : "tertiary"} type={data.notNull ? "primary" : "tertiary"}
title="Not Null" title={t("not_null")}
theme={data.notNull ? "solid" : "light"} theme={data.notNull ? "solid" : "light"}
onClick={() => { onClick={() => {
setUndoStack((prev) => [ setUndoStack((prev) => [
@ -136,9 +145,10 @@ export default function TableField({ data, tid, index }) {
fid: index, fid: index,
undo: { notNull: data.notNull }, undo: { notNull: data.notNull },
redo: { notNull: !data.notNull }, redo: { notNull: !data.notNull },
message: `Edit table field to${ message: t("edit_table", {
data.notNull ? "" : " not" tableName: tables[tid].name,
} null`, extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -151,7 +161,7 @@ export default function TableField({ data, tid, index }) {
<Col span={3}> <Col span={3}>
<Button <Button
type={data.primary ? "primary" : "tertiary"} type={data.primary ? "primary" : "tertiary"}
title="Primary" title={t("primary")}
theme={data.primary ? "solid" : "light"} theme={data.primary ? "solid" : "light"}
onClick={() => { onClick={() => {
setUndoStack((prev) => [ setUndoStack((prev) => [
@ -164,9 +174,10 @@ export default function TableField({ data, tid, index }) {
fid: index, fid: index,
undo: { primary: data.primary }, undo: { primary: data.primary },
redo: { primary: !data.primary }, redo: { primary: !data.primary },
message: `Edit table field to${ message: t("edit_table", {
data.primary ? " not" : "" tableName: tables[tid].name,
} primary`, extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);

View File

@ -1,23 +1,22 @@
import { useState } from "react"; import { useState } from "react";
import { import {
Collapse, Collapse,
Row,
Col,
Input, Input,
TextArea, TextArea,
Button, Button,
Card, Card,
Popover, Popover,
Toast,
} from "@douyinfe/semi-ui"; } from "@douyinfe/semi-ui";
import { IconDeleteStroked } from "@douyinfe/semi-icons"; import { IconDeleteStroked } from "@douyinfe/semi-icons";
import { useTables, useUndoRedo } from "../../../hooks"; import { useTables, useUndoRedo } from "../../../hooks";
import { Action, ObjectType, defaultBlue } from "../../../data/constants"; import { Action, ObjectType, defaultBlue } from "../../../data/constants";
import ColorPalette from "../../ColorPalette"; import ColorPalette from "../../ColorPicker";
import TableField from "./TableField"; import TableField from "./TableField";
import IndexDetails from "./IndexDetails"; import IndexDetails from "./IndexDetails";
import { useTranslation } from "react-i18next";
export default function TableInfo({ data }) { export default function TableInfo({ data }) {
const { t } = useTranslation();
const [indexActiveKey, setIndexActiveKey] = useState(""); const [indexActiveKey, setIndexActiveKey] = useState("");
const { deleteTable, updateTable, updateField, setRelationships } = const { deleteTable, updateTable, updateField, setRelationships } =
useTables(); useTables();
@ -31,11 +30,11 @@ export default function TableInfo({ data }) {
return ( return (
<div> <div>
<div className="flex items-center mb-2.5"> <div className="flex items-center mb-2.5">
<div className="text-md font-semibold">Name: </div> <div className="text-md font-semibold break-keep">{t("name")}: </div>
<Input <Input
value={data.name} value={data.name}
validateStatus={data.name === "" ? "error" : "default"} validateStatus={data.name === "" ? "error" : "default"}
placeholder="Name" placeholder={t("name")}
className="ms-2" className="ms-2"
onChange={(value) => updateTable(data.id, { name: value })} onChange={(value) => updateTable(data.id, { name: value })}
onFocus={(e) => setEditField({ name: e.target.value })} onFocus={(e) => setEditField({ name: e.target.value })}
@ -50,7 +49,10 @@ export default function TableInfo({ data }) {
tid: data.id, tid: data.id,
undo: editField, undo: editField,
redo: { name: e.target.value }, redo: { name: e.target.value },
message: `Edit table name to ${e.target.value}`, message: t("edit_table", {
tableName: e.target.value,
extra: "[name]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -149,7 +151,7 @@ export default function TableInfo({ data }) {
onChange={(itemKey) => setIndexActiveKey(itemKey)} onChange={(itemKey) => setIndexActiveKey(itemKey)}
accordion accordion
> >
<Collapse.Panel header="Indices" itemKey="1"> <Collapse.Panel header={t("indices")} itemKey="1">
{data.indices.map((idx, k) => ( {data.indices.map((idx, k) => (
<IndexDetails <IndexDetails
key={"index_" + k} key={"index_" + k}
@ -172,12 +174,12 @@ export default function TableInfo({ data }) {
headerLine={false} headerLine={false}
> >
<Collapse keepDOM lazyRender> <Collapse keepDOM lazyRender>
<Collapse.Panel header="Comment" itemKey="1"> <Collapse.Panel header={t("comment")} itemKey="1">
<TextArea <TextArea
field="comment" field="comment"
value={data.comment} value={data.comment}
autosize autosize
placeholder="Add comment" placeholder={t("comment")}
rows={1} rows={1}
onChange={(value) => onChange={(value) =>
updateTable(data.id, { comment: value }, false) updateTable(data.id, { comment: value }, false)
@ -194,7 +196,10 @@ export default function TableInfo({ data }) {
tid: data.id, tid: data.id,
undo: editField, undo: editField,
redo: { comment: e.target.value }, redo: { comment: e.target.value },
message: `Edit table comment to ${e.target.value}`, message: t("edit_table", {
tableName: e.target.value,
extra: "[comment]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -203,8 +208,8 @@ export default function TableInfo({ data }) {
</Collapse.Panel> </Collapse.Panel>
</Collapse> </Collapse>
</Card> </Card>
<Row gutter={6} className="mt-2"> <div className="flex justify-between items-center gap-1 mb-2">
<Col span={8}> <div>
<Popover <Popover
content={ content={
<div className="popover-theme"> <div className="popover-theme">
@ -220,7 +225,10 @@ export default function TableInfo({ data }) {
tid: data.id, tid: data.id,
undo: { color: data.color }, undo: { color: data.color },
redo: { color: defaultBlue }, redo: { color: defaultBlue },
message: `Edit table color to default`, message: t("edit_table", {
tableName: data.name,
extra: "[color]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -236,7 +244,10 @@ export default function TableInfo({ data }) {
tid: data.id, tid: data.id,
undo: { color: data.color }, undo: { color: data.color },
redo: { color: c }, redo: { color: c },
message: `Edit table color to ${c}`, message: t("edit_table", {
tableName: data.name,
extra: "[color]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -250,12 +261,12 @@ export default function TableInfo({ data }) {
showArrow showArrow
> >
<div <div
className="h-[32px] w-[32px] rounded mb-2" className="h-[32px] w-[32px] rounded"
style={{ backgroundColor: data.color }} style={{ backgroundColor: data.color }}
/> />
</Popover> </Popover>
</Col> </div>
<Col span={7}> <div className="flex gap-1">
<Button <Button
block block
onClick={() => { onClick={() => {
@ -267,7 +278,10 @@ export default function TableInfo({ data }) {
element: ObjectType.TABLE, element: ObjectType.TABLE,
component: "index_add", component: "index_add",
tid: data.id, tid: data.id,
message: `Add index`, message: t("edit_table", {
tableName: data.name,
extra: "[add index]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -284,10 +298,8 @@ export default function TableInfo({ data }) {
}); });
}} }}
> >
Add index {t("add_index")}
</Button> </Button>
</Col>
<Col span={6}>
<Button <Button
onClick={() => { onClick={() => {
setUndoStack((prev) => [ setUndoStack((prev) => [
@ -297,7 +309,10 @@ export default function TableInfo({ data }) {
element: ObjectType.TABLE, element: ObjectType.TABLE,
component: "field_add", component: "field_add",
tid: data.id, tid: data.id,
message: `Add field`, message: t("edit_table", {
tableName: data.name,
extra: "[add field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -321,20 +336,15 @@ export default function TableInfo({ data }) {
}} }}
block block
> >
Add field {t("add_field")}
</Button> </Button>
</Col>
<Col span={3}>
<Button <Button
icon={<IconDeleteStroked />} icon={<IconDeleteStroked />}
type="danger" type="danger"
onClick={() => { onClick={() => deleteTable(data.id)}
Toast.success(`Table deleted!`);
deleteTable(data.id);
}}
/> />
</Col> </div>
</Row> </div>
</div> </div>
); );
} }

View File

@ -1,29 +1,29 @@
import { Collapse, Row, Col, Button } from "@douyinfe/semi-ui"; import { Collapse, Button } from "@douyinfe/semi-ui";
import { IconPlus } from "@douyinfe/semi-icons"; import { IconPlus } from "@douyinfe/semi-icons";
import { useSelect, useTables } from "../../../hooks"; import { useSelect, useTables } from "../../../hooks";
import { ObjectType } from "../../../data/constants"; import { ObjectType } from "../../../data/constants";
import SearchBar from "./SearchBar"; import SearchBar from "./SearchBar";
import Empty from "../Empty"; import Empty from "../Empty";
import TableInfo from "./TableInfo"; import TableInfo from "./TableInfo";
import { useTranslation } from "react-i18next";
export default function TablesTab() { export default function TablesTab() {
const { tables, addTable } = useTables(); const { tables, addTable } = useTables();
const { selectedElement, setSelectedElement } = useSelect(); const { selectedElement, setSelectedElement } = useSelect();
const { t } = useTranslation();
return ( return (
<> <>
<Row gutter={6}> <div className="flex gap-2">
<Col span={16}>
<SearchBar tables={tables} /> <SearchBar tables={tables} />
</Col> <div>
<Col span={8}>
<Button icon={<IconPlus />} block onClick={() => addTable()}> <Button icon={<IconPlus />} block onClick={() => addTable()}>
Add table {t("add_table")}
</Button> </Button>
</Col> </div>
</Row> </div>
{tables.length === 0 ? ( {tables.length === 0 ? (
<Empty title="No tables" text="Start building your diagram!" /> <Empty title={t("no_tables")} text={t("no_tables_text")} />
) : ( ) : (
<Collapse <Collapse
activeKey={ activeKey={

View File

@ -3,11 +3,13 @@ import { AutoComplete } from "@douyinfe/semi-ui";
import { IconSearch } from "@douyinfe/semi-icons"; import { IconSearch } from "@douyinfe/semi-icons";
import { useSelect, useTypes } from "../../../hooks"; import { useSelect, useTypes } from "../../../hooks";
import { ObjectType } from "../../../data/constants"; import { ObjectType } from "../../../data/constants";
import { useTranslation } from "react-i18next";
export default function Searchbar() { export default function Searchbar() {
const { types } = useTypes(); const { types } = useTypes();
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const { setSelectedElement } = useSelect(); const { setSelectedElement } = useSelect();
const { t } = useTranslation();
const [filteredResult, setFilteredResult] = useState( const [filteredResult, setFilteredResult] = useState(
types.map((t) => t.name), types.map((t) => t.name),
@ -25,9 +27,9 @@ export default function Searchbar() {
value={value} value={value}
showClear showClear
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder="Search..." placeholder={t("search")}
onSearch={(v) => handleStringSearch(v)} onSearch={(v) => handleStringSearch(v)}
emptyContent={<div className="p-3 popover-theme">No types found</div>} emptyContent={<div className="p-3 popover-theme">{t("not_found")}</div>}
onChange={(v) => setValue(v)} onChange={(v) => setValue(v)}
onSelect={(v) => { onSelect={(v) => {
const i = types.findIndex((t) => t.name === v); const i = types.findIndex((t) => t.name === v);

View File

@ -13,22 +13,25 @@ import {
import { IconDeleteStroked, IconMore } from "@douyinfe/semi-icons"; import { IconDeleteStroked, IconMore } from "@douyinfe/semi-icons";
import { isSized, hasPrecision, getSize } from "../../../utils/toSQL"; import { isSized, hasPrecision, getSize } from "../../../utils/toSQL";
import { useUndoRedo, useTypes } from "../../../hooks"; import { useUndoRedo, useTypes } from "../../../hooks";
import { useTranslation } from "react-i18next";
export default function TypeField({ data, tid, fid }) { export default function TypeField({ data, tid, fid }) {
const { types, updateType } = useTypes(); const { types, updateType } = useTypes();
const { setUndoStack, setRedoStack } = useUndoRedo(); const { setUndoStack, setRedoStack } = useUndoRedo();
const [editField, setEditField] = useState({}); const [editField, setEditField] = useState({});
const { t } = useTranslation();
return ( return (
<Row gutter={6} className="hover-1 my-2"> <Row gutter={6} className="hover-1 my-2">
<Col span={10}> <Col span={10}>
<Input <Input
value={data.name} value={data.name}
validateStatus={data.name === "" ? "error" : "default"} validateStatus={data.name === "" ? "error" : "default"}
placeholder="Name" placeholder={t("name")}
onChange={(value) => onChange={(value) =>
updateType(tid, { updateType(tid, {
fields: types[tid].fields.map((e, id) => fields: types[tid].fields.map((e, id) =>
id === fid ? { ...data, name: value } : e id === fid ? { ...data, name: value } : e,
), ),
}) })
} }
@ -45,7 +48,10 @@ export default function TypeField({ data, tid, fid }) {
fid: fid, fid: fid,
undo: editField, undo: editField,
redo: { name: e.target.value }, redo: { name: e.target.value },
message: `Edit type field name to ${e.target.value}`, message: t("edit_type", {
typeName: data.name,
extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -62,7 +68,7 @@ export default function TypeField({ data, tid, fid }) {
})), })),
...types ...types
.filter( .filter(
(type) => type.name.toLowerCase() !== data.name.toLowerCase() (type) => type.name.toLowerCase() !== data.name.toLowerCase(),
) )
.map((type) => ({ .map((type) => ({
label: type.name.toUpperCase(), label: type.name.toUpperCase(),
@ -72,7 +78,7 @@ export default function TypeField({ data, tid, fid }) {
filter filter
value={data.type} value={data.type}
validateStatus={data.type === "" ? "error" : "default"} validateStatus={data.type === "" ? "error" : "default"}
placeholder="Type" placeholder={t("type")}
onChange={(value) => { onChange={(value) => {
if (value === data.type) return; if (value === data.type) return;
setUndoStack((prev) => [ setUndoStack((prev) => [
@ -85,7 +91,10 @@ export default function TypeField({ data, tid, fid }) {
fid: fid, fid: fid,
undo: { type: data?.type }, undo: { type: data?.type },
redo: { type: value }, redo: { type: value },
message: `Edit type field type to ${value}`, message: t("edit_type", {
typeName: data.name,
extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -98,7 +107,7 @@ export default function TypeField({ data, tid, fid }) {
type: value, type: value,
values: data.values ? [...data.values] : [], values: data.values ? [...data.values] : [],
} }
: e : e,
), ),
}); });
} else if (isSized(value) || hasPrecision(value)) { } else if (isSized(value) || hasPrecision(value)) {
@ -106,13 +115,13 @@ export default function TypeField({ data, tid, fid }) {
fields: types[tid].fields.map((e, id) => fields: types[tid].fields.map((e, id) =>
id === fid id === fid
? { ...data, type: value, size: getSize(value) } ? { ...data, type: value, size: getSize(value) }
: e : e,
), ),
}); });
} else { } else {
updateType(tid, { updateType(tid, {
fields: types[tid].fields.map((e, id) => fields: types[tid].fields.map((e, id) =>
id === fid ? { ...data, type: value } : e id === fid ? { ...data, type: value } : e,
), ),
}); });
} }
@ -125,7 +134,9 @@ export default function TypeField({ data, tid, fid }) {
<div className="popover-theme w-[240px]"> <div className="popover-theme w-[240px]">
{(data.type === "ENUM" || data.type === "SET") && ( {(data.type === "ENUM" || data.type === "SET") && (
<> <>
<div className="font-semibold mb-1">{data.type} values</div> <div className="font-semibold mb-1">
{data.type} {t("values")}
</div>
<TagInput <TagInput
separator={[",", ", ", " ,"]} separator={[",", ", ", " ,"]}
value={data.values} value={data.values}
@ -135,11 +146,11 @@ export default function TypeField({ data, tid, fid }) {
: "default" : "default"
} }
className="my-2" className="my-2"
placeholder="Use ',' for batch input" placeholder={t("use_for_batch_input")}
onChange={(v) => onChange={(v) =>
updateType(tid, { updateType(tid, {
fields: types[tid].fields.map((e, id) => fields: types[tid].fields.map((e, id) =>
id === fid ? { ...data, values: v } : e id === fid ? { ...data, values: v } : e,
), ),
}) })
} }
@ -160,9 +171,10 @@ export default function TypeField({ data, tid, fid }) {
fid: fid, fid: fid,
undo: editField, undo: editField,
redo: { values: data.values }, redo: { values: data.values },
message: `Edit type field values to "${JSON.stringify( message: t("edit_type", {
data.values typeName: data.name,
)}"`, extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -172,15 +184,15 @@ export default function TypeField({ data, tid, fid }) {
)} )}
{isSized(data.type) && ( {isSized(data.type) && (
<> <>
<div className="font-semibold">Size</div> <div className="font-semibold">{t("size")}</div>
<InputNumber <InputNumber
className="my-2 w-full" className="my-2 w-full"
placeholder="Set length" placeholder={t("size")}
value={data.size} value={data.size}
onChange={(value) => onChange={(value) =>
updateType(tid, { updateType(tid, {
fields: types[tid].fields.map((e, id) => fields: types[tid].fields.map((e, id) =>
id === fid ? { ...data, size: value } : e id === fid ? { ...data, size: value } : e,
), ),
}) })
} }
@ -197,7 +209,10 @@ export default function TypeField({ data, tid, fid }) {
fid: fid, fid: fid,
undo: editField, undo: editField,
redo: { size: e.target.value }, redo: { size: e.target.value },
message: `Edit type field size to ${e.target.value}`, message: t("edit_type", {
typeName: data.name,
extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -207,10 +222,10 @@ export default function TypeField({ data, tid, fid }) {
)} )}
{hasPrecision(data.type) && ( {hasPrecision(data.type) && (
<> <>
<div className="font-semibold">Precision</div> <div className="font-semibold">{t("precision")}</div>
<Input <Input
className="my-2 w-full" className="my-2 w-full"
placeholder="Set precision: (size, d)" placeholder={t("set_precision")}
validateStatus={ validateStatus={
/^\(\d+,\s*\d+\)$|^$/.test(data.size) /^\(\d+,\s*\d+\)$|^$/.test(data.size)
? "default" ? "default"
@ -220,7 +235,7 @@ export default function TypeField({ data, tid, fid }) {
onChange={(value) => onChange={(value) =>
updateType(tid, { updateType(tid, {
fields: types[tid].fields.map((e, id) => fields: types[tid].fields.map((e, id) =>
id === fid ? { ...data, size: value } : e id === fid ? { ...data, size: value } : e,
), ),
}) })
} }
@ -237,7 +252,10 @@ export default function TypeField({ data, tid, fid }) {
fid: fid, fid: fid,
undo: editField, undo: editField,
redo: { size: e.target.value }, redo: { size: e.target.value },
message: `Edit type field precision to ${e.target.value}`, message: t("edit_type", {
typeName: data.name,
extra: "[field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -259,7 +277,10 @@ export default function TypeField({ data, tid, fid }) {
tid: tid, tid: tid,
fid: fid, fid: fid,
data: data, data: data,
message: `Delete field`, message: t("edit_type", {
typeName: data.name,
extra: "[delete field]",
}),
}, },
]); ]);
updateType(tid, { updateType(tid, {
@ -267,7 +288,7 @@ export default function TypeField({ data, tid, fid }) {
}); });
}} }}
> >
Delete field {t("delete")}
</Button> </Button>
</div> </div>
} }

View File

@ -8,16 +8,17 @@ import {
TextArea, TextArea,
Button, Button,
Card, Card,
Toast,
} from "@douyinfe/semi-ui"; } from "@douyinfe/semi-ui";
import { IconDeleteStroked, IconPlus } from "@douyinfe/semi-icons"; import { IconDeleteStroked, IconPlus } from "@douyinfe/semi-icons";
import { useUndoRedo, useTypes } from "../../../hooks"; import { useUndoRedo, useTypes } from "../../../hooks";
import TypeField from "./TypeField"; import TypeField from "./TypeField";
import { useTranslation } from "react-i18next";
export default function TypeInfo({ index, data }) { export default function TypeInfo({ index, data }) {
const { deleteType, updateType } = useTypes(); const { deleteType, updateType } = useTypes();
const { setUndoStack, setRedoStack } = useUndoRedo(); const { setUndoStack, setRedoStack } = useUndoRedo();
const [editField, setEditField] = useState({}); const [editField, setEditField] = useState({});
const { t } = useTranslation();
return ( return (
<div id={`scroll_type_${index}`}> <div id={`scroll_type_${index}`}>
@ -30,11 +31,11 @@ export default function TypeInfo({ index, data }) {
itemKey={`${index}`} itemKey={`${index}`}
> >
<div className="flex items-center mb-2.5"> <div className="flex items-center mb-2.5">
<div className="text-md font-semibold">Name: </div> <div className="text-md font-semibold break-keep">{t("name")}: </div>
<Input <Input
value={data.name} value={data.name}
validateStatus={data.name === "" ? "error" : "default"} validateStatus={data.name === "" ? "error" : "default"}
placeholder="Name" placeholder={t("name")}
className="ms-2" className="ms-2"
onChange={(value) => updateType(index, { name: value })} onChange={(value) => updateType(index, { name: value })}
onFocus={(e) => setEditField({ name: e.target.value })} onFocus={(e) => setEditField({ name: e.target.value })}
@ -49,7 +50,10 @@ export default function TypeInfo({ index, data }) {
tid: index, tid: index,
undo: editField, undo: editField,
redo: { name: e.target.value }, redo: { name: e.target.value },
message: `Edit type name to ${e.target.value}`, message: t("edit_type", {
typeName: data.name,
extra: "[name]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -65,12 +69,12 @@ export default function TypeInfo({ index, data }) {
headerLine={false} headerLine={false}
> >
<Collapse keepDOM lazyRender> <Collapse keepDOM lazyRender>
<Collapse.Panel header="Comment" itemKey="1"> <Collapse.Panel header={t("comment")} itemKey="1">
<TextArea <TextArea
field="comment" field="comment"
value={data.comment} value={data.comment}
autosize autosize
placeholder="Add comment" placeholder={t("comment")}
rows={1} rows={1}
onChange={(value) => onChange={(value) =>
updateType(index, { comment: value }, false) updateType(index, { comment: value }, false)
@ -87,7 +91,10 @@ export default function TypeInfo({ index, data }) {
tid: index, tid: index,
undo: editField, undo: editField,
redo: { comment: e.target.value }, redo: { comment: e.target.value },
message: `Edit type comment to ${e.target.value}`, message: t("edit_type", {
typeName: data.name,
extra: "[comment]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -108,7 +115,10 @@ export default function TypeInfo({ index, data }) {
element: ObjectType.TYPE, element: ObjectType.TYPE,
component: "field_add", component: "field_add",
tid: index, tid: index,
message: `Add field to type`, message: t("edit_type", {
typeName: data.name,
extra: "[add field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -124,20 +134,17 @@ export default function TypeInfo({ index, data }) {
}} }}
block block
> >
Add field {t("add_field")}
</Button> </Button>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Button <Button
icon={<IconDeleteStroked />} icon={<IconDeleteStroked />}
type="danger" type="danger"
onClick={() => { onClick={() => deleteType(index)}
Toast.success(`Type deleted!`);
deleteType(index);
}}
block block
> >
Delete {t("delete")}
</Button> </Button>
</Col> </Col>
</Row> </Row>

View File

@ -1,47 +1,34 @@
import { Collapse, Row, Col, Button, Popover } from "@douyinfe/semi-ui"; import { Collapse, Button, Popover } from "@douyinfe/semi-ui";
import { IconPlus, IconInfoCircle } from "@douyinfe/semi-icons"; import { IconPlus, IconInfoCircle } from "@douyinfe/semi-icons";
import { useSelect, useTypes } from "../../../hooks"; import { useSelect, useTypes } from "../../../hooks";
import { ObjectType } from "../../../data/constants"; import { ObjectType } from "../../../data/constants";
import Searchbar from "./SearchBar"; import Searchbar from "./SearchBar";
import Empty from "../Empty"; import Empty from "../Empty";
import TypeInfo from "./TypeInfo"; import TypeInfo from "./TypeInfo";
import { useTranslation } from "react-i18next";
export default function TypesTab() { export default function TypesTab() {
const { types, addType } = useTypes(); const { types, addType } = useTypes();
const { selectedElement, setSelectedElement } = useSelect(); const { selectedElement, setSelectedElement } = useSelect();
const { t } = useTranslation();
return ( return (
<> <>
<Row gutter={6}> <div className="flex gap-2">
<Col span={13}>
<Searchbar /> <Searchbar />
</Col> <div>
<Col span={8}>
<Button icon={<IconPlus />} block onClick={() => addType()}> <Button icon={<IconPlus />} block onClick={() => addType()}>
Add type {t("add_type")}
</Button> </Button>
</Col> </div>
<Col span={3}>
<Popover <Popover
content={ content={
<div className="w-[240px] text-sm space-y-2 popover-theme"> <div className="w-[240px] text-sm space-y-2 popover-theme">
<div> {t("types_info")
This feature is meant for object-relational DBMSs like{" "} .split("\n")
<strong>PostgreSQL</strong>. .map((line, index) => (
</div> <div key={index}>{line}</div>
<div> ))}
If used for <strong>MySQL</strong> or <strong>MariaDB</strong>{" "}
a <code>JSON</code> type will be generated with the
corresponding json validation check.
</div>
<div>
If used for <strong>SQLite</strong> it will be translated to a{" "}
<code>BLOB</code>.
</div>
<div>
If used for <strong>MSSQL</strong> a type alias to the first
field will be generated.
</div>
</div> </div>
} }
showArrow showArrow
@ -49,10 +36,9 @@ export default function TypesTab() {
> >
<Button theme="borderless" icon={<IconInfoCircle />} /> <Button theme="borderless" icon={<IconInfoCircle />} />
</Popover> </Popover>
</Col> </div>
</Row>
{types.length <= 0 ? ( {types.length <= 0 ? (
<Empty title="No types" text="Make your own custom data types" /> <Empty title={t("no_types")} text={t("no_types_text")} />
) : ( ) : (
<Collapse <Collapse
activeKey={ activeKey={

View File

@ -1,10 +1,12 @@
import { Divider, Tooltip } from "@douyinfe/semi-ui"; import { Divider, Tooltip } from "@douyinfe/semi-ui";
import { useTransform, useLayout } from "../hooks"; import { useTransform, useLayout } from "../hooks";
import { exitFullscreen } from "../utils/fullscreen"; import { exitFullscreen } from "../utils/fullscreen";
import { useTranslation } from "react-i18next";
export default function FloatingControls() { export default function FloatingControls() {
const { transform, setTransform } = useTransform(); const { transform, setTransform } = useTransform();
const { setLayout } = useLayout(); const { setLayout } = useLayout();
const { t } = useTranslation();
return ( return (
<div className="flex gap-2"> <div className="flex gap-2">
@ -35,7 +37,7 @@ export default function FloatingControls() {
<i className="bi bi-plus-lg" /> <i className="bi bi-plus-lg" />
</button> </button>
</div> </div>
<Tooltip content="Exit"> <Tooltip content={t("exit")}>
<button <button
className="px-3 py-2 rounded-lg popover-theme" className="px-3 py-2 rounded-lg popover-theme"
onClick={() => { onClick={() => {

View File

@ -3,10 +3,13 @@ import { Action, ObjectType, defaultBlue } from "../data/constants";
import useUndoRedo from "../hooks/useUndoRedo"; import useUndoRedo from "../hooks/useUndoRedo";
import useTransform from "../hooks/useTransform"; import useTransform from "../hooks/useTransform";
import useSelect from "../hooks/useSelect"; import useSelect from "../hooks/useSelect";
import { Toast } from "@douyinfe/semi-ui";
import { useTranslation } from "react-i18next";
export const AreasContext = createContext(null); export const AreasContext = createContext(null);
export default function AreasContextProvider({ children }) { export default function AreasContextProvider({ children }) {
const { t } = useTranslation();
const [areas, setAreas] = useState([]); const [areas, setAreas] = useState([]);
const { transform } = useTransform(); const { transform } = useTransform();
const { selectedElement, setSelectedElement } = useSelect(); const { selectedElement, setSelectedElement } = useSelect();
@ -39,7 +42,7 @@ export default function AreasContextProvider({ children }) {
{ {
action: Action.ADD, action: Action.ADD,
element: ObjectType.AREA, element: ObjectType.AREA,
message: `Add new subject area`, message: t("add_area"),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -48,19 +51,20 @@ export default function AreasContextProvider({ children }) {
const deleteArea = (id, addToHistory = true) => { const deleteArea = (id, addToHistory = true) => {
if (addToHistory) { if (addToHistory) {
Toast.success(t("area_deleted"));
setUndoStack((prev) => [ setUndoStack((prev) => [
...prev, ...prev,
{ {
action: Action.DELETE, action: Action.DELETE,
element: ObjectType.AREA, element: ObjectType.AREA,
data: areas[id], data: areas[id],
message: `Delete subject area`, message: t("delete_area", areas[id].name),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
} }
setAreas((prev) => setAreas((prev) =>
prev.filter((e) => e.id !== id).map((e, i) => ({ ...e, id: i })) prev.filter((e) => e.id !== id).map((e, i) => ({ ...e, id: i })),
); );
if (id === selectedElement.id) { if (id === selectedElement.id) {
setSelectedElement((prev) => ({ setSelectedElement((prev) => ({
@ -82,7 +86,7 @@ export default function AreasContextProvider({ children }) {
}; };
} }
return t; return t;
}) }),
); );
}; };

View File

@ -3,10 +3,13 @@ import useTransform from "../hooks/useTransform";
import { Action, ObjectType, defaultNoteTheme } from "../data/constants"; import { Action, ObjectType, defaultNoteTheme } from "../data/constants";
import useUndoRedo from "../hooks/useUndoRedo"; import useUndoRedo from "../hooks/useUndoRedo";
import useSelect from "../hooks/useSelect"; import useSelect from "../hooks/useSelect";
import { Toast } from "@douyinfe/semi-ui";
import { useTranslation } from "react-i18next";
export const NotesContext = createContext(null); export const NotesContext = createContext(null);
export default function NotesContextProvider({ children }) { export default function NotesContextProvider({ children }) {
const { t } = useTranslation();
const [notes, setNotes] = useState([]); const [notes, setNotes] = useState([]);
const { transform } = useTransform(); const { transform } = useTransform();
const { setUndoStack, setRedoStack } = useUndoRedo(); const { setUndoStack, setRedoStack } = useUndoRedo();
@ -39,7 +42,7 @@ export default function NotesContextProvider({ children }) {
{ {
action: Action.ADD, action: Action.ADD,
element: ObjectType.NOTE, element: ObjectType.NOTE,
message: `Add new note`, message: t("add_note"),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -48,19 +51,20 @@ export default function NotesContextProvider({ children }) {
const deleteNote = (id, addToHistory = true) => { const deleteNote = (id, addToHistory = true) => {
if (addToHistory) { if (addToHistory) {
Toast.success(t("note_deleted"));
setUndoStack((prev) => [ setUndoStack((prev) => [
...prev, ...prev,
{ {
action: Action.DELETE, action: Action.DELETE,
element: ObjectType.NOTE, element: ObjectType.NOTE,
data: notes[id], data: notes[id],
message: `Delete note`, message: t("delete_note", { noteTitle: notes[id].title }),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
} }
setNotes((prev) => setNotes((prev) =>
prev.filter((e) => e.id !== id).map((e, i) => ({ ...e, id: i })) prev.filter((e) => e.id !== id).map((e, i) => ({ ...e, id: i })),
); );
if (id === selectedElement.id) { if (id === selectedElement.id) {
setSelectedElement((prev) => ({ setSelectedElement((prev) => ({
@ -82,7 +86,7 @@ export default function NotesContextProvider({ children }) {
}; };
} }
return t; return t;
}) }),
); );
}; };

View File

@ -3,10 +3,13 @@ import { Action, ObjectType, defaultBlue } from "../data/constants";
import useTransform from "../hooks/useTransform"; import useTransform from "../hooks/useTransform";
import useUndoRedo from "../hooks/useUndoRedo"; import useUndoRedo from "../hooks/useUndoRedo";
import useSelect from "../hooks/useSelect"; import useSelect from "../hooks/useSelect";
import { Toast } from "@douyinfe/semi-ui";
import { useTranslation } from "react-i18next";
export const TablesContext = createContext(null); export const TablesContext = createContext(null);
export default function TablesContextProvider({ children }) { export default function TablesContextProvider({ children }) {
const { t } = useTranslation();
const [tables, setTables] = useState([]); const [tables, setTables] = useState([]);
const [relationships, setRelationships] = useState([]); const [relationships, setRelationships] = useState([]);
const { transform } = useTransform(); const { transform } = useTransform();
@ -55,7 +58,7 @@ export default function TablesContextProvider({ children }) {
{ {
action: Action.ADD, action: Action.ADD,
element: ObjectType.TABLE, element: ObjectType.TABLE,
message: `Add new table`, message: t("add_table"),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -64,13 +67,14 @@ export default function TablesContextProvider({ children }) {
const deleteTable = (id, addToHistory = true) => { const deleteTable = (id, addToHistory = true) => {
if (addToHistory) { if (addToHistory) {
Toast.success(t("table_deleted"));
setUndoStack((prev) => [ setUndoStack((prev) => [
...prev, ...prev,
{ {
action: Action.DELETE, action: Action.DELETE,
element: ObjectType.TABLE, element: ObjectType.TABLE,
data: tables[id], data: tables[id],
message: `Delete table`, message: t("delete_table", { tableName: tables[id] }),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -106,7 +110,7 @@ export default function TablesContextProvider({ children }) {
const updateTable = (id, updatedValues) => { const updateTable = (id, updatedValues) => {
setTables((prev) => setTables((prev) =>
prev.map((t) => (t.id === id ? { ...t, ...updatedValues } : t)) prev.map((t) => (t.id === id ? { ...t, ...updatedValues } : t)),
); );
}; };
@ -117,12 +121,12 @@ export default function TablesContextProvider({ children }) {
return { return {
...table, ...table,
fields: table.fields.map((field, j) => fields: table.fields.map((field, j) =>
fid === j ? { ...field, ...updatedValues } : field fid === j ? { ...field, ...updatedValues } : field,
), ),
}; };
} }
return table; return table;
}) }),
); );
}; };
@ -135,7 +139,10 @@ export default function TablesContextProvider({ children }) {
component: "field_delete", component: "field_delete",
tid: tid, tid: tid,
data: field, data: field,
message: `Delete field`, message: t("edit_table", {
tableName: tables[tid].name,
extra: "[delete field]",
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -146,9 +153,9 @@ export default function TablesContextProvider({ children }) {
!( !(
(e.startTableId === tid && e.startFieldId === field.id) || (e.startTableId === tid && e.startFieldId === field.id) ||
(e.endTableId === tid && e.endFieldId === field.id) (e.endTableId === tid && e.endFieldId === field.id)
),
) )
) .map((e, i) => ({ ...e, id: i })),
.map((e, i) => ({ ...e, id: i }))
); );
setRelationships((prev) => { setRelationships((prev) => {
return prev.map((e) => { return prev.map((e) => {
@ -185,7 +192,7 @@ export default function TablesContextProvider({ children }) {
action: Action.ADD, action: Action.ADD,
element: ObjectType.RELATIONSHIP, element: ObjectType.RELATIONSHIP,
data: data, data: data,
message: `Add new relationship`, message: t("add_relationship"),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -208,13 +215,15 @@ export default function TablesContextProvider({ children }) {
action: Action.DELETE, action: Action.DELETE,
element: ObjectType.RELATIONSHIP, element: ObjectType.RELATIONSHIP,
data: relationships[id], data: relationships[id],
message: `Delete relationship`, message: t("delete_relationship", {
refName: relationships[id].name,
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
} }
setRelationships((prev) => setRelationships((prev) =>
prev.filter((e) => e.id !== id).map((e, i) => ({ ...e, id: i })) prev.filter((e) => e.id !== id).map((e, i) => ({ ...e, id: i })),
); );
}; };

View File

@ -1,10 +1,13 @@
import { createContext, useState } from "react"; import { createContext, useState } from "react";
import { Action, ObjectType } from "../data/constants"; import { Action, ObjectType } from "../data/constants";
import useUndoRedo from "../hooks/useUndoRedo"; import useUndoRedo from "../hooks/useUndoRedo";
import { Toast } from "@douyinfe/semi-ui";
import { useTranslation } from "react-i18next";
export const TypesContext = createContext(null); export const TypesContext = createContext(null);
export default function TypesContextProvider({ children }) { export default function TypesContextProvider({ children }) {
const { t } = useTranslation();
const [types, setTypes] = useState([]); const [types, setTypes] = useState([]);
const { setUndoStack, setRedoStack } = useUndoRedo(); const { setUndoStack, setRedoStack } = useUndoRedo();
@ -31,7 +34,7 @@ export default function TypesContextProvider({ children }) {
{ {
action: Action.ADD, action: Action.ADD,
element: ObjectType.TYPE, element: ObjectType.TYPE,
message: `Add new type`, message: t("add_type"),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -40,6 +43,7 @@ export default function TypesContextProvider({ children }) {
const deleteType = (id, addToHistory = true) => { const deleteType = (id, addToHistory = true) => {
if (addToHistory) { if (addToHistory) {
Toast.success(t("type_deleted"));
setUndoStack((prev) => [ setUndoStack((prev) => [
...prev, ...prev,
{ {
@ -47,7 +51,9 @@ export default function TypesContextProvider({ children }) {
element: ObjectType.TYPE, element: ObjectType.TYPE,
id: id, id: id,
data: types[id], data: types[id],
message: `Delete type`, message: t("delete_type", {
typeName: types[id].name,
}),
}, },
]); ]);
setRedoStack([]); setRedoStack([]);
@ -57,7 +63,7 @@ export default function TypesContextProvider({ children }) {
const updateType = (id, values) => { const updateType = (id, values) => {
setTypes((prev) => setTypes((prev) =>
prev.map((e, i) => (i === id ? { ...e, ...values } : e)) prev.map((e, i) => (i === id ? { ...e, ...values } : e)),
); );
}; };

View File

@ -1,3 +1,5 @@
import i18n from "../i18n/i18n";
export const sqlDataTypes = [ export const sqlDataTypes = [
"INT", "INT",
"SMALLINT", "SMALLINT",
@ -55,9 +57,9 @@ export const tableFieldHeight = 36;
export const tableColorStripHeight = 7; export const tableColorStripHeight = 7;
export const Cardinality = { export const Cardinality = {
ONE_TO_ONE: "One to one", ONE_TO_ONE: i18n.t("one_to_one"),
ONE_TO_MANY: "One to many", ONE_TO_MANY: i18n.t("one_to_many"),
MANY_TO_ONE: "Many to one", MANY_TO_ONE: i18n.t("many_to_one"),
}; };
export const Constraint = { export const Constraint = {
@ -112,6 +114,7 @@ export const MODAL = {
NEW: 7, NEW: 7,
IMPORT_SRC: 8, IMPORT_SRC: 8,
TABLE_WIDTH: 9, TABLE_WIDTH: 9,
LANGUAGE: 10,
}; };
export const STATUS = { export const STATUS = {

33
src/i18n/i18n.js Normal file
View File

@ -0,0 +1,33 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { en, english } from "./locales/en";
import { zh, chinese } from "./locales/zh";
import { es, spanish } from "./locales/es";
import { da, danish } from "./locales/da";
export const languages = [
english,
chinese,
danish,
spanish,
].sort((a, b) => a.name.localeCompare(b.name));
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: "en",
debug: false,
interpolation: {
escapeValue: false,
},
resources: {
en,
zh,
es,
da,
},
});
export default i18n;

218
src/i18n/locales/da.js Normal file
View File

@ -0,0 +1,218 @@
const danish = {
name: "Danish",
native_name: "Dansk",
code: "da",
};
const da = {
translation: {
report_bug: "Rapportér en fejl",
import: "Importér",
file: "Fil",
new: "Ny",
new_window: "Nyt vindue",
open: "Åben",
save: "Gem",
save_as: "Gem som",
save_as_template: "Gem som skabelon",
template_saved: "Skabelon gemt!",
rename: "Omdøb",
delete_diagram: "Slet diagram",
are_you_sure_delete_diagram:
"Er du sikker på at du vil slette dette diagram? Denne handling er irreversibel.",
oops_smth_went_wrong: "Ups! Noget gik galt.",
import_diagram: "Importér diagram",
import_from_source: "Importér fra kilde",
export_as: "Eksportér som",
export_source: "Eksportér kilde",
models: "Modeller",
exit: "Afslut",
edit: "Redigér",
undo: "Fortryd",
redo: "Gentag",
clear: "Ryd",
are_you_sure_clear:
"Er du sikker på at du vil rydde diagrammet? Denne handling er irreversibel.",
cut: "Klip",
copy: "Kopiér",
paste: "Indsæt",
duplicate: "Duplikér",
delete: "Slet",
copy_as_image: "Kopiér som billede",
view: "Visning",
header: "Hoved",
sidebar: "Sidebar",
issues: "Problemer",
presentation_mode: "Præsentations tilstand",
strict_mode: "Streng tilstand",
field_details: "Felt detaljer",
reset_view: "Nulstil visning",
show_grid: "Vis gitter",
show_cardinality: "Vis kardinalitet",
theme: "Tema",
light: "Lyst",
dark: "Mørkt",
zoom_in: "Zoom ind",
zoom_out: "Zoom ud",
fullscreen: "Fuld skærm",
settings: "Indstillinger",
show_timeline: "Vis tidslinje",
autosave: "Gem automatisk",
panning: "Panorering",
table_width: "Tabel bredde",
language: "Sprog",
flush_storage: "Tøm lagring",
are_you_sure_flush_storage:
"Er du sikker på at du vil tømme lagringen? Dette gør at alle dine diagrammer og individuelle skabeloner bliver slettet irreversibelt",
storage_flushed: "Lagring tømt",
help: "Hjælp",
shortcuts: "Genveje",
ask_on_discord: "Spørg os på Discord",
feedback: "Feedback",
no_changes: "Ingen ændringer",
loading: "Loader...",
last_saved: "Sidst gemt",
saving: "Gemmer...",
failed_to_save: "Gem fejlede",
fit_window_reset: "Tilpas vindue / Nulstil",
zoom: "Zoom",
add_table: "Tilføj tabel",
add_area: "Tilføj område",
add_note: "Tilføj note",
add_type: "Tilføj type",
to_do: "To-do",
tables: "Tabeller",
relationships: "Forhold",
subject_areas: "Emne områder",
notes: "Noter",
types: "Typer",
search: "Søg...",
no_tables: "Ingen tabeller",
no_tables_text: "Begynd at bygge dit diagram!",
no_relationships: "Ingen forhold",
no_relationships_text: "Træk for at forbinde felter og dan forhold!",
no_subject_areas: "Ingen emne områder",
no_subject_areas_text: "Tilføj emne områder for at gruppere tabeller!",
no_notes: "Ingen noter",
no_notes_text: "Brug noter for at registrere ekstra info",
no_types: "Ingen typer",
no_types_text: "Lav dine egne tilpassede data typer",
no_issues: "Ingen problemer blev opdaget.",
strict_mode_is_on_no_issues:
"Streng tilstand er slået fra, så ingen problemer vil blive vist.",
name: "Navn",
type: "Type",
null: "Nul",
not_null: "Ikke nul",
primary: "Primær",
unique: "Unik",
autoincrement: "Auto-inkrementel",
default_value: "Standardværdi",
check: "Tjek udtryk",
this_will_appear_as_is: "*Dette vil fremstå i det generede script som det er.",
comment: "Kommentar",
add_field: "Tilføj felt",
values: "værdier",
size: "Størrelse",
precision: "Præcision",
set_precision: "Sæt præcision: (størrelse, cifre)",
use_for_batch_input: "Brug , for samlet indtastning",
indices: "Indekser",
add_index: "Tilføj indeks",
select_fields: "Vælg felter",
title: "Titel",
not_set: "Ikke sat",
foreign: "Fremmed",
cardinality: "Kardinalitet",
on_update: "På opdater",
on_delete: "På slet",
swap: "Swap",
one_to_one: "En til en",
one_to_many: "En til mange",
many_to_one: "Mange til en",
content: "Indhold",
types_info:
"Denne feature er ment til objekt-relationelle DBMSer ligesom PostgreSQL.\nHvis brugt til MySQL eller MariaDB, vil en JSON type blive genereret med tilsvarende JSON validerings-tjek.\nHvis brugt til SQLite, vil det blive oversat til en BLOB.\nHvis brugt til MSSQL vil et type-alias til det første felt blive genereret.",
table_deleted: "Tabel slettet",
area_deleted: "Område slettet",
note_deleted: "Note slettet",
relationship_deleted: "Forhold slettet",
type_deleted: "Type slettet",
cannot_connect: "Kan ikke forbinde, kolonnerne har forskellige typer",
copied_to_clipboard: "Kopiér til clipboard",
create_new_diagram: "Opret nyt diagram",
cancel: "Afbryd",
open_diagram: "Åben diagram",
rename_diagram: "Omdøb diagram",
export: "Eksportér",
export_image: "Eksportér billede",
create: "Opret",
confirm: "Bekræft",
last_modified: "Sidst ændret",
drag_and_drop_files: "Træk og drop filen her eller klik for at uploade.",
support_json_and_ddb: "JSON og DDB filer er understøttet",
upload_sql_to_generate_diagrams:
"Upload en sql fil for at auto-generere dine tabeller og kolonner.",
overwrite_existing_diagram: "Overskriv eksisterende diagram",
only_mysql_supported:
"*For tiden er det kun MySQL scripts der er understøttet.",
blank: "Blank",
filename: "Filnavn",
table_w_no_name: "Erklærede en tabel med intet navn",
duplicate_table_by_name: "Dupliker tabellen med navnet '{{tableName}}'",
empty_field_name: "Tomt felt `navn` i tabellen '{{tableName}}'",
empty_field_type: "Tomt felt `type` i tabellen '{{tableName}}'",
no_values_for_field:
"'{{fieldName}}' felt fra tabellen '{{tableName}}' er af type `{{type}}` men ingen værdi er blevet specificeret",
default_doesnt_match_type:
"Standardværdien for feltet '{{fieldName}}' i tabellen '{{table.name}}' stemmer ikke overens med dens type",
not_null_is_null:
"'{{fieldName}}' felt fra tabellen '{{tableName}}' er IKKE NUL, men har standardværdien NUL",
duplicate_fields:
"Duplikat tabel felter med navn '{{fieldName}}' på tabellen '{{tableName}}'",
duplicate_index:
"Duplikat indeks med navn '{{indexName}}' på tabellen '{{tableName}}'",
empty_index: "Indeks på tabel '{{tableName}}' indekser ingen kolonner",
no_primary_key: "Tabel '{{tableName}}' har ingen primær nøgle",
type_with_no_name: "Erklæret en type med intet navn",
duplicate_types: "Duplikat typer med navnet '{{typeName}}'",
type_w_no_fields: "Erklæret en tom type '{{typeName}}' med ingen felter",
empty_type_field_name: "Tomt felt `navn` på typen '{{typeName}}'",
empty_type_field_type: "Tomt felt `type` på typen '{{typeName}}'",
no_values_for_type_field:
"'{{fieldName}}' felt af typen '{{typeName}}' er af typen `{{type}}` men ingen værdier er blevet specificeret",
duplicate_type_fields:
"Duplikat type felter med navnet '{{fieldName}}' på typen '{{typeName}}'",
duplicate_reference: "Duplikat reference med navnet '{{refName}}'",
circular_dependency: "Cirkulær afhængighed involveret tabel '{{refName}}'",
timeline: "Tidslinje",
priority: "Prioritet",
none: "Ingen",
low: "Lav",
medium: "Middel",
high: "Høj",
sort_by: "Sortér på",
my_order: "Min prioritering",
completed: "Færdiggjort",
alphabetically: "Alfabetisk",
add_task: "Tilføj opgave",
details: "Detaljer",
no_tasks: "Du har ingen opgaver endnu.",
no_activity: "Du har ingen aktivitet endnu.",
move_element: "Flyt {{name}} til {{coords}}",
edit_area: "{{extra}} Redigér område {{areaName}}",
delete_area: "Slet område {{areaName}}",
edit_note: "{{extra}} Redigér note {{noteTitle}}",
delete_note: "Slet note {{noteTitle}}",
edit_table: "{{extra}} Redigér tabel {{tableName}}",
delete_table: "Slet tabel {{tableName}}",
edit_type: "{{extra}} Redigér type {{typeName}}",
delete_type: "Slet type {{typeName}}",
add_relationship: "Tilføj forhold",
edit_relationship: "{{extra}} Redigér forhold {{refName}}",
delete_relationship: "Slet forhold {{refName}}",
not_found: "Ikke fundet",
},
};
export { da, danish };

218
src/i18n/locales/en.js Normal file
View File

@ -0,0 +1,218 @@
const english = {
name: "English",
native_name: "English",
code: "en",
};
const en = {
translation: {
report_bug: "Report a bug",
import: "Import",
file: "File",
new: "New",
new_window: "New window",
open: "Open",
save: "Save",
save_as: "Save as",
save_as_template: "Save as template",
template_saved: "Template saved!",
rename: "Rename",
delete_diagram: "Delete diagram",
are_you_sure_delete_diagram:
"Are you sure you want to delete this diagram? This operation is irreversible.",
oops_smth_went_wrong: "Oops! Something went wrong.",
import_diagram: "Import diagram",
import_from_source: "Import from SQL",
export_as: "Export as",
export_source: "Export SQL",
models: "Models",
exit: "Exit",
edit: "Edit",
undo: "Undo",
redo: "Redo",
clear: "Clear",
are_you_sure_clear:
"Are you sure you want to clear the diagram? This is irreversible.",
cut: "Cut",
copy: "Copy",
paste: "Paste",
duplicate: "Duplicate",
delete: "Delete",
copy_as_image: "Copy as image",
view: "View",
header: "Menubar",
sidebar: "Sidebar",
issues: "Issues",
presentation_mode: "Presentation mode",
strict_mode: "Strict mode",
field_details: "Field details",
reset_view: "Reset view",
show_grid: "Show grid",
show_cardinality: "Show cardinality",
theme: "Theme",
light: "Light",
dark: "Dark",
zoom_in: "Zoom in",
zoom_out: "Zoom out",
fullscreen: "Fullscreen",
settings: "Settings",
show_timeline: "Show timeline",
autosave: "Autosave",
panning: "Panning",
table_width: "Table width",
language: "Language",
flush_storage: "Flush storage",
are_you_sure_flush_storage:
"Are you sure you want to flush the storage? This will irreversibly delete all your diagrams and custom templates.",
storage_flushed: "Storage flushed",
help: "Help",
shortcuts: "Shortcuts",
ask_on_discord: "Ast us on Discord",
feedback: "Feedback",
no_changes: "No changes",
loading: "Loading...",
last_saved: "Last saved",
saving: "Saving...",
failed_to_save: "Failed to save",
fit_window_reset: "Fit window / Reset",
zoom: "Zoom",
add_table: "Add table",
add_area: "Add area",
add_note: "Add note",
add_type: "Add type",
to_do: "To-do",
tables: "Tables",
relationships: "Relationships",
subject_areas: "Subject areas",
notes: "Notes",
types: "Types",
search: "Search...",
no_tables: "No tables",
no_tables_text: "Start building your diagram!",
no_relationships: "No relationships",
no_relationships_text: "Drag to connect fields and form relationships!",
no_subject_areas: "No subject areas",
no_subject_areas_text: "Add subject areas to group tables!",
no_notes: "No notes",
no_notes_text: "Use notes to record extra info",
no_types: "No types",
no_types_text: "Make your own custom data types",
no_issues: "No issues were detected.",
strict_mode_is_on_no_issues:
"Strict mode is off so no issues will be displayed.",
name: "Name",
type: "Type",
null: "Null",
not_null: "Not null",
primary: "Primary",
unique: "Unique",
autoincrement: "Autoincrement",
default_value: "Default",
check: "Check expression",
this_will_appear_as_is: "*This will appear in the generated script as is.",
comment: "Comment",
add_field: "Add field",
values: "values",
size: "Size",
precision: "Precision",
set_precision: "Set precision: (size, digits)",
use_for_batch_input: "Use , for batch input",
indices: "Indices",
add_index: "Add index",
select_fields: "Select fields",
title: "Title",
not_set: "Not set",
foreign: "Foreign",
cardinality: "Cardinality",
on_update: "On update",
on_delete: "On delete",
swap: "Swap",
one_to_one: "One to one",
one_to_many: "One to many",
many_to_one: "Many to one",
content: "Content",
types_info:
"This feature is meant for object-relational DBMSs like PostgreSQL.\nIf used for MySQL or MariaDB a JSON type will be generated with the corresponding json validation check.\nIf used for SQLite it will be translated to a BLOB.\nIf used for MSSQL a type alias to the first field will be generated.",
table_deleted: "Table deleted",
area_deleted: "Area deleted",
note_deleted: "Note deleted",
relationship_deleted: "Relationship deleted",
type_deleted: "Type deleted",
cannot_connect: "Cannot connect, the columns have different types",
copied_to_clipboard: "Copied to clipboard",
create_new_diagram: "Create new diagram",
cancel: "Cancel",
open_diagram: "Open diagram",
rename_diagram: "Rename diagram",
export: "Export",
export_image: "Export image",
create: "Create",
confirm: "Confirm",
last_modified: "Last modified",
drag_and_drop_files: "Drag and drop the file here or click to upload.",
support_json_and_ddb: "JSON and DDB files are supported",
upload_sql_to_generate_diagrams:
"Upload an sql file to autogenerate your tables and columns.",
overwrite_existing_diagram: "Overwrite existing diagram",
only_mysql_supported:
"*For the time being loading only MySQL scripts is supported.",
blank: "Blank",
filename: "Filename",
table_w_no_name: "Declared a table with no name",
duplicate_table_by_name: "Duplicate table by the name '{{tableName}}'",
empty_field_name: "Empty field `name` in table '{{tableName}}'",
empty_field_type: "Empty field `type` in table '{{tableName}}'",
no_values_for_field:
"'{{fieldName}}' field of table '{{tableName}}' is of type `{{type}}` but no values have been specified",
default_doesnt_match_type:
"Default value for field '{{fieldName}}' in table '{{table.name}}' does not match its type",
not_null_is_null:
"'{{fieldName}}' field of table '{{tableName}}' is NOT NULL but has default NULL",
duplicate_fields:
"Duplicate table fields by name '{{fieldName}}' in table '{{tableName}}'",
duplicate_index:
"Duplicate index by name '{{indexName}}' in table '{{tableName}}'",
empty_index: "Index in table '{{tableName}}' indexes no columns",
no_primary_key: "Table '{{tableName}}' has no primary key",
type_with_no_name: "Declared a type with no name",
duplicate_types: "Duplicate types by the name '{{typeName}}'",
type_w_no_fields: "Declared an empty type '{{typeName}}'with no fields",
empty_type_field_name: "Empty field `name` in type '{{typeName}}'",
empty_type_field_type: "Empty field `type` in type '{{typeName}}'",
no_values_for_type_field:
"'{{fieldName}}' field of type '{{typeName}}' is of type `{{type}}` but no values have been specified",
duplicate_type_fields:
"Duplicate type fields by name '{{fieldName}}' in type '{{typeName}}'",
duplicate_reference: "Duplicate reference by the name '{{refName}}'",
circular_dependency: "Circular dependency involvind table '{{refName}}'",
timeline: "Timeline",
priority: "Priority",
none: "None",
low: "Low",
medium: "Medium",
high: "High",
sort_by: "Sort by",
my_order: "My order",
completed: "Completed",
alphabetically: "Alphabetically",
add_task: "Add task",
details: "Details",
no_tasks: "You have no tasks yet.",
no_activity: "You have no activity yet.",
move_element: "Move {{name}} to {{coords}}",
edit_area: "{{extra}} Edit area {{areaName}}",
delete_area: "Delete area {{areaName}}",
edit_note: "{{extra}} Edit note {{noteTitle}}",
delete_note: "Delete note {{noteTitle}}",
edit_table: "{{extra}} Edit table {{tableName}}",
delete_table: "Delete table {{tableName}}",
edit_type: "{{extra}} Edit type {{typeName}}",
delete_type: "Delete type {{typeName}}",
add_relationship: "Add relationship",
edit_relationship: "{{extra}} Edit relationship {{refName}}",
delete_relationship: "Delete relationship {{refName}}",
not_found: "Not found",
},
};
export { en, english };

218
src/i18n/locales/es.js Normal file
View File

@ -0,0 +1,218 @@
const spanish = {
name: "Spanish",
native_name: "Español",
code: "es",
};
const es = {
translation: {
report_bug: "Reportar Error",
import: "Importar",
file: "Archivo ",
new: "Nuevo",
new_window: "Nueva Ventana",
open: "Abrir",
save: "Guardar",
save_as: "Guardar como",
save_as_template: "Guardar como plantilla",
template_saved: "Guardado de plantilla!",
rename: "Renombrar",
delete_diagram: "Eliminar diagrama",
are_you_sure_delete_diagram:
"Estás seguro de que quieres eliminar este diagrama? Esta operación es irreversible.",
oops_smth_went_wrong: "Oops! Algo salió mal.",
import_diagram: "Importar diagrama",
import_from_source: "Importar desde fuente",
export_as: "Exportar como",
export_source: "Exportar fuente",
models: "Modelos",
exit: "Salir",
edit: "Editar",
undo: "Deshacer",
redo: "Rehacer",
clear: "limpiar",
are_you_sure_clear:
"Estás seguro de que quieres borrar el diagrama? Esto es irreversible.",
cut: "cortar",
copy: "Copiar",
paste: "Pegar",
duplicate: "Duplicar",
delete: "Eliminar",
copy_as_image: "Copiar como imagen",
view: "Ver",
header: "Encabezado",
sidebar: "Barra lateral",
issues: "Problemas",
presentation_mode: "Modo de presentación",
strict_mode: "Modo estricto",
field_details: "Detalles del campo",
reset_view: "Restablecer vista",
show_grid: "Mostrar cuadrícula",
show_cardinality: "Mostrar cardinalidad",
theme: "Tema",
light: "Claro",
dark: "Oscuro",
zoom_in: "Acercar",
zoom_out: "Alejar",
fullscreen: "Pantalla completa",
settings: "Configuraciones",
show_timeline: "Mostrar línea de tiempo",
autosave: "Guardado automático",
panning: "Desplazamiento",
table_width: "Ancho de la tabla",
language: "Idioma",
flush_storage: "Vaciar almacenamiento",
are_you_sure_flush_storage:
"Estás seguro de que quieres vaciar el almacenamiento? Esta operación es irreversible.",
storage_flushed: "Almacenamiento vaciado!",
help: "Ayuda",
shortcuts: "Atajos",
ask_on_discord: "Pregúntanos en Discord",
feedback: "Retroalimentación",
no_changes: "Sin cambios",
loading: "Cargando...",
last_saved: "Último guardado",
saving: "Guardando... ",
failed_to_save: "Error al guardar",
fit_window_reset: "Ajustar ventana / Restablecer",
zoom: "Zoom",
add_table: "Añadir tabla",
add_area: "Añadir área",
add_note: "Añadir nota",
add_type: "Añadir tipo",
to_do: "Por hacer",
tables: "Tablas",
relationships: "Relaciones",
subject_areas: "Áreas de tema",
notes: "Notas",
types: "Tipos",
search: "Buscar...",
no_tables: "Sin tablas",
no_tables_text: "¡Comienza a construir tu diagrama!",
no_relationships: "Sin relaciones",
no_relationships_text: "¡Añade relaciones entre tablas!",
no_subject_areas: "Sin áreas de tema",
no_subject_areas_text: "¡Añade áreas de tema!",
no_notes: "Sin notas",
no_notes_text: "¡Añade notas!",
no_types: "Sin tipos",
no_types_text: " ¡Añade tipos!",
no_issues: " Sin problemas",
strict_mode_is_on_no_issues:
"El modo estricto está activado y no hay problemas.",
name: "Nombre",
type: "Tipo",
null: "Nulo",
not_null: "No nulo",
primary: "Primario",
unique: "Único",
autoincrement: "Autoincremental",
default_value: "Valor predeterminado",
check: "Expresión de verificación",
this_will_appear_as_is: "*Esto aparecerá en el script generado tal cual.",
comment: "Comentario",
add_field: "Agregar campo",
values: "valores",
size: "Tamaño",
precision: "Precisión",
set_precision: "Establecer precisión: (tamaño, dígitos)",
use_for_batch_input: "Usar, para entrada por lotes",
indices: "Índices",
add_index: "Agregar índice",
select_fields: "Seleccionar campos",
title: "Título",
not_set: "No establecido",
foreign: "Extranjero",
cardinality: "Cardinalidad",
on_update: "Al actualizar",
on_delete: "Al eliminar",
swap: "Intercambiar",
one_to_one: "Uno a uno",
one_to_many: "Uno a muchos",
many_to_one: "Muchos a uno",
content: "Contenido",
types_info:
"Esta característica está destinada a DBMSs objeto-relacionales como PostgreSQL.\nSi se usa para MySQL o MariaDB, se generará un tipo JSON con la verificación de validación json correspondiente.\nSi se usa para SQLite, se traducirá a un BLOB.\nSi se usa para MSSQL, se generará un alias de tipo al primer campo.",
table_deleted: "Tabla eliminada",
area_deleted: "Área eliminada",
note_deleted: "Nota eliminada",
relationship_deleted: "Relación eliminada",
type_deleted: "Tipo eliminado",
cannot_connect: "No se puede conectar, las columnas tienen diferentes tipos",
copied_to_clipboard: "Copiado al portapapeles",
create_new_diagram: "Crear nuevo diagrama",
cancel: "Cancelar",
open_diagram: "Abrir diagrama",
rename_diagram: "Renombrar diagrama",
export: "Exportar",
export_image: "Exportar imagen",
create: "Crear",
confirm: "Confirmar",
last_modified: "Última modificación",
drag_and_drop_files: "Arrastra y suelta el archivo aquí o haz clic para subir.",
support_json_and_ddb: "Se admiten archivos JSON y DDB",
upload_sql_to_generate_diagrams:
"Sube un archivo sql para autogenerar tus tablas y columnas.",
overwrite_existing_diagram: "Sobrescribir diagrama existente",
only_mysql_supported:
"*Por el momento, solo se admite la carga de scripts de MySQL.",
blank: "En blanco",
filename: "Nombre del archivo",
table_w_no_name: "Declarada una tabla sin nombre",
duplicate_table_by_name: "Tabla duplicada con el nombre '{{tableName}}'",
empty_field_name: "Campo `name` vacío en la tabla '{{tableName}}'",
empty_field_type: "Campo `type` vacío en la tabla '{{tableName}}'",
no_values_for_field:
"El campo '{{fieldName}}' de la tabla '{{tableName}}' es de tipo `{{type}}` pero no se han especificado valores",
default_doesnt_match_type:
"El valor predeterminado para el campo '{{fieldName}}' en la tabla '{{table.name}}' no coincide con su tipo",
not_null_is_null:
"El campo '{{fieldName}}' de la tabla '{{tableName}}' es NOT NULL pero tiene NULL por defecto",
duplicate_fields:
"Campos de tabla duplicados por nombre '{{fieldName}}' en la tabla '{{tableName}}'",
duplicate_index:
"Índice duplicado por nombre '{{indexName}}' en la tabla '{{tableName}}'",
empty_index: "Índice en la tabla '{{tableName}}' no indexa columnas",
no_primary_key: "La tabla '{{tableName}}' no tiene clave primaria",
type_with_no_name: "Declarado un tipo sin nombre",
duplicate_types: "Tipos duplicados con el nombre '{{typeName}}'",
type_w_no_fields: "Declarado un tipo vacío '{{typeName}}' sin campos",
empty_type_field_name: "Campo `name` vacío en el tipo '{{typeName}}'",
empty_type_field_type: "Campo `type` vacío en el tipo '{{typeName}}'",
no_values_for_type_field:
"El campo '{{fieldName}}' del tipo '{{typeName}}' es de tipo `{{type}}` pero no se han especificado valores",
duplicate_type_fields:
"Campos de tipo duplicados por nombre '{{fieldName}}' en el tipo '{{typeName}}'",
duplicate_reference: "Referencia duplicada con el nombre '{{refName}}'",
circular_dependency: "Dependencia circular involucrando la tabla '{{refName}}'",
timeline: "Linea del tiempo",
priority: "Prioridad",
none: "Ninguno",
low: "Bajo",
medium: "Medio",
high: "Alto",
sort_by: "Ordenar por",
my_order: "Mi orden",
completed: "Completado",
alphabetically: "Alfabéticamente",
add_task: "Agregar tarea",
details: "Detalles",
no_tasks: "Aún no tienes tareas.",
no_activity: "Aún no tienes actividad.",
move_element: "Mover {{name}} a {{coords}}",
edit_area: "{{extra}} Editar área {{areaName}}",
delete_area: "Eliminar área {{areaName}}",
edit_note: "{{extra}} Editar nota {{noteTitle}}",
delete_note: "Eliminar nota {{noteTitle}}",
edit_table: "{{extra}} Editar tabla {{tableName}}",
delete_table: "Eliminar tabla {{tableName}}",
edit_type: "{{extra}} Editar tipo {{typeName}}",
delete_type: "Eliminar tipo {{typeName}}",
add_relationship: "Agregar relación",
edit_relationship: "{{extra}} Editar relación {{refName}}",
delete_relationship: "Eliminar relación {{refName}}",
not_found: "No encontrado",
},
};
export { es, spanish };

213
src/i18n/locales/zh.js Normal file
View File

@ -0,0 +1,213 @@
const chinese = {
name: "Simplified Chinese",
native_name: "简体中文",
code: "zh",
};
const zh = {
translation: {
report_bug: "报告问题",
import: "导入",
file: "文件",
new: "新建",
new_window: "在新标签中打开",
open: "打开",
save: "保存",
save_as: "另存为",
save_as_template: "保存为模板",
template_saved: "模板已保存!",
rename: "重命名",
delete_diagram: "删除图表",
are_you_sure_delete_diagram: "确定要删除此图表吗?此操作不可逆转。",
oops_smth_went_wrong: "糟糕!出了些问题。",
import_diagram: "导入图表",
import_from_source: "导入 SQL 源代码",
export_as: "导出为",
export_source: "导出为 SQL 源代码",
models: "模型",
exit: "退出",
edit: "编辑",
undo: "撤销",
redo: "恢复",
clear: "清除",
are_you_sure_clear: "确定要清除图表吗?此操作不可逆转。",
cut: "剪切",
copy: "复制",
paste: "粘贴",
duplicate: "克隆",
delete: "删除",
copy_as_image: "复制画布为图片",
view: "视图",
header: "菜单栏",
sidebar: "侧边栏",
issues: "问题",
presentation_mode: "演示模式",
strict_mode: "严格模式",
field_details: "字段详情",
reset_view: "重置视图",
show_grid: "显示网格",
show_cardinality: "显示关系",
theme: "主题",
light: "浅色",
dark: "深色",
zoom_in: "放大",
zoom_out: "缩小",
fullscreen: "全屏",
settings: "设置",
show_timeline: "修改记录",
autosave: "自动保存",
panning: "画布可拖动",
table_width: "表格宽度",
language: "语言",
flush_storage: "清除存储",
are_you_sure_flush_storage:
"您确定要清除存储吗?此操作将无法恢复地删除您所有的图表和自定义模板。",
storage_flushed: "存储已清空",
help: "帮助",
shortcuts: "快捷键",
ask_on_discord: "在 Discord 联系我们",
feedback: "反馈",
no_changes: "没有更改",
loading: "加载中...",
last_saved: "上次保存",
saving: "保存中...",
failed_to_save: "保存失败",
fit_window_reset: "适应窗口/重置",
zoom: "缩放",
add_table: "添加表",
add_area: "添加区域",
add_note: "添加注释",
add_type: "添加类型",
to_do: "待办事项",
tables: "表",
relationships: "关系",
subject_areas: "主题区域",
notes: "注释",
types: "类型",
search: "搜索...",
no_tables: "空空如也",
no_tables_text: "开始构建您的图表!",
no_relationships: "空空如也",
no_relationships_text: "拖动以连接字段并形成关系!",
no_subject_areas: "空空如也",
no_subject_areas_text: "添加主题区域以分组表!",
no_notes: "空空如也",
no_notes_text: "使用注释记录额外信息",
no_types: "空空如也",
no_types_text: "制作您自己的自定义数据类型",
no_issues: "未检测到问题。",
strict_mode_is_on_no_issues: "严格模式已关闭,因此不会显示任何问题。",
name: "名称",
type: "类型",
null: "空",
not_null: "非空",
primary: "主键",
unique: "唯一",
autoincrement: "自增",
default_value: "默认值",
check: "检查表达式",
this_will_appear_as_is: "*此内容将按原样显示在生成的脚本中。",
comment: "评论",
add_field: "添加字段",
values: "值",
size: "大小",
precision: "精度",
set_precision: "设置精度:(大小,位数)",
use_for_batch_input: "用于批量输入,使用逗号",
indices: "索引",
add_index: "添加索引",
select_fields: "选择字段",
title: "标题",
not_set: "未设置",
foreign: "外键",
cardinality: "关系映射",
on_update: "更新时",
on_delete: "删除时",
swap: "交换",
one_to_one: "一对一",
one_to_many: "一对多",
many_to_one: "多对一",
content: "内容",
types_info:
"此功能适用于像 PostgreSQL 这样的对象关系型数据库管理系统。\n如果用于 MySQL 或 MariaDB将生成具有相应 JSON 验证检查的 JSON 类型。\n如果用于 SQLite它将被转换为 BLOB。\n如果用于 MSSQL将生成到第一个字段的类型别名。",
table_deleted: "表已删除",
area_deleted: "区域已删除",
note_deleted: "注释已删除",
relationship_deleted: "关系已删除",
type_deleted: "类型已删除",
cannot_connect: "无法连接,列具有不同的类型",
copied_to_clipboard: "已复制到剪贴板",
create_new_diagram: "创建新图表",
cancel: "取消",
open_diagram: "打开图表",
rename_diagram: "重命名图表",
export: "导出",
export_image: "导出图像",
create: "创建",
confirm: "确认",
last_modified: "最后修改",
drag_and_drop_files: "拖放文件到此处或点击上传。",
support_json_and_ddb: "支持 JSON 和 DDB 文件",
upload_sql_to_generate_diagrams: "上传 SQL 文件以自动生成表和列。",
overwrite_existing_diagram: "覆盖现有图表",
only_mysql_supported: "目前仅支持加载 MySQL 脚本。",
blank: "空",
filename: "文件名",
table_w_no_name: "声明了一个没有名称的表",
duplicate_table_by_name: "重复声明了名为 '{{tableName}}' 的表",
empty_field_name: "表 '{{tableName}}' 中的字段 `name` 为空",
empty_field_type: "表 '{{tableName}}' 中的字段 `type` 为空",
no_values_for_field:
"表 '{{tableName}}' 的 '{{fieldName}}' 字段类型为 `{{type}}`,但未指定任何值",
default_doesnt_match_type:
"表 '{{table.name}}' 中字段 '{{fieldName}}' 的默认值与其类型不匹配",
not_null_is_null:
"表 '{{tableName}}' 中的 '{{fieldName}}' 字段为 NOT NULL但默认值为 NULL",
duplicate_fields:
"在表 '{{tableName}}' 中重复声明了名为 '{{fieldName}}' 的字段",
duplicate_index:
"在表 '{{tableName}}' 中重复声明了名为 '{{indexName}}' 的索引",
empty_index: "在表 '{{tableName}}' 中的索引未指定任何列",
no_primary_key: "表 '{{tableName}}' 没有主键",
type_with_no_name: "声明了一个没有名称的类型",
duplicate_types: "重复声明了名为 '{{typeName}}' 的类型",
type_w_no_fields: "声明了一个没有字段的空类型 '{{typeName}}'",
empty_type_field_name: "类型 '{{typeName}}' 中的字段 `name` 为空",
empty_type_field_type: "类型 '{{typeName}}' 中的字段 `type` 为空",
no_values_for_type_field:
"类型 '{{typeName}}' 的 '{{fieldName}}' 字段类型为 `{{type}}`,但未指定任何值",
duplicate_type_fields:
"在自定义类 '{{typeName}}' 中重复声明了名为 '{{fieldName}}' 的字段",
duplicate_reference: "重复声明了名为 '{{refName}}' 的引用",
circular_dependency: "涉及到表 '{{refName}}' 的循环依赖",
timeline: "时间轴",
priority: "优先级",
none: "无",
low: "低",
medium: "中",
high: "高",
sort_by: "排序方式",
my_order: "我的排序",
completed: "已完成",
alphabetically: "按字母顺序",
add_task: "添加任务",
details: "详情",
no_tasks: "您还没有任务。",
no_activity: "您还没有活动。",
move_element: "将 {{name}} 移动到 {{coords}}",
edit_area: "{{extra}} 编辑区域 {{areaName}}",
delete_area: "删除区域 {{areaName}}",
edit_note: "{{extra}} 编辑注释 {{noteTitle}}",
delete_note: "删除注释 {{noteTitle}}",
edit_table: "{{extra}} 编辑表格 {{tableName}}",
delete_table: "删除表格 {{tableName}}",
edit_type: "{{extra}} 编辑类型 {{typeName}}",
delete_type: "删除类型 {{typeName}}",
add_relationship: "添加关系",
edit_relationship: "{{extra}} 编辑关系 {{refName}}",
delete_relationship: "删除关系 {{refName}}",
not_found: "未找到",
},
};
export { zh, chinese };

View File

@ -4,11 +4,12 @@ import { Analytics } from "@vercel/analytics/react";
import App from "./App.jsx"; import App from "./App.jsx";
import en_US from "@douyinfe/semi-ui/lib/es/locale/source/en_US"; import en_US from "@douyinfe/semi-ui/lib/es/locale/source/en_US";
import "./index.css"; import "./index.css";
import "./i18n/i18n.js";
const root = ReactDOM.createRoot(document.getElementById("root")); const root = ReactDOM.createRoot(document.getElementById("root"));
root.render( root.render(
<LocaleProvider locale={en_US}> <LocaleProvider locale={en_US}>
<App /> <App />
<Analytics /> <Analytics />
</LocaleProvider> </LocaleProvider>,
); );

View File

@ -1,3 +1,4 @@
import i18n from "../i18n/i18n";
import { isFunction, strHasQuotes } from "./utils"; import { isFunction, strHasQuotes } from "./utils";
function validateDateStr(str) { function validateDateStr(str) {
@ -88,11 +89,11 @@ export function getIssues(diagram) {
diagram.tables.forEach((table) => { diagram.tables.forEach((table) => {
if (table.name === "") { if (table.name === "") {
issues.push(`Declared a table with no name`); issues.push(i18n.t("table_w_no_name"));
} }
if (duplicateTableNames[table.name]) { if (duplicateTableNames[table.name]) {
issues.push(`Duplicate table by the name "${table.name}"`); issues.push(i18n.t("duplicate_table_by_name", { tableName: table.name }));
} else { } else {
duplicateTableNames[table.name] = true; duplicateTableNames[table.name] = true;
} }
@ -105,33 +106,48 @@ export function getIssues(diagram) {
hasPrimaryKey = true; hasPrimaryKey = true;
} }
if (field.name === "") { if (field.name === "") {
issues.push(`Empty field name in table "${table.name}"`); issues.push(i18n.t("empty_field_name", { tableName: table.name }));
} }
if (field.type === "") { if (field.type === "") {
issues.push(`Empty field type in table "${table.name}"`); issues.push(i18n.t("empty_field_type", { tableName: table.name }));
} else if (field.type === "ENUM" || field.type === "SET") { } else if (field.type === "ENUM" || field.type === "SET") {
if (!field.values || field.values.length === 0) { if (!field.values || field.values.length === 0) {
issues.push( issues.push(
`"${field.name}" field of table "${table.name}" is of type ${field.type} but no values have been specified`, i18n.t("no_values_for_field", {
tableName: table.name,
fieldName: field.name,
type: field.type,
}),
); );
} }
} }
if (!checkDefault(field)) { if (!checkDefault(field)) {
issues.push( issues.push(
`Default value for field "${field.name}" in table "${table.name}" does not match its type.`, i18n.t("default_doesnt_match_type", {
tableName: table.name,
fieldName: field.name,
}),
); );
} }
if (field.notNull && field.default.toLowerCase() === "null") { if (field.notNull && field.default.toLowerCase() === "null") {
issues.push( issues.push(
`"${field.name}" field of table "${table.name}" is NOT NULL but has default NULL`, i18n.t("not_null_is_null", {
tableName: table.name,
fieldName: field.name,
}),
); );
} }
if (duplicateFieldNames[field.name]) { if (duplicateFieldNames[field.name]) {
issues.push(`Duplicate table fields in table "${table.name}"`); issues.push(
i18n.t("duplicate_fields", {
tableName: table.name,
fieldName: field.name,
}),
);
} else { } else {
duplicateFieldNames[field.name] = true; duplicateFieldNames[field.name] = true;
} }
@ -140,7 +156,12 @@ export function getIssues(diagram) {
const duplicateIndices = {}; const duplicateIndices = {};
table.indices.forEach((index) => { table.indices.forEach((index) => {
if (duplicateIndices[index.name]) { if (duplicateIndices[index.name]) {
issues.push(`Duplicate index by the name "${index.name}"`); issues.push(
i18n.t("duplicate_index", {
tableName: table.name,
indexName: index.name,
}),
);
} else { } else {
duplicateIndices[index.name] = true; duplicateIndices[index.name] = true;
} }
@ -148,50 +169,69 @@ export function getIssues(diagram) {
table.indices.forEach((index) => { table.indices.forEach((index) => {
if (index.fields.length === 0) { if (index.fields.length === 0) {
issues.push(`Empty index type in table "${table.name}"`); issues.push(
i18n.t("empty_index", {
tableName: table.name,
}),
);
} }
}); });
if (!hasPrimaryKey) { if (!hasPrimaryKey) {
issues.push(`Table "${table.name}" has no primary key`); issues.push(i18n.t("no_primary_key", { tableName: table.name }));
} }
}); });
const duplicateTypeNames = {}; const duplicateTypeNames = {};
diagram.types.forEach((type) => { diagram.types.forEach((type) => {
if (type.name === "") { if (type.name === "") {
issues.push(`Declared a type with no name`); issues.push(i18n.t("type_with_no_name"));
} }
if (duplicateTypeNames[type.name]) { if (duplicateTypeNames[type.name]) {
issues.push(`Duplicate types by the name "${type.name}"`); issues.push(i18n.t("duplicate_types", { typeName: type.name }));
} else { } else {
duplicateTypeNames[type.name] = true; duplicateTypeNames[type.name] = true;
} }
if (type.fields.length === 0) { if (type.fields.length === 0) {
issues.push(`Declared an empty type "${type.name}" with no fields`); issues.push(i18n.t("type_w_no_fields", { typeName: type.name }));
return; return;
} }
const duplicateFieldNames = {}; const duplicateFieldNames = {};
type.fields.forEach((field) => { type.fields.forEach((field) => {
if (field.name === "") { if (field.name === "") {
issues.push(`Empty field name in type "${type.name}"`); issues.push(
i18n.t("empty_type_field_name", {
typeName: type.name,
}),
);
} }
if (field.type === "") { if (field.type === "") {
issues.push(`Empty field type in "${type.name}"`); issues.push(
i18n.t("empty_type_field_type", {
typeName: type.name,
}),
);
} else if (field.type === "ENUM" || field.type === "SET") { } else if (field.type === "ENUM" || field.type === "SET") {
if (!field.values || field.values.length === 0) { if (!field.values || field.values.length === 0) {
issues.push( issues.push(
`"${field.name}" field of type "${type.name}" is of type ${field.type} but no values have been specified`, i18n.t("no_values_for_type_field", {
typeName: type.name,
fieldName: field.name,
type: field.type,
}),
); );
} }
} }
if (duplicateFieldNames[field.name]) { if (duplicateFieldNames[field.name]) {
issues.push(`Duplicate type fields in "${type.name}"`); i18n.t("duplicate_type_fields", {
typeName: type.name,
fieldName: field.name,
});
} else { } else {
duplicateFieldNames[field.name] = true; duplicateFieldNames[field.name] = true;
} }
@ -201,22 +241,14 @@ export function getIssues(diagram) {
const duplicateFKName = {}; const duplicateFKName = {};
diagram.relationships.forEach((r) => { diagram.relationships.forEach((r) => {
if (duplicateFKName[r.name]) { if (duplicateFKName[r.name]) {
issues.push(`Duplicate reference by the name "${r.name}"`); issues.push(
i18n.t("duplicate_reference", {
refName: r.name,
}),
);
} else { } else {
duplicateFKName[r.name] = true; duplicateFKName[r.name] = true;
} }
if (
diagram.tables[r.startTableId].fields[r.startFieldId].type !==
diagram.tables[r.endTableId].fields[r.endFieldId].type
) {
issues.push(`Referencing column "${
diagram.tables[r.endTableId].fields[r.endFieldId].name
}" and referenced column "${
diagram.tables[r.startTableId].fields[r.startFieldId].name
}" are incompatible.
`);
}
}); });
const visitedTables = new Set(); const visitedTables = new Set();
@ -224,7 +256,9 @@ export function getIssues(diagram) {
function checkCircularRelationships(tableId, visited = []) { function checkCircularRelationships(tableId, visited = []) {
if (visited.includes(tableId)) { if (visited.includes(tableId)) {
issues.push( issues.push(
`Circular relationship involving table: "${diagram.tables[tableId].name}"`, i18n.t("circular_dependency", {
refName: diagram.tables[tableId].name,
}),
); );
return; return;
} }

View File

@ -1,24 +1,27 @@
import { MODAL } from "../data/constants"; import { MODAL } from "../data/constants";
import i18n from "../i18n/i18n";
export const getModalTitle = (modal) => { export const getModalTitle = (modal) => {
switch (modal) { switch (modal) {
case MODAL.IMPORT: case MODAL.IMPORT:
case MODAL.IMPORT_SRC: case MODAL.IMPORT_SRC:
return "Import diagram"; return i18n.t("import_diagram");
case MODAL.CODE: case MODAL.CODE:
return "Export source"; return i18n.t("export_source");
case MODAL.IMG: case MODAL.IMG:
return "Export image"; return i18n.t("export_image");
case MODAL.RENAME: case MODAL.RENAME:
return "Rename diagram"; return i18n.t("rename_diagram");
case MODAL.OPEN: case MODAL.OPEN:
return "Open diagram"; return i18n.t("open_diagram");
case MODAL.SAVEAS: case MODAL.SAVEAS:
return "Save as"; return i18n.t("save_as");
case MODAL.NEW: case MODAL.NEW:
return "Create new diagram"; return i18n.t("create_new_diagram");
case MODAL.TABLE_WIDTH: case MODAL.TABLE_WIDTH:
return "Set the table width"; return i18n.t("table_width");
case MODAL.LANGUAGE:
return i18n.t("language");
default: default:
return ""; return "";
} }
@ -28,19 +31,19 @@ export const getOkText = (modal) => {
switch (modal) { switch (modal) {
case MODAL.IMPORT: case MODAL.IMPORT:
case MODAL.IMPORT_SRC: case MODAL.IMPORT_SRC:
return "Import"; return i18n.t("import");
case MODAL.CODE: case MODAL.CODE:
case MODAL.IMG: case MODAL.IMG:
return "Export"; return i18n.t("export");
case MODAL.RENAME: case MODAL.RENAME:
return "Rename"; return i18n.t("rename");
case MODAL.OPEN: case MODAL.OPEN:
return "Open"; return i18n.t("open");
case MODAL.SAVEAS: case MODAL.SAVEAS:
return "Save as"; return i18n.t("save_as");
case MODAL.NEW: case MODAL.NEW:
return "Create"; return i18n.t("create");
default: default:
return "Confirm"; return i18n.t("confirm");
} }
}; };