Compare commits

...

38 Commits

Author SHA1 Message Date
Guy Ben-Aharon
0530f9172c chore(main): release 1.2.0 (#396) 2024-11-17 11:55:44 +02:00
Twilight
e1e55c4b2a fix(i18n): add Nepali translations (#406)
* feat(i18n): add nepali locale

* feat(i18n): add nepali locale

* feat: add missing translations

* fix build + add type declaration

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2024-11-17 11:51:37 +02:00
ABHISHEK FADAKE
c6f7ff70f8 fix(i18n): Create Translations in Marathi language (#266)
* Create Translations in marathi language

* Remaining code for adding marathi  i18n support

* fix build

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2024-11-17 11:18:34 +02:00
Guy Ben-Aharon
02aaabdc4e fix(i18n): fix language nav: close when lang selected, hide tooltip when lang selected (#411) 2024-11-16 20:29:48 +02:00
Jonathan Fishner
0f673947af fix(templates): add five more templates (Sylius, Monica, Attendize, SaaS Pegasus & BookStack) (#408) 2024-11-16 17:06:23 +02:00
Guy Ben-Aharon
f35f62fdf3 fix(i18n): change language keeps selected language also after refreshing the page (#409) 2024-11-16 17:02:20 +02:00
Guy Ben-Aharon
68474e75d5 fix(export image): Add support for displaying cardinality relationships + background (#407) 2024-11-16 16:57:13 +02:00
Nic
eaf75cedb0 fix(dockerfile): support SPA refresh to resolve nginx return 404 (#384)
* feat(dockerfile): support SPA refresh to resolve nginx return 404

* remove comments & set server_name to default

---------

Co-authored-by: Guy Ben-Aharon <guybenah@gmail.com>
2024-11-14 20:00:14 +02:00
Guy Ben-Aharon
fe8b9f9e91 fix(templates): fix tags urls (#405) 2024-11-14 18:30:10 +02:00
Jonathan Fishner
07d3745747 fix(templates): add six more templates (ticketit, snipe-it, refinerycms, comfortable-mexican-sofa, buddypress, lobsters) (#402) 2024-11-14 17:42:04 +02:00
Guy Ben-Aharon
44cf5ca264 feat(duplicate table): duplicate table from the canvas and sidebar (#404)
* feat(duplicate table): duplicate table from the canvas and sidebar

* feat(duplicate table): underscore instead of space

* feat(duplicate table): underscore instead of space
2024-11-14 17:35:53 +02:00
Emmanuel Ferdman
44d10c2390 fix(docs): update license reference (#403)
Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
2024-11-14 17:26:20 +02:00
Guy Ben-Aharon
9698821828 export clone (diagram & table) logic (#401) 2024-11-14 15:11:21 +02:00
Guy Ben-Aharon
9f8500fc7e fix(templates): fix cloned indexes from a template (#398) 2024-11-13 18:58:50 +02:00
Jonathan Fishner
e5dbbf2eaa fix(AI exports): add cahching layer to SQL exports (#390)
* fix(AI exports): add cahching layer to SQL exports

* remove logs

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2024-11-13 17:18:27 +02:00
Guy Ben-Aharon
959e5402b8 fix(templates): tag urls lowercase to support browsers (#397) 2024-11-13 16:38:10 +02:00
Guy Ben-Aharon
492c9324d2 fix(canvas): fix auto zoom on diagram load (#395) 2024-11-13 16:17:03 +02:00
Guy Ben-Aharon
42c159605d chore(main): release 1.1.0 (#364) 2024-11-13 15:26:17 +02:00
Guy Ben-Aharon
78c427f38e fix(templates): fix issue with double-clone on localhost (#394) 2024-11-13 15:22:00 +02:00
Jonathan Fishner
bae74d1693 feat(add templates) add five more templates (gravity, koel.dev, laravel-permission, laravel-spark, voyager) (#392) 2024-11-13 13:24:01 +02:00
Ian Cheng
123f40f39e fix(i18n): added traditional Chinese language translation (#356)
* feat: added traditional chinese language translation

* feat: added traditional chinese language translation

---------

Co-authored-by: Jonathan Fishner <jonathanfishner11@gmail.com>
2024-11-12 17:26:42 +02:00
ntoniazzi
e3129cec74 fix(i18n): french translation update - share menu (#391) 2024-11-12 17:22:40 +02:00
Eva
5508c1e084 fix(i18n): Fixed part of RU lang introduced in #365 feat(share) (#380)
Co-authored-by: Jonathan Fishner <jonathanfishner11@gmail.com>
2024-11-12 13:43:36 +02:00
lkjhxx
9f2893319a fix(i18n): Add simplified chinese (#385)
* feat: add Simplified Chinese

* feat: add Simplified Chinese

* fix linter Update zh_CN.ts

---------

Co-authored-by: Jonathan Fishner <jonathanfishner11@gmail.com>
2024-11-12 13:23:57 +02:00
Guy Ben-Aharon
125a39fb5b fix(sql export): make loading for export interactive (#388) 2024-11-12 12:37:56 +02:00
Guy Ben-Aharon
4ca1832732 fix(bundle): fix bundle size (#382) 2024-11-11 01:13:44 +02:00
Guy Ben-Aharon
3609bfea4d fix(share): add loader to the export (#381)
* fix(share): add loader to the export

* fix(share): add loader to the export
2024-11-10 23:33:16 +02:00
Guy Ben-Aharon
94a5d84fae feat(share): add sharing capabilities to import and export diagrams (#365)
* feat(share): add sharing capabilities to import and export diagrams

* remove use client

* fix build

* add error parse indication

* add import from initial dialog

* fix build
2024-11-10 16:30:15 +02:00
Jonathan Fishner
85e691fcbe fix for tempalte name novel database (#379) 2024-11-10 09:02:51 +02:00
Daniel Cruz
709ccff8fa Adds missing spanish translations (#372)
fix(translations): Add missing Spanish translations
2024-11-09 20:06:10 +02:00
Elton Costa
6c7eb4609d feat(canvas): Added Snap to grid functionality. Toggle/hold shift to enable snap to grid. (#373)
* asd

* add translations & useKeyPress

* fix build

* fix build

---------

Co-authored-by: Guy Ben-Aharon <guybenah@gmail.com>
Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2024-11-09 19:55:40 +02:00
Eva
2c69b08eae fix(i18n): Added Russian language (#376)
* (i18n): Added Russian language.

* Update src/i18n/locales/ru.ts

Co-authored-by: Eva <29357907+nikelborm@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Eva <29357907+nikelborm@users.noreply.github.com>

* Refined and added missing fields to RU translation

* Refined and added missing fields to RU translation

---------

Co-authored-by: Aditya Kale <kaleaditya779@gmail.com>
Co-authored-by: Jonathan Fishner <jonathanfishner11@gmail.com>
2024-11-09 19:01:36 +02:00
Favor
84e7591d05 fix: improve title name edit interaction (#367)
* chore(main): improve table name edit interaction

* chore: fix lint issues

* feat(i18n): add double-click functionality and tooltip translations

- Change editable titles action to `onDoubleClick`
- Add tooltip translations for table name editing feature
- Support for 9 languages: Russian, Japanese, Hindi, French, Spanish,
  German, Ukrainian, Portuguese, and Korean
- Improve UX by indicating double-click edit functionality across languages

* naming + some padding

---------

Co-authored-by: Guy Ben-Aharon <guybenah@gmail.com>
2024-11-09 15:41:22 +02:00
orig
545e8578c9 fix(dockerfile): support openai key in docker build (#366) 2024-11-09 14:19:02 +02:00
Jonathan Fishner
f1d073d053 fix(templates): change the template url to be database instead of db (#374)
* fix(templates): change the template url to be database instead of db

* add tag

* layout fixes

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2024-11-09 14:02:29 +02:00
Jonathan Fishner
20b3396ec2 feat(add templates): add five more templates (laravel, django, twitter… (#371)
* feat(add templates): add five more templates (laravel, django, twitter, adonis-acl, akaunting)

* fix build

* fix tags

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2024-11-09 03:38:03 +02:00
☁️dungsil
b305be82ae fix(i18n): add korean (#362)
Co-authored-by: Jonathan Fishner <jonathanfishner11@gmail.com>
2024-11-07 16:27:24 +02:00
Jonathan Fishner
1430d2c236 fix(import json): for Check Script Result, default with quotes (#358) 2024-11-07 16:18:56 +02:00
131 changed files with 63437 additions and 788 deletions

View File

@@ -1,5 +1,57 @@
# Changelog
## [1.2.0](https://github.com/chartdb/chartdb/compare/v1.1.0...v1.2.0) (2024-11-17)
### Features
* **duplicate table:** duplicate table from the canvas and sidebar ([#404](https://github.com/chartdb/chartdb/issues/404)) ([44cf5ca](https://github.com/chartdb/chartdb/commit/44cf5ca264f52851f2dffb51a752a52b6fa7ec8d))
### Bug Fixes
* **AI exports:** add cahching layer to SQL exports ([#390](https://github.com/chartdb/chartdb/issues/390)) ([e5dbbf2](https://github.com/chartdb/chartdb/commit/e5dbbf2eaab6d80a531d451211b6f5a415bc7ce3))
* **canvas:** fix auto zoom on diagram load ([#395](https://github.com/chartdb/chartdb/issues/395)) ([492c932](https://github.com/chartdb/chartdb/commit/492c9324d27b561470c4967ce2e99f82eec467d8))
* **dockerfile:** support SPA refresh to resolve nginx return 404 ([#384](https://github.com/chartdb/chartdb/issues/384)) ([eaf75ce](https://github.com/chartdb/chartdb/commit/eaf75cedb0e024236c7684bb533856d7f80074da))
* **docs:** update license reference ([#403](https://github.com/chartdb/chartdb/issues/403)) ([44d10c2](https://github.com/chartdb/chartdb/commit/44d10c23907165288951a9d2ec3165ad23f81c61))
* **export image:** Add support for displaying cardinality relationships + background ([#407](https://github.com/chartdb/chartdb/issues/407)) ([68474e7](https://github.com/chartdb/chartdb/commit/68474e75d56ed4b4b445cc9b7f59cca96a4ca5db))
* **i18n:** add Nepali translations ([#406](https://github.com/chartdb/chartdb/issues/406)) ([e1e55c4](https://github.com/chartdb/chartdb/commit/e1e55c4b2ac7755b0810dc1f21da44903fe68a54))
* **i18n:** change language keeps selected language also after refreshing the page ([#409](https://github.com/chartdb/chartdb/issues/409)) ([f35f62f](https://github.com/chartdb/chartdb/commit/f35f62fdf38ca84065f171a31b80aa8123b1d8b9))
* **i18n:** Create Translations in Marathi language ([#266](https://github.com/chartdb/chartdb/issues/266)) ([c6f7ff7](https://github.com/chartdb/chartdb/commit/c6f7ff70f841efb9cf1766338f409fe0ea7bb998))
* **i18n:** fix language nav: close when lang selected, hide tooltip when lang selected ([#411](https://github.com/chartdb/chartdb/issues/411)) ([02aaabd](https://github.com/chartdb/chartdb/commit/02aaabdc4e9b1570d81ff03fe1e6da0307f22999))
* **templates:** add five more templates (Sylius, Monica, Attendize, SaaS Pegasus & BookStack) ([#408](https://github.com/chartdb/chartdb/issues/408)) ([0f67394](https://github.com/chartdb/chartdb/commit/0f673947af469e86f70737427ac8fb3c2420d1a2))
* **templates:** add six more templates (ticketit, snipe-it, refinerycms, comfortable-mexican-sofa, buddypress, lobsters) ([#402](https://github.com/chartdb/chartdb/issues/402)) ([07d3745](https://github.com/chartdb/chartdb/commit/07d374574775d132e1cba0908c47dcbbd6cd2c3f))
* **templates:** fix cloned indexes from a template ([#398](https://github.com/chartdb/chartdb/issues/398)) ([9f8500f](https://github.com/chartdb/chartdb/commit/9f8500fc7e36e6a819ecb9029f263d80eac88279))
* **templates:** fix tags urls ([#405](https://github.com/chartdb/chartdb/issues/405)) ([fe8b9f9](https://github.com/chartdb/chartdb/commit/fe8b9f9e91481d8a3272113b6f4be4da8d61ad04))
* **templates:** tag urls lowercase to support browsers ([#397](https://github.com/chartdb/chartdb/issues/397)) ([959e540](https://github.com/chartdb/chartdb/commit/959e5402b8c112fae6243ce9283947057506c128))
## [1.1.0](https://github.com/chartdb/chartdb/compare/v1.0.1...v1.1.0) (2024-11-13)
### Features
* **add templates:** add five more templates (laravel, django, twitter… ([#371](https://github.com/chartdb/chartdb/issues/371)) ([20b3396](https://github.com/chartdb/chartdb/commit/20b3396ec2afff09ca8bcdd91f5c6284c93cd959))
* **canvas:** Added Snap to grid functionality. Toggle/hold shift to enable snap to grid. ([#373](https://github.com/chartdb/chartdb/issues/373)) ([6c7eb46](https://github.com/chartdb/chartdb/commit/6c7eb4609d8466278de30317665929ec529c1f94))
* **share:** add sharing capabilities to import and export diagrams ([#365](https://github.com/chartdb/chartdb/issues/365)) ([94a5d84](https://github.com/chartdb/chartdb/commit/94a5d84fae819b0de6c1e471d1aad16dc8f39dd6))
### Bug Fixes
* **bundle:** fix bundle size ([#382](https://github.com/chartdb/chartdb/issues/382)) ([4ca1832](https://github.com/chartdb/chartdb/commit/4ca18327324106950f0d1af851b9b74379b67b7b))
* **dockerfile:** support openai key in docker build ([#366](https://github.com/chartdb/chartdb/issues/366)) ([545e857](https://github.com/chartdb/chartdb/commit/545e8578c9e8aa71696f6aa8bec81cacaa602c2d))
* **i18n:** add korean ([#362](https://github.com/chartdb/chartdb/issues/362)) ([b305be8](https://github.com/chartdb/chartdb/commit/b305be82aee00994ef576ca6fd62d72dd491f771))
* **i18n:** Add simplified chinese ([#385](https://github.com/chartdb/chartdb/issues/385)) ([9f28933](https://github.com/chartdb/chartdb/commit/9f2893319a1a2aed9a7c03d15e25a17ab37c2465))
* **i18n:** Added Russian language ([#376](https://github.com/chartdb/chartdb/issues/376)) ([2c69b08](https://github.com/chartdb/chartdb/commit/2c69b08eaea6b86ce0c1ddb18a23e22629198bf5))
* **i18n:** added traditional Chinese language translation ([#356](https://github.com/chartdb/chartdb/issues/356)) ([123f40f](https://github.com/chartdb/chartdb/commit/123f40f39e703ad612635964af530ac72c387d3c))
* **i18n:** Fixed part of RU lang introduced in [#365](https://github.com/chartdb/chartdb/issues/365) feat(share) ([#380](https://github.com/chartdb/chartdb/issues/380)) ([5508c1e](https://github.com/chartdb/chartdb/commit/5508c1e084e0ee24d1a54f721f760b9fc14df107))
* **i18n:** french translation update - share menu ([#391](https://github.com/chartdb/chartdb/issues/391)) ([e3129ce](https://github.com/chartdb/chartdb/commit/e3129cec744d18f09953544d9e74cd5adc4e8afb))
* **import json:** for Check Script Result, default with quotes ([#358](https://github.com/chartdb/chartdb/issues/358)) ([1430d2c](https://github.com/chartdb/chartdb/commit/1430d2c2365b7b74e36b8ff9d32a163d7437448a))
* improve title name edit interaction ([#367](https://github.com/chartdb/chartdb/issues/367)) ([84e7591](https://github.com/chartdb/chartdb/commit/84e7591d0586b9a457f31737c6e363ef41574142))
* **share:** add loader to the export ([#381](https://github.com/chartdb/chartdb/issues/381)) ([3609bfe](https://github.com/chartdb/chartdb/commit/3609bfea4d4c78b03711ff8d721b4e67bf82185a))
* **sql export:** make loading for export interactive ([#388](https://github.com/chartdb/chartdb/issues/388)) ([125a39f](https://github.com/chartdb/chartdb/commit/125a39fb5be803f0e6db0b68fb5bc8e290fa8dae))
* **templates:** change the template url to be database instead of db ([#374](https://github.com/chartdb/chartdb/issues/374)) ([f1d073d](https://github.com/chartdb/chartdb/commit/f1d073d05383955da6f60a9a66ed2be879b103e4))
* **templates:** fix issue with double-clone on localhost ([#394](https://github.com/chartdb/chartdb/issues/394)) ([78c427f](https://github.com/chartdb/chartdb/commit/78c427f38e5c64fc340d13ceb2153c2b85db437e))
## [1.0.1](https://github.com/chartdb/chartdb/compare/v1.0.0...v1.0.1) (2024-11-06)

View File

@@ -30,7 +30,7 @@ To get started:
### License
By contributing, you agree that your work will be licensed under ChartDB's [license](https://github.com/chartdb/chartdb/blob/main/LICENSE.md).
By contributing, you agree that your work will be licensed under ChartDB's [license](https://github.com/chartdb/chartdb/blob/main/LICENSE).
## Questions?

View File

@@ -1,5 +1,7 @@
FROM node:22-alpine AS builder
ARG VITE_OPENAI_API_KEY
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
@@ -14,6 +16,7 @@ RUN npm run build
FROM nginx:stable-alpine AS production
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
COPY ./default.conf /etc/nginx/conf.d/default.conf
# Expose the default port for the Nginx web server
EXPOSE 80

View File

@@ -97,7 +97,7 @@ VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> npm run build
### Running the Docker Container
```bash
docker build -t chartdb .
docker build -t chartdb . (If you want AI capabilities, use `docker build --build-arg VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -t chartdb .`)
docker run -p 8080:80 chartdb
```

15
default.conf Normal file
View File

@@ -0,0 +1,15 @@
server {
listen 80;
listen [::]:80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

18
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "chartdb",
"version": "1.0.1",
"version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chartdb",
"version": "1.0.1",
"version": "1.2.0",
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dnd-kit/sortable": "^8.0.0",
@@ -44,6 +44,7 @@
"fast-deep-equal": "^3.1.3",
"html-to-image": "^1.11.11",
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0",
"lucide-react": "^0.441.0",
"monaco-editor": "^0.52.0",
"nanoid": "^5.0.7",
@@ -60,7 +61,8 @@
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"timeago-react": "^3.0.6",
"vaul": "^0.9.1"
"vaul": "^0.9.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22.1.0",
@@ -6754,6 +6756,15 @@
"@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==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -10539,7 +10550,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.0.1",
"version": "1.2.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -48,6 +48,7 @@
"fast-deep-equal": "^3.1.3",
"html-to-image": "^1.11.11",
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0",
"lucide-react": "^0.441.0",
"monaco-editor": "^0.52.0",
"nanoid": "^5.0.7",
@@ -64,7 +65,8 @@
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"timeago-react": "^3.0.6",
"vaul": "^0.9.1"
"vaul": "^0.9.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22.1.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

View File

@@ -0,0 +1,62 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const alertVariants = cva(
'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: 'default',
},
}
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = 'Alert';
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn(
'mb-1 font-medium leading-none tracking-tight',
className
)}
{...props}
/>
));
AlertTitle.displayName = 'AlertTitle';
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-sm [&_p]:leading-relaxed', className)}
{...props}
/>
));
AlertDescription.displayName = 'AlertDescription';
export { Alert, AlertTitle, AlertDescription };

View File

@@ -16,6 +16,8 @@ export interface CodeSnippetProps {
code: string;
language?: 'sql' | 'shell';
loading?: boolean;
autoScroll?: boolean;
isComplete?: boolean;
}
export const Editor = lazy(() =>
@@ -25,7 +27,14 @@ export const Editor = lazy(() =>
);
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
({ className, code, loading, language = 'sql' }) => {
({
className,
code,
loading,
language = 'sql',
autoScroll = false,
isComplete = true,
}) => {
const { t } = useTranslation();
const monaco = useMonaco();
const { effectiveTheme } = useTheme();
@@ -47,6 +56,16 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
}, 1500);
}, [isCopied]);
useEffect(() => {
if (monaco) {
const editor = monaco.editor.getModels()[0];
if (editor && autoScroll) {
const lineCount = editor.getLineCount();
monaco.editor.getEditors()[0]?.revealLine(lineCount);
}
}
}, [code, monaco, autoScroll]);
const copyToClipboard = useCallback(() => {
navigator.clipboard.writeText(code);
setIsCopied(true);
@@ -63,32 +82,38 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
<Spinner />
) : (
<Suspense fallback={<Spinner />}>
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<TooltipTrigger
asChild
className="absolute right-1 top-1 z-10"
{isComplete ? (
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<span>
<Button
className=" h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t(isCopied ? 'copied' : 'copy_to_clipboard')}
</TooltipContent>
</Tooltip>
<TooltipTrigger
asChild
className="absolute right-1 top-1 z-10"
>
<span>
<Button
className=" h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t(
isCopied
? 'copied'
: 'copy_to_clipboard'
)}
</TooltipContent>
</Tooltip>
) : null}
<Editor
value={code}
@@ -118,6 +143,9 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
contextmenu: false,
}}
/>
{!isComplete ? (
<div className="absolute bottom-2 right-2 size-2 animate-blink rounded-full bg-pink-600" />
) : null}
</Suspense>
)}
</div>

View File

@@ -0,0 +1,168 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Upload, FileIcon, AlertCircle, Trash2 } from 'lucide-react';
import { Button } from '../button/button';
interface FileWithPreview extends File {
preview?: string;
}
export interface FileUploaderProps {
onFilesChange?: (files: File[]) => void;
multiple?: boolean;
supportedExtensions?: string[];
}
export const FileUploader: React.FC<FileUploaderProps> = ({
onFilesChange,
multiple,
supportedExtensions,
}) => {
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [isDragging, setIsDragging] = useState(false);
const [error, setError] = useState<string | null>(null);
const isFileSupported = useCallback(
(file: File) => {
if (!supportedExtensions) return true;
const fileExtension = file.name.split('.').pop()?.toLowerCase();
return fileExtension
? supportedExtensions.includes(`.${fileExtension}`)
: false;
},
[supportedExtensions]
);
const handleFiles = useCallback(
(selectedFiles: FileList) => {
const newFiles = Array.from(selectedFiles)
.filter((file) => {
if (!isFileSupported(file)) {
setError(
`File type not supported. Supported types: ${supportedExtensions?.join(', ')}`
);
return false;
}
return true;
})
.map((file) =>
Object.assign(file, { preview: URL.createObjectURL(file) })
);
if (newFiles.length === 0) return;
setError(null);
setFiles((prevFiles) => {
if (multiple) {
return [...prevFiles, ...newFiles];
} else {
return newFiles.slice(0, 1);
}
});
},
[multiple, supportedExtensions, isFileSupported]
);
const onDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(true);
}, []);
const onDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(false);
}, []);
const onDrop = useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(false);
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
handleFiles(e.dataTransfer.files);
}
},
[handleFiles]
);
useEffect(() => {
if (onFilesChange) {
onFilesChange(files.length > 0 ? files : []);
}
}, [files, onFilesChange]);
const removeFile = useCallback((fileToRemove: File) => {
setFiles((prevFiles) =>
prevFiles.filter((file) => file !== fileToRemove)
);
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
className={`cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors ${
isDragging
? 'border-primary bg-primary/10 dark:bg-primary/20'
: 'border-gray-300 hover:border-primary dark:border-gray-600 dark:hover:border-primary'
}`}
>
<input
type="file"
multiple={multiple}
onChange={(e) =>
e.target.files && handleFiles(e.target.files)
}
className="hidden"
id="fileInput"
accept={supportedExtensions?.join(',')}
/>
<label htmlFor="fileInput" className="cursor-pointer">
<Upload className="mx-auto size-12 text-gray-400 dark:text-gray-500" />
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
{multiple
? 'Drag and drop files here or click to select'
: 'Drag and drop a file here or click to select'}
</p>
{supportedExtensions ? (
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Supported types: {supportedExtensions.join(', ')}
</p>
) : null}
</label>
</div>
{error ? (
<div className="mt-4 flex items-center rounded-lg bg-red-100 p-3 text-red-700 dark:bg-red-900 dark:text-red-200">
<AlertCircle className="mr-2 size-5" />
<span className="text-sm">{error}</span>
</div>
) : null}
{files.length > 0 ? (
<ul className="mt-4 space-y-4">
{files.map((file) => (
<li
key={file.name}
className="flex items-center justify-between rounded-lg bg-gray-100 p-3 dark:bg-gray-800"
>
<div className="flex min-w-0 flex-1 items-center space-x-2">
<FileIcon className="size-5 text-primary" />
<span className="truncate text-sm font-medium text-gray-700 dark:text-gray-300">
{file.name}
</span>
</div>
<Button
variant="ghost"
className="size-5 p-0 hover:bg-primary-foreground"
onClick={() => removeFile(file)}
>
<Trash2 className="size-3.5 text-red-700" />
</Button>
</li>
))}
</ul>
) : null}
</div>
);
};

View File

@@ -34,7 +34,7 @@ export const ListMenu = React.forwardRef<HTMLDivElement, ListMenuProps>(
strokeWidth={item.selected ? 2.4 : 2}
/>
) : null}
{item.title}
<span className="min-w-0 truncate">{item.title}</span>
</Link>
))}
</div>

View File

@@ -26,7 +26,7 @@ export interface SelectBoxOption {
description?: string;
}
interface SelectBoxProps {
export interface SelectBoxProps {
options: SelectBoxOption[];
value?: string[] | string;
onChange?: (values: string[] | string) => void;

View File

@@ -5,6 +5,8 @@ import type { TableSchemaDialogProps } from '@/dialogs/table-schema-dialog/table
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog';
import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
export interface DialogContext {
// Create diagram dialog
@@ -48,6 +50,18 @@ export interface DialogContext {
params: Omit<ExportImageDialogProps, 'dialog'>
) => void;
closeExportImageDialog: () => void;
// Export diagram dialog
openExportDiagramDialog: (
params: Omit<ExportDiagramDialogProps, 'dialog'>
) => void;
closeExportDiagramDialog: () => void;
// Import diagram dialog
openImportDiagramDialog: (
params: Omit<ImportDiagramDialogProps, 'dialog'>
) => void;
closeImportDiagramDialog: () => void;
}
export const dialogContext = createContext<DialogContext>({
@@ -69,4 +83,8 @@ export const dialogContext = createContext<DialogContext>({
closeStarUsDialog: emptyFn,
openExportImageDialog: emptyFn,
closeExportImageDialog: emptyFn,
openExportDiagramDialog: emptyFn,
closeExportDiagramDialog: emptyFn,
openImportDiagramDialog: emptyFn,
closeImportDiagramDialog: emptyFn,
});

View File

@@ -17,6 +17,8 @@ import { emptyFn } from '@/lib/utils';
import { StarUsDialog } from '@/dialogs/star-us-dialog/star-us-dialog';
import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog';
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -86,6 +88,14 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
[setOpenTableSchemaDialog]
);
// Export image dialog
const [openExportDiagramDialog, setOpenExportDiagramDialog] =
useState(false);
// Import diagram dialog
const [openImportDiagramDialog, setOpenImportDiagramDialog] =
useState(false);
// Alert dialog
const [showAlert, setShowAlert] = useState(false);
const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({
@@ -126,6 +136,12 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
closeStarUsDialog: () => setOpenStarUsDialog(false),
closeExportImageDialog: () => setOpenExportImageDialog(false),
openExportImageDialog: openExportImageDialogHandler,
openExportDiagramDialog: () => setOpenExportDiagramDialog(true),
closeExportDiagramDialog: () =>
setOpenExportDiagramDialog(false),
openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
closeImportDiagramDialog: () =>
setOpenImportDiagramDialog(false),
}}
>
{children}
@@ -152,6 +168,8 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
dialog={{ open: openExportImageDialog }}
{...exportImageDialogParams}
/>
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
</dialogContext.Provider>
);
};

View File

@@ -5,12 +5,14 @@ import { toJpeg, toPng, toSvg } from 'html-to-image';
import { useReactFlow } from '@xyflow/react';
import { useChartDB } from '@/hooks/use-chartdb';
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
import { useTheme } from '@/hooks/use-theme';
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const { hideLoader, showLoader } = useFullScreenLoader();
const { setNodes, getViewport } = useReactFlow();
const { effectiveTheme } = useTheme();
const { diagramName } = useChartDB();
const downloadImage = useCallback(
@@ -59,13 +61,101 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
const imageCreateFn = imageCreatorMap[type];
setTimeout(async () => {
const dataUrl = await imageCreateFn(
window.document.querySelector(
'.react-flow__viewport'
) as HTMLElement,
{
const viewportElement = window.document.querySelector(
'.react-flow__viewport'
) as HTMLElement;
const markerDefs = document.querySelector(
'.marker-definitions defs'
);
const tempSvg = document.createElementNS(
'http://www.w3.org/2000/svg',
'svg'
);
tempSvg.style.position = 'absolute';
tempSvg.style.top = '0';
tempSvg.style.left = '0';
tempSvg.style.width = '100%';
tempSvg.style.height = '100%';
tempSvg.style.overflow = 'visible';
tempSvg.style.zIndex = '-50';
tempSvg.setAttribute(
'viewBox',
`0 0 ${reactFlowBounds.width} ${reactFlowBounds.height}`
);
const defs = document.createElementNS(
'http://www.w3.org/2000/svg',
'defs'
);
if (markerDefs) {
defs.innerHTML = markerDefs.innerHTML;
}
const pattern = document.createElementNS(
'http://www.w3.org/2000/svg',
'pattern'
);
pattern.setAttribute('id', 'background-pattern');
pattern.setAttribute('width', String(16 * viewport.zoom));
pattern.setAttribute('height', String(16 * viewport.zoom));
pattern.setAttribute('patternUnits', 'userSpaceOnUse');
pattern.setAttribute(
'patternTransform',
`translate(${viewport.x % (16 * viewport.zoom)} ${viewport.y % (16 * viewport.zoom)})`
);
const dot = document.createElementNS(
'http://www.w3.org/2000/svg',
'circle'
);
const dotSize = viewport.zoom * 0.5;
dot.setAttribute('cx', String(viewport.zoom));
dot.setAttribute('cy', String(viewport.zoom));
dot.setAttribute('r', String(dotSize));
const dotColor =
effectiveTheme === 'light' ? '#92939C' : '#777777';
dot.setAttribute('fill', dotColor);
pattern.appendChild(dot);
defs.appendChild(pattern);
tempSvg.appendChild(defs);
const backgroundRect = document.createElementNS(
'http://www.w3.org/2000/svg',
'rect'
);
const padding = 2000;
backgroundRect.setAttribute('x', String(-viewport.x - padding));
backgroundRect.setAttribute('y', String(-viewport.y - padding));
backgroundRect.setAttribute(
'width',
String(reactFlowBounds.width + 2 * padding)
);
backgroundRect.setAttribute(
'height',
String(reactFlowBounds.height + 2 * padding)
);
backgroundRect.setAttribute('fill', 'url(#background-pattern)');
tempSvg.appendChild(backgroundRect);
viewportElement.insertBefore(
tempSvg,
viewportElement.firstChild
);
try {
const dataUrl = await imageCreateFn(viewportElement, {
...(type === 'jpeg' || type === 'png'
? { backgroundColor: '#ffffff' }
? {
backgroundColor:
effectiveTheme === 'light'
? '#ffffff'
: '#141414',
}
: {}),
width: reactFlowBounds.width,
height: reactFlowBounds.height,
@@ -76,11 +166,13 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
},
quality: 1,
pixelRatio: scale,
}
);
});
downloadImage(dataUrl, type);
hideLoader();
downloadImage(dataUrl, type);
} finally {
viewportElement.removeChild(tempSvg);
hideLoader();
}
}, 0);
},
[
@@ -90,6 +182,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
imageCreatorMap,
setNodes,
showLoader,
effectiveTheme,
]
);

View File

@@ -10,6 +10,7 @@ import {
import { DatabaseType } from '@/lib/domain/database-type';
import { useTranslation } from 'react-i18next';
import { SelectDatabaseContent } from './select-database-content';
import { useDialog } from '@/hooks/use-dialog';
export interface SelectDatabaseProps {
onContinue: () => void;
@@ -27,6 +28,7 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({
createNewDiagram,
}) => {
const { t } = useTranslation();
const { openImportDiagramDialog } = useDialog();
return (
<>
@@ -51,7 +53,13 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({
</Button>
</DialogClose>
) : (
<div></div>
<Button
type="button"
variant="ghost"
onClick={openImportDiagramDialog}
>
{t('new_diagram_dialog.import_from_file')}
</Button>
)}
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2">
<Button

View File

@@ -0,0 +1,110 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDialog } from '@/hooks/use-dialog';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/dialog/dialog';
import { Button } from '@/components/button/button';
import type { SelectBoxOption } from '@/components/select-box/select-box';
import { SelectBox } from '@/components/select-box/select-box';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { useTranslation } from 'react-i18next';
import { useChartDB } from '@/hooks/use-chartdb';
import { diagramToJSONOutput } from '@/lib/export-import-utils';
import { Spinner } from '@/components/spinner/spinner';
import { waitFor } from '@/lib/utils';
export interface ExportDiagramDialogProps extends BaseDialogProps {}
export const ExportDiagramDialog: React.FC<ExportDiagramDialogProps> = ({
dialog,
}) => {
const { t } = useTranslation();
const { diagramName, currentDiagram } = useChartDB();
const [isLoading, setIsLoading] = useState(false);
const { closeExportDiagramDialog } = useDialog();
useEffect(() => {
if (!dialog.open) return;
setIsLoading(false);
}, [dialog.open]);
const downloadOutput = useCallback(
(dataUrl: string) => {
const a = document.createElement('a');
a.setAttribute('download', `ChartDB(${diagramName}).json`);
a.setAttribute('href', dataUrl);
a.click();
},
[diagramName]
);
const handleExport = useCallback(async () => {
setIsLoading(true);
await waitFor(1000);
const json = diagramToJSONOutput(currentDiagram);
const blob = new Blob([json], { type: 'application/json' });
const dataUrl = URL.createObjectURL(blob);
downloadOutput(dataUrl);
setIsLoading(false);
closeExportDiagramDialog();
}, [downloadOutput, currentDiagram, closeExportDiagramDialog]);
const outputTypeOptions: SelectBoxOption[] = useMemo(
() =>
['json'].map((format) => ({
value: format,
label: t(`export_diagram_dialog.format_${format}`),
})),
[t]
);
return (
<Dialog
{...dialog}
onOpenChange={(open) => {
if (!open) {
closeExportDiagramDialog();
}
}}
>
<DialogContent className="flex flex-col" showClose>
<DialogHeader>
<DialogTitle>
{t('export_diagram_dialog.title')}
</DialogTitle>
<DialogDescription>
{t('export_diagram_dialog.description')}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-1">
<div className="grid w-full items-center gap-4">
<SelectBox
options={outputTypeOptions}
multiple={false}
value="json"
/>
</div>
</div>
<DialogFooter className="flex gap-1 md:justify-between">
<DialogClose asChild>
<Button variant="secondary">
{t('export_diagram_dialog.cancel')}
</Button>
</DialogClose>
<Button onClick={handleExport} disabled={isLoading}>
{isLoading ? (
<Spinner className="mr-1 size-5 text-primary-foreground" />
) : null}
{t('export_diagram_dialog.export')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -20,7 +20,7 @@ import {
import { databaseTypeToLabelMap } from '@/lib/databases';
import { DatabaseType } from '@/lib/domain/database-type';
import { Annoyed, Sparkles } from 'lucide-react';
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import type { BaseDialogProps } from '../common/base-dialog-props';
@@ -37,28 +37,47 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
const { t } = useTranslation();
const [script, setScript] = React.useState<string>();
const [error, setError] = React.useState<boolean>(false);
const [isScriptLoading, setIsScriptLoading] =
React.useState<boolean>(false);
const abortControllerRef = useRef<AbortController | null>(null);
const exportSQLScript = useCallback(async () => {
if (targetDatabaseType === DatabaseType.GENERIC) {
return Promise.resolve(exportBaseSQL(currentDiagram));
} else {
return exportSQL(currentDiagram, targetDatabaseType);
return exportSQL(currentDiagram, targetDatabaseType, {
stream: true,
onResultStream: (text) =>
setScript((prev) => (prev ? prev + text : text)),
signal: abortControllerRef.current?.signal,
});
}
}, [targetDatabaseType, currentDiagram]);
useEffect(() => {
if (!dialog.open) return;
if (!dialog.open) {
abortControllerRef.current?.abort();
return;
}
abortControllerRef.current = new AbortController();
setScript(undefined);
setError(false);
const fetchScript = async () => {
try {
setIsScriptLoading(true);
const script = await exportSQLScript();
setScript(script);
setIsScriptLoading(false);
} catch (e) {
setError(true);
}
};
fetchScript();
return () => {
abortControllerRef.current?.abort();
};
}, [dialog.open, setScript, exportSQLScript, setError]);
const renderError = useCallback(
@@ -156,7 +175,12 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
) : script.length === 0 ? (
renderError()
) : (
<CodeSnippet className="h-96 w-full" code={script!} />
<CodeSnippet
className="h-96 w-full"
code={script!}
autoScroll={true}
isComplete={!isScriptLoading}
/>
)}
</div>

View File

@@ -0,0 +1,129 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDialog } from '@/hooks/use-dialog';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/dialog/dialog';
import { Button } from '@/components/button/button';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { useTranslation } from 'react-i18next';
import { FileUploader } from '@/components/file-uploader/file-uploader';
import { useStorage } from '@/hooks/use-storage';
import { useNavigate } from 'react-router-dom';
import { diagramFromJSONInput } from '@/lib/export-import-utils';
import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert';
import { AlertCircle } from 'lucide-react';
export interface ImportDiagramDialogProps extends BaseDialogProps {}
export const ImportDiagramDialog: React.FC<ImportDiagramDialogProps> = ({
dialog,
}) => {
const { t } = useTranslation();
const [file, setFile] = useState<File | null>(null);
const { addDiagram } = useStorage();
const navigate = useNavigate();
const [error, setError] = useState(false);
const onFileChange = useCallback((files: File[]) => {
if (files.length === 0) {
setFile(null);
return;
}
setFile(files[0]);
}, []);
useEffect(() => {
if (!dialog.open) return;
setError(false);
setFile(null);
}, [dialog.open]);
const { closeImportDiagramDialog, closeCreateDiagramDialog } = useDialog();
const handleImport = useCallback(() => {
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
const json = e.target?.result;
if (typeof json !== 'string') return;
try {
const diagram = diagramFromJSONInput(json);
await addDiagram({ diagram });
closeImportDiagramDialog();
closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`);
} catch (e) {
setError(true);
throw e;
}
};
reader.readAsText(file);
}, [
file,
addDiagram,
navigate,
closeImportDiagramDialog,
closeCreateDiagramDialog,
]);
return (
<Dialog
{...dialog}
onOpenChange={(open) => {
if (!open) {
closeImportDiagramDialog();
}
}}
>
<DialogContent className="flex flex-col" showClose>
<DialogHeader>
<DialogTitle>
{t('import_diagram_dialog.title')}
</DialogTitle>
<DialogDescription>
{t('import_diagram_dialog.description')}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col p-1">
<FileUploader
supportedExtensions={['.json']}
onFilesChange={onFileChange}
/>
{error ? (
<Alert variant="destructive" className="mt-2">
<AlertCircle className="size-4" />
<AlertTitle>
{t('import_diagram_dialog.error.title')}
</AlertTitle>
<AlertDescription>
{t('import_diagram_dialog.error.description')}
</AlertDescription>
</Alert>
) : null}
</div>
<DialogFooter className="flex gap-1 md:justify-between">
<DialogClose asChild>
<Button variant="secondary">
{t('import_diagram_dialog.cancel')}
</Button>
</DialogClose>
<Button onClick={handleImport} disabled={file === null}>
{t('import_diagram_dialog.import')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -3,69 +3,73 @@
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--subtitle: 215.3 19.3% 34.5%;
}
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--subtitle: 215.3 19.3% 34.5%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--subtitle: 212.7 26.8% 83.9%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--subtitle: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
.text-editable {
@apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] rounded-md cursor-pointer;
}
}

View File

@@ -1,5 +1,6 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import type { LanguageMetadata } from './types';
import { en, enMetadata } from './locales/en';
import { es, esMetadata } from './locales/es';
@@ -7,8 +8,14 @@ import { fr, frMetadata } from './locales/fr';
import { de, deMetadata } from './locales/de';
import { hi, hiMetadata } from './locales/hi';
import { ja, jaMetadata } from './locales/ja';
import { ko_KR, ko_KRMetadata } from './locales/ko_KR';
import { pt_BR, pt_BRMetadata } from './locales/pt_BR';
import { uk, ukMetadata } from './locales/uk';
import { ru, ruMetadata } from './locales/ru';
import { zh_CN, zh_CNMetadata } from './locales/zh_CN';
import { zh_TW, zh_TWMetadata } from './locales/zh_TW';
import { ne, neMetadata } from './locales/ne';
import { mr, mrMetadata } from './locales/mr';
export const languages: LanguageMetadata[] = [
enMetadata,
@@ -17,8 +24,14 @@ export const languages: LanguageMetadata[] = [
deMetadata,
hiMetadata,
jaMetadata,
ko_KRMetadata,
pt_BRMetadata,
ukMetadata,
ruMetadata,
zh_CNMetadata,
zh_TWMetadata,
neMetadata,
mrMetadata,
];
const resources = {
@@ -28,18 +41,25 @@ const resources = {
de,
hi,
ja,
ko_KR,
pt_BR,
uk,
ru,
zh_CN,
zh_TW,
ne,
mr,
};
i18n.use(initReactI18next).init({
resources,
lng: enMetadata.code,
interpolation: {
escapeValue: false,
},
fallbackLng: enMetadata.code,
debug: false,
});
i18n.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
interpolation: {
escapeValue: false,
},
fallbackLng: enMetadata.code,
debug: false,
});
export { i18n };

View File

@@ -28,10 +28,15 @@ export const de: LanguageTranslation = {
show_cardinality: 'Kardinalität anzeigen',
zoom_on_scroll: 'Zoom beim Scrollen',
theme: 'Stil',
change_language: 'Sprache',
show_dependencies: 'Abhängigkeiten anzeigen',
hide_dependencies: 'Abhängigkeiten ausblenden',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'Hilfe',
visit_website: 'ChartDB Webseite',
@@ -139,6 +144,7 @@ export const de: LanguageTranslation = {
change_schema: 'Schema ändern',
add_field: 'Feld hinzufügen',
add_index: 'Index hinzufügen',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Tabelle löschen',
},
},
@@ -226,6 +232,8 @@ export const de: LanguageTranslation = {
cancel: 'Abbrechen',
back: 'Zurück',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: 'Leeres Diagramm',
continue: 'Weiter',
import: 'Importieren',
@@ -329,7 +337,26 @@ export const de: LanguageTranslation = {
close: 'Nicht jetzt',
confirm: 'Natürlich!',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'Ein zu Eins (1:1)',
one_to_many: 'Ein zu Viele (1:n)',
@@ -344,12 +371,25 @@ export const de: LanguageTranslation = {
table_node_context_menu: {
edit_table: 'Tabelle bearbeiten',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Tabelle löschen',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'Doppelklicken zum Bearbeiten',
},
language_select: {
change_language: 'Sprache',
},
},
};
export const deMetadata: LanguageMetadata = {
name: 'Deutsch',
name: 'German',
nativeName: 'Deutsch',
code: 'de',
};

View File

@@ -28,10 +28,14 @@ export const en = {
show_cardinality: 'Show Cardinality',
zoom_on_scroll: 'Zoom on Scroll',
theme: 'Theme',
change_language: 'Language',
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
},
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'Help',
visit_website: 'Visit ChartDB',
@@ -139,6 +143,7 @@ export const en = {
change_schema: 'Change Schema',
add_field: 'Add Field',
add_index: 'Add Index',
duplicate_table: 'Duplicate Table',
delete_table: 'Delete Table',
},
},
@@ -224,6 +229,7 @@ export const en = {
},
cancel: 'Cancel',
import_from_file: 'Import from File',
back: 'Back',
empty_diagram: 'Empty diagram',
continue: 'Continue',
@@ -328,7 +334,25 @@ export const en = {
close: 'Not now',
confirm: 'Of course!',
},
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'One to One',
one_to_many: 'One to Many',
@@ -343,12 +367,24 @@ export const en = {
table_node_context_menu: {
edit_table: 'Edit Table',
duplicate_table: 'Duplicate Table',
delete_table: 'Delete Table',
},
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'Double-click to edit',
},
language_select: {
change_language: 'Language',
},
},
};
export const enMetadata: LanguageMetadata = {
name: 'English',
nativeName: 'English',
code: 'en',
};

View File

@@ -28,10 +28,14 @@ export const es: LanguageTranslation = {
hide_sidebar: 'Ocultar Barra Lateral',
zoom_on_scroll: 'Zoom al Desplazarse',
theme: 'Tema',
change_language: 'Idioma',
// TODO: Translate
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
show_dependencies: 'Mostrar dependencias',
hide_dependencies: 'Ocultar dependencias',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'Ayuda',
@@ -80,20 +84,19 @@ export const es: LanguageTranslation = {
saved: 'Guardado',
diagrams: 'Diagramas',
loading_diagram: 'Cargando diagrama...',
deselect_all: 'Deselect All', // TODO: Translate
select_all: 'Select All', // TODO: Translate
clear: 'Clear', // TODO: Translate
show_more: 'Show More', // TODO: Translate
show_less: 'Show Less', // TODO: Translate
// TODO: Translate
deselect_all: 'Deseleccionar todo',
select_all: 'Seleccionar todo',
clear: 'Limpiar',
show_more: 'Mostrar más',
show_less: 'Mostrar menos',
copy_to_clipboard: 'Copy to Clipboard',
copied: 'Copied!',
side_panel: {
schema: 'Schema:', // TODO: Translate
filter_by_schema: 'Filter by schema', // TODO: Translate
search_schema: 'Search schema...', // TODO: Translate
no_schemas_found: 'No schemas found.', // TODO: Translate
schema: 'Esquema:',
filter_by_schema: 'Filtrar por esquema',
search_schema: 'Buscar esquema...',
no_schemas_found: 'No se encontraron esquemas.',
view_all_options: 'Ver todas las opciones...',
tables_section: {
tables: 'Tablas',
@@ -113,7 +116,7 @@ export const es: LanguageTranslation = {
index_select_fields: 'Seleccionar campos',
field_name: 'Nombre',
field_type: 'Tipo',
no_types_found: 'No types found', // TODO: Translate
no_types_found: 'No se encontraron tipos',
field_actions: {
title: 'Atributos del Campo',
unique: 'Único',
@@ -132,6 +135,7 @@ export const es: LanguageTranslation = {
change_schema: 'Cambiar Esquema',
add_field: 'Agregar Campo',
add_index: 'Agregar Índice',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Eliminar Tabla',
},
},
@@ -160,23 +164,22 @@ export const es: LanguageTranslation = {
description: 'Crea una relación para conectar tablas',
},
},
// TODO: Translate
dependencies_section: {
dependencies: 'Dependencies',
filter: 'Filter',
collapse: 'Collapse All',
dependencies: 'Dependencias',
filter: 'Filtro',
collapse: 'Colapsar todo',
dependency: {
table: 'Table',
dependent_table: 'Dependent View',
delete_dependency: 'Delete',
table: 'Tabla',
dependent_table: 'Vista dependiente',
delete_dependency: 'Eliminar',
dependency_actions: {
title: 'Actions',
delete_dependency: 'Delete',
title: 'Acciones',
delete_dependency: 'Eliminar',
},
},
empty_state: {
title: 'No dependencies',
description: 'Create a view to get started',
title: 'Sin dependencias',
description: 'Crea una vista para comenzar',
},
},
},
@@ -189,8 +192,7 @@ export const es: LanguageTranslation = {
undo: 'Deshacer',
redo: 'Rehacer',
reorder_diagram: 'Reordenar Diagrama',
// TODO: Translate
highlight_overlapping_tables: 'Highlight Overlapping Tables',
highlight_overlapping_tables: 'Resaltar tablas superpuestas',
},
new_diagram_dialog: {
@@ -214,20 +216,20 @@ export const es: LanguageTranslation = {
step_1: 'Ve a Herramientas > Opciones > Resultados de Consulta > SQL Server.',
step_2: 'Si estás usando "Resultados en Cuadrícula", cambia el Máximo de Caracteres Recuperados para Datos No XML (configúralo en 9999999).',
},
// TODO: Translate
instructions_link: 'Need help? Watch how',
check_script_result: 'Check Script Result',
instructions_link: '¿Necesitas ayuda? mira cómo',
check_script_result: 'Revisa el resultado del script',
},
cancel: 'Cancelar',
back: 'Atrás',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: 'Diagrama vacío',
continue: 'Continuar',
import: 'Importar',
},
open_diagram_dialog: {
// TODO: Translate
title: 'Abrir Diagrama',
description:
'Selecciona un diagrama para abrir de la lista a continuación.',
@@ -293,16 +295,15 @@ export const es: LanguageTranslation = {
},
},
// TODO: Translate
export_image_dialog: {
title: 'Export Image',
description: 'Choose the scale factor for export:',
scale_1x: '1x Regular',
scale_2x: '2x (Recommended)',
title: 'Exportar imagen',
description: 'Escoge el factor de escalamiento para exportar:',
scale_1x: '1x regular',
scale_2x: '2x (recomendado)',
scale_3x: '3x',
scale_4x: '4x',
cancel: 'Cancel',
export: 'Export',
cancel: 'Cancelar',
export: 'Exportar',
},
new_table_schema_dialog: {
@@ -336,7 +337,26 @@ export const es: LanguageTranslation = {
change_schema: 'Cambiar',
none: 'nada',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'Uno a Uno',
one_to_many: 'Uno a Muchos',
@@ -351,12 +371,25 @@ export const es: LanguageTranslation = {
table_node_context_menu: {
edit_table: 'Editar Tabla',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Eliminar Tabla',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'Doble clic para editar',
},
language_select: {
change_language: 'Idioma',
},
},
};
export const esMetadata: LanguageMetadata = {
name: 'Español',
name: 'Spanish',
nativeName: 'Español',
code: 'es',
};

View File

@@ -15,7 +15,7 @@ export const fr: LanguageTranslation = {
exit: 'Quitter',
},
edit: {
edit: 'Éditer',
edit: 'Édition',
undo: 'Annuler',
redo: 'Rétablir',
clear: 'Effacer',
@@ -28,10 +28,14 @@ export const fr: LanguageTranslation = {
show_cardinality: 'Afficher la Cardinalité',
zoom_on_scroll: 'Zoom sur le Défilement',
theme: 'Thème',
change_language: 'Langue',
show_dependencies: 'Afficher les Dépendances',
hide_dependencies: 'Masquer les Dépendances',
},
share: {
share: 'Partage',
export_diagram: 'Exporter le diagramme',
import_diagram: 'Importer un diagramme',
},
help: {
help: 'Aide',
visit_website: 'Visitez ChartDB',
@@ -130,6 +134,7 @@ export const fr: LanguageTranslation = {
title: 'Actions de la Table',
add_field: 'Ajouter un Champ',
add_index: 'Ajouter un Index',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Supprimer la Table',
change_schema: 'Changer le Schéma',
},
@@ -218,6 +223,8 @@ export const fr: LanguageTranslation = {
cancel: 'Annuler',
back: 'Retour',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: 'Diagramme vide',
continue: 'Continuer',
import: 'Importer',
@@ -332,7 +339,26 @@ export const fr: LanguageTranslation = {
cancel: 'Annuler',
},
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'Un à Un',
one_to_many: 'Un à Plusieurs',
@@ -347,12 +373,25 @@ export const fr: LanguageTranslation = {
table_node_context_menu: {
edit_table: 'Éditer la Table',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Supprimer la Table',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'Double-cliquez pour modifier',
},
language_select: {
change_language: 'Langue',
},
},
};
export const frMetadata: LanguageMetadata = {
name: 'Français',
name: 'French',
nativeName: 'Français',
code: 'fr',
};

View File

@@ -28,10 +28,15 @@ export const hi: LanguageTranslation = {
show_cardinality: 'कार्डिनैलिटी दिखाएँ',
zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
theme: 'थीम',
change_language: 'भाषा बदलें',
show_dependencies: 'निर्भरता दिखाएँ',
hide_dependencies: 'निर्भरता छिपाएँ',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'मदद',
visit_website: 'ChartDB वेबसाइट पर जाएँ',
@@ -140,6 +145,7 @@ export const hi: LanguageTranslation = {
change_schema: 'स्कीमा बदलें',
add_field: 'फ़ील्ड जोड़ें',
add_index: 'सूचकांक जोड़ें',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'तालिका हटाएँ',
},
},
@@ -228,6 +234,8 @@ export const hi: LanguageTranslation = {
cancel: 'रद्द करें',
back: 'वापस',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: 'खाली आरेख',
continue: 'जारी रखें',
import: 'आयात करें',
@@ -331,7 +339,26 @@ export const hi: LanguageTranslation = {
close: 'अभी नहीं',
confirm: 'बिलकुल!',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'एक से एक',
one_to_many: 'एक से कई',
@@ -346,12 +373,25 @@ export const hi: LanguageTranslation = {
table_node_context_menu: {
edit_table: 'तालिका संपादित करें',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'तालिका हटाएँ',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'संपादित करने के लिए डबल-क्लिक करें',
},
language_select: {
change_language: 'भाषा बदलें',
},
},
};
export const hiMetadata: LanguageMetadata = {
name: 'Hindi',
nativeName: 'हिन्दी',
code: 'hi',
};

View File

@@ -28,11 +28,16 @@ export const ja: LanguageTranslation = {
show_cardinality: 'カーディナリティを表示',
zoom_on_scroll: 'スクロールでズーム',
theme: 'テーマ',
change_language: '言語',
// TODO: Translate
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'ヘルプ',
visit_website: 'ChartDBにアクセス',
@@ -141,6 +146,7 @@ export const ja: LanguageTranslation = {
change_schema: 'スキーマを変更',
add_field: 'フィールドを追加',
add_index: 'インデックスを追加',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'テーブルを削除',
},
},
@@ -230,6 +236,8 @@ export const ja: LanguageTranslation = {
cancel: 'キャンセル',
back: '戻る',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: '空のダイアグラム',
continue: '続行',
import: 'インポート',
@@ -333,7 +341,26 @@ export const ja: LanguageTranslation = {
close: '今はしない',
confirm: 'もちろん!',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: '1対1',
one_to_many: '1対多',
@@ -348,12 +375,25 @@ export const ja: LanguageTranslation = {
table_node_context_menu: {
edit_table: 'テーブルを編集',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'テーブルを削除',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'ダブルクリックして編集',
},
language_select: {
change_language: '言語',
},
},
};
export const jaMetadata: LanguageMetadata = {
name: 'Japanese',
nativeName: '日本語',
code: 'ja',
};

393
src/i18n/locales/ko_KR.ts Normal file
View File

@@ -0,0 +1,393 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ko_KR: LanguageTranslation = {
translation: {
menu: {
file: {
file: '파일',
new: '새 다이어그램',
open: '열기',
save: '저장',
import_database: '데이터베이스 가져오기',
export_sql: 'SQL로 저장',
export_as: '다른 형식으로 저장',
delete_diagram: '다이어그램 삭제',
exit: '종료',
},
edit: {
edit: '편집',
undo: '실행 취소',
redo: '다시 실행',
clear: '모두 지우기',
},
view: {
view: '보기',
show_sidebar: '사이드바 보이기',
hide_sidebar: '사이드바 숨기기',
hide_cardinality: '카디널리티 숨기기',
show_cardinality: '카디널리티 보이기',
zoom_on_scroll: '스크롤 시 확대',
theme: '테마',
show_dependencies: '종속성 보이기',
hide_dependencies: '종속성 숨기기',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: '도움말',
visit_website: 'ChartDB 사이트 방문',
join_discord: 'Discord 가입',
schedule_a_call: 'Talk with us!',
},
},
delete_diagram_alert: {
title: '다이어그램 삭제',
description:
'이 작업은 되돌릴 수 없으며 다이어그램이 영구적으로 삭제됩니다.',
cancel: '취소',
delete: '삭제',
},
clear_diagram_alert: {
title: '다이어그램 지우기',
description:
'이 작업은 되돌릴 수 없으며 다이어그램의 모든 데이터가 지워집니다.',
cancel: '취소',
clear: '지우기',
},
reorder_diagram_alert: {
title: '다이어그램 재정렬',
description:
'이 작업은 모든 다이어그램이 재정렬됩니다. 계속하시겠습니까?',
reorder: '재정렬',
cancel: '취소',
},
multiple_schemas_alert: {
title: '다중 스키마',
description:
'현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.',
dont_show_again: '다시 보여주지 마세요',
change_schema: '변경',
none: '없음',
},
theme: {
system: '시스템 설정에 따름',
light: '밝게',
dark: '어둡게',
},
zoom: {
on: '활성화',
off: '비활성화',
},
last_saved: '최근 저장일시: ',
saved: '저장됨',
diagrams: '다이어그램',
loading_diagram: '다이어그램 로딩중...',
deselect_all: '모두 선택 해제',
select_all: '모두 선택',
clear: '지우기',
show_more: '더 보기',
show_less: '간략히',
copy_to_clipboard: '클립보드에 복사',
copied: '복사됨!',
side_panel: {
schema: '스키마:',
filter_by_schema: '스키마로 필터링',
search_schema: '스키마 검색...',
no_schemas_found: '스키마를 찾을 수 없습니다.',
view_all_options: '전체 옵션 보기...',
tables_section: {
tables: '테이블',
add_table: '테이블 추가',
filter: '필터',
collapse: '모두 접기',
table: {
fields: '필드',
nullable: 'null 여부',
primary_key: '기본키',
indexes: '인덱스',
comments: '주석',
no_comments: '주석 없음',
add_field: '필드 추가',
add_index: '인덱스 추가',
index_select_fields: '필드 선택',
no_types_found: '타입을 찾을 수 없습니다.',
field_name: '이름',
field_type: '타입',
field_actions: {
title: '필드 속성',
unique: '유니크 여부',
comments: '주석',
no_comments: '주석 없음',
delete_field: '필드 삭제',
},
index_actions: {
title: '인덱스 속성',
name: '인덱스 명',
unique: '유니크 여부',
delete_index: '인덱스 삭제',
},
table_actions: {
title: '테이블 작업',
change_schema: '스키마 변경',
add_field: '필드 추가',
add_index: '인덱스 추가',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: '테이블 삭제',
},
},
empty_state: {
title: '테이블 없음',
description: '테이블을 만들어 시작하세요.',
},
},
relationships_section: {
relationships: '연관 관계',
filter: '필터',
add_relationship: '연관 관계 추가',
collapse: '모두 접기',
relationship: {
primary: '주 테이블',
foreign: '참조 테이블',
cardinality: '카디널리티',
delete_relationship: '제거',
relationship_actions: {
title: '연관 관계 작업',
delete_relationship: '연관 관계 삭제',
},
},
empty_state: {
title: '연관 관계',
description: '테이블 연결을 위해 연관 관계를 생성하세요',
},
},
dependencies_section: {
dependencies: '종속성',
filter: '필터',
collapse: '모두 접기',
dependency: {
table: '테이블',
dependent_table: '뷰 테이블',
delete_dependency: '삭제',
dependency_actions: {
title: '종속성 작업',
delete_dependency: '뷰 테이블 삭제',
},
},
empty_state: {
title: '뷰 테이블 없음',
description: '뷰 테이블을 만들어 시작하세요.',
},
},
},
toolbar: {
zoom_in: '확대',
zoom_out: '축소',
save: '저장',
show_all: '전체 저장',
undo: '실행 취소',
redo: '다시 실행',
reorder_diagram: '다이어그램 재정렬',
highlight_overlapping_tables: '겹치는 테이블 강조 표시',
},
new_diagram_dialog: {
database_selection: {
title: '당신의 데이터베이스 종류가 무엇인가요?',
description:
'각 데이터베이스에는 고유한 기능과 특징이 있습니다.',
check_examples_long: '예제 확인',
check_examples_short: '예제들',
},
import_database: {
title: '당신의 데이터베이스를 가져오세요',
database_edition: '데이터베이스 세부 종류:',
step_1: '데이터베이스에서 아래의 SQL을 실행해주세요:',
step_2: '이곳에 결과를 붙여넣어주세요:',
script_results_placeholder: '이곳에 스크립트 결과를 입력...',
ssms_instructions: {
button_text: 'SSMS을 사용하시는 경우',
title: '지침',
step_1: '도구 > 옵션 > 쿼리 응답 > SQL Server',
step_2: '"결과를 그리드로 표시"를 사용하는 경우 비 XML 데이터에 대해 검색되는 최대 문자 수를 변경합니다. (9999999로 설정)',
},
instructions_link: '도움이 필요하신가요? 영상 가이드 보기',
check_script_result: '스크립트 결과 확인',
},
cancel: '취소',
back: '뒤로가기',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: '빈 다이어그램으로 시작',
continue: '계속',
import: '가져오기',
},
open_diagram_dialog: {
title: '다이어그램 열기',
description: '아래의 목록에서 다이어그램을 선택하세요.',
table_columns: {
name: '이름',
created_at: '생성일시',
last_modified: '최근 수정일시',
tables_count: '테이블 갯수',
},
cancel: '취소',
open: '열기',
},
export_sql_dialog: {
title: 'SQL로 내보내기',
description: '다이어그램 스키마를 {{databaseType}} SQL로 내보내기',
close: '닫기',
loading: {
text: '{{databaseType}} SQL을 AI가 생성하고 있습니다...',
description: '30초 정도 걸릴 수 있습니다.',
},
error: {
message:
'SQL 생성에 실패하였습니다. 잠시후 다시 시도해주세요 계속해서 증상이 발생하는 경우 <0>우리에게 연락해주세요</0>.',
description:
'당신의 OPENAI_TOKEN가 있는 경우, <0>여기에서</0> 메뉴얼을 참고하여 사용하실 수 있습니다.',
},
},
create_relationship_dialog: {
title: '연관 관계 생성',
primary_table: '주 테이블',
primary_field: '주 필드',
referenced_table: '참조 테이블',
referenced_field: '참조 필드',
primary_table_placeholder: '테이블 선택',
primary_field_placeholder: '필드 선택',
referenced_table_placeholder: '테이블 선택',
referenced_field_placeholder: '필드 선택',
no_tables_found: '테이블을 찾을 수 없습니다',
no_fields_found: '필드를 찾을 수 없습니다',
create: '생성',
cancel: '취소',
},
import_database_dialog: {
title: '현재 다이어그램 가져오기',
override_alert: {
title: '데이터베이스 가져오기',
content: {
alert: '이 다이어그램을 가져오면 기존 테이블 및 연관 관계에 영향을 미칩니다.',
new_tables:
'<bold>{{newTablesNumber}}</bold>개의 신규 테이블 생성됨',
new_relationships:
'<bold>{{newRelationshipsNumber}}</bold>개의 신규 연관 관계 생성됨',
tables_override:
'<bold>{{tablesOverrideNumber}}</bold>개의 테이블이 덮어씌워짐',
proceed: '정말로 가져오시겠습니까?',
},
import: '가져오기',
cancel: '취소',
},
},
export_image_dialog: {
title: '이미지로 내보내기',
description: '내보낼 배율을 선택해주세요:',
scale_1x: '1x 기본',
scale_2x: '2x (권장)',
scale_3x: '3x',
scale_4x: '4x',
cancel: '취소',
export: '내보내기',
},
new_table_schema_dialog: {
title: '스키마 선택',
description:
'현재 여러 스키마가 표시됩니다. 새 테이블을 위해 하나를 선택합니다.',
cancel: '취소',
confirm: 'Confirm',
},
update_table_schema_dialog: {
title: '스키마 변경',
description: '"{{tableName}}" 테이블 스키마를 수정합니다',
cancel: '취소',
confirm: '변경',
},
star_us_dialog: {
title: '개선할 수 있도록 도와주세요!',
description:
'GitHub에 별을 찍어주시겠습니까? 클릭 한번이면 됩니다!',
close: '아직은 괜찮아요',
confirm: '당연하죠!',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: '일대일 (1:1)',
one_to_many: '일대다 (1:N)',
many_to_one: '다대일 (N:1)',
many_to_many: '다대다 (N:N)',
},
canvas_context_menu: {
new_table: '새 테이블',
new_relationship: '새 연관관계',
},
table_node_context_menu: {
edit_table: '테이블 수정',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: '테이블 삭제',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: '더블클릭하여 편집',
},
language_select: {
change_language: '언어',
},
},
};
export const ko_KRMetadata: LanguageMetadata = {
name: 'Korean',
nativeName: '한국어',
code: 'ko_KR',
};

404
src/i18n/locales/mr.ts Normal file
View File

@@ -0,0 +1,404 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const mr: LanguageTranslation = {
translation: {
menu: {
file: {
file: 'फाइल',
new: 'नवीन',
open: 'उघडा',
save: 'जतन करा',
import_database: 'डेटाबेस इम्पोर्ट करा',
export_sql: 'SQL एक्स्पोर्ट करा',
export_as: 'म्हणून एक्स्पोर्ट करा',
delete_diagram: 'आरेख हटवा',
exit: 'बाहेर पडा',
},
edit: {
edit: 'संपादन करा',
undo: 'पूर्ववत करा',
redo: 'पुन्हा करा',
clear: 'साफ करा',
},
view: {
view: 'दृश्य',
show_sidebar: 'साइडबार दाखवा',
hide_sidebar: 'साइडबार लपवा',
hide_cardinality: 'कार्डिनॅलिटी लपवा',
show_cardinality: 'कार्डिनॅलिटी दाखवा',
zoom_on_scroll: 'स्क्रोलवर झूम करा',
theme: 'थीम',
show_dependencies: 'डिपेंडेन्सि दाखवा',
hide_dependencies: 'डिपेंडेन्सि लपवा',
},
share: {
// TODO: Add translations
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'मदत',
visit_website: 'ChartDB ला भेट द्या',
join_discord: 'आमच्या डिस्कॉर्डमध्ये सामील व्हा',
schedule_a_call: 'आमच्याशी बोला!',
},
},
delete_diagram_alert: {
title: 'आरेख हटवा',
description:
'ही क्रिया पूर्ववत केली जाऊ शकत नाही. हे आरेख कायमचे हटवेल.',
cancel: 'रद्द करा',
delete: 'हटवा',
},
clear_diagram_alert: {
title: 'आरेख साफ करा',
description:
'ही क्रिया पूर्ववत केली जाऊ शकत नाही. हे आरेखातील सर्व डेटा कायमचे हटवेल.',
cancel: 'रद्द करा',
clear: 'साफ करा',
},
reorder_diagram_alert: {
title: 'आरेख पुनःक्रमित करा',
description:
'ही क्रिया आरेखातील सर्व टेबल्सची पुनर्रचना करेल. तुम्हाला पुढे जायचे आहे का?',
reorder: 'पुनःक्रमित करा',
cancel: 'रद्द करा',
},
multiple_schemas_alert: {
title: 'एकाधिक स्कीमा',
description:
'{{schemasCount}} स्कीमा या आरेखात आहेत. सध्या दाखवत आहोत: {{formattedSchemas}}.',
dont_show_again: 'पुन्हा दाखवू नका',
change_schema: 'बदला',
none: 'काहीही नाही',
},
theme: {
system: 'सिस्टम',
light: 'लाईट',
dark: 'डार्क',
},
zoom: {
on: 'चालू',
off: 'बंद',
},
last_saved: 'शेवटचे जतन केले',
saved: 'जतन केले',
diagrams: 'आरेख',
loading_diagram: 'आरेख लोड करत आहे...',
deselect_all: 'सर्व निवड रद्द करा',
select_all: 'सर्व निवडा',
clear: 'साफ करा',
show_more: 'अधिक दाखवा',
show_less: 'कमी दाखवा',
// TODO: Add translations
copy_to_clipboard: 'Copy to Clipboard',
// TODO: Add translations
copied: 'Copied!',
side_panel: {
schema: 'स्कीमा:',
filter_by_schema: 'स्कीमा द्वारे फिल्टर करा',
search_schema: 'स्कीमा शोधा...',
no_schemas_found: 'कोणतेही स्कीमा सापडले नाहीत.',
view_all_options: 'सर्व पर्याय पहा...',
tables_section: {
tables: 'टेबल्स',
add_table: 'टेबल जोडा',
filter: 'फिल्टर',
collapse: 'सर्व संकुचित करा',
table: {
fields: 'फील्ड्स',
nullable: 'नल करण्यायोग्य?',
primary_key: 'प्राथमिक की',
indexes: 'सूचकांक',
comments: 'टिप्पण्या',
no_comments: 'कोणत्याही टिप्पणी नाहीत',
add_field: 'फील्ड जोडा',
add_index: 'सूचकांक जोडा',
index_select_fields: 'फील्ड निवडा',
no_types_found: 'कोणतेही प्रकार सापडले नाहीत',
field_name: 'नाव',
field_type: 'प्रकार',
field_actions: {
title: 'फील्ड गुणधर्म',
unique: 'युनिक',
comments: 'टिप्पण्या',
no_comments: 'कोणत्याही टिप्पणी नाहीत',
delete_field: 'फील्ड हटवा',
},
index_actions: {
title: 'इंडेक्स गुणधर्म',
name: 'नाव',
unique: 'युनिक',
delete_index: 'इंडेक्स हटवा',
},
table_actions: {
title: 'टेबल एक्शन',
change_schema: 'स्कीमा बदला',
add_field: 'फील्ड जोडा',
add_index: 'इंडेक्स जोडा',
delete_table: 'टेबल हटवा',
// TODO: Add translations
duplicate_table: 'Duplicate Table',
},
},
empty_state: {
title: 'कोणतेही टेबल नाहीत',
description: 'सुरू करण्यासाठी एक टेबल तयार करा',
},
},
relationships_section: {
relationships: 'रिलेशनशिप',
filter: 'फिल्टर',
add_relationship: 'रिलेशनशिप जोडा',
collapse: 'सर्व संकुचित करा',
relationship: {
primary: 'प्राथमिक टेबल',
foreign: 'रेफरंस टेबल',
cardinality: 'कार्डिनॅलिटी',
delete_relationship: 'हटवा',
relationship_actions: {
title: 'क्रिया',
delete_relationship: 'हटवा',
},
},
empty_state: {
title: 'कोणतेही रिलेशनशिप नाहीत',
description:
'टेबल्स कनेक्ट करण्यासाठी एक रिलेशनशिप तयार करा',
},
},
dependencies_section: {
dependencies: 'डिपेंडेन्सि',
filter: 'फिल्टर',
collapse: 'सर्व संकुचित करा',
dependency: {
table: 'टेबल',
dependent_table: 'डिपेंडेन्सि दृश्य',
delete_dependency: 'हटवा',
dependency_actions: {
title: 'क्रिया',
delete_dependency: 'हटवा',
},
},
empty_state: {
title: 'कोणत्याही डिपेंडेन्सि नाहीत',
description: 'सुरू करण्यासाठी एक दृश्य तयार करा',
},
},
},
toolbar: {
zoom_in: 'झूम इन',
zoom_out: 'झूम आउट',
save: 'जतन करा',
show_all: 'सर्व दाखवा',
undo: 'पूर्ववत करा',
redo: 'पुन्हा करा',
reorder_diagram: 'आरेख पुनःक्रमित करा',
highlight_overlapping_tables: 'ओव्हरलॅपिंग टेबल्स हायलाइट करा',
},
new_diagram_dialog: {
database_selection: {
title: 'तुमचा डेटाबेस कोणता आहे?',
description:
'प्रत्येक डेटाबेसचे स्वतःचे युनिक वैशिष्ट्ये आणि क्षमता आहेत.',
check_examples_long: 'उदाहरणे तपासा',
check_examples_short: 'उदाहरणे',
},
import_database: {
title: 'तुमचा डेटाबेस आयात करा',
database_edition: 'डेटाबेस संस्करण:',
step_1: 'तुमच्या डेटाबेसमध्ये हा स्क्रिप्ट चालवा:',
step_2: 'स्क्रिप्टचा परिणाम येथे पेस्ट करा:',
script_results_placeholder: 'स्क्रिप्ट परिणाम येथे...',
ssms_instructions: {
button_text: 'SSMS सूचना',
title: 'सूचना',
step_1: 'टूल्स > पर्याय > क्वेरी परिणाम > SQL सर्व्हर वर जा.',
step_2: 'जर तुम्ही "ग्रिडला परिणाम" वापरत असाल, तर नॉन-XML डेटासाठी जास्तीत जास्त वर्ण पुनर्प्राप्ती बदला (9999999 वर सेट करा).',
},
// TODO: Add translations
instructions_link: 'Need help? Watch how',
check_script_result: 'Check Script Result',
},
cancel: 'रद्द करा',
// TODO: Add translations
import_from_file: 'Import from File',
back: 'मागे',
empty_diagram: 'रिक्त आरेख',
continue: 'सुरू ठेवा',
import: 'आयात करा',
},
open_diagram_dialog: {
title: 'आरेख उघडा',
description: 'खालील यादीतून उघडण्यासाठी एक आरेख निवडा.',
table_columns: {
name: 'नाव',
created_at: 'तयार केले',
last_modified: 'शेवटचे बदलले',
tables_count: 'टेबल्स',
},
cancel: 'रद्द करा',
open: 'उघडा',
},
export_sql_dialog: {
title: 'SQL निर्यात करा',
description:
'तुमच्या आरेख स्कीमाला {{databaseType}} स्क्रिप्टमध्ये निर्यात करा',
close: 'बंद करा',
loading: {
text: 'AI {{databaseType}} साठी SQL तयार करत आहे...',
description: 'याला 30 सेकंद लागतील.',
},
error: {
message:
'SQL स्क्रिप्ट तयार करताना एरर. कृपया नंतर पुन्हा प्रयत्न करा किंवा <0>आमच्याशी संपर्क साधा</0>.',
description:
'तुमचा OPENAI_TOKEN वापरण्यास मोकळे रहा, मॅन्युअल <0>येथे</0> पहा.',
},
},
create_relationship_dialog: {
title: 'रिलेशनशिप तयार करा',
primary_table: 'प्राथमिक टेबल',
primary_field: 'रेफरन्स फील्ड',
referenced_table: 'रेफरन्स टेबल',
referenced_field: 'रेफरन्स फील्ड',
primary_table_placeholder: 'टेबल निवडा',
primary_field_placeholder: 'फील्ड निवडा',
referenced_table_placeholder: 'टेबल निवडा',
referenced_field_placeholder: 'फील्ड निवडा',
no_tables_found: 'कोणतेही टेबल सापडले नाहीत',
no_fields_found: 'कोणतेही फील्ड सापडले नाहीत',
create: 'तयार करा',
cancel: 'रद्द करा',
},
import_database_dialog: {
title: 'सध्याच्या आरेखात आयात करा',
override_alert: {
title: 'डेटाबेस आयात करा',
content: {
alert: 'हा आरेख आयात केल्याने सध्याचे टेबल्स आणि रिलेशनशिप वर फरक पडेल.',
new_tables:
'<bold>{{newTablesNumber}}</bold> नवीन टेबल्स जोडले जातील.',
new_relationships:
'<bold>{{newRelationshipsNumber}}</bold> नवीन रिलेशनशिप तयार केले जातील.',
tables_override:
'<bold>{{tablesOverrideNumber}}</bold> टेबल्स अधिलिखित केले जातील.',
proceed: 'तुम्हाला पुढे जायचे आहे का?',
},
import: 'आयात करा',
cancel: 'रद्द करा',
},
},
export_image_dialog: {
title: 'इमेज निर्यात करा',
description: 'एक्स्पोर्ट करण्यासाठी स्केल फॅक्टर निवडा:',
scale_1x: '1x नियमित',
scale_2x: '2x (शिफारस केलेले)',
scale_3x: '3x',
scale_4x: '4x',
cancel: 'रद्द करा',
export: 'निर्यात करा',
},
new_table_schema_dialog: {
title: 'स्कीमा निवडा',
description:
'सध्या एकाधिक स्कीमा प्रदर्शित आहेत. नवीन टेबलसाठी एक निवडा.',
cancel: 'रद्द करा',
confirm: 'पुष्टी करा',
},
update_table_schema_dialog: {
title: 'स्कीमा बदला',
description: 'टेबल "{{tableName}}" स्कीमा अपडेट करा',
cancel: 'रद्द करा',
confirm: 'बदला',
},
star_us_dialog: {
title: 'आम्हाला सुधारण्यास मदत करा!',
description:
'तुम्हाला GitHub वर आम्हाला स्टार करायचे आहे का? हे फक्त एक क्लिक दूर आहे!',
close: 'आता नाही',
confirm: 'नक्कीच!',
},
// TODO: Add translations
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TO
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'एक ते एक',
one_to_many: 'एक ते अनेक',
many_to_one: 'अनेक ते एक',
many_to_many: 'अनेक ते अनेक',
},
canvas_context_menu: {
new_table: 'नवीन टेबल',
new_relationship: 'नवीन रिलेशनशिप',
},
table_node_context_menu: {
edit_table: 'टेबल संपादित करा',
delete_table: 'टेबल हटवा',
// TODO: Add translations
duplicate_table: 'Duplicate Table',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
// TODO: Add translations
tool_tips: {
double_click_to_edit: 'Double-click to edit',
},
language_select: {
change_language: 'भाषा बदला',
},
},
};
export const mrMetadata: LanguageMetadata = {
name: 'Marathi',
nativeName: 'मराठी',
code: 'mr',
};

394
src/i18n/locales/ne.ts Normal file
View File

@@ -0,0 +1,394 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ne: LanguageTranslation = {
translation: {
menu: {
file: {
file: 'फाइल',
new: 'नयाँ',
open: 'खोल्नुहोस्',
save: 'सुरक्षित गर्नुहोस्',
import_database: 'डाटाबेस आयात गर्नुहोस्',
export_sql: 'SQL निर्यात गर्नुहोस्',
export_as: 'निर्यात गर्नुहोस्',
delete_diagram: 'डायाग्राम हटाउनुहोस्',
exit: 'बाहिर निस्कनुहोस्',
},
edit: {
edit: 'सम्पादन',
undo: 'पूर्ववत',
redo: 'पुनः गर्नुहोस्',
clear: 'स्पष्ट',
},
view: {
view: 'हेर्नुहोस्',
show_sidebar: 'साइडबार देखाउनुहोस्',
hide_sidebar: 'साइडबार लुकाउनुहोस्',
hide_cardinality: 'कार्डिन्यालिटी लुकाउनुहोस्',
show_cardinality: 'कार्डिन्यालिटी देखाउनुहोस्',
zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्',
theme: 'थिम',
show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
hide_dependencies: 'डिपेन्डेन्सीहरू लुकाउनुहोस्',
},
share: {
share: 'शेयर गर्नुहोस्',
export_diagram: 'डायाग्राम निर्यात गर्नुहोस्',
import_diagram: 'डायाग्राम आयात गर्नुहोस्',
},
help: {
help: 'मद्दत',
visit_website: 'वेबसाइटमा जानुहोस्',
join_discord: 'डिस्कोर्डमा सामिल हुनुहोस्',
schedule_a_call: 'कल अनुसूची गर्नुहोस्',
},
},
delete_diagram_alert: {
title: 'डायाग्राम हटाउनुहोस्',
description:
'यो कार्य पूर्ववत गर्न सकिँदैन। यो डायाग्राम स्थायी रूपमा हटाउनेछ।',
cancel: 'रद्द गर्नुहोस्',
delete: 'हटाउनुहोस्',
},
clear_diagram_alert: {
title: 'डायाग्राम स्पष्ट गर्नुहोस्',
description:
'यो कार्य पूर्ववत गर्न सकिँदैन। यो डायाग्राम स्थायी रूपमा हटाउनेछ।',
cancel: 'रद्द गर्नुहोस्',
clear: 'स्पष्ट गर्नुहोस्',
},
reorder_diagram_alert: {
title: 'डायाग्राम पुनः क्रमबद्ध गर्नुहोस्',
description:
'यो कार्य पूर्ववत गर्न सकिँदैन। यो डायाग्राम स्थायी रूपमा हटाउनेछ।',
reorder: 'पुनः क्रमबद्ध गर्नुहोस्',
cancel: 'रद्द गर्नुहोस्',
},
multiple_schemas_alert: {
title: 'विविध स्कीमहरू',
description:
'{{schemasCount}} डायाग्राममा स्कीमहरू। हालको रूपमा देखाइएको छ: {{formattedSchemas}}।',
dont_show_again: 'फेरि देखाउन नदिनुहोस्',
change_schema: 'स्कीम परिवर्तन गर्नुहोस्',
none: 'कुनै पनि छैन',
},
theme: {
system: 'सिस्टम',
light: 'लाइट',
dark: 'डार्क',
},
zoom: {
on: 'चालू',
off: 'बन्द',
},
last_saved: 'अन्तिम सुरक्षित',
saved: 'सुरक्षित',
diagrams: 'डायाग्रामहरू',
loading_diagram: 'डायाग्राम लोड हुँदैछ...',
deselect_all: 'सबै चयन हटाउनुहोस्',
select_all: 'सबै चयन गर्नुहोस्',
clear: 'स्पष्ट',
show_more: 'थप देखाउनुहोस्',
show_less: 'कम देखाउनुहोस्',
copy_to_clipboard: 'क्लिपबोर्डमा प्रतिलिपि गर्नुहोस्',
copied: 'प्रतिलिपि गरियो!',
side_panel: {
schema: 'स्कीम:',
filter_by_schema: 'स्कीम अनुसार फिल्टर गर्नुहोस्',
search_schema: 'स्कीम खोज्नुहोस्...',
no_schemas_found: 'कुनै स्कीमहरू फेला परेनन्',
view_all_options: 'सबै विकल्पहरू हेर्नुहोस्',
tables_section: {
tables: 'तालिकाहरू',
add_table: 'तालिका थप्नुहोस्',
filter: 'फिल्टर',
collapse: 'सबै लुकाउनुहोस्',
table: {
fields: 'क्षेत्रहरू',
nullable: 'नलेबल?',
primary_key: 'प्राथमिक कुंजी',
indexes: 'सूचकहरू',
comments: 'टिप्पणीहरू',
no_comments: 'कुनै टिप्पणीहरू छैनन्',
add_field: 'क्षेत्र थप्नुहोस्',
add_index: 'सूचक थप्नुहोस्',
index_select_fields: 'क्षेत्रहरू चयन गर्नुहोस्',
no_types_found: 'कुनै प्रकारहरू फेला परेनन्',
field_name: 'नाम',
field_type: 'प्रकार',
field_actions: {
title: 'क्षेत्र विशेषताहरू',
unique: 'अनन्य',
comments: 'टिप्पणीहरू',
no_comments: 'कुनै टिप्पणीहरू छैनन्',
delete_field: 'क्षेत्र हटाउनुहोस्',
},
index_actions: {
title: 'सूचक विशेषताहरू',
name: 'नाम',
unique: 'अनन्य',
delete_index: 'सूचक हटाउनुहोस्',
},
table_actions: {
title: 'तालिका विशेषताहरू',
change_schema: 'स्कीम परिवर्तन गर्नुहोस्',
add_field: 'क्षेत्र थप्नुहोस्',
add_index: 'सूचक थप्नुहोस्',
duplicate_table: 'तालिकाको नक्कली रुप बनाउनुहोस',
delete_table: 'तालिका हटाउनुहोस्',
},
},
empty_state: {
title: 'कुनै तालिकाहरू छैनन्',
description: 'सुरु गर्नका लागि एक तालिका बनाउनुहोस्',
},
},
relationships_section: {
relationships: 'सम्बन्धहरू',
filter: 'फिल्टर',
add_relationship: 'सम्बन्ध थप्नुहोस्',
collapse: 'सबै लुकाउनुहोस्',
relationship: {
primary: 'मुख्य तालिका',
foreign: 'परिचित तालिका',
cardinality: 'कार्डिन्यालिटी',
delete_relationship: 'हटाउनुहोस्',
relationship_actions: {
title: 'कार्यहरू',
delete_relationship: 'हटाउनुहोस्',
},
},
empty_state: {
title: 'कुनै सम्बन्धहरू छैनन्',
description: 'तालिकाहरू जोड्नका लागि एक सम्बन्ध बनाउनुहोस्',
},
},
dependencies_section: {
dependencies: 'डिपेन्डेन्सीहरू',
filter: 'फिल्टर',
collapse: 'सबै लुकाउनुहोस्',
dependency: {
table: 'तालिका',
dependent_table: 'विचलित तालिका',
delete_dependency: 'हटाउनुहोस्',
dependency_actions: {
title: 'कार्यहरू',
delete_dependency: 'हटाउनुहोस्',
},
},
empty_state: {
title: 'कुनै डिपेन्डेन्सीहरू छैनन्',
description:
'डिपेन्डेन्सीहरू देखाउनका लागि एक व्यू बनाउनुहोस्',
},
},
},
toolbar: {
zoom_in: 'जुम इन',
zoom_out: 'जुम आउट',
save: 'सुरक्षित गर्नुहोस्',
show_all: 'सबै देखाउनुहोस्',
undo: 'पूर्ववत',
redo: 'पुनः गर्नुहोस्',
reorder_diagram: 'पुनः क्रमबद्ध गर्नुहोस्',
highlight_overlapping_tables:
'अतिरिक्त तालिकाहरू हाइलाइट गर्नुहोस्',
},
new_diagram_dialog: {
database_selection: {
title: 'तपाईंको डाटाबेस के हो?',
description:
'प्रत्येक डाटाबेसलाई आफ्नो विशेषता र क्षमताहरू छन्।',
check_examples_long: 'उदाहरणहरू हेर्नुहोस्',
check_examples_short: 'उदाहरणहरू',
},
import_database: {
title: 'तपाईंको डाटाबेस आयात गर्नुहोस्',
database_edition: 'डाटाबेस संस्करण:',
step_1: 'तपाईंको डाटाबेसमा यो स्क्रिप्ट चलाउनुहोस्:',
step_2: 'यो स्क्रिप्ट परिणाम यहाँ पेस्ट गर्नुहोस्:',
script_results_placeholder: 'स्क्रिप्ट परिणाम यहाँ...',
ssms_instructions: {
button_text: 'SSMS निर्देशन',
title: 'निर्देशन',
step_1: 'टुल्स > विकल्प > क्वेरी परिणाम > SQL सर्भरमा जानुहोस्।',
step_2: 'तपाईं "नतिजा ग्रिड" प्रयोग गरिरहेको छ भने, गैर-XML डाटाका लागि अधिकतम वर्णहरू प्राप्त गर्नका लागि परिणामहरू परिवर्तन गर्नुहोस् (९९९९९९९ मा सेट गर्नुहोस्)।',
},
instructions_link: 'मद्दत चाहिन्छ? हेर्नुहोस् कसरी',
check_script_result: 'स्क्रिप्ट परिणाम जाँच गर्नुहोस्',
},
cancel: 'रद्द गर्नुहोस्',
import_from_file: 'फाइलबाट आयात गर्नुहोस्',
back: 'फर्क',
empty_diagram: 'रिक्त डायाग्राम',
continue: 'जारी राख्नुहोस्',
import: 'आयात गर्नुहोस्',
},
open_diagram_dialog: {
title: 'डायाग्राम खोल्नुहोस्',
description:
'तलको सूचीबाट खोल्नका लागि एक डायाग्राम चयन गर्नुहोस्।',
table_columns: {
name: 'नाम',
created_at: 'मा सिर्जना',
last_modified: 'अन्तिम परिवर्तन',
tables_count: 'तालिकाहरू',
},
cancel: 'रद्द गर्नुहोस्',
open: 'खोल्नुहोस्',
},
export_sql_dialog: {
title: 'SQL निर्यात गर्नुहोस्',
description:
'तलको विकल्पहरूबाट तपाईंको डायाग्राम स्कीम निर्यात गर्नुहोस्।',
close: 'बन्द गर्नुहोस्',
loading: {
text: 'AI ले {{databaseType}} को लागि SQL उत्पन्न गर्दैछ...',
description: 'यो ३० सेकेण्डसम्म समय लिन्छ।',
},
error: {
message:
'SQL स्क्रिप्ट उत्पन्न गर्नमा त्रुटि। कृपया पछि प्रयास गर्नुहोस् वा <0>हामीलाई सम्पर्क गर्नुहोस्</0>।',
description:
'तपाईंले OPENAI_TOKEN प्रयोग गर्न सक्नुहुन्छ, यहाँ <0>यहाँ</0> म्यानुअल हेर्नुहोस्।',
},
},
create_relationship_dialog: {
title: 'सम्बन्ध बनाउनुहोस्',
primary_table: 'मुख्य तालिका',
primary_field: 'मुख्य क्षेत्र',
referenced_table: 'संदर्भित तालिका',
referenced_field: 'संदर्भित क्षेत्र',
primary_table_placeholder: 'तालिका चयन गर्नुहोस्',
primary_field_placeholder: 'क्षेत्र चयन गर्नुहोस्',
referenced_table_placeholder: 'तालिका चयन गर्नुहोस्',
referenced_field_placeholder: 'क्षेत्र चयन गर्नुहोस्',
no_tables_found: 'कुनै तालिकाहरू फेला परेनन्',
no_fields_found: 'कुनै क्षेत्रहरू फेला परेनन्',
create: 'बनाउनुहोस्',
cancel: 'रद्द गर्नुहोस्',
},
import_database_dialog: {
title: 'डाटाबेस आयात गर्नुहोस्',
override_alert: {
title: 'डाटाबेस आयात गर्नुहोस्',
content: {
alert: 'यो डायाग्राममा आयात गर्ने असर गर्नेछ।',
new_tables:
'<bold>{{newTablesNumber}}</bold> नयाँ तालिकाहरू थपिनेछन्।',
new_relationships:
'<bold>{{newRelationshipsNumber}}</bold> नयाँ सम्बन्धहरू बनाइनेछन्।',
tables_override:
'<bold>{{tablesOverrideNumber}}</bold> तालिकाहरू ओभरराइड गरिनेछन्।',
proceed: 'के तपाईं जारी गर्न चाहनुहुन्छ?',
},
import: 'आयात गर्नुहोस्',
cancel: 'रद्द गर्नुहोस्',
},
},
export_image_dialog: {
title: 'इमेज निर्यात गर्नुहोस्',
description: 'निर्यात गर्नका लागि गणना कारक छान्नुहोस्:',
scale_1x: '१x सामान्य',
scale_2x: '२x (सिफारिस गरिएको)',
scale_3x: '३x',
scale_4x: '४x',
cancel: 'रद्द गर्नुहोस्',
export: 'निर्यात गर्नुहोस्',
},
new_table_schema_dialog: {
title: 'स्कीम चयन गर्नुहोस्',
description:
'विभिन्न स्कीमहरू वर्तमानमा देखाइएको छन्। नयाँ तालिकाका लागि एक चयन गर्नुहोस्।',
cancel: 'रद्द गर्नुहोस्',
confirm: 'पुष्टि गर्नुहोस्',
},
update_table_schema_dialog: {
title: 'स्कीम परिवर्तन गर्नुहोस्',
description: 'तालिका "{{tableName}}" स्कीम अपडेट गर्नुहोस्',
cancel: 'रद्द गर्नुहोस्',
confirm: 'परिवर्तन गर्नुहोस्',
},
star_us_dialog: {
title: 'हामीलाई अझ राम्रो हुन मदत गर्नुहोस!',
description:
'के तपाईं हामीलाई GitHub मा स्टार गर्न चाहनुहुन्छ? यो केवल एक क्लिक पर छ!',
close: 'अहिले हैन',
confirm: 'अवस्य!',
},
export_diagram_dialog: {
title: 'डायाग्राम निर्यात गर्नुहोस्',
description: 'निर्यात गर्नका लागि निर्यात फरम्याट छान्नुहोस:',
format_json: 'JSON',
cancel: 'रद्द गर्नुहोस्',
export: 'निर्यात गर्नुहोस्',
},
import_diagram_dialog: {
title: 'डायाग्राम आयात गर्नुहोस्',
description: 'डायाग्राम JSON डेटा पेस्ट गर्नुहोस:',
cancel: 'रद्द गर्नुहोस्',
import: 'आयात गर्नुहोस्',
error: {
title: 'डायाग्राम आयात गर्दा समस्या आयो',
description:
'डायाग्राम JSON अमान्य छ। कृपया JSON जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्। मद्दत चाहिन्छ? chartdb.io@gmail.com मा सम्पर्क गर्नुहोस्',
},
},
relationship_type: {
one_to_one: 'एक देखि एक',
one_to_many: 'एक देखि धेरै',
many_to_one: 'धेरै देखि एक',
many_to_many: 'धेरै देखि धेरै',
},
canvas_context_menu: {
new_table: 'नयाँ तालिका',
new_relationship: 'नयाँ सम्बन्ध',
},
table_node_context_menu: {
edit_table: 'तालिका सम्पादन गर्नुहोस्',
duplicate_table: 'तालिका नक्कली गर्नुहोस्',
delete_table: 'तालिका हटाउनुहोस्',
},
snap_to_grid_tooltip: 'ग्रिडमा स्न्याप गर्नुहोस् ({{key}} थिच्नुहोस)',
tool_tips: {
double_click_to_edit: 'सम्पादन गर्नका लागि डबल क्लिक गर्नुहोस्',
},
language_select: {
change_language: 'भाषा परिवर्तन गर्नुहोस्',
},
},
};
export const neMetadata: LanguageMetadata = {
name: 'Nepali',
nativeName: 'नेपाली',
code: 'ne',
};

View File

@@ -28,10 +28,15 @@ export const pt_BR: LanguageTranslation = {
show_cardinality: 'Mostrar Cardinalidade',
zoom_on_scroll: 'Zoom ao Rolar',
theme: 'Tema',
change_language: 'Idioma',
show_dependencies: 'Mostrar Dependências',
hide_dependencies: 'Ocultar Dependências',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'Ajuda',
visit_website: 'Visitar ChartDB',
@@ -139,6 +144,7 @@ export const pt_BR: LanguageTranslation = {
change_schema: 'Alterar Esquema',
add_field: 'Adicionar Campo',
add_index: 'Adicionar Índice',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Excluir Tabela',
},
},
@@ -225,6 +231,8 @@ export const pt_BR: LanguageTranslation = {
cancel: 'Cancelar',
back: 'Voltar',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: 'Diagrama vazio',
continue: 'Continuar',
import: 'Importar',
@@ -328,7 +336,26 @@ export const pt_BR: LanguageTranslation = {
close: 'Agora não',
confirm: 'Claro!',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'Um para Um',
one_to_many: 'Um para Muitos',
@@ -343,12 +370,25 @@ export const pt_BR: LanguageTranslation = {
table_node_context_menu: {
edit_table: 'Editar Tabela',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Excluir Tabela',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'Duplo clique para editar',
},
language_select: {
change_language: 'Idioma',
},
},
};
export const pt_BRMetadata: LanguageMetadata = {
name: 'Portuguese',
nativeName: 'Português',
code: 'pt_BR',
};

389
src/i18n/locales/ru.ts Normal file
View File

@@ -0,0 +1,389 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ru: LanguageTranslation = {
translation: {
menu: {
file: {
file: 'Файл',
new: 'Создать',
open: 'Открыть',
save: 'Сохранить',
import_database: 'Импортировать базу данных',
export_sql: 'Экспорт SQL',
export_as: 'Экспортировать как',
delete_diagram: 'Удалить диаграмму',
exit: 'Выход',
},
edit: {
edit: 'Изменение',
undo: 'Отменить',
redo: 'Вернуть',
clear: 'Очистить',
},
view: {
view: 'Вид',
show_sidebar: 'Показать боковую панель',
hide_sidebar: 'Скрыть боковую панель',
hide_cardinality: 'Скрыть множественность связи',
show_cardinality: 'Показать множественность связи',
zoom_on_scroll: 'Увеличение при прокрутке',
theme: 'Тема',
show_dependencies: 'Показать зависимости',
hide_dependencies: 'Скрыть зависимости',
},
share: {
share: 'Поделиться',
export_diagram: 'Экспорт кода диаграммы',
import_diagram: 'Импорт кода диаграммы',
},
help: {
help: 'Помощь',
visit_website: 'Перейти на сайт ChartDB',
join_discord: 'Присоединиться к сообществу в Discord',
schedule_a_call: 'Поговорите с нами!',
},
},
delete_diagram_alert: {
title: 'Удалить диаграмму',
description:
'Это действие нельзя отменить. Это навсегда удалит диаграмму.',
cancel: 'Отменить',
delete: 'Удалить',
},
clear_diagram_alert: {
title: 'Очистить диаграмму',
description:
'Это действие нельзя отменить. Это навсегда удалит все данные в диаграмме.',
cancel: 'Отменить',
clear: 'Очистить',
},
reorder_diagram_alert: {
title: 'Переупорядочить диаграмму',
description:
'Это действие переставит все таблицы на диаграмме. Хотите продолжить?',
reorder: 'Изменить порядок',
cancel: 'Отменить',
},
multiple_schemas_alert: {
title: 'Множественные схемы',
description:
'{{schemasCount}} схем в этой диаграмме. В данный момент отображается: {{formattedSchemas}}.',
dont_show_again: 'Больше не показывать',
change_schema: 'Изменить',
none: 'никто',
},
theme: {
system: 'Системная',
light: 'Светлая',
dark: 'Темная',
},
zoom: {
on: 'Включено',
off: 'Выключено',
},
last_saved: 'Последнее сохранение',
saved: 'Сохранено',
diagrams: 'Диаграммы',
loading_diagram: 'Загрузка диаграммы...',
deselect_all: 'Отменить выбор всех',
select_all: 'Выбрать все',
clear: 'Очистить',
show_more: 'Показать больше',
show_less: 'Показать меньше',
side_panel: {
schema: 'Схема:',
filter_by_schema: 'Фильтр по схеме',
search_schema: 'Схема поиска...',
no_schemas_found: 'Схемы не найдены.',
view_all_options: 'Просмотреть все варианты...',
tables_section: {
tables: 'Таблицы',
add_table: 'Добавить таблицу',
filter: 'Фильтр',
collapse: 'Свернуть все',
table: {
fields: 'Поля',
nullable: 'Может содержать NULL?',
primary_key: 'Первичный ключ,',
indexes: 'Индексы',
comments: 'Комментарии',
no_comments: 'Нет комментария',
add_field: 'Добавить поле',
add_index: 'Добавить индекс',
index_select_fields: 'Выберите поля',
no_types_found: 'Типы не найдены',
field_name: 'Имя',
field_type: 'Тип',
field_actions: {
title: 'Атрибуты поля',
unique: 'Уникальный',
comments: 'Комментарии',
no_comments: 'Нет комментария',
delete_field: 'Удалить поле',
},
index_actions: {
title: 'Атрибуты индекса',
name: 'Имя',
unique: 'Уникальный',
delete_index: 'Удалить индекс',
},
table_actions: {
title: 'Действия',
change_schema: 'Изменить схему',
add_field: 'Добавить поле',
add_index: 'Добавить индекс',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Удалить таблицу',
},
},
empty_state: {
title: 'Нет таблиц',
description: 'Создайте таблицу, чтобы начать',
},
},
relationships_section: {
relationships: 'Отношения',
filter: 'Фильтр',
add_relationship: 'Добавить отношение',
collapse: 'Свернуть все',
relationship: {
primary: 'Основная таблица',
foreign: 'Справочная таблица',
cardinality: 'Тип множественности связи',
delete_relationship: 'Удалить',
relationship_actions: {
title: 'Действия',
delete_relationship: 'Удалить',
},
},
empty_state: {
title: 'Нет отношений',
description: 'Создайте связь для соединения таблиц',
},
},
dependencies_section: {
dependencies: 'Зависимости',
filter: 'Фильтр',
collapse: 'Свернуть все',
dependency: {
table: 'Стол',
dependent_table: 'Зависимый вид',
delete_dependency: 'Удалить',
dependency_actions: {
title: 'Действия',
delete_dependency: 'Удалить',
},
},
empty_state: {
title: 'Нет зависимостей',
description: 'Создайте представление, чтобы начать',
},
},
},
toolbar: {
zoom_in: 'Увеличить масштаб',
zoom_out: 'Уменьшить масштаб',
save: 'Сохранить',
show_all: 'Показать все',
undo: 'Отменить',
redo: 'Вернуть',
reorder_diagram: 'Переупорядочить диаграмму',
highlight_overlapping_tables: 'Выделение перекрывающихся таблиц',
},
new_diagram_dialog: {
database_selection: {
title: 'Какова ваша база данных?',
description:
'Каждая база данных имеет свои уникальные функции и возможности.',
check_examples_long: 'Открыть примеры',
check_examples_short: 'Примеры',
},
import_database: {
title: 'Импортируйте свою базу данных',
database_edition: 'Версия базы данных:',
step_1: 'Запустите этот скрипт в своей базе данных:',
step_2: 'Вставьте вывод скрипта сюда:',
script_results_placeholder: 'Вывод скрипта здесь...',
ssms_instructions: {
button_text: 'SSMS Инструкции',
title: 'Инструкции',
step_1: 'Откройте в меню пункты Инструменты > Параметры > Результаты запроса > SQL Сервер.',
step_2: 'Если вы используете "Результат в сетке," измените Максимальное количество извлекаемых символов для данных, отличных от XML (установите на 9999999).',
},
instructions_link: 'Нужна помощь? Посмотрите, как',
check_script_result: 'Проверить результат выполнения скрипта',
},
cancel: 'Отменить',
back: 'Назад',
import_from_file: 'Импортировать из файла',
empty_diagram: 'Пустая диаграмма',
continue: 'Продолжить',
import: 'Импорт',
},
open_diagram_dialog: {
title: 'Открыть диаграмму',
description:
'Выберите диаграмму, которую нужно открыть, из списка ниже.',
table_columns: {
name: 'Имя',
created_at: 'Создано в',
last_modified: 'Последнее изменение',
tables_count: 'Таблицы',
},
cancel: 'Отмена',
open: 'Открыть',
},
export_sql_dialog: {
title: 'Экспорт SQL',
description:
'Экспортируйте схему диаграммы в {{databaseType}} скрипт',
close: 'Закрыть',
loading: {
text: 'ИИ генерирует SQL для {{databaseType}}...',
description: 'Это должно занять до 30 секунд.',
},
error: {
message:
'Ошибка создания скрипта SQL. Попробуйте еще раз позже или <0>свяжитесь с нами</0>.',
description:
'Не стесняйтесь использовать ваш OPENAI_TOKEN, см. руководство <0>здесь</0>.',
},
},
create_relationship_dialog: {
title: 'Создать отношениe',
primary_table: 'Основная таблица',
primary_field: 'Основное поле',
referenced_table: 'Ссылается на таблицу',
referenced_field: 'Ссылается на поле',
primary_table_placeholder: 'Выберите таблицу',
primary_field_placeholder: 'Выберите поле',
referenced_table_placeholder: 'Выберите таблицу',
referenced_field_placeholder: 'Выберите поле',
no_tables_found: 'Таблицы не найдены',
no_fields_found: 'Поля не найдены',
create: 'Создать',
cancel: 'Отменить',
},
import_database_dialog: {
title: 'Импорт в текущую диаграмму',
override_alert: {
title: 'Импортировать базу данных',
content: {
alert: 'Импорт этой диаграммы повлияет на существующие таблицы и связи.',
new_tables:
'<bold>{{newTablesNumber}}</bold> будут добавлены новые таблицы.',
new_relationships:
'<bold>{{newRelationshipsNumber}}</bold> будут созданы новые отношения.',
tables_override:
'<bold>{{tablesOverrideNumber}}</bold> таблицы будут перезаписаны.',
proceed: 'Хотите продолжить?',
},
import: 'Импорт',
cancel: 'Отмена',
},
},
export_image_dialog: {
title: 'Экспортировать изображение',
description: 'Выберите детализацию изображения при экспорте:',
scale_1x: '1x Обычный',
scale_2x: '2x (Рекомендовано)',
scale_3x: '3x',
scale_4x: '4x',
cancel: 'Отменить',
export: 'Экспортировать',
},
new_table_schema_dialog: {
title: 'Выбрать схему',
description:
'В настоящее время отображается несколько схем. Выберите одну для новой таблицы.',
cancel: 'Отменить',
confirm: 'Подтвердить',
},
update_table_schema_dialog: {
title: 'Изменить схему',
description: 'Обновить таблицу "{{tableName}}" схема',
cancel: 'Отменить',
confirm: 'Изменить',
},
star_us_dialog: {
title: 'Помогите нам стать лучше!',
description:
'Хотите отметить нас на GitHub? Это всего лишь один клик!',
close: 'Не сейчас',
confirm: 'Конечно!',
},
export_diagram_dialog: {
title: 'Экспорт кода диаграммы',
description: 'Выберите формат экспорта:',
format_json: 'JSON',
cancel: 'Отменить',
export: 'Экспортировать',
},
import_diagram_dialog: {
title: 'Импорт кода диаграммы',
description: 'Вставьте JSON код диаграммы ниже:',
cancel: 'Отменить',
import: 'Импортировать',
error: {
title: 'Ошибка при импорте диаграммы',
description:
'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'Один к одному',
one_to_many: 'Один ко многим',
many_to_one: 'Многие к одному',
many_to_many: 'Многие ко многим',
},
canvas_context_menu: {
new_table: 'Создать таблицу',
new_relationship: 'Создать отношение',
},
table_node_context_menu: {
edit_table: 'Изменить таблицу',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Удалить таблицу',
},
copy_to_clipboard: 'Скопировать в буфер обмена',
copied: 'Скопировано!',
snap_to_grid_tooltip: 'Выравнивание по сетке (Удерживайте {{key}})',
tool_tips: {
double_click_to_edit: 'Кликните дважды, чтобы изменить',
},
language_select: {
change_language: 'Сменить язык',
},
},
};
export const ruMetadata: LanguageMetadata = {
name: 'Russian',
nativeName: 'Русский',
code: 'ru',
};

View File

@@ -28,10 +28,15 @@ export const uk: LanguageTranslation = {
show_cardinality: 'Показати кардинальність',
zoom_on_scroll: 'Збільшити прокручування',
theme: 'Тема',
change_language: 'Мова',
show_dependencies: 'Показати залежності',
hide_dependencies: 'Приховати залежності',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'Допомога',
visit_website: 'Відвідайте ChartDB',
@@ -139,6 +144,7 @@ export const uk: LanguageTranslation = {
change_schema: 'Змінити схему',
add_field: 'Додати поле',
add_index: 'Додати індекс',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Видалити таблицю',
},
},
@@ -225,6 +231,8 @@ export const uk: LanguageTranslation = {
cancel: 'Скасувати',
back: 'Назад',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: 'Порожня діаграма',
continue: 'Продовжити',
import: 'Імпорт',
@@ -328,7 +336,26 @@ export const uk: LanguageTranslation = {
close: 'Не зараз',
confirm: 'звичайно!',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'Один до одного',
one_to_many: 'Один до багатьох',
@@ -343,12 +370,25 @@ export const uk: LanguageTranslation = {
table_node_context_menu: {
edit_table: 'Редагувати таблицю',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Видалити таблицю',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'Двойной клик для редактирования',
},
language_select: {
change_language: 'Мова',
},
},
};
export const ukMetadata: LanguageMetadata = {
name: 'Українська',
name: 'Ukrainian',
nativeName: 'Українська',
code: 'uk',
};

384
src/i18n/locales/zh_CN.ts Normal file
View File

@@ -0,0 +1,384 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const zh_CN: LanguageTranslation = {
translation: {
menu: {
file: {
file: '文件',
new: '新建',
open: '打开',
save: '保存',
import_database: '导入数据库',
export_sql: '导出 SQL 语句',
export_as: '导出为',
delete_diagram: '删除关系图',
exit: '退出',
},
edit: {
edit: '编辑',
undo: '撤销',
redo: '重做',
clear: '清空',
},
view: {
view: '视图',
show_sidebar: '展示侧边栏',
hide_sidebar: '隐藏侧边栏',
hide_cardinality: '隐藏基数',
show_cardinality: '展示基数',
zoom_on_scroll: '滚动缩放',
theme: '主题',
show_dependencies: '展示依赖',
hide_dependencies: '隐藏依赖',
},
share: {
share: '分享',
export_diagram: '导出关系图',
import_diagram: '导入关系图',
},
help: {
help: '帮助',
visit_website: '访问 ChartDB',
join_discord: '在 Discord 上加入我们',
schedule_a_call: '和我们交流!',
},
},
delete_diagram_alert: {
title: '删除关系图',
description: '此操作无法撤销。这将永久删除关系图。',
cancel: '取消',
delete: '删除',
},
clear_diagram_alert: {
title: '清除关系图',
description: '此操作无法撤销。这将永久删除关系图中的所有数据。',
cancel: '取消',
clear: '清空',
},
reorder_diagram_alert: {
title: '重新排列关系图',
description: '此操作将重新排列关系图中的所有表。是否要继续?',
reorder: '重新排列',
cancel: '取消',
},
multiple_schemas_alert: {
title: '多个模式',
description:
'此关系图中有 {{schemasCount}} 个模式,当前显示:{{formattedSchemas}}。',
dont_show_again: '不再展示',
change_schema: '更改',
none: '无',
},
theme: {
system: '系统',
light: '浅色',
dark: '深色',
},
zoom: {
on: '启用',
off: '禁用',
},
last_saved: '上次保存时间:',
saved: '已保存',
diagrams: '关系图',
loading_diagram: '加载关系图...',
deselect_all: '取消全选',
select_all: '全选',
clear: '清空',
show_more: '展开',
show_less: '收起',
copy_to_clipboard: '复制到剪切板',
copied: '复制了!',
side_panel: {
schema: '模式:',
filter_by_schema: '按模式筛选',
search_schema: '搜索模式...',
no_schemas_found: '未找到模式。',
view_all_options: '查看所有选项...',
tables_section: {
tables: '表',
add_table: '添加表',
filter: '筛选',
collapse: '全部折叠',
table: {
fields: '字段',
nullable: '可为空?',
primary_key: '主键',
indexes: '索引',
comments: '注释',
no_comments: '空',
add_field: '添加字段',
add_index: '添加索引',
index_select_fields: '选择字段',
no_types_found: '未找到类型',
field_name: '名称',
field_type: '类型',
field_actions: {
title: '字段属性',
unique: '唯一',
comments: '注释',
no_comments: '空',
delete_field: '删除字段',
},
index_actions: {
title: '索引属性',
name: '名称',
unique: '唯一',
delete_index: '删除索引',
},
table_actions: {
title: '表操作',
change_schema: '更改模式',
add_field: '添加字段',
add_index: '添加索引',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: '删除表',
},
},
empty_state: {
title: '没有表',
description: '新建表以开始',
},
},
relationships_section: {
relationships: '关系',
filter: '筛选',
add_relationship: '添加关系',
collapse: '全部折叠',
relationship: {
primary: '主表',
foreign: '被引用表',
cardinality: '基数',
delete_relationship: '删除',
relationship_actions: {
title: '操作',
delete_relationship: '删除',
},
},
empty_state: {
title: '无关系',
description: '创建关系以连接表',
},
},
dependencies_section: {
dependencies: '依赖关系',
filter: '筛选',
collapse: '全部折叠',
dependency: {
table: '表',
dependent_table: '依赖视图',
delete_dependency: '删除',
dependency_actions: {
title: '操作',
delete_dependency: '删除',
},
},
empty_state: {
title: '无依赖',
description: '创建视图以开始',
},
},
},
toolbar: {
zoom_in: '放大',
zoom_out: '缩小',
save: '保存',
show_all: '展示全部',
undo: '撤销',
redo: '重做',
reorder_diagram: '重新排列关系图',
highlight_overlapping_tables: '突出显示重叠的表',
},
new_diagram_dialog: {
database_selection: {
title: '您是哪种数据库?',
description: '每种数据库都有其特性和功能。',
check_examples_long: '查看样例',
check_examples_short: '样例',
},
import_database: {
title: '导入您的数据库',
database_edition: '数据库类型:',
step_1: '在您的数据库中执行以下脚本:',
step_2: '将结果粘贴于此:',
script_results_placeholder: '结果...',
ssms_instructions: {
button_text: 'SSMS 说明',
title: '说明',
step_1: '前往 工具 > 选项 > 查询结果 > SQL Server。',
// TODO: Add translations
step_2: '如果您使用“Result to Grid”功能请将非 XML 数据的最大提取字符数更改为 9999999。',
},
instructions_link: '需要帮助?看看如何操作',
check_script_result: '检查脚本结果',
},
cancel: '取消',
import_from_file: '从文件导入',
back: '上一步',
empty_diagram: '新建空关系图',
continue: '下一步',
import: '导入',
},
open_diagram_dialog: {
title: '打开关系图',
description: '从下面的列表中选择一个图表打开。',
table_columns: {
name: '名称',
created_at: '创建于',
last_modified: '最后修改于',
tables_count: '表数量',
},
cancel: '取消',
open: '打开',
},
export_sql_dialog: {
title: '导出 SQL 语句',
description: '将您的图表模式导出为 {{databaseType}} 脚本。',
close: '关闭',
loading: {
text: 'AI 正在为 {{databaseType}} 生成 SQL 语句...',
description: '此操作最多需要 30 秒。',
},
error: {
message:
'生成 SQL 脚本时出错。请稍后再试,或者 <0>联系我们</0>。',
description:
'随时使用您的 OPENAI_TOKEN在<0>这里</0>查看手册。',
},
},
create_relationship_dialog: {
title: '创建关系',
primary_table: '主表',
primary_field: '主键字段',
referenced_table: '被引用表',
referenced_field: '被引用字段',
primary_table_placeholder: '选择表',
primary_field_placeholder: '选择字段',
referenced_table_placeholder: '选择表',
referenced_field_placeholder: '选择字段',
no_tables_found: '未找到表',
no_fields_found: '未找到字段',
create: '创建',
cancel: '取消',
},
import_database_dialog: {
title: '导入到当前关系图',
override_alert: {
title: '导入数据库',
content: {
alert: '导入此关系图将影响现有的表和关系。',
new_tables:
'将添加 <bold>{{newTablesNumber}}</bold> 个新表。',
new_relationships:
'将创建 <bold>{{newRelationshipsNumber}}</bold> 个新关系。',
tables_override:
'将覆盖 <bold>{{tablesOverrideNumber}}</bold> 个表。',
proceed: '您是否要继续操作?',
},
import: '导入',
cancel: '取消',
},
},
export_image_dialog: {
title: '导出图片',
description: '选择导出的缩放比例:',
scale_1x: '1x 常规',
scale_2x: '2x (推荐)',
scale_3x: '3x',
scale_4x: '4x',
cancel: '取消',
export: '导出',
},
new_table_schema_dialog: {
title: '选择模式',
description: '当前显示多个模式。请选择一个用于新表。',
cancel: '取消',
confirm: '确认',
},
update_table_schema_dialog: {
title: '更改模式',
description: '更新表 "{{tableName}}" 的模式。',
cancel: '取消',
confirm: '更改',
},
star_us_dialog: {
title: '帮助我们改进!',
description: '您想在 GitHub 上为我们加注星标吗?只需点击一下即可!',
close: '以后再说',
confirm: '当然!',
},
export_diagram_dialog: {
title: '导出关系图',
description: '选择导出格式:',
format_json: 'JSON',
cancel: '取消',
export: '导出',
},
import_diagram_dialog: {
title: '导入关系图',
description: '在下方粘贴关系图的 JSON',
cancel: '取消',
import: '导入',
error: {
title: '导入关系图时出错',
description:
'关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: '一对一',
one_to_many: '一对多',
many_to_one: '多对一',
many_to_many: '多对多',
},
canvas_context_menu: {
new_table: '新建表',
new_relationship: '新建关系',
},
table_node_context_menu: {
edit_table: '编辑表',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: '删除表',
},
snap_to_grid_tooltip: '对齐到网格(按住 {{key}}',
tool_tips: {
double_click_to_edit: '双击编辑',
},
language_select: {
change_language: '语言',
},
},
};
export const zh_CNMetadata: LanguageMetadata = {
name: 'Chinese',
nativeName: '简体中文',
code: 'zh_CN',
};

383
src/i18n/locales/zh_TW.ts Normal file
View File

@@ -0,0 +1,383 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const zh_TW: LanguageTranslation = {
translation: {
menu: {
file: {
file: '檔案',
new: '新增',
open: '開啟',
save: '儲存',
import_database: '匯入資料庫',
export_sql: '匯出 SQL',
export_as: '匯出為特定格式',
delete_diagram: '刪除圖表',
exit: '退出',
},
edit: {
edit: '編輯',
undo: '復原',
redo: '重做',
clear: '清除',
},
view: {
view: '檢視',
show_sidebar: '顯示側邊欄',
hide_sidebar: '隱藏側邊欄',
hide_cardinality: '隱藏基數',
show_cardinality: '顯示基數',
zoom_on_scroll: '滾動縮放',
theme: '主題',
show_dependencies: '顯示相依性',
hide_dependencies: '隱藏相依性',
},
share: {
share: '分享',
export_diagram: '匯出圖表',
import_diagram: '匯入圖表',
},
help: {
help: '幫助',
visit_website: '訪問 ChartDB 網站',
join_discord: '加入 Discord',
schedule_a_call: '與我們聯絡!',
},
},
delete_diagram_alert: {
title: '刪除圖表',
description: '此操作無法復原,圖表將被永久刪除。',
cancel: '取消',
delete: '刪除',
},
clear_diagram_alert: {
title: '清除圖表',
description: '此操作無法復原,圖表中的所有資料將被永久刪除。',
cancel: '取消',
clear: '清除',
},
reorder_diagram_alert: {
title: '重新排列圖表',
description: '此操作將重新排列圖表中的所有表格。是否繼續?',
reorder: '重新排列',
cancel: '取消',
},
multiple_schemas_alert: {
title: '多重 Schema',
description:
'此圖表中包含 {{schemasCount}} 個 Schema目前顯示{{formattedSchemas}}。',
dont_show_again: '不再顯示',
change_schema: '變更',
none: '無',
},
theme: {
system: '系統',
light: '淺色',
dark: '深色',
},
zoom: {
on: '開啟',
off: '關閉',
},
last_saved: '上次儲存於',
saved: '已儲存',
diagrams: '圖表',
loading_diagram: '正在載入圖表...',
deselect_all: '取消所有選取',
select_all: '全選',
clear: '清除',
show_more: '顯示更多',
show_less: '顯示較少',
copy_to_clipboard: '複製到剪貼簿',
copied: '已複製!',
side_panel: {
schema: 'Schema:',
filter_by_schema: '依 Schema 篩選',
search_schema: '搜尋 Schema...',
no_schemas_found: '未找到 Schema。',
view_all_options: '顯示所有選項...',
tables_section: {
tables: '表格',
add_table: '新增表格',
filter: '篩選',
collapse: '全部摺疊',
table: {
fields: '欄位',
nullable: '可為 NULL?',
primary_key: '主鍵',
indexes: '索引',
comments: '註解',
no_comments: '無註解',
add_field: '新增欄位',
add_index: '新增索引',
index_select_fields: '選擇欄位',
no_types_found: '未找到類型',
field_name: '名稱',
field_type: '類型',
field_actions: {
title: '欄位屬性',
unique: '唯一',
comments: '註解',
no_comments: '無註解',
delete_field: '刪除欄位',
},
index_actions: {
title: '索引屬性',
name: '名稱',
unique: '唯一',
delete_index: '刪除索引',
},
table_actions: {
title: '表格操作',
change_schema: '變更 Schema',
add_field: '新增欄位',
add_index: '新增索引',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: '刪除表格',
},
},
empty_state: {
title: '尚無表格',
description: '請新增表格以開始',
},
},
relationships_section: {
relationships: '關聯',
filter: '篩選',
add_relationship: '新增關聯',
collapse: '全部摺疊',
relationship: {
primary: '主表格',
foreign: '參照表格',
cardinality: '基數',
delete_relationship: '刪除',
relationship_actions: {
title: '操作',
delete_relationship: '刪除',
},
},
empty_state: {
title: '尚無關聯',
description: '請新增關聯以連接表格',
},
},
dependencies_section: {
dependencies: '相依性',
filter: '篩選',
collapse: '全部摺疊',
dependency: {
table: '表格',
dependent_table: '相依檢視',
delete_dependency: '刪除',
dependency_actions: {
title: '操作',
delete_dependency: '刪除',
},
},
empty_state: {
title: '尚無相依性',
description: '請建立檢視以開始',
},
},
},
toolbar: {
zoom_in: '放大',
zoom_out: '縮小',
save: '儲存',
show_all: '顯示全部',
undo: '復原',
redo: '重做',
reorder_diagram: '重新排列圖表',
highlight_overlapping_tables: '突出顯示重疊表格',
},
new_diagram_dialog: {
database_selection: {
title: '您使用的是哪種資料庫?',
description: '每種資料庫都有其獨特的功能和能力。',
check_examples_long: '查看範例',
check_examples_short: '範例',
},
import_database: {
title: '匯入資料庫',
database_edition: '資料庫版本:',
step_1: '請在資料庫中執行以下腳本:',
step_2: '將腳本結果貼到此處:',
script_results_placeholder: '在此處貼上腳本結果...',
ssms_instructions: {
button_text: 'SSMS 操作步驟',
title: '操作步驟',
step_1: '導航至 工具 > 選項 > 查詢結果 > SQL Server。',
step_2: '若使用「結果至網格」,請更改非 XML 資料的最大取得字元數(設定為 9999999。',
},
instructions_link: '需要幫助?觀看教學影片',
check_script_result: '檢查腳本結果',
},
cancel: '取消',
import_from_file: '從檔案匯入',
back: '返回',
empty_diagram: '空白圖表',
continue: '繼續',
import: '匯入',
},
open_diagram_dialog: {
title: '開啟圖表',
description: '請從以下列表中選擇一個圖表。',
table_columns: {
name: '名稱',
created_at: '創建時間',
last_modified: '最後修改時間',
tables_count: '表格數',
},
cancel: '取消',
open: '開啟',
},
export_sql_dialog: {
title: '匯出 SQL',
description: '將圖表 Schema 匯出為 {{databaseType}} 格式的腳本',
close: '關閉',
loading: {
text: 'AI 正在生成 {{databaseType}} 的 SQL...',
description: '最多需要 30 秒。',
},
error: {
message:
'生成 SQL 腳本時發生錯誤。稍後再試,或<0>聯繫我們</0>。',
description:
'可以自由使用 OPENAI_TOKEN詳細說明可參考<0>此處</0>。',
},
},
create_relationship_dialog: {
title: '新增關聯',
primary_table: '主表格',
primary_field: '主欄位',
referenced_table: '參照表格',
referenced_field: '參照欄位',
primary_table_placeholder: '選擇表格',
primary_field_placeholder: '選擇欄位',
referenced_table_placeholder: '選擇表格',
referenced_field_placeholder: '選擇欄位',
no_tables_found: '未找到表格',
no_fields_found: '未找到欄位',
create: '建立',
cancel: '取消',
},
import_database_dialog: {
title: '匯入至當前圖表',
override_alert: {
title: '匯入資料庫',
content: {
alert: '匯入此圖表將影響現有表格和關聯。',
new_tables:
'<bold>{{newTablesNumber}}</bold> 個新表格將被新增。',
new_relationships:
'<bold>{{newRelationshipsNumber}}</bold> 個新關聯將被建立。',
tables_override:
'<bold>{{tablesOverrideNumber}}</bold> 個表格將被覆蓋。',
proceed: '是否繼續?',
},
import: '匯入',
cancel: '取消',
},
},
export_image_dialog: {
title: '匯出圖片',
description: '請選擇匯出的倍率:',
scale_1x: '1x 標準',
scale_2x: '2x (推薦)',
scale_3x: '3x',
scale_4x: '4x',
cancel: '取消',
export: '匯出',
},
new_table_schema_dialog: {
title: '選擇 Schema',
description: '目前顯示多個 Schema請為新表格選擇一個。',
cancel: '取消',
confirm: '確認',
},
update_table_schema_dialog: {
title: '變更 Schema',
description: '更新表格「{{tableName}}」的 Schema',
cancel: '取消',
confirm: '變更',
},
star_us_dialog: {
title: '協助我們改善!',
description: '請在 GitHub 上給我們一顆星,只需點擊一下!',
close: '先不要',
confirm: '當然!',
},
export_diagram_dialog: {
title: '匯出圖表',
description: '選擇匯出格式:',
format_json: 'JSON',
cancel: '取消',
export: '匯出',
},
import_diagram_dialog: {
title: '匯入圖表',
description: '請在下方貼上圖表的 JSON',
cancel: '取消',
import: '匯入',
error: {
title: '匯入圖表時發生錯誤',
description:
'圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: '一對一',
one_to_many: '一對多',
many_to_one: '多對一',
many_to_many: '多對多',
},
canvas_context_menu: {
new_table: '新建表格',
new_relationship: '新建關聯',
},
table_node_context_menu: {
edit_table: '編輯表格',
duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: '刪除表格',
},
snap_to_grid_tooltip: '對齊網格(按住 {{key}}',
tool_tips: {
double_click_to_edit: '雙擊以編輯',
},
language_select: {
change_language: '變更語言',
},
},
};
export const zh_TWMetadata: LanguageMetadata = {
nativeName: '繁體中文',
name: 'Traditional Chinese',
code: 'zh_TW',
};

View File

@@ -4,5 +4,6 @@ export type LanguageTranslation = typeof en;
export type LanguageMetadata = {
name: string;
nativeName: string;
code: string;
};

View File

@@ -19,4 +19,7 @@
.scrollable-flex > div {
@apply !flex;
}
.marker-definitions {
}
}

149
src/lib/clone.ts Normal file
View File

@@ -0,0 +1,149 @@
import type { DBDependency } from './domain/db-dependency';
import type { DBField } from './domain/db-field';
import type { DBIndex } from './domain/db-index';
import type { DBRelationship } from './domain/db-relationship';
import type { DBTable } from './domain/db-table';
import type { Diagram } from './domain/diagram';
import { generateId as defaultGenerateId } from './utils';
const generateIdsMapFromTable = (
table: DBTable,
generateId: () => string = defaultGenerateId
): Map<string, string> => {
const idsMap = new Map<string, string>();
idsMap.set(table.id, generateId());
table.fields.forEach((field) => {
idsMap.set(field.id, generateId());
});
table.indexes.forEach((index) => {
idsMap.set(index.id, generateId());
});
return idsMap;
};
const generateIdsMapFromDiagram = (
diagram: Diagram,
generateId: () => string = defaultGenerateId
): Map<string, string> => {
let idsMap = new Map<string, string>();
diagram.tables?.forEach((table) => {
const tableIdsMap = generateIdsMapFromTable(table, generateId);
idsMap = new Map([...idsMap, ...tableIdsMap]);
});
diagram.relationships?.forEach((relationship) => {
idsMap.set(relationship.id, generateId());
});
diagram.dependencies?.forEach((dependency) => {
idsMap.set(dependency.id, generateId());
});
return idsMap;
};
export const cloneTable = (
table: DBTable,
options: {
generateId: () => string;
idsMap: Map<string, string>;
} = {
generateId: defaultGenerateId,
idsMap: new Map<string, string>(),
}
): DBTable => {
const { generateId } = options;
const idsMap = new Map([
...generateIdsMapFromTable(table, generateId),
...options.idsMap,
]);
const getNewId = (id: string) => {
const newId = idsMap.get(id);
if (!newId) {
throw new Error(`Id not found for ${id}`);
}
return newId;
};
const newTable: DBTable = { ...table, id: getNewId(table.id) };
newTable.fields = table.fields.map(
(field): DBField => ({
...field,
id: getNewId(field.id),
})
);
newTable.indexes = table.indexes.map(
(index): DBIndex => ({
...index,
fieldIds: index.fieldIds.map((id) => getNewId(id)),
id: getNewId(index.id),
})
);
return newTable;
};
export const cloneDiagram = (
diagram: Diagram,
options: {
generateId: () => string;
} = {
generateId: defaultGenerateId,
}
): Diagram => {
const { generateId } = options;
const diagramId = generateId();
const idsMap = generateIdsMapFromDiagram(diagram, generateId);
const getNewId = (id: string) => {
const newId = idsMap.get(id);
if (!newId) {
throw new Error(`Id not found for ${id}`);
}
return newId;
};
const tables: DBTable[] =
diagram.tables?.map((table) =>
cloneTable(table, { generateId, idsMap })
) ?? [];
const relationships: DBRelationship[] =
diagram.relationships?.map(
(relationship): DBRelationship => ({
...relationship,
id: getNewId(relationship.id),
sourceTableId: getNewId(relationship.sourceTableId),
targetTableId: getNewId(relationship.targetTableId),
sourceFieldId: getNewId(relationship.sourceFieldId),
targetFieldId: getNewId(relationship.targetFieldId),
})
) ?? [];
const dependencies: DBDependency[] =
diagram.dependencies?.map(
(dependency): DBDependency => ({
...dependency,
id: getNewId(dependency.id),
dependentTableId: getNewId(dependency.dependentTableId),
tableId: getNewId(dependency.tableId),
})
) ?? [];
return {
...diagram,
id: diagramId,
dependencies,
relationships,
tables,
createdAt: new Date(),
updatedAt: new Date(),
};
};

View File

@@ -1,3 +1,4 @@
import { z } from 'zod';
import { DatabaseType } from '../../domain/database-type';
import { genericDataTypes } from './generic-data-types';
import { mariadbDataTypes } from './mariadb-data-types';
@@ -11,6 +12,11 @@ export interface DataType {
name: string;
}
export const dataTypeSchema: z.ZodType<DataType> = z.object({
id: z.string(),
name: z.string(),
});
export const dataTypeMap: Record<DatabaseType, readonly DataType[]> = {
[DatabaseType.GENERIC]: genericDataTypes,
[DatabaseType.POSTGRESQL]: postgresDataTypes,

View File

@@ -0,0 +1,27 @@
import type { DatabaseType } from '@/lib/domain/database-type';
import { sha256 } from '@/lib/utils';
export const getFromCache = (key: string): string | null => {
try {
return localStorage.getItem(`sql-export-${key}`);
} catch (e) {
console.warn('Failed to read from localStorage:', e);
return null;
}
};
export const setInCache = (key: string, value: string): void => {
try {
localStorage.setItem(`sql-export-${key}`, value);
} catch (e) {
console.warn('Failed to write to localStorage:', e);
}
};
export const generateCacheKey = async (
databaseType: DatabaseType,
sqlScript: string
): Promise<string> => {
const rawKey = `${databaseType}:${sqlScript}`;
return await sha256(rawKey);
};

View File

@@ -3,6 +3,7 @@ import { OPENAI_API_KEY } from '@/lib/env';
import type { DatabaseType } from '@/lib/domain/database-type';
import type { DBTable } from '@/lib/domain/db-table';
import type { DataType } from '../data-types/data-types';
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
export const exportBaseSQL = (diagram: Diagram): string => {
const { tables, relationships } = diagram;
@@ -190,21 +191,57 @@ export const exportBaseSQL = (diagram: Diagram): string => {
export const exportSQL = async (
diagram: Diagram,
databaseType: DatabaseType
databaseType: DatabaseType,
options?: {
stream: boolean;
onResultStream: (text: string) => void;
signal?: AbortSignal;
}
): Promise<string> => {
const { generateText } = await import('ai');
const { createOpenAI } = await import('@ai-sdk/openai');
const sqlScript = exportBaseSQL(diagram);
const cacheKey = await generateCacheKey(databaseType, sqlScript);
const cachedResult = getFromCache(cacheKey);
if (cachedResult) {
return cachedResult;
}
const [{ streamText, generateText }, { createOpenAI }] = await Promise.all([
import('ai'),
import('@ai-sdk/openai'),
]);
const openai = createOpenAI({
apiKey: OPENAI_API_KEY,
});
const sqlScript = exportBaseSQL(diagram);
const prompt = generateSQLPrompt(databaseType, sqlScript);
if (options?.stream) {
const { textStream, text: textPromise } = await streamText({
model: openai('gpt-4o-mini-2024-07-18'),
prompt: prompt,
});
for await (const textPart of textStream) {
if (options.signal?.aborted) {
return '';
}
options.onResultStream(textPart);
}
const text = await textPromise;
setInCache(cacheKey, text);
return text;
}
const { text } = await generateText({
model: openai('gpt-4o-mini-2024-07-18'),
prompt: prompt,
});
setInCache(cacheKey, text);
return text;
};

View File

@@ -5,17 +5,24 @@ export const fixMetadataJson = async (
metadataJson: string
): Promise<string> => {
await waitFor(1000);
return metadataJson
.trim()
.replace(/^[^{]*/, '') // Remove everything before the first '{'
.replace(/}[^}]*$/, '}') // Remove everything after the last '}'
.replace(/^\s+|\s+$/g, '')
.replace(/^"|"$/g, '')
.replace(/^'|'$/g, '')
.replace(/(?<=:\s*)""(?=\s*[,}])/g, '___EMPTY___') // Temporarily replace empty strings
.replace(/""/g, '"') // Replace remaining double quotes
.replace(/___EMPTY___/g, '""') // Restore empty strings
.replace(/\n/g, '');
// TODO: remove this temporary eslint disable
return (
metadataJson
.trim()
.replace(/^[^{]*/, '') // Remove everything before the first '{'
.replace(/}[^}]*$/, '}') // Remove everything after the last '}'
.replace(/^\s+|\s+$/g, '')
.replace(/^"|"$/g, '')
.replace(/^'|'$/g, '')
/* eslint-disable-next-line no-useless-escape */
.replace(/\"/g, '___ESCAPED_QUOTE___') // Temporarily replace empty strings
.replace(/(?<=:\s*)""(?=\s*[,}])/g, '___EMPTY___') // Temporarily replace empty strings
.replace(/""/g, '"') // Replace remaining double quotes
.replace(/___ESCAPED_QUOTE___/g, '"') // Restore empty strings
.replace(/___EMPTY___/g, '""') // Restore empty strings
.replace(/\n/g, '')
);
};
export const isStringMetadataJson = (metadataJsonString: string): boolean => {

View File

@@ -1,3 +1,4 @@
import { z } from 'zod';
import type { ViewInfo } from '../data/import-metadata/metadata-types/view-info';
import { DatabaseType } from './database-type';
import {
@@ -17,6 +18,15 @@ export interface DBDependency {
createdAt: number;
}
export const dbDependencySchema: z.ZodType<DBDependency> = z.object({
id: z.string(),
schema: z.string().optional(),
tableId: z.string(),
dependentSchema: z.string().optional(),
dependentTableId: z.string(),
createdAt: z.number(),
});
export const shouldShowDependencyBySchemaFilter = (
dependency: DBDependency,
filteredSchemas?: string[]

View File

@@ -1,4 +1,5 @@
import type { DataType } from '../data/data-types/data-types';
import { z } from 'zod';
import { dataTypeSchema, type DataType } from '../data/data-types/data-types';
import type { ColumnInfo } from '../data/import-metadata/metadata-types/column-info';
import type { AggregatedIndexInfo } from '../data/import-metadata/metadata-types/index-info';
import type { PrimaryKeyInfo } from '../data/import-metadata/metadata-types/primary-key-info';
@@ -22,6 +23,22 @@ export interface DBField {
comments?: string;
}
export const dbFieldSchema: z.ZodType<DBField> = z.object({
id: z.string(),
name: z.string(),
type: dataTypeSchema,
primaryKey: z.boolean(),
unique: z.boolean(),
nullable: z.boolean(),
createdAt: z.number(),
characterMaximumLength: z.string().optional(),
precision: z.number().optional(),
scale: z.number().optional(),
default: z.string().optional(),
collation: z.string().optional(),
comments: z.string().optional(),
});
export const createFieldsFromMetadata = ({
columns,
tableSchema,

View File

@@ -1,3 +1,4 @@
import { z } from 'zod';
import type { AggregatedIndexInfo } from '../data/import-metadata/metadata-types/index-info';
import { generateId } from '../utils';
import type { DBField } from './db-field';
@@ -10,6 +11,14 @@ export interface DBIndex {
createdAt: number;
}
export const dbIndexSchema: z.ZodType<DBIndex> = z.object({
id: z.string(),
name: z.string(),
unique: z.boolean(),
fieldIds: z.array(z.string()),
createdAt: z.number(),
});
export const createIndexesFromMetadata = ({
aggregatedIndexes,
fields,

View File

@@ -1,3 +1,4 @@
import { z } from 'zod';
import type { ForeignKeyInfo } from '../data/import-metadata/metadata-types/foreign-key-info';
import type { DBField } from './db-field';
import {
@@ -21,6 +22,20 @@ export interface DBRelationship {
createdAt: number;
}
export const dbRelationshipSchema: z.ZodType<DBRelationship> = z.object({
id: z.string(),
name: z.string(),
sourceSchema: z.string().optional(),
sourceTableId: z.string(),
targetSchema: z.string().optional(),
targetTableId: z.string(),
sourceFieldId: z.string(),
targetFieldId: z.string(),
sourceCardinality: z.union([z.literal('one'), z.literal('many')]),
targetCardinality: z.union([z.literal('one'), z.literal('many')]),
createdAt: z.number(),
});
export type RelationshipType =
| 'one_to_one'
| 'one_to_many'

View File

@@ -1,5 +1,13 @@
import { createIndexesFromMetadata, type DBIndex } from './db-index';
import { createFieldsFromMetadata, type DBField } from './db-field';
import {
createIndexesFromMetadata,
dbIndexSchema,
type DBIndex,
} from './db-index';
import {
createFieldsFromMetadata,
dbFieldSchema,
type DBField,
} from './db-field';
import type { TableInfo } from '../data/import-metadata/metadata-types/table-info';
import { createAggregatedIndexes } from '../data/import-metadata/metadata-types/index-info';
import { materializedViewColor, viewColor, randomColor } from '@/lib/colors';
@@ -16,6 +24,7 @@ import {
} from './db-schema';
import { DatabaseType } from './database-type';
import type { DatabaseMetadata } from '../data/import-metadata/metadata-types/database-metadata';
import { z } from 'zod';
export interface DBTable {
id: string;
@@ -34,6 +43,23 @@ export interface DBTable {
hidden?: boolean;
}
export const dbTableSchema: z.ZodType<DBTable> = z.object({
id: z.string(),
name: z.string(),
schema: z.string().optional(),
x: z.number(),
y: z.number(),
fields: z.array(dbFieldSchema),
indexes: z.array(dbIndexSchema),
color: z.string(),
isView: z.boolean(),
isMaterializedView: z.boolean().optional(),
createdAt: z.number(),
width: z.number().optional(),
comments: z.string().optional(),
hidden: z.boolean().optional(),
});
export const shouldShowTablesBySchemaFilter = (
table: DBTable,
filteredSchemas?: string[]

View File

@@ -1,12 +1,23 @@
import { z } from 'zod';
import type { DatabaseMetadata } from '../data/import-metadata/metadata-types/database-metadata';
import type { DatabaseEdition } from './database-edition';
import { DatabaseEdition } from './database-edition';
import { DatabaseType } from './database-type';
import type { DBDependency } from './db-dependency';
import { createDependenciesFromMetadata } from './db-dependency';
import {
createDependenciesFromMetadata,
dbDependencySchema,
} from './db-dependency';
import type { DBRelationship } from './db-relationship';
import { createRelationshipsFromMetadata } from './db-relationship';
import {
createRelationshipsFromMetadata,
dbRelationshipSchema,
} from './db-relationship';
import type { DBTable } from './db-table';
import { adjustTablePositions, createTablesFromMetadata } from './db-table';
import {
adjustTablePositions,
createTablesFromMetadata,
dbTableSchema,
} from './db-table';
import { generateDiagramId } from '@/lib/utils';
export interface Diagram {
id: string;
@@ -20,6 +31,18 @@ export interface Diagram {
updatedAt: Date;
}
export const diagramSchema: z.ZodType<Diagram> = z.object({
id: z.string(),
name: z.string(),
databaseType: z.nativeEnum(DatabaseType),
databaseEdition: z.nativeEnum(DatabaseEdition).optional(),
tables: z.array(dbTableSchema).optional(),
relationships: z.array(dbRelationshipSchema).optional(),
dependencies: z.array(dbDependencySchema).optional(),
createdAt: z.date(),
updatedAt: z.date(),
});
export const loadFromDatabaseMetadata = async ({
databaseType,
databaseMetadata,

View File

@@ -0,0 +1,33 @@
import { cloneDiagram } from './clone';
import { diagramSchema, type Diagram } from './domain/diagram';
import { generateDiagramId } from './utils';
export const runningIdGenerator = (): (() => string) => {
let id = 0;
return () => (id++).toString();
};
const cloneDiagramWithRunningIds = (diagram: Diagram) =>
cloneDiagram(diagram, { generateId: runningIdGenerator() });
const cloneDiagramWithIds = (diagram: Diagram): Diagram => ({
...cloneDiagram(diagram),
id: generateDiagramId(),
});
export const diagramToJSONOutput = (diagram: Diagram): string => {
const clonedDiagram = cloneDiagramWithRunningIds(diagram);
return JSON.stringify(clonedDiagram, null, 2);
};
export const diagramFromJSONInput = (json: string): Diagram => {
const loadedDiagram = JSON.parse(json);
const diagram = diagramSchema.parse({
...loadedDiagram,
createdAt: new Date(),
updatedAt: new Date(),
});
return cloneDiagramWithIds(diagram);
};

View File

@@ -88,3 +88,16 @@ export const decodeBase64ToUtf8 = (base64: string) => {
export const waitFor = async (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
export const sha256 = async (message: string): Promise<string> => {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
return hashHex;
};

View File

@@ -1,5 +1,5 @@
import { Spinner } from '@/components/spinner/spinner';
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { useLoaderData, useNavigate } from 'react-router-dom';
import type { TemplatePageLoaderData } from '../template-page/template-page';
import { convertTemplateToNewDiagram } from '@/templates-data/template-utils';
@@ -12,6 +12,7 @@ import { ThemeProvider } from '@/context/theme-context/theme-provider';
export const CloneTemplateComponent: React.FC = () => {
const navigate = useNavigate();
const { addDiagram, deleteDiagram } = useStorage();
const clonedBefore = useRef<boolean>(false);
const data = useLoaderData() as TemplatePageLoaderData;
const template = data.template;
@@ -21,6 +22,11 @@ export const CloneTemplateComponent: React.FC = () => {
return;
}
if (clonedBefore.current) {
return;
}
clonedBefore.current = true;
const diagram = convertTemplateToNewDiagram(template);
await deleteDiagram(diagram.id);

View File

@@ -22,6 +22,7 @@ import {
MiniMap,
Controls,
useReactFlow,
useKeyPress,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import equal from 'fast-deep-equal';
@@ -36,7 +37,7 @@ import {
} from './table-node/table-node-field';
import { Toolbar } from './toolbar/toolbar';
import { useToast } from '@/components/toast/use-toast';
import { Pencil, LayoutGrid, AlertTriangle } from 'lucide-react';
import { Pencil, LayoutGrid, AlertTriangle, Magnet } from 'lucide-react';
import { Button } from '@/components/button/button';
import { useLayout } from '@/hooks/use-layout';
import { useBreakpoint } from '@/hooks/use-breakpoint';
@@ -66,7 +67,7 @@ import {
import type { Graph } from '@/lib/graph';
import { createGraph, removeVertex } from '@/lib/graph';
import type { ChartDBEvent } from '@/context/chartdb-context/chartdb-context';
import { debounce } from '@/lib/utils';
import { cn, debounce, getOperatingSystem } from '@/lib/utils';
import type { DependencyEdgeType } from './dependency-edge';
import { DependencyEdge } from './dependency-edge';
import {
@@ -148,6 +149,8 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
const [edges, setEdges, onEdgesChange] =
useEdgesState<EdgeType>(initialEdges);
const [snapToGridEnabled, setSnapToGridEnabled] = useState(false);
useEffect(() => {
setIsInitialLoadingNodes(true);
}, [initialTables]);
@@ -163,7 +166,13 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
useEffect(() => {
if (!isInitialLoadingNodes) {
setTimeout(() => fitView({ maxZoom: 1, duration: 0 }), 0);
debounce(() => {
fitView({
duration: 200,
padding: 0.1,
maxZoom: 0.8,
});
}, 500)();
}
}, [isInitialLoadingNodes, fitView]);
@@ -688,6 +697,9 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
setTimeout(() => setHighlightOverlappingTables(false), 600);
}, []);
const shiftPressed = useKeyPress('Shift');
const operatingSystem = getOperatingSystem();
return (
<CanvasContextMenu>
<div className="relative flex h-full">
@@ -712,6 +724,8 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
type: 'relationship-edge',
}}
panOnScroll={scrollAction === 'pan'}
snapToGrid={shiftPressed || snapToGridEnabled}
snapGrid={[20, 20]}
>
<Controls
position="top-left"
@@ -722,24 +736,57 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
>
<div className="flex flex-col items-center gap-2 md:flex-row">
{!readonly ? (
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="secondary"
className="size-8 p-1 shadow-none"
onClick={
showReorderConfirmation
}
>
<LayoutGrid className="size-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t('toolbar.reorder_diagram')}
</TooltipContent>
</Tooltip>
<>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="secondary"
className="size-8 p-1 shadow-none"
onClick={
showReorderConfirmation
}
>
<LayoutGrid className="size-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t('toolbar.reorder_diagram')}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="secondary"
className={cn(
'size-8 p-1 shadow-none',
snapToGridEnabled ||
shiftPressed
? 'bg-pink-600 text-white hover:bg-pink-500 dark:hover:bg-pink-700 hover:text-white'
: ''
)}
onClick={() =>
setSnapToGridEnabled(
(prev) => !prev
)
}
>
<Magnet className="size-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t('snap_to_grid_tooltip', {
key:
operatingSystem === 'mac'
? '⇧'
: 'Shift',
})}
</TooltipContent>
</Tooltip>
</>
) : null}
<div

View File

@@ -19,7 +19,7 @@ export const MarkerDefinitions: React.FC = () => {
}
return (
<svg className="absolute left-0 top-0 z-0 size-0">
<svg className="marker-definitions absolute left-0 top-0 z-0 size-0">
<defs>
{Object.entries(cardinalityOptions).map(([cardinality, text]) =>
sideOptions.map((side) =>

View File

@@ -7,7 +7,9 @@ import {
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { useChartDB } from '@/hooks/use-chartdb';
import { useLayout } from '@/hooks/use-layout';
import { cloneTable } from '@/lib/clone';
import type { DBTable } from '@/lib/domain/db-table';
import { Copy, Pencil, Trash2 } from 'lucide-react';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -18,11 +20,21 @@ export interface TableNodeContextMenuProps {
export const TableNodeContextMenu: React.FC<
React.PropsWithChildren<TableNodeContextMenuProps>
> = ({ children, table }) => {
const { removeTable, readonly } = useChartDB();
const { removeTable, readonly, createTable } = useChartDB();
const { openTableFromSidebar } = useLayout();
const { t } = useTranslation();
const { isMd: isDesktop } = useBreakpoint('md');
const duplicateTableHandler = useCallback(() => {
const clonedTable = cloneTable(table);
clonedTable.name = `${clonedTable.name}_copy`;
clonedTable.x += 30;
clonedTable.y += 50;
createTable(clonedTable);
}, [createTable, table]);
const editTableHandler = useCallback(() => {
openTableFromSidebar(table.id);
}, [openTableFromSidebar, table.id]);
@@ -38,11 +50,26 @@ export const TableNodeContextMenu: React.FC<
<ContextMenu>
<ContextMenuTrigger>{children}</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={editTableHandler}>
{t('table_node_context_menu.edit_table')}
<ContextMenuItem
onClick={editTableHandler}
className="flex justify-between gap-3"
>
<span>{t('table_node_context_menu.edit_table')}</span>
<Pencil className="size-3.5" />
</ContextMenuItem>
<ContextMenuItem onClick={removeTableHandler}>
{t('table_node_context_menu.delete_table')}
<ContextMenuItem
onClick={duplicateTableHandler}
className="flex justify-between gap-3"
>
<span>{t('table_node_context_menu.duplicate_table')}</span>
<Copy className="size-3.5" />
</ContextMenuItem>
<ContextMenuItem
onClick={removeTableHandler}
className="flex justify-between gap-3"
>
<span>{t('table_node_context_menu.delete_table')}</span>
<Trash2 className="size-3.5 text-red-700" />
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>

View File

@@ -8,6 +8,7 @@ import {
FileKey2,
Check,
Group,
Copy,
} from 'lucide-react';
import { ListItemHeaderButton } from '@/pages/editor-page/side-panel/list-item-header-button/list-item-header-button';
import type { DBTable } from '@/lib/domain/db-table';
@@ -28,6 +29,12 @@ import { useLayout } from '@/hooks/use-layout';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { useTranslation } from 'react-i18next';
import { useDialog } from '@/hooks/use-dialog';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { cloneTable } from '@/lib/clone';
export interface TableListItemHeaderProps {
table: DBTable;
@@ -41,6 +48,7 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
removeTable,
createIndex,
createField,
createTable,
schemas,
filteredSchemas,
} = useChartDB();
@@ -65,10 +73,8 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
useClickAway(inputRef, editTableName);
useKeyPressEvent('Enter', editTableName);
const enterEditMode = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.stopPropagation();
const enterEditMode = (e: React.MouseEvent) => {
e.stopPropagation();
setEditMode(true);
};
@@ -125,6 +131,20 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
});
}, [openTableSchemaDialog, table, schemas, updateTableSchema]);
const duplicateTableHandler = useCallback(
(e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
const clonedTable = cloneTable(table);
clonedTable.name = `${clonedTable.name}_copy`;
clonedTable.x += 30;
clonedTable.y += 50;
createTable(clonedTable);
},
[createTable, table]
);
const renderDropDownMenu = useCallback(
() => (
<DropdownMenu>
@@ -186,6 +206,18 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={duplicateTableHandler}
className="flex justify-between"
>
{t(
'side_panel.tables_section.table.table_actions.duplicate_table'
)}
<Copy className="size-3.5" />
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={deleteTableHandler}
@@ -205,6 +237,7 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
createField,
createIndex,
deleteTableHandler,
duplicateTableHandler,
t,
changeSchema,
schemas.length,
@@ -219,7 +252,7 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
return (
<div className="group flex h-11 flex-1 items-center justify-between gap-1 overflow-hidden">
<div className="flex min-w-0 flex-1">
<div className="flex min-w-0 flex-1 px-1">
{editMode ? (
<Input
ref={inputRef}
@@ -232,12 +265,24 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
className="h-7 w-full focus-visible:ring-0"
/>
) : (
<div className="truncate">
{table.name}
<span className="text-xs text-muted-foreground">
{schemaToDisplay ? ` (${schemaToDisplay})` : ''}
</span>
</div>
<Tooltip>
<TooltipTrigger asChild>
<div
onDoubleClick={enterEditMode}
className="text-editable truncate px-2 py-0.5"
>
{table.name}
<span className="text-xs text-muted-foreground">
{schemaToDisplay
? ` (${schemaToDisplay})`
: ''}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
{t('tool_tips.double_click_to_edit')}
</TooltipContent>
</Tooltip>
)}
</div>
<div className="flex flex-row-reverse">

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Label } from '@/components/label/label';
import { Button } from '@/components/button/button';
import { Check, Pencil } from 'lucide-react';
import { Check } from 'lucide-react';
import { Input } from '@/components/input/input';
import { useChartDB } from '@/hooks/use-chartdb';
import { useClickAway, useKeyPressEvent } from 'react-use';
@@ -10,6 +10,11 @@ import { DiagramIcon } from '@/components/diagram-icon/diagram-icon';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { labelVariants } from '@/components/label/label-variants';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
export interface DiagramNameProps {}
@@ -39,54 +44,73 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
useKeyPressEvent('Enter', editDiagramName);
const enterEditMode = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
event: React.MouseEvent<HTMLHeadingElement, MouseEvent>
) => {
event.stopPropagation();
setEditMode(true);
};
return (
<>
<DiagramIcon diagram={currentDiagram} />
<div className="flex">
{isDesktop ? <Label>{t('diagrams')}/</Label> : null}
</div>
<div className="flex flex-row items-center gap-1">
{editMode ? (
<>
<Input
ref={inputRef}
autoFocus
type="text"
placeholder={diagramName}
value={editedDiagramName}
onClick={(e) => e.stopPropagation()}
onChange={(e) =>
setEditedDiagramName(e.target.value)
}
className="ml-1 h-7 focus-visible:ring-0"
/>
<Button
variant="ghost"
className="hidden size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 group-hover:flex dark:text-slate-400 dark:hover:text-slate-300"
onClick={editDiagramName}
>
<Check />
</Button>
</>
) : (
<>
<h1 className={cn(labelVariants())}>{diagramName}</h1>
<Button
variant="ghost"
className="hidden size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 group-hover:flex dark:text-slate-400 dark:hover:text-slate-300"
onClick={enterEditMode}
>
<Pencil />
</Button>
</>
<div className="group">
<div
className={cn(
'flex flex-1 flex-row items-center justify-center px-2 py-1',
{
'text-editable': !editMode,
}
)}
>
<DiagramIcon diagram={currentDiagram} />
<div className="flex">
{isDesktop ? <Label>{t('diagrams')}/</Label> : null}
</div>
<div className="flex flex-row items-center gap-1">
{editMode ? (
<>
<Input
ref={inputRef}
autoFocus
type="text"
placeholder={diagramName}
value={editedDiagramName}
onClick={(e) => e.stopPropagation()}
onChange={(e) =>
setEditedDiagramName(e.target.value)
}
className="ml-1 h-7 focus-visible:ring-0"
/>
<Button
variant="ghost"
className="flex size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300"
onClick={editDiagramName}
>
<Check />
</Button>
</>
) : (
<>
<Tooltip>
<TooltipTrigger asChild>
<h1
className={cn(
labelVariants(),
'group-hover:underline'
)}
onDoubleClick={(e) => {
enterEditMode(e);
}}
>
{diagramName}
</h1>
</TooltipTrigger>
<TooltipContent>
{t('tool_tips.double_click_to_edit')}
</TooltipContent>
</Tooltip>
</>
)}
</div>
</div>
</>
</div>
);
};

View File

@@ -0,0 +1,84 @@
import React, { useCallback, useMemo, useState } from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/dropdown-menu/dropdown-menu';
import { useTranslation } from 'react-i18next';
import { Globe } from 'lucide-react';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import type {
SelectBoxOption,
SelectBoxProps,
} from '@/components/select-box/select-box';
import { SelectBox } from '@/components/select-box/select-box';
import { languages } from '@/i18n/i18n';
import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/button/button-variants';
export interface LanguageNavProps {}
export const LanguageNav: React.FC<LanguageNavProps> = () => {
const { t, i18n } = useTranslation();
const [dropdownOpen, setDropdownOpen] = useState(false);
const languagesOptions: SelectBoxOption[] = languages.map((lang) => ({
label: lang.nativeName,
value: lang.code,
description: `(${lang.name})`,
}));
const handleLanguageChange: SelectBoxProps['onChange'] = useCallback(
(language: string | string[]) => {
i18n.changeLanguage(language as string);
setDropdownOpen(false);
},
[i18n]
);
const language = useMemo(() => {
return i18n.languages
.map((lang) => languagesOptions.find((opt) => opt.value === lang))
.find((opt) => opt !== undefined)?.value;
}, [i18n, languagesOptions]);
return (
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<div
className={cn(
buttonVariants({
variant: 'outline',
size: 'icon',
}),
'size-6 rounded-full md:size-8 cursor-pointer'
)}
>
<Globe className="size-3.5 md:size-4" />
<span className="sr-only">Change language</span>
</div>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>
{t('language_select.change_language')}
</TooltipContent>
</Tooltip>
<DropdownMenuContent className="w-56">
<div className="p-2">
<SelectBox
className="flex h-8 min-h-8 w-full"
options={languagesOptions}
value={language}
onChange={handleLanguageChange}
/>
</div>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -33,8 +33,8 @@ import { useTheme } from '@/hooks/use-theme';
import { useLocalConfig } from '@/hooks/use-local-config';
import { DiagramName } from './diagram-name';
import { LastSaved } from './last-saved';
import { languages } from '@/i18n/i18n';
import { useNavigate } from 'react-router-dom';
import { LanguageNav } from './language-nav/language-nav';
export interface TopNavbarProps {}
@@ -48,6 +48,8 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
openImportDatabaseDialog,
showAlert,
openExportImageDialog,
openExportDiagramDialog,
openImportDiagramDialog,
} = useDialog();
const { setTheme, theme } = useTheme();
const { hideSidePanel, isSidePanelShowed, showSidePanel } = useLayout();
@@ -60,7 +62,7 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
showDependenciesOnCanvas,
} = useLocalConfig();
const { effectiveTheme } = useTheme();
const { t, i18n } = useTranslation();
const { t } = useTranslation();
const { redo, undo, hasRedo, hasUndo } = useHistory();
const { isMd: isDesktop } = useBreakpoint('md');
const { config, updateConfig } = useConfig();
@@ -196,17 +198,10 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
const emojiAI = '✨';
const changeLanguage = useCallback(
(language: string) => {
i18n.changeLanguage(language);
},
[i18n]
);
return (
<nav className="flex h-20 flex-col justify-between border-b px-3 md:h-12 md:flex-row md:items-center md:px-4">
<div className="flex flex-1 justify-between gap-x-3 md:justify-normal">
<div className="flex py-[10px] font-primary md:items-center md:py-0">
<nav className="flex flex-col justify-between border-b px-3 md:h-12 md:flex-row md:items-center md:px-4">
<div className="flex flex-1 flex-col justify-between gap-x-3 md:flex-row md:justify-normal">
<div className="flex items-center justify-between pt-[8px] font-primary md:py-[10px]">
<a
href="https://chartdb.io"
className="cursor-pointer"
@@ -222,408 +217,357 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
className="h-4 max-w-fit"
/>
</a>
{!isDesktop ? (
<div className="flex items-center gap-2">
{renderStars()}
<LanguageNav />
</div>
) : null}
</div>
<div>
<Menubar className="border-none shadow-none">
<MenubarMenu>
<MenubarTrigger>
{t('menu.file.file')}
</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={createNewDiagram}>
{t('menu.file.new')}
</MenubarItem>
<MenubarItem onClick={openDiagram}>
{t('menu.file.open')}
<MenubarShortcut>
{
keyboardShortcutsForOS[
KeyboardShortcutAction
.OPEN_DIAGRAM
].keyCombinationLabel
}
</MenubarShortcut>
</MenubarItem>
<MenubarItem onClick={updateDiagramUpdatedAt}>
{t('menu.file.save')}
<MenubarShortcut>
{
keyboardShortcutsForOS[
KeyboardShortcutAction
.SAVE_DIAGRAM
].keyCombinationLabel
}
</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.file.import_database')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType:
DatabaseType.POSTGRESQL,
})
}
>
{
databaseTypeToLabelMap[
'postgresql'
]
}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType:
DatabaseType.MYSQL,
})
}
>
{databaseTypeToLabelMap['mysql']}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType:
DatabaseType.SQL_SERVER,
})
}
>
{
databaseTypeToLabelMap[
'sql_server'
]
}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType:
DatabaseType.MARIADB,
})
}
>
{databaseTypeToLabelMap['mariadb']}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType:
DatabaseType.SQLITE,
})
}
>
{databaseTypeToLabelMap['sqlite']}
</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.file.export_sql')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem
onClick={() =>
exportSQL(DatabaseType.GENERIC)
}
>
{databaseTypeToLabelMap['generic']}
</MenubarItem>
<MenubarItem
onClick={() =>
exportSQL(
DatabaseType.POSTGRESQL
)
}
>
{
databaseTypeToLabelMap[
'postgresql'
]
}
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
</MenubarItem>
<MenubarItem
onClick={() =>
exportSQL(DatabaseType.MYSQL)
}
>
{databaseTypeToLabelMap['mysql']}
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
</MenubarItem>
<MenubarItem
onClick={() =>
exportSQL(
DatabaseType.SQL_SERVER
)
}
>
{
databaseTypeToLabelMap[
'sql_server'
]
}
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
</MenubarItem>
<MenubarItem
onClick={() =>
exportSQL(DatabaseType.MARIADB)
}
>
{databaseTypeToLabelMap['mariadb']}
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
</MenubarItem>
<MenubarItem
onClick={() =>
exportSQL(DatabaseType.SQLITE)
}
>
{databaseTypeToLabelMap['sqlite']}
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSub>
<MenubarSubTrigger>
{t('menu.file.export_as')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem onClick={exportPNG}>
PNG
</MenubarItem>
<MenubarItem onClick={exportJPG}>
JPG
</MenubarItem>
<MenubarItem onClick={exportSVG}>
SVG
</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarItem
onClick={() =>
showAlert({
title: t(
'delete_diagram_alert.title'
),
description: t(
'delete_diagram_alert.description'
),
actionLabel: t(
'delete_diagram_alert.delete'
),
closeLabel: t(
'delete_diagram_alert.cancel'
),
onAction: handleDeleteDiagramAction,
})
<Menubar className="h-8 border-none py-2 shadow-none md:h-10 md:py-0">
<MenubarMenu>
<MenubarTrigger>{t('menu.file.file')}</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={createNewDiagram}>
{t('menu.file.new')}
</MenubarItem>
<MenubarItem onClick={openDiagram}>
{t('menu.file.open')}
<MenubarShortcut>
{
keyboardShortcutsForOS[
KeyboardShortcutAction.OPEN_DIAGRAM
].keyCombinationLabel
}
>
{t('menu.file.delete_diagram')}
</MenubarItem>
<MenubarSeparator />
<MenubarItem>{t('menu.file.exit')}</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>
{t('menu.edit.edit')}
</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={undo} disabled={!hasUndo}>
{t('menu.edit.undo')}
<MenubarShortcut>
{
keyboardShortcutsForOS[
KeyboardShortcutAction.UNDO
].keyCombinationLabel
}
</MenubarShortcut>
</MenubarItem>
<MenubarItem onClick={redo} disabled={!hasRedo}>
{t('menu.edit.redo')}
<MenubarShortcut>
{
keyboardShortcutsForOS[
KeyboardShortcutAction.REDO
].keyCombinationLabel
}
</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem
onClick={() =>
showAlert({
title: t(
'clear_diagram_alert.title'
),
description: t(
'clear_diagram_alert.description'
),
actionLabel: t(
'clear_diagram_alert.clear'
),
closeLabel: t(
'clear_diagram_alert.cancel'
),
onAction: clearDiagramData,
})
</MenubarShortcut>
</MenubarItem>
<MenubarItem onClick={updateDiagramUpdatedAt}>
{t('menu.file.save')}
<MenubarShortcut>
{
keyboardShortcutsForOS[
KeyboardShortcutAction.SAVE_DIAGRAM
].keyCombinationLabel
}
>
{t('menu.edit.clear')}
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>
{t('menu.view.view')}
</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={showOrHideSidePanel}>
{isSidePanelShowed
? t('menu.view.hide_sidebar')
: t('menu.view.show_sidebar')}
</MenubarItem>
<MenubarSeparator />
<MenubarItem onClick={showOrHideCardinality}>
{showCardinality
? t('menu.view.hide_cardinality')
: t('menu.view.show_cardinality')}
</MenubarItem>
<MenubarItem onClick={showOrHideDependencies}>
{showDependenciesOnCanvas
? t('menu.view.hide_dependencies')
: t('menu.view.show_dependencies')}
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.view.zoom_on_scroll')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarCheckboxItem
checked={scrollAction === 'zoom'}
onClick={() =>
setScrollAction('zoom')
}
>
{t('zoom.on')}
</MenubarCheckboxItem>
<MenubarCheckboxItem
checked={scrollAction === 'pan'}
onClick={() =>
setScrollAction('pan')
}
>
{t('zoom.off')}
</MenubarCheckboxItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.view.theme')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarCheckboxItem
checked={theme === 'system'}
onClick={() => setTheme('system')}
>
{t('theme.system')}
</MenubarCheckboxItem>
<MenubarCheckboxItem
checked={theme === 'light'}
onClick={() => setTheme('light')}
>
{t('theme.light')}
</MenubarCheckboxItem>
<MenubarCheckboxItem
checked={theme === 'dark'}
onClick={() => setTheme('dark')}
>
{t('theme.dark')}
</MenubarCheckboxItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.view.change_language')}
</MenubarSubTrigger>
<MenubarSubContent>
{languages.map((language) => (
<MenubarCheckboxItem
key={language.code}
onClick={() =>
changeLanguage(
language.code
)
}
checked={
i18n.language ===
language.code
}
>
{language.name}
</MenubarCheckboxItem>
))}
</MenubarSubContent>
</MenubarSub>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>
{t('menu.help.help')}
</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={openChartDBIO}>
{t('menu.help.visit_website')}
</MenubarItem>
<MenubarItem onClick={openJoinDiscord}>
{t('menu.help.join_discord')}
</MenubarItem>
<MenubarItem onClick={openCalendly}>
{t('menu.help.schedule_a_call')}
</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
</div>
</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.file.import_database')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType:
DatabaseType.POSTGRESQL,
})
}
>
{databaseTypeToLabelMap['postgresql']}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType:
DatabaseType.MYSQL,
})
}
>
{databaseTypeToLabelMap['mysql']}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType:
DatabaseType.SQL_SERVER,
})
}
>
{databaseTypeToLabelMap['sql_server']}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType:
DatabaseType.MARIADB,
})
}
>
{databaseTypeToLabelMap['mariadb']}
</MenubarItem>
<MenubarItem
onClick={() =>
openImportDatabaseDialog({
databaseType:
DatabaseType.SQLITE,
})
}
>
{databaseTypeToLabelMap['sqlite']}
</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.file.export_sql')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem
onClick={() =>
exportSQL(DatabaseType.GENERIC)
}
>
{databaseTypeToLabelMap['generic']}
</MenubarItem>
<MenubarItem
onClick={() =>
exportSQL(DatabaseType.POSTGRESQL)
}
>
{databaseTypeToLabelMap['postgresql']}
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
</MenubarItem>
<MenubarItem
onClick={() =>
exportSQL(DatabaseType.MYSQL)
}
>
{databaseTypeToLabelMap['mysql']}
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
</MenubarItem>
<MenubarItem
onClick={() =>
exportSQL(DatabaseType.SQL_SERVER)
}
>
{databaseTypeToLabelMap['sql_server']}
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
</MenubarItem>
<MenubarItem
onClick={() =>
exportSQL(DatabaseType.MARIADB)
}
>
{databaseTypeToLabelMap['mariadb']}
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
</MenubarItem>
<MenubarItem
onClick={() =>
exportSQL(DatabaseType.SQLITE)
}
>
{databaseTypeToLabelMap['sqlite']}
<MenubarShortcut className="text-base">
{emojiAI}
</MenubarShortcut>
</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSub>
<MenubarSubTrigger>
{t('menu.file.export_as')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem onClick={exportPNG}>
PNG
</MenubarItem>
<MenubarItem onClick={exportJPG}>
JPG
</MenubarItem>
<MenubarItem onClick={exportSVG}>
SVG
</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarItem
onClick={() =>
showAlert({
title: t('delete_diagram_alert.title'),
description: t(
'delete_diagram_alert.description'
),
actionLabel: t(
'delete_diagram_alert.delete'
),
closeLabel: t(
'delete_diagram_alert.cancel'
),
onAction: handleDeleteDiagramAction,
})
}
>
{t('menu.file.delete_diagram')}
</MenubarItem>
<MenubarSeparator />
<MenubarItem>{t('menu.file.exit')}</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>{t('menu.edit.edit')}</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={undo} disabled={!hasUndo}>
{t('menu.edit.undo')}
<MenubarShortcut>
{
keyboardShortcutsForOS[
KeyboardShortcutAction.UNDO
].keyCombinationLabel
}
</MenubarShortcut>
</MenubarItem>
<MenubarItem onClick={redo} disabled={!hasRedo}>
{t('menu.edit.redo')}
<MenubarShortcut>
{
keyboardShortcutsForOS[
KeyboardShortcutAction.REDO
].keyCombinationLabel
}
</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem
onClick={() =>
showAlert({
title: t('clear_diagram_alert.title'),
description: t(
'clear_diagram_alert.description'
),
actionLabel: t(
'clear_diagram_alert.clear'
),
closeLabel: t(
'clear_diagram_alert.cancel'
),
onAction: clearDiagramData,
})
}
>
{t('menu.edit.clear')}
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>{t('menu.view.view')}</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={showOrHideSidePanel}>
{isSidePanelShowed
? t('menu.view.hide_sidebar')
: t('menu.view.show_sidebar')}
</MenubarItem>
<MenubarSeparator />
<MenubarItem onClick={showOrHideCardinality}>
{showCardinality
? t('menu.view.hide_cardinality')
: t('menu.view.show_cardinality')}
</MenubarItem>
<MenubarItem onClick={showOrHideDependencies}>
{showDependenciesOnCanvas
? t('menu.view.hide_dependencies')
: t('menu.view.show_dependencies')}
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.view.zoom_on_scroll')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarCheckboxItem
checked={scrollAction === 'zoom'}
onClick={() => setScrollAction('zoom')}
>
{t('zoom.on')}
</MenubarCheckboxItem>
<MenubarCheckboxItem
checked={scrollAction === 'pan'}
onClick={() => setScrollAction('pan')}
>
{t('zoom.off')}
</MenubarCheckboxItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.view.theme')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarCheckboxItem
checked={theme === 'system'}
onClick={() => setTheme('system')}
>
{t('theme.system')}
</MenubarCheckboxItem>
<MenubarCheckboxItem
checked={theme === 'light'}
onClick={() => setTheme('light')}
>
{t('theme.light')}
</MenubarCheckboxItem>
<MenubarCheckboxItem
checked={theme === 'dark'}
onClick={() => setTheme('dark')}
>
{t('theme.dark')}
</MenubarCheckboxItem>
</MenubarSubContent>
</MenubarSub>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>{t('menu.share.share')}</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={openExportDiagramDialog}>
{t('menu.share.export_diagram')}
</MenubarItem>
<MenubarItem onClick={openImportDiagramDialog}>
{t('menu.share.import_diagram')}
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>{t('menu.help.help')}</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={openChartDBIO}>
{t('menu.help.visit_website')}
</MenubarItem>
<MenubarItem onClick={openJoinDiscord}>
{t('menu.help.join_discord')}
</MenubarItem>
<MenubarItem onClick={openCalendly}>
{t('menu.help.schedule_a_call')}
</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
</div>
{isDesktop ? (
<>
<div className="group flex flex-1 flex-row items-center justify-center">
<DiagramName />
</div>
<DiagramName />
<div className="hidden flex-1 items-center justify-end gap-2 sm:flex">
<LastSaved />
{renderStars()}
<LanguageNav />
</div>
</>
) : (
<div className="flex flex-1 flex-row justify-between gap-2">
<div className="group flex flex-1 flex-row items-center">
<DiagramName />
</div>
<div className="flex items-center">
<LastSaved />
</div>
<div className="flex items-center">{renderStars()}</div>
<div className="flex flex-1 justify-center pb-2 pt-1">
<DiagramName />
</div>
)}
</nav>

Some files were not shown because too many files have changed in this diff Show More