Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
0530f9172c | ||
|
e1e55c4b2a | ||
|
c6f7ff70f8 | ||
|
02aaabdc4e | ||
|
0f673947af | ||
|
f35f62fdf3 | ||
|
68474e75d5 | ||
|
eaf75cedb0 | ||
|
fe8b9f9e91 | ||
|
07d3745747 | ||
|
44cf5ca264 | ||
|
44d10c2390 | ||
|
9698821828 | ||
|
9f8500fc7e | ||
|
e5dbbf2eaa | ||
|
959e5402b8 | ||
|
492c9324d2 | ||
|
42c159605d | ||
|
78c427f38e | ||
|
bae74d1693 | ||
|
123f40f39e | ||
|
e3129cec74 | ||
|
5508c1e084 | ||
|
9f2893319a | ||
|
125a39fb5b | ||
|
4ca1832732 | ||
|
3609bfea4d | ||
|
94a5d84fae | ||
|
85e691fcbe | ||
|
709ccff8fa | ||
|
6c7eb4609d | ||
|
2c69b08eae | ||
|
84e7591d05 | ||
|
545e8578c9 | ||
|
f1d073d053 | ||
|
20b3396ec2 | ||
|
b305be82ae | ||
|
1430d2c236 |
52
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
@@ -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?
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
|
@@ -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",
|
||||
|
BIN
src/assets/templates/adonis-acl-dark.png
Normal file
After Width: | Height: | Size: 290 KiB |
BIN
src/assets/templates/adonis-acl.png
Normal file
After Width: | Height: | Size: 322 KiB |
BIN
src/assets/templates/akaunting-dark.png
Normal file
After Width: | Height: | Size: 327 KiB |
BIN
src/assets/templates/akaunting.png
Normal file
After Width: | Height: | Size: 345 KiB |
BIN
src/assets/templates/attendize-db-dark.png
Normal file
After Width: | Height: | Size: 525 KiB |
BIN
src/assets/templates/attendize-db.png
Normal file
After Width: | Height: | Size: 613 KiB |
BIN
src/assets/templates/bookstack-db-dark.png
Normal file
After Width: | Height: | Size: 452 KiB |
BIN
src/assets/templates/bookstack-db.png
Normal file
After Width: | Height: | Size: 578 KiB |
BIN
src/assets/templates/buddypress-dark.png
Normal file
After Width: | Height: | Size: 417 KiB |
BIN
src/assets/templates/buddypress.png
Normal file
After Width: | Height: | Size: 496 KiB |
BIN
src/assets/templates/comfortable-mexican-sofa-db-dark.png
Normal file
After Width: | Height: | Size: 375 KiB |
BIN
src/assets/templates/comfortable-mexican-sofa-db.png
Normal file
After Width: | Height: | Size: 409 KiB |
BIN
src/assets/templates/django-db-dark.png
Normal file
After Width: | Height: | Size: 354 KiB |
BIN
src/assets/templates/django-db.png
Normal file
After Width: | Height: | Size: 389 KiB |
BIN
src/assets/templates/gravity-db-dark.png
Normal file
After Width: | Height: | Size: 382 KiB |
BIN
src/assets/templates/gravity-db.png
Normal file
After Width: | Height: | Size: 415 KiB |
BIN
src/assets/templates/koel-db-dark.png
Normal file
After Width: | Height: | Size: 369 KiB |
BIN
src/assets/templates/koel-db.png
Normal file
After Width: | Height: | Size: 402 KiB |
BIN
src/assets/templates/laravel-db-dark.png
Normal file
After Width: | Height: | Size: 168 KiB |
BIN
src/assets/templates/laravel-db.png
Normal file
After Width: | Height: | Size: 172 KiB |
BIN
src/assets/templates/laravel-permission-db-dark.png
Normal file
After Width: | Height: | Size: 215 KiB |
BIN
src/assets/templates/laravel-permission-db.png
Normal file
After Width: | Height: | Size: 216 KiB |
BIN
src/assets/templates/laravel-spark-db-dark.png
Normal file
After Width: | Height: | Size: 412 KiB |
BIN
src/assets/templates/laravel-spark-db.png
Normal file
After Width: | Height: | Size: 449 KiB |
BIN
src/assets/templates/lobsters-db-dark.png
Normal file
After Width: | Height: | Size: 403 KiB |
BIN
src/assets/templates/lobsters-db.png
Normal file
After Width: | Height: | Size: 450 KiB |
BIN
src/assets/templates/monica-db-dark.png
Normal file
After Width: | Height: | Size: 746 KiB |
BIN
src/assets/templates/monica-db.png
Normal file
After Width: | Height: | Size: 846 KiB |
BIN
src/assets/templates/refinerycms-db-dark.png
Normal file
After Width: | Height: | Size: 414 KiB |
BIN
src/assets/templates/refinerycms-db.png
Normal file
After Width: | Height: | Size: 434 KiB |
BIN
src/assets/templates/saas-pegasus-db-dark.png
Normal file
After Width: | Height: | Size: 474 KiB |
BIN
src/assets/templates/saas-pegasus-db.png
Normal file
After Width: | Height: | Size: 500 KiB |
BIN
src/assets/templates/snipe-it-db-dark.png
Normal file
After Width: | Height: | Size: 383 KiB |
BIN
src/assets/templates/snipe-it-db.png
Normal file
After Width: | Height: | Size: 428 KiB |
BIN
src/assets/templates/sylius-db-dark.png
Normal file
After Width: | Height: | Size: 673 KiB |
BIN
src/assets/templates/sylius-db.png
Normal file
After Width: | Height: | Size: 804 KiB |
BIN
src/assets/templates/ticketit-db-dark.png
Normal file
After Width: | Height: | Size: 337 KiB |
BIN
src/assets/templates/ticketit-db.png
Normal file
After Width: | Height: | Size: 374 KiB |
BIN
src/assets/templates/twitter-db-dark.png
Normal file
After Width: | Height: | Size: 370 KiB |
BIN
src/assets/templates/twitter-db.png
Normal file
After Width: | Height: | Size: 404 KiB |
BIN
src/assets/templates/voyager-db-dark.png
Normal file
After Width: | Height: | Size: 401 KiB |
BIN
src/assets/templates/voyager-db.png
Normal file
After Width: | Height: | Size: 431 KiB |
62
src/components/alert/alert.tsx
Normal 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 };
|
@@ -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>
|
||||
|
168
src/components/file-uploader/file-uploader.tsx
Normal 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>
|
||||
);
|
||||
};
|
@@ -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>
|
||||
|
@@ -26,7 +26,7 @@ export interface SelectBoxOption {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface SelectBoxProps {
|
||||
export interface SelectBoxProps {
|
||||
options: SelectBoxOption[];
|
||||
value?: string[] | string;
|
||||
onChange?: (values: string[] | string) => void;
|
||||
|
@@ -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,
|
||||
});
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
@@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
@@ -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
|
||||
|
110
src/dialogs/export-diagram-dialog/export-diagram-dialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
@@ -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>
|
||||
|
||||
|
129
src/dialogs/import-diagram-dialog/import-diagram-dialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
128
src/globals.css
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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 };
|
||||
|
@@ -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',
|
||||
};
|
||||
|
@@ -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',
|
||||
};
|
||||
|
@@ -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',
|
||||
};
|
||||
|
@@ -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',
|
||||
};
|
||||
|
@@ -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',
|
||||
};
|
||||
|
@@ -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
@@ -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
@@ -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
@@ -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',
|
||||
};
|
@@ -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
@@ -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',
|
||||
};
|
@@ -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
@@ -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
@@ -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',
|
||||
};
|
@@ -4,5 +4,6 @@ export type LanguageTranslation = typeof en;
|
||||
|
||||
export type LanguageMetadata = {
|
||||
name: string;
|
||||
nativeName: string;
|
||||
code: string;
|
||||
};
|
||||
|
@@ -19,4 +19,7 @@
|
||||
.scrollable-flex > div {
|
||||
@apply !flex;
|
||||
}
|
||||
|
||||
.marker-definitions {
|
||||
}
|
||||
}
|
||||
|
149
src/lib/clone.ts
Normal 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(),
|
||||
};
|
||||
};
|
@@ -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,
|
||||
|
27
src/lib/data/export-metadata/export-sql-cache.ts
Normal 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);
|
||||
};
|
@@ -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;
|
||||
};
|
||||
|
||||
|
@@ -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 => {
|
||||
|
@@ -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[]
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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'
|
||||
|
@@ -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[]
|
||||
|
@@ -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,
|
||||
|
33
src/lib/export-import-utils.ts
Normal 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);
|
||||
};
|
@@ -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;
|
||||
};
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
|
@@ -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) =>
|
||||
|
@@ -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>
|
||||
|
@@ -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">
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -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>
|
||||
|