mirror of
https://github.com/docmost/docmost.git
synced 2025-11-11 08:42:06 +10:00
Compare commits
205 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 08829ea721 | |||
| 6c502b4749 | |||
| 6b41538b60 | |||
| 496f5d7384 | |||
| 32c7a16d06 | |||
| 64ecef09bc | |||
| 3e5cb92621 | |||
| fd5ad2f576 | |||
| 74a5360561 | |||
| 7580e8d1fe | |||
| f92d63261d | |||
| 4d51986250 | |||
| e209aaa272 | |||
| 0ef6b1978a | |||
| ae842f94d0 | |||
| 7121771f92 | |||
| 040d6625df | |||
| 33ddd92198 | |||
| 54e8d60840 | |||
| db986038c2 | |||
| de0b5f0046 | |||
| 638b811857 | |||
| d775a61c95 | |||
| 0f74f03264 | |||
| f8b93ce93f | |||
| 85d18b8cc8 | |||
| 4d9fe6f804 | |||
| 85159a2c95 | |||
| 990612793f | |||
| f2235fd2a2 | |||
| 2044cbb21c | |||
| 3d52b82cd4 | |||
| 89a2dd602b | |||
| 3cb954db69 | |||
| 71cfe3cd8e | |||
| f7efb6c2c9 | |||
| 59b514fa26 | |||
| 0c1f9304f4 | |||
| e876214eeb | |||
| 5fece5fc68 | |||
| f3dbf7cc5d | |||
| f7ac6bb4bb | |||
| 1f5ffe7f9d | |||
| 95715421c6 | |||
| f5bc99b449 | |||
| 287b833838 | |||
| 0cbbcb8eb1 | |||
| 670ee64179 | |||
| 290b7d9d94 | |||
| 2503bfd3a2 | |||
| f48d6dd60b | |||
| 1302b1b602 | |||
| 89a3f4cfc2 | |||
| e48b1c0dae | |||
| 4a2a5a7a4d | |||
| 532001fd82 | |||
| e6bf4cdd6c | |||
| a9a4a26db5 | |||
| ede5633415 | |||
| a25cf84671 | |||
| a37d558bac | |||
| ddb0f9225f | |||
| c717847ca8 | |||
| fe83557767 | |||
| 9fa432dba9 | |||
| c6aaefecbd | |||
| 311d81bc71 | |||
| f178e6654f | |||
| ca186f3c0e | |||
| a16d5d1bf4 | |||
| d97baf5824 | |||
| 8349d8271c | |||
| 2e6d16dbc3 | |||
| 4107793e73 | |||
| a1b6ac7f3e | |||
| dd0319a14d | |||
| 8194c7d42d | |||
| d01ced078b | |||
| da9c971050 | |||
| 4e7af507c6 | |||
| f7426a0b45 | |||
| b85b34d6b1 | |||
| e064e58f79 | |||
| 4f1a97ceb9 | |||
| d07338861b | |||
| 95159625aa | |||
| 9e0fbae1de | |||
| a52c86a180 | |||
| 31feb38def | |||
| ba32e42ece | |||
| a574d13f43 | |||
| ab70cee278 | |||
| 978fadd6b9 | |||
| b57be9c736 | |||
| d4b219d608 | |||
| 36e720920b | |||
| fa3c8a03e1 | |||
| 46d92fbabc | |||
| e17b975aaa | |||
| 038d21b438 | |||
| 078361b367 | |||
| 384f11f2b7 | |||
| e333eee08b | |||
| 7ec6a36515 | |||
| 2721ab6a29 | |||
| a2bc374f47 | |||
| eaa80a5546 | |||
| e9e668bd39 | |||
| 9390b39e35 | |||
| 2ae3816324 | |||
| e96330afbf | |||
| e56f7933f4 | |||
| b152c858b4 | |||
| e43ea66442 | |||
| f34812653e | |||
| 6a3a7721be | |||
| fb27282886 | |||
| dea9f4c063 | |||
| 0b6730c06f | |||
| be0d97661a | |||
| 4e2b23c97e | |||
| dc3ce27762 | |||
| 8af2d4e8cf | |||
| 73ddec4ca7 | |||
| 2b9765fb35 | |||
| 7fdd355cc3 | |||
| 6c6b47599a | |||
| 7e6a71fa2d | |||
| 1141796f24 | |||
| 11dbc079be | |||
| 87b99f8646 | |||
| 38e9eef2dc | |||
| 77b541ec71 | |||
| 7dc37b933f | |||
| 7e80797e3f | |||
| 17475bf123 | |||
| 4433d5174d | |||
| c810d0b314 | |||
| 463480ae67 | |||
| 2449d69fab | |||
| e0d74fcb0e | |||
| 4967849e3a | |||
| 0a447e91bb | |||
| 48e76aa9f4 | |||
| 2bd6422a35 | |||
| 407a1aff3b | |||
| b4bc184cb3 | |||
| 109dbdbe02 | |||
| 2df7de5828 | |||
| 373fc86e47 | |||
| 5052a9ea40 | |||
| cd47c79d86 | |||
| 78746938b7 | |||
| 4d2936627c | |||
| d2ecd28047 | |||
| bb92ca75e9 | |||
| 8f3e2ff663 | |||
| 89f6311e46 | |||
| e5a97d2a26 | |||
| 7f0fd45f3a | |||
| 078959dfa0 | |||
| 937a07059a | |||
| 227ac30d5e | |||
| a2ae341934 | |||
| 3c70e40d16 | |||
| 14197d7365 | |||
| f388540293 | |||
| b43de81013 | |||
| 6659adc7fe | |||
| 24adff9679 | |||
| e960b8c1a9 | |||
| 1958067110 | |||
| 3e519ebcd8 | |||
| 07cd650205 | |||
| 949d782a28 | |||
| 295d4325bf | |||
| 40a40bb3c7 | |||
| bc1579b022 | |||
| 85b3073681 | |||
| 4af3a54649 | |||
| c4c169b17a | |||
| a0536d852f | |||
| f12f93b373 | |||
| 35dcd5f254 | |||
| 9496ec9b57 | |||
| 5ace7616d0 | |||
| ce6a05ab66 | |||
| 7383673636 | |||
| 3e7b2495c5 | |||
| 0fc8edeb52 | |||
| bbf865b2f6 | |||
| 0c622a0dc1 | |||
| f52cd011a4 | |||
| a4d53468c3 | |||
| 66773dfaca | |||
| cc93abfb7e | |||
| 13f26f9c31 | |||
| a4ec2dac6c | |||
| 681d7c789c | |||
| 9f583174a9 | |||
| 05633082c5 | |||
| 491fbad4ac | |||
| e824aeced7 | |||
| 8f056d1071 | |||
| 99cf6dab62 |
10
.env.example
10
.env.example
@ -2,7 +2,7 @@
|
||||
APP_URL=http://localhost:3000
|
||||
PORT=3000
|
||||
|
||||
# make sure to replace this.
|
||||
# minimum of 32 characters. Generate one with: openssl rand -hex 32
|
||||
APP_SECRET=REPLACE_WITH_LONG_SECRET
|
||||
|
||||
JWT_TOKEN_EXPIRES_IN=30d
|
||||
@ -19,6 +19,10 @@ AWS_S3_SECRET_ACCESS_KEY=
|
||||
AWS_S3_REGION=
|
||||
AWS_S3_BUCKET=
|
||||
AWS_S3_ENDPOINT=
|
||||
AWS_S3_FORCE_PATH_STYLE=
|
||||
|
||||
# default: 50mb
|
||||
FILE_UPLOAD_SIZE_LIMIT=
|
||||
|
||||
# options: smtp | postmark
|
||||
MAIL_DRIVER=smtp
|
||||
@ -30,7 +34,11 @@ SMTP_HOST=127.0.0.1
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_SECURE=false
|
||||
SMTP_IGNORETLS=false
|
||||
|
||||
# Postmark driver config
|
||||
POSTMARK_TOKEN=
|
||||
|
||||
# for custom drawio server
|
||||
DRAWIO_URL=
|
||||
@ -1,4 +1,5 @@
|
||||
FROM node:21-alpine AS base
|
||||
LABEL org.opencontainers.image.source="https://github.com/docmost/docmost"
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
@ -29,6 +30,9 @@ COPY --from=builder /app/packages/editor-ext/package.json /app/packages/editor-e
|
||||
COPY --from=builder /app/package.json /app/package.json
|
||||
COPY --from=builder /app/pnpm*.yaml /app/
|
||||
|
||||
# Copy patches
|
||||
COPY --from=builder /app/patches /app/patches
|
||||
|
||||
RUN npm install -g pnpm
|
||||
|
||||
RUN chown -R node:node /app
|
||||
|
||||
@ -17,6 +17,7 @@ To get started with Docmost, please refer to our [documentation](https://docmost
|
||||
|
||||
## Features
|
||||
- Real-time collaboration
|
||||
- Diagrams (Draw.io, Excalidraw and Mermaid)
|
||||
- Spaces
|
||||
- Permissions management
|
||||
- Groups
|
||||
@ -32,4 +33,4 @@ To get started with Docmost, please refer to our [documentation](https://docmost
|
||||
</p>
|
||||
|
||||
### Contributing
|
||||
See the [development doc](https://docmost.com/docs/self-hosting/development)
|
||||
See the [development documentation](https://docmost.com/docs/self-hosting/development)
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:@tanstack/eslint-plugin-query/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
},
|
||||
}
|
||||
36
apps/client/eslint.config.mjs
Normal file
36
apps/client/eslint.config.mjs
Normal file
@ -0,0 +1,36 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginQuery from "@tanstack/eslint-plugin-query";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist"] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
"@tanstack/query": pluginQuery,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"@typescript-eslint/no-unused-expressions": "off",
|
||||
"no-useless-escape": "off",
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -1,67 +1,77 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.2.1",
|
||||
"version": "0.8.3",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@casl/ability": "^6.7.1",
|
||||
"@casl/ability": "^6.7.2",
|
||||
"@casl/react": "^4.0.0",
|
||||
"@docmost/editor-ext": "workspace:*",
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@mantine/core": "^7.11.0",
|
||||
"@mantine/form": "^7.11.0",
|
||||
"@mantine/hooks": "^7.11.0",
|
||||
"@mantine/modals": "^7.11.0",
|
||||
"@mantine/notifications": "^7.11.0",
|
||||
"@mantine/spotlight": "^7.11.0",
|
||||
"@tabler/icons-react": "^3.7.0",
|
||||
"@tanstack/react-query": "^5.48.0",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.4.0",
|
||||
"axios": "^1.7.2",
|
||||
"@excalidraw/excalidraw": "^0.17.6",
|
||||
"@mantine/core": "^7.14.2",
|
||||
"@mantine/form": "^7.14.2",
|
||||
"@mantine/hooks": "^7.14.2",
|
||||
"@mantine/modals": "^7.14.2",
|
||||
"@mantine/notifications": "^7.14.2",
|
||||
"@mantine/spotlight": "^7.14.2",
|
||||
"@tabler/icons-react": "^3.22.0",
|
||||
"@tanstack/react-query": "^5.61.4",
|
||||
"axios": "^1.7.9",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"jotai": "^2.8.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next-http-backend": "^2.6.1",
|
||||
"jotai": "^2.10.3",
|
||||
"jotai-optics": "^0.4.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"katex": "^0.16.10",
|
||||
"lowlight": "^3.1.0",
|
||||
"katex": "0.16.21",
|
||||
"lowlight": "^3.2.0",
|
||||
"mermaid": "^11.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-arborist": "^3.4.0",
|
||||
"react-arborist": "3.4.0",
|
||||
"react-clear-modal": "^2.0.11",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-drawio": "^1.0.1",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-moveable": "^0.56.0",
|
||||
"react-router-dom": "^6.24.0",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-router-dom": "^7.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tiptap-extension-global-drag-handle": "^0.1.16",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/eslint-plugin-query": "^5.47.0",
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.62.1",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/node": "20.14.9",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||
"@typescript-eslint/parser": "^7.14.1",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"eslint": "^9.5.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.7",
|
||||
"@types/node": "22.10.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"globals": "^15.13.0",
|
||||
"optics-ts": "^2.4.1",
|
||||
"postcss": "^8.4.38",
|
||||
"postcss-preset-mantine": "^1.15.0",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "^3.3.2",
|
||||
"typescript": "^5.5.2",
|
||||
"vite": "^5.3.1"
|
||||
"prettier": "^3.4.1",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.17.0",
|
||||
"vite": "^6.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
342
apps/client/public/locales/de-DE/translation.json
Normal file
342
apps/client/public/locales/de-DE/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "Konto",
|
||||
"Active": "Aktiv",
|
||||
"Add": "Hinzufügen",
|
||||
"Add group members": "Gruppenmitglieder hinzufügen",
|
||||
"Add groups": "Gruppen hinzufügen",
|
||||
"Add members": "Mitglieder hinzufügen",
|
||||
"Add to groups": "Zu Gruppen hinzufügen",
|
||||
"Add space members": "Bereichsmitglieder hinzufügen",
|
||||
"Admin": "Administrator",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Sind Sie sicher, dass Sie diese Gruppe löschen möchten? Mitglieder verlieren den Zugang zu den Ressourcen, auf die diese Gruppe zugreifen kann.",
|
||||
"Are you sure you want to delete this page?": "Sind Sie sicher, dass Sie diese Seite löschen möchten?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Sind Sie sicher, dass Sie diesen Benutzer aus der Gruppe entfernen möchten? Der Benutzer verliert den Zugang zu den Ressourcen, auf die diese Gruppe zugreifen kann.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Sind Sie sicher, dass Sie diesen Benutzer aus dem Bereich entfernen möchten? Der Benutzer verliert den gesamten Zugang zu diesem Bereich.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Sind Sie sicher, dass Sie diese Version wiederherstellen möchten? Alle nicht versionierten Änderungen gehen verloren.",
|
||||
"Can become members of groups and spaces in workspace": "Kann Mitglied von Gruppen und Bereichen im Arbeitsbereich werden",
|
||||
"Can create and edit pages in space.": "Kann Seiten im Bereich erstellen und bearbeiten.",
|
||||
"Can edit": "Kann bearbeiten",
|
||||
"Can manage workspace": "Kann Arbeitsbereich verwalten",
|
||||
"Can manage workspace but cannot delete it": "Kann Arbeitsbereich verwalten, aber nicht löschen",
|
||||
"Can view": "Kann anzeigen",
|
||||
"Can view pages in space but not edit.": "Kann Seiten im Bereich anzeigen, aber nicht bearbeiten.",
|
||||
"Cancel": "Abbrechen",
|
||||
"Change email": "E-Mail ändern",
|
||||
"Change password": "Passwort ändern",
|
||||
"Change photo": "Foto ändern",
|
||||
"Choose a role": "Wählen Sie eine Rolle",
|
||||
"Choose your preferred color scheme.": "Wählen Sie Ihr bevorzugtes Farbschema.",
|
||||
"Choose your preferred interface language.": "Wählen Sie Ihre bevorzugte Benutzersprache.",
|
||||
"Choose your preferred page width.": "Wählen Sie Ihre bevorzugte Seitenbreite.",
|
||||
"Confirm": "Bestätigen",
|
||||
"Copy link": "Link kopieren",
|
||||
"Create": "Erstellen",
|
||||
"Create group": "Gruppe erstellen",
|
||||
"Create page": "Seite erstellen",
|
||||
"Create space": "Bereich erstellen",
|
||||
"Create workspace": "Arbeitsbereich erstellen",
|
||||
"Current password": "Aktuelles Passwort",
|
||||
"Dark": "Dunkel",
|
||||
"Date": "Datum",
|
||||
"Delete": "Löschen",
|
||||
"Delete group": "Gruppe löschen",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sind Sie sicher, dass Sie diese Seite löschen möchten? Dadurch werden ihre Unterseiten und die Seitengeschichte gelöscht. Diese Aktion ist unwiderruflich.",
|
||||
"Description": "Beschreibung",
|
||||
"Details": "Einzelheiten",
|
||||
"e.g ACME": "z.B. ACME",
|
||||
"e.g ACME Inc": "z.B. ACME Inc.",
|
||||
"e.g Developers": "z.B. Entwickler",
|
||||
"e.g Group for developers": "z.B. Gruppe für Entwickler",
|
||||
"e.g product": "z.B. Produkt",
|
||||
"e.g Product Team": "z.B. Produktteam",
|
||||
"e.g Sales": "z.B. Vertrieb",
|
||||
"e.g Space for product team": "z.B. Bereich für das Produktteam",
|
||||
"e.g Space for sales team to collaborate": "z.B. Bereich für das Vertriebsteam zur Zusammenarbeit",
|
||||
"Edit": "Bearbeiten",
|
||||
"Edit group": "Gruppe bearbeiten",
|
||||
"Email": "E-Mail",
|
||||
"Enter a strong password": "Geben Sie ein starkes Passwort ein",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Geben Sie gültige E-Mail-Adressen ein, getrennt durch Kommas oder Leerzeichen [max: 50]",
|
||||
"enter valid emails addresses": "gültige E-Mail-Adressen eingeben",
|
||||
"Enter your current password": "Geben Sie Ihr aktuelles Passwort ein",
|
||||
"enter your full name": "Geben Sie Ihren vollständigen Namen ein",
|
||||
"Enter your new password": "Geben Sie Ihr neues Passwort ein",
|
||||
"Enter your new preferred email": "Geben Sie Ihre neue bevorzugte E-Mail ein",
|
||||
"Enter your password": "Geben Sie Ihr Passwort ein",
|
||||
"Error fetching page data.": "Fehler beim Abrufen der Seitendaten.",
|
||||
"Error loading page history.": "Fehler beim Laden der Seitengeschichte.",
|
||||
"Export": "Exportieren",
|
||||
"Failed to create page": "Erstellung der Seite fehlgeschlagen",
|
||||
"Failed to delete page": "Löschen der Seite fehlgeschlagen",
|
||||
"Failed to fetch recent pages": "Fehler beim Abrufen der letzten Seiten",
|
||||
"Failed to import pages": "Import der Seiten fehlgeschlagen",
|
||||
"Failed to load page. An error occurred.": "Seite konnte nicht geladen werden. Es ist ein Fehler aufgetreten.",
|
||||
"Failed to update data": "Aktualisierung der Daten fehlgeschlagen",
|
||||
"Full access": "Voller Zugriff",
|
||||
"Full page width": "Volle Seitenbreite",
|
||||
"Full width": "Volle Breite",
|
||||
"General": "Allgemein",
|
||||
"Group": "Gruppe",
|
||||
"Group description": "Gruppenbeschreibung",
|
||||
"Group name": "Gruppenname",
|
||||
"Groups": "Gruppen",
|
||||
"Has full access to space settings and pages.": "Hat vollen Zugriff auf die Bereichseinstellungen und Seiten.",
|
||||
"Home": "Startseite",
|
||||
"Import pages": "Seiten importieren",
|
||||
"Import pages & space settings": "Seiten und Bereichseinstellungen importieren",
|
||||
"Importing pages": "Seiten werden importiert",
|
||||
"invalid invitation link": "ungültiger Einladungslink",
|
||||
"Invitation signup": "Einladung zur Anmeldung",
|
||||
"Invite by email": "Einladen per E-Mail",
|
||||
"Invite members": "Mitglieder einladen",
|
||||
"Invite new members": "Neue Mitglieder einladen",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Eingeladene Mitglieder, die ihre Einladung noch nicht angenommen haben, werden hier angezeigt.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Eingeladene Mitglieder erhalten Zugriff auf die Bereiche, auf die die Gruppen zugreifen können",
|
||||
"Join the workspace": "Dem Arbeitsbereich beitreten",
|
||||
"Language": "Sprache",
|
||||
"Light": "Hell",
|
||||
"Link copied": "Link kopiert",
|
||||
"Login": "Anmelden",
|
||||
"Logout": "Abmelden",
|
||||
"Manage Group": "Gruppe verwalten",
|
||||
"Manage members": "Mitglieder verwalten",
|
||||
"member": "Mitglied",
|
||||
"Member": "Mitglied",
|
||||
"members": "Mitglieder",
|
||||
"Members": "Mitglieder",
|
||||
"My preferences": "Meine Vorlieben",
|
||||
"My Profile": "Mein Profil",
|
||||
"My profile": "Mein Profil",
|
||||
"Name": "Name",
|
||||
"New email": "Neue E-Mail",
|
||||
"New page": "Neue Seite",
|
||||
"New password": "Neues Passwort",
|
||||
"No group found": "Keine Gruppe gefunden",
|
||||
"No page history saved yet.": "Es wurde noch keine Seitengeschichte gespeichert.",
|
||||
"No pages yet": "Noch keine Seiten",
|
||||
"No results found...": "Keine Ergebnisse gefunden...",
|
||||
"No user found": "Kein Benutzer gefunden",
|
||||
"Overview": "Überblick",
|
||||
"Owner": "Besitzer",
|
||||
"page": "Seite",
|
||||
"Page deleted successfully": "Seite erfolgreich gelöscht",
|
||||
"Page history": "Seitengeschichte",
|
||||
"Page import is in progress. Please do not close this tab.": "Der Seitenimport läuft. Bitte schließen Sie diesen Tab nicht.",
|
||||
"Pages": "Seiten",
|
||||
"pages": "Seiten",
|
||||
"Password": "Passwort",
|
||||
"Password changed successfully": "Passwort erfolgreich geändert",
|
||||
"Pending": "Ausstehend",
|
||||
"Please confirm your action": "Bitte bestätigen Sie Ihre Aktion",
|
||||
"Preferences": "Vorlieben",
|
||||
"Print PDF": "PDF drucken",
|
||||
"Profile": "Profil",
|
||||
"Recently updated": "Kürzlich aktualisiert",
|
||||
"Remove": "Entfernen",
|
||||
"Remove group member": "Gruppenmitglied entfernen",
|
||||
"Remove space member": "Bereichsmitglied entfernen",
|
||||
"Restore": "Wiederherstellen",
|
||||
"Role": "Rolle",
|
||||
"Save": "Speichern",
|
||||
"Search": "Suche",
|
||||
"Search for groups": "Suche nach Gruppen",
|
||||
"Search for users": "Suche nach Benutzern",
|
||||
"Search for users and groups": "Suche nach Benutzern und Gruppen",
|
||||
"Search...": "Suche...",
|
||||
"Select language": "Sprache auswählen",
|
||||
"Select role": "Rolle auswählen",
|
||||
"Select role to assign to all invited members": "Rolle für alle eingeladenen Mitglieder auswählen",
|
||||
"Select theme": "Design auswählen",
|
||||
"Send invitation": "Einladung senden",
|
||||
"Settings": "Einstellungen",
|
||||
"Setup workspace": "Arbeitsbereich einrichten",
|
||||
"Sign In": "Anmelden",
|
||||
"Sign Up": "Registrieren",
|
||||
"Slug": "Slug",
|
||||
"Space": "Bereich",
|
||||
"Space description": "Bereichsbeschreibung",
|
||||
"Space menu": "Bereichsmenü",
|
||||
"Space name": "Bereichsname",
|
||||
"Space settings": "Bereichseinstellungen",
|
||||
"Space slug": "Slug des Bereichs",
|
||||
"Spaces": "Bereiche",
|
||||
"Spaces you belong to": "Bereiche, denen Sie angehören",
|
||||
"No space found": "Keine Bereiche gefunden",
|
||||
"Search for spaces": "Nach Bereichen suchen",
|
||||
"Start typing to search...": "Anfangen zu tippen, um zu suchen...",
|
||||
"Status": "Status",
|
||||
"Successfully imported": "Erfolgreich importiert",
|
||||
"Successfully restored": "Erfolgreich wiederhergestellt",
|
||||
"System settings": "Systemeinstellungen",
|
||||
"Theme": "Design",
|
||||
"To change your email, you have to enter your password and new email.": "Um Ihre E-Mail-Adresse zu ändern, müssen Sie Ihr Passwort und Ihre neue E-Mail-Adresse eingeben.",
|
||||
"Toggle full page width": "Volle Seitenbreite umschalten",
|
||||
"Unable to import pages. Please try again.": "Seiten konnten nicht importiert werden. Bitte versuchen Sie es erneut.",
|
||||
"untitled": "ohne Titel",
|
||||
"Untitled": "Ohne Titel",
|
||||
"Updated successfully": "Erfolgreich aktualisiert",
|
||||
"User": "Benutzer",
|
||||
"Workspace": "Arbeitsbereich",
|
||||
"Workspace Name": "Arbeitsbereichsname",
|
||||
"Workspace settings": "Arbeitsbereich-Einstellungen",
|
||||
"You can change your password here.": "Hier können Sie Ihr Passwort ändern.",
|
||||
"Your Email": "Ihre E-Mail",
|
||||
"Your import is complete.": "Ihr Import ist abgeschlossen.",
|
||||
"Your name": "Ihr Name",
|
||||
"Your Name": "Ihr Name",
|
||||
"Your password": "Ihr Passwort",
|
||||
"Your password must be a minimum of 8 characters.": "Ihr Passwort muss mindestens 8 Zeichen lang sein.",
|
||||
"Sidebar toggle": "Seitenleiste umschalten",
|
||||
"Comments": "Kommentare",
|
||||
"404 page not found": "404 Seite nicht gefunden",
|
||||
"Sorry, we can't find the page you are looking for.": "Entschuldigung, wir können die gesuchte Seite nicht finden.",
|
||||
"Take me back to homepage": "Zurück zur Startseite",
|
||||
"Forgot password": "Passwort vergessen",
|
||||
"Forgot your password?": "Passwort vergessen?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail gesendet. Bitte überprüfen Sie Ihren Posteingang.",
|
||||
"Send reset link": "Zurücksetzungslink senden",
|
||||
"Password reset": "Passwort zurücksetzen",
|
||||
"Your new password": "Ihr neues Passwort",
|
||||
"Set password": "Passwort festlegen",
|
||||
"Write a comment": "Einen Kommentar schreiben",
|
||||
"Reply...": "Antworten...",
|
||||
"Error loading comments.": "Fehler beim Laden der Kommentare.",
|
||||
"No comments yet.": "Noch keine Kommentare.",
|
||||
"Edit comment": "Kommentar bearbeiten",
|
||||
"Delete comment": "Kommentar löschen",
|
||||
"Are you sure you want to delete this comment?": "Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?",
|
||||
"Comment created successfully": "Kommentar erfolgreich erstellt",
|
||||
"Error creating comment": "Fehler beim Erstellen des Kommentars",
|
||||
"Comment updated successfully": "Kommentar erfolgreich aktualisiert",
|
||||
"Failed to update comment": "Aktualisierung des Kommentars fehlgeschlagen",
|
||||
"Comment deleted successfully": "Kommentar erfolgreich gelöscht",
|
||||
"Failed to delete comment": "Löschen des Kommentars fehlgeschlagen",
|
||||
"Comment resolved successfully": "Kommentar erfolgreich gelöst",
|
||||
"Failed to resolve comment": "Lösen des Kommentars fehlgeschlagen",
|
||||
"Revoke invitation": "Einladung widerrufen",
|
||||
"Revoke": "Widerrufen",
|
||||
"Don't": "Nicht",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Sind Sie sicher, dass Sie diese Einladung widerrufen möchten? Der Benutzer kann dem Arbeitsbereich nicht beitreten.",
|
||||
"Resend invitation": "Einladung erneut senden",
|
||||
"Anyone with this link can join this workspace.": "Jeder mit diesem Link kann dem Arbeitsbereich beitreten.",
|
||||
"Invite link": "Einladungslink",
|
||||
"Copy": "Kopieren",
|
||||
"Copied": "Kopiert",
|
||||
"Select a user": "Benutzer auswählen",
|
||||
"Select a group": "Gruppe auswählen",
|
||||
"Export all pages and attachments in this space.": "Alle Seiten und Anhänge in diesem Bereich exportieren.",
|
||||
"Delete space": "Bereich löschen",
|
||||
"Are you sure you want to delete this space?": "Sind Sie sicher, dass Sie diesen Bereich löschen möchten?",
|
||||
"Delete this space with all its pages and data.": "Diesen Bereich mit allen Seiten und Daten löschen.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Alle Seiten, Kommentare, Anhänge und Berechtigungen in diesem Bereich werden unwiderruflich gelöscht.",
|
||||
"Confirm space name": "Bestätigen Sie den Namen des Arbeitsbereichs",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Geben Sie den Namen des Bereichs <b>{{spaceName}}</b> ein, um Ihre Aktion zu bestätigen.",
|
||||
"Format": "Format",
|
||||
"Include subpages": "Unterseiten einbeziehen",
|
||||
"Include attachments": "Anhänge einbeziehen",
|
||||
"Select export format": "Exportformat auswählen",
|
||||
"Export failed:": "Export fehlgeschlagen:",
|
||||
"export error": "Exportfehler",
|
||||
"Export page": "Seite exportieren",
|
||||
"Export space": "Bereich exportieren",
|
||||
"Export {{type}}": "Exportiere {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Datei überschreitet das Anhängelimit von {{limit}}",
|
||||
"Align left": "Links ausrichten",
|
||||
"Align right": "Rechts ausrichten",
|
||||
"Align center": "Zentrieren",
|
||||
"Merge cells": "Zellen zusammenführen",
|
||||
"Split cell": "Zelle teilen",
|
||||
"Delete column": "Spalte löschen",
|
||||
"Delete row": "Zeile löschen",
|
||||
"Add left column": "Linke Spalte hinzufügen",
|
||||
"Add right column": "Rechte Spalte hinzufügen",
|
||||
"Add row above": "Zeile oben hinzufügen",
|
||||
"Add row below": "Zeile unten hinzufügen",
|
||||
"Delete table": "Tabelle löschen",
|
||||
"Info": "Info",
|
||||
"Success": "Erfolg",
|
||||
"Warning": "Warnung",
|
||||
"Danger": "Gefahr",
|
||||
"Mermaid diagram error:": "Fehler im Mermaid-Diagramm:",
|
||||
"Invalid Mermaid diagram": "Ungültiges Mermaid-Diagramm",
|
||||
"Double-click to edit Draw.io diagram": "Zum Bearbeiten des Draw.io-Diagramms doppelklicken",
|
||||
"Exit": "Beenden",
|
||||
"Save & Exit": "Speichern & Beenden",
|
||||
"Double-click to edit Excalidraw diagram": "Zum Bearbeiten des Excalidraw-Diagramms doppelklicken",
|
||||
"Paste link": "Link einfügen",
|
||||
"Edit link": "Link bearbeiten",
|
||||
"Remove link": "Link entfernen",
|
||||
"Add link": "Link hinzufügen",
|
||||
"Please enter a valid url": "Bitte geben Sie eine gültige URL ein",
|
||||
"Empty equation": "Leere Gleichung",
|
||||
"Invalid equation": "Ungültige Gleichung",
|
||||
"Color": "Farbe",
|
||||
"Text color": "Textfarbe",
|
||||
"Default": "Standard",
|
||||
"Blue": "Blau",
|
||||
"Green": "Grün",
|
||||
"Purple": "Lila",
|
||||
"Red": "Rot",
|
||||
"Yellow": "Gelb",
|
||||
"Orange": "Orange",
|
||||
"Pink": "Rosa",
|
||||
"Gray": "Grau",
|
||||
"Embed link": "Link einbetten",
|
||||
"Invalid {{provider}} embed link": "Ungültiger {{provider}}-Einbettungslink",
|
||||
"Embed {{provider}}": "{{provider}} einbetten",
|
||||
"Enter {{provider}} link to embed": "Geben Sie den Einbettungslink für {{provider}} ein",
|
||||
"Bold": "Fett",
|
||||
"Italic": "Kursiv",
|
||||
"Underline": "Unterstreichen",
|
||||
"Strike": "Durchstreichen",
|
||||
"Code": "Code",
|
||||
"Comment": "Kommentar",
|
||||
"Text": "Text",
|
||||
"Heading 1": "Überschrift 1",
|
||||
"Heading 2": "Überschrift 2",
|
||||
"Heading 3": "Überschrift 3",
|
||||
"To-do List": "To-do-Liste",
|
||||
"Bullet List": "Aufzählungsliste",
|
||||
"Numbered List": "Nummerierte Liste",
|
||||
"Blockquote": "Blockzitat",
|
||||
"Just start typing with plain text.": "Tippen Sie einfach mit normalem Text los.",
|
||||
"Track tasks with a to-do list.": "Verfolgen Sie Aufgaben mit einer To-do-Liste.",
|
||||
"Big section heading.": "Große Abschnittsüberschrift.",
|
||||
"Medium section heading.": "Mittlere Abschnittsüberschrift.",
|
||||
"Small section heading.": "Kleine Abschnittsüberschrift.",
|
||||
"Create a simple bullet list.": "Erstellen Sie eine einfache Aufzählungsliste.",
|
||||
"Create a list with numbering.": "Erstellen Sie eine nummerierte Liste.",
|
||||
"Create block quote.": "Erstellen Sie ein Blockzitat.",
|
||||
"Insert code snippet.": "Code-Snippet einfügen.",
|
||||
"Insert horizontal rule divider": "Horizontale Trennlinie einfügen",
|
||||
"Upload any image from your device.": "Laden Sie ein beliebiges Bild von Ihrem Gerät hoch.",
|
||||
"Upload any video from your device.": "Laden Sie ein beliebiges Video von Ihrem Gerät hoch.",
|
||||
"Upload any file from your device.": "Laden Sie eine beliebige Datei von Ihrem Gerät hoch.",
|
||||
"Table": "Tabelle",
|
||||
"Insert a table.": "Tabelle einfügen.",
|
||||
"Insert collapsible block.": "Einklappbaren Block einfügen.",
|
||||
"Video": "Video",
|
||||
"Divider": "Trennlinie",
|
||||
"Quote": "Zitat",
|
||||
"Image": "Bild",
|
||||
"File attachment": "Dateianhang",
|
||||
"Toggle block": "Block umschalten",
|
||||
"Callout": "Hinweisbox",
|
||||
"Insert callout notice.": "Hinweisbox einfügen.",
|
||||
"Math inline": "Mathe inline",
|
||||
"Insert inline math equation.": "Mathe-Gleichung inline einfügen.",
|
||||
"Math block": "Matheblock",
|
||||
"Insert math equation": "Mathe-Gleichung einfügen",
|
||||
"Mermaid diagram": "Mermaid-Diagramm",
|
||||
"Insert mermaid diagram": "Mermaid-Diagramm einfügen",
|
||||
"Insert and design Drawio diagrams": "Drawio-Diagramme einfügen und gestalten",
|
||||
"Insert current date": "Aktuelles Datum einfügen",
|
||||
"Draw and sketch excalidraw diagrams": "Excalidraw-Diagramme zeichnen und skizzieren",
|
||||
"Multiple": "Mehrere",
|
||||
"Heading {{level}}": "Überschrift {{level}}",
|
||||
"Toggle title": "Titel umschalten",
|
||||
"Write anything. Enter \"/\" for commands": "Schreiben Sie irgendetwas. Geben Sie \"/\" für Befehle ein",
|
||||
"Names do not match": "Namen stimmen nicht überein",
|
||||
"Today, {{time}}": "Heute, {{time}}",
|
||||
"Yesterday, {{time}}": "Gestern, {{time}}"
|
||||
}
|
||||
343
apps/client/public/locales/en-US/translation.json
Normal file
343
apps/client/public/locales/en-US/translation.json
Normal file
@ -0,0 +1,343 @@
|
||||
{
|
||||
"Account": "Account",
|
||||
"Active": "Active",
|
||||
"Add": "Add",
|
||||
"Add group members": "Add group members",
|
||||
"Add groups": "Add groups",
|
||||
"Add members": "Add members",
|
||||
"Add to groups": "Add to groups",
|
||||
"Add space members": "Add space members",
|
||||
"Admin": "Admin",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Are you sure you want to delete this group? Members will lose access to resources this group has access to.",
|
||||
"Are you sure you want to delete this page?": "Are you sure you want to delete this page?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Are you sure you want to remove this user from the space? The user will lose all access to this space.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Are you sure you want to restore this version? Any changes not versioned will be lost.",
|
||||
"Can become members of groups and spaces in workspace": "Can become members of groups and spaces in workspace",
|
||||
"Can create and edit pages in space.": "Can create and edit pages in space.",
|
||||
"Can edit": "Can edit",
|
||||
"Can manage workspace": "Can manage workspace",
|
||||
"Can manage workspace but cannot delete it": "Can manage workspace but cannot delete it",
|
||||
"Can view": "Can view",
|
||||
"Can view pages in space but not edit.": "Can view pages in space but not edit.",
|
||||
"Cancel": "Cancel",
|
||||
"Change email": "Change email",
|
||||
"Change password": "Change password",
|
||||
"Change photo": "Change photo",
|
||||
"Choose a role": "Choose a role",
|
||||
"Choose your preferred color scheme.": "Choose your preferred color scheme.",
|
||||
"Choose your preferred interface language.": "Choose your preferred interface language.",
|
||||
"Choose your preferred page width.": "Choose your preferred page width.",
|
||||
"Confirm": "Confirm",
|
||||
"Copy link": "Copy link",
|
||||
"Create": "Create",
|
||||
"Create group": "Create group",
|
||||
"Create page": "Create page",
|
||||
"Create space": "Create space",
|
||||
"Create workspace": "Create workspace",
|
||||
"Current password": "Current password",
|
||||
"Dark": "Dark",
|
||||
"Date": "Date",
|
||||
"Delete": "Delete",
|
||||
"Delete group": "Delete group",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.",
|
||||
"Description": "Description",
|
||||
"Details": "Details",
|
||||
"e.g ACME": "e.g ACME",
|
||||
"e.g ACME Inc": "e.g ACME Inc",
|
||||
"e.g Developers": "e.g Developers",
|
||||
"e.g Group for developers": "e.g Group for developers",
|
||||
"e.g product": "e.g product",
|
||||
"e.g Product Team": "e.g Product Team",
|
||||
"e.g Sales": "e.g Sales",
|
||||
"e.g Space for product team": "e.g Space for product team",
|
||||
"e.g Space for sales team to collaborate": "e.g Space for sales team to collaborate",
|
||||
"Edit": "Edit",
|
||||
"Edit group": "Edit group",
|
||||
"Email": "Email",
|
||||
"Enter a strong password": "Enter a strong password",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Enter valid email addresses separated by comma or space [max: 50]",
|
||||
"enter valid emails addresses": "enter valid emails addresses",
|
||||
"Enter your current password": "Enter your current password",
|
||||
"enter your full name": "enter your full name",
|
||||
"Enter your new password": "Enter your new password",
|
||||
"Enter your new preferred email": "Enter your new preferred email",
|
||||
"Enter your password": "Enter your password",
|
||||
"Error fetching page data.": "Error fetching page data.",
|
||||
"Error loading page history.": "Error loading page history.",
|
||||
"Export": "Export",
|
||||
"Failed to create page": "Failed to create page",
|
||||
"Failed to delete page": "Failed to delete page",
|
||||
"Failed to fetch recent pages": "Failed to fetch recent pages",
|
||||
"Failed to import pages": "Failed to import pages",
|
||||
"Failed to load page. An error occurred.": "Failed to load page. An error occurred.",
|
||||
"Failed to update data": "Failed to update data",
|
||||
"Full access": "Full access",
|
||||
"Full page width": "Full page width",
|
||||
"Full width": "Full width",
|
||||
"General": "General",
|
||||
"Group": "Group",
|
||||
"Group description": "Group description",
|
||||
"Group name": "Group name",
|
||||
"Groups": "Groups",
|
||||
"Has full access to space settings and pages.": "Has full access to space settings and pages.",
|
||||
"Home": "Home",
|
||||
"Import pages": "Import pages",
|
||||
"Import pages & space settings": "Import pages & space settings",
|
||||
"Importing pages": "Importing pages",
|
||||
"invalid invitation link": "invalid invitation link",
|
||||
"Invitation signup": "Invitation signup",
|
||||
"Invite by email": "Invite by email",
|
||||
"Invite members": "Invite members",
|
||||
"Invite new members": "Invite new members",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Invited members who are yet to accept their invitation will appear here.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Invited members will be granted access to spaces the groups can access",
|
||||
"Join the workspace": "Join the workspace",
|
||||
"Language": "Language",
|
||||
"Light": "Light",
|
||||
"Link copied": "Link copied",
|
||||
"Login": "Login",
|
||||
"Logout": "Logout",
|
||||
"Manage Group": "Manage Group",
|
||||
"Manage members": "Manage members",
|
||||
"member": "member",
|
||||
"Member": "Member",
|
||||
"members": "members",
|
||||
"Members": "Members",
|
||||
"My preferences": "My preferences",
|
||||
"My Profile": "My Profile",
|
||||
"My profile": "My profile",
|
||||
"Name": "Name",
|
||||
"New email": "New email",
|
||||
"New page": "New page",
|
||||
"New password": "New password",
|
||||
"No group found": "No group found",
|
||||
"No page history saved yet.": "No page history saved yet.",
|
||||
"No pages yet": "No pages yet",
|
||||
"No results found...": "No results found...",
|
||||
"No user found": "No user found",
|
||||
"Overview": "Overview",
|
||||
"Owner": "Owner",
|
||||
"page": "page",
|
||||
"Page deleted successfully": "Page deleted successfully",
|
||||
"Page history": "Page history",
|
||||
"Page import is in progress. Please do not close this tab.": "Page import is in progress. Please do not close this tab.",
|
||||
"Pages": "Pages",
|
||||
"pages": "pages",
|
||||
"Password": "Password",
|
||||
"Password changed successfully": "Password changed successfully",
|
||||
"Pending": "Pending",
|
||||
"Please confirm your action": "Please confirm your action",
|
||||
"Preferences": "Preferences",
|
||||
"Print PDF": "Print PDF",
|
||||
"Profile": "Profile",
|
||||
"Recently updated": "Recently updated",
|
||||
"Remove": "Remove",
|
||||
"Remove group member": "Remove group member",
|
||||
"Remove space member": "Remove space member",
|
||||
"Restore": "Restore",
|
||||
"Role": "Role",
|
||||
"Save": "Save",
|
||||
"Search": "Search",
|
||||
"Search for groups": "Search for groups",
|
||||
"Search for users": "Search for users",
|
||||
"Search for users and groups": "Search for users and groups",
|
||||
"Search...": "Search...",
|
||||
"Select language": "Select language",
|
||||
"Select role": "Select role",
|
||||
"Select role to assign to all invited members": "Select role to assign to all invited members",
|
||||
"Select theme": "Select theme",
|
||||
"Send invitation": "Send invitation",
|
||||
"Settings": "Settings",
|
||||
"Setup workspace": "Setup workspace",
|
||||
"Sign In": "Sign In",
|
||||
"Sign Up": "Sign Up",
|
||||
"Slug": "Slug",
|
||||
"Space": "Space",
|
||||
"Space description": "Space description",
|
||||
"Space menu": "Space menu",
|
||||
"Space name": "Space name",
|
||||
"Space settings": "Space settings",
|
||||
"Space slug": "Space slug",
|
||||
"Spaces": "Spaces",
|
||||
"Spaces you belong to": "Spaces you belong to",
|
||||
"No space found": "No space found",
|
||||
"Search for spaces": "Search for spaces",
|
||||
"Start typing to search...": "Start typing to search...",
|
||||
"Status": "Status",
|
||||
"Successfully imported": "Successfully imported",
|
||||
"Successfully restored": "Successfully restored",
|
||||
"System settings": "System settings",
|
||||
"Theme": "Theme",
|
||||
"To change your email, you have to enter your password and new email.": "To change your email, you have to enter your password and new email.",
|
||||
"Toggle full page width": "Toggle full page width",
|
||||
"Unable to import pages. Please try again.": "Unable to import pages. Please try again.",
|
||||
"untitled": "untitled",
|
||||
"Untitled": "Untitled",
|
||||
"Updated successfully": "Updated successfully",
|
||||
"User": "User",
|
||||
"Workspace": "Workspace",
|
||||
"Workspace Name": "Workspace Name",
|
||||
"Workspace settings": "Workspace settings",
|
||||
"You can change your password here.": "You can change your password here.",
|
||||
"Your Email": "Your Email",
|
||||
"Your import is complete.": "Your import is complete.",
|
||||
"Your name": "Your name",
|
||||
"Your Name": "Your Name",
|
||||
"Your password": "Your password",
|
||||
"Your password must be a minimum of 8 characters.": "Your password must be a minimum of 8 characters.",
|
||||
"Sidebar toggle": "Sidebar toggle",
|
||||
"Comments": "Comments",
|
||||
"404 page not found": "404 page not found",
|
||||
"Sorry, we can't find the page you are looking for.": "Sorry, we can't find the page you are looking for.",
|
||||
"Take me back to homepage": "Take me back to homepage",
|
||||
"Forgot password": "Forgot password",
|
||||
"Forgot your password?": "Forgot your password?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "A password reset link has been sent to your email. Please check your inbox.",
|
||||
"Send reset link": "Send reset link",
|
||||
"Password reset": "Password reset",
|
||||
"Your new password": "Your new password",
|
||||
"Set password": "Set password",
|
||||
"Write a comment": "Write a comment",
|
||||
"Reply...": "Reply...",
|
||||
"Error loading comments.": "Error loading comments.",
|
||||
"No comments yet.": "No comments yet.",
|
||||
"Edit comment": "Edit comment",
|
||||
"Delete comment": "Delete comment",
|
||||
"Are you sure you want to delete this comment?": "Are you sure you want to delete this comment?",
|
||||
"Comment created successfully": "Comment created successfully",
|
||||
"Error creating comment": "Error creating comment",
|
||||
"Comment updated successfully": "Comment updated successfully",
|
||||
"Failed to update comment": "Failed to update comment",
|
||||
"Comment deleted successfully": "Comment deleted successfully",
|
||||
"Failed to delete comment": "Failed to delete comment",
|
||||
"Comment resolved successfully": "Comment resolved successfully",
|
||||
"Failed to resolve comment": "Failed to resolve comment",
|
||||
"Revoke invitation": "Revoke invitation",
|
||||
"Revoke": "Revoke",
|
||||
"Don't": "Don't",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Are you sure you want to revoke this invitation? The user will not be able to join the workspace.",
|
||||
"Resend invitation": "Resend invitation",
|
||||
"Anyone with this link can join this workspace.": "Anyone with this link can join this workspace.",
|
||||
"Invite link": "Invite link",
|
||||
"Copy": "Copy",
|
||||
"Copied": "Copied",
|
||||
"Select a user": "Select a user",
|
||||
"Select a group": "Select a group",
|
||||
"Export all pages and attachments in this space.": "Export all pages and attachments in this space.",
|
||||
"Delete space": "Delete space",
|
||||
"Are you sure you want to delete this space?": "Are you sure you want to delete this space?",
|
||||
"Delete this space with all its pages and data.": "Delete this space with all its pages and data.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "All pages, comments, attachments and permissions in this space will be deleted irreversibly.",
|
||||
"Confirm space name": "Confirm space name",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Type the space name <b>{{spaceName}}</b> to confirm your action.",
|
||||
"Format": "Format",
|
||||
"Include subpages": "Include subpages",
|
||||
"Include attachments": "Include attachments",
|
||||
"Select export format": "Select export format",
|
||||
"Export failed:": "Export failed:",
|
||||
"export error": "export error",
|
||||
"Export page": "Export page",
|
||||
"Export space": "Export space",
|
||||
"Export {{type}}": "Export {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "File exceeds the {{limit}} attachment limit",
|
||||
"Align left": "Align left",
|
||||
"Align right": "Align right",
|
||||
"Align center": "Align center",
|
||||
"Justify": "Justify",
|
||||
"Merge cells": "Merge cells",
|
||||
"Split cell": "Split cell",
|
||||
"Delete column": "Delete column",
|
||||
"Delete row": "Delete row",
|
||||
"Add left column": "Add left column",
|
||||
"Add right column": "Add right column",
|
||||
"Add row above": "Add row above",
|
||||
"Add row below": "Add row below",
|
||||
"Delete table": "Delete table",
|
||||
"Info": "Info",
|
||||
"Success": "Success",
|
||||
"Warning": "Warning",
|
||||
"Danger": "Danger",
|
||||
"Mermaid diagram error:": "Mermaid diagram error:",
|
||||
"Invalid Mermaid diagram": "Invalid Mermaid diagram",
|
||||
"Double-click to edit Draw.io diagram": "Double-click to edit Draw.io diagram",
|
||||
"Exit": "Exit",
|
||||
"Save & Exit": "Save & Exit",
|
||||
"Double-click to edit Excalidraw diagram": "Double-click to edit Excalidraw diagram",
|
||||
"Paste link": "Paste link",
|
||||
"Edit link": "Edit link",
|
||||
"Remove link": "Remove link",
|
||||
"Add link": "Add link",
|
||||
"Please enter a valid url": "Please enter a valid url",
|
||||
"Empty equation": "Empty equation",
|
||||
"Invalid equation": "Invalid equation",
|
||||
"Color": "Color",
|
||||
"Text color": "Text color",
|
||||
"Default": "Default",
|
||||
"Blue": "Blue",
|
||||
"Green": "Green",
|
||||
"Purple": "Purple",
|
||||
"Red": "Red",
|
||||
"Yellow": "Yellow",
|
||||
"Orange": "Orange",
|
||||
"Pink": "Pink",
|
||||
"Gray": "Gray",
|
||||
"Embed link": "Embed link",
|
||||
"Invalid {{provider}} embed link": "Invalid {{provider}} embed link",
|
||||
"Embed {{provider}}": "Embed {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Enter {{provider}} link to embed",
|
||||
"Bold": "Bold",
|
||||
"Italic": "Italic",
|
||||
"Underline": "Underline",
|
||||
"Strike": "Strike",
|
||||
"Code": "Code",
|
||||
"Comment": "Comment",
|
||||
"Text": "Text",
|
||||
"Heading 1": "Heading 1",
|
||||
"Heading 2": "Heading 2",
|
||||
"Heading 3": "Heading 3",
|
||||
"To-do List": "To-do List",
|
||||
"Bullet List": "Bullet List",
|
||||
"Numbered List": "Numbered List",
|
||||
"Blockquote": "Blockquote",
|
||||
"Just start typing with plain text.": "Just start typing with plain text.",
|
||||
"Track tasks with a to-do list.": "Track tasks with a to-do list.",
|
||||
"Big section heading.": "Big section heading.",
|
||||
"Medium section heading.": "Medium section heading.",
|
||||
"Small section heading.": "Small section heading.",
|
||||
"Create a simple bullet list.": "Create a simple bullet list.",
|
||||
"Create a list with numbering.": "Create a list with numbering.",
|
||||
"Create block quote.": "Create block quote.",
|
||||
"Insert code snippet.": "Insert code snippet.",
|
||||
"Insert horizontal rule divider": "Insert horizontal rule divider",
|
||||
"Upload any image from your device.": "Upload any image from your device.",
|
||||
"Upload any video from your device.": "Upload any video from your device.",
|
||||
"Upload any file from your device.": "Upload any file from your device.",
|
||||
"Table": "Table",
|
||||
"Insert a table.": "Insert a table.",
|
||||
"Insert collapsible block.": "Insert collapsible block.",
|
||||
"Video": "Video",
|
||||
"Divider": "Divider",
|
||||
"Quote": "Quote",
|
||||
"Image": "Image",
|
||||
"File attachment": "File attachment",
|
||||
"Toggle block": "Toggle block",
|
||||
"Callout": "Callout",
|
||||
"Insert callout notice.": "Insert callout notice.",
|
||||
"Math inline": "Math inline",
|
||||
"Insert inline math equation.": "Insert inline math equation.",
|
||||
"Math block": "Math block",
|
||||
"Insert math equation": "Insert math equation",
|
||||
"Mermaid diagram": "Mermaid diagram",
|
||||
"Insert mermaid diagram": "Insert mermaid diagram",
|
||||
"Insert and design Drawio diagrams": "Insert and design Drawio diagrams",
|
||||
"Insert current date": "Insert current date",
|
||||
"Draw and sketch excalidraw diagrams": "Draw and sketch excalidraw diagrams",
|
||||
"Multiple": "Multiple",
|
||||
"Heading {{level}}": "Heading {{level}}",
|
||||
"Toggle title": "Toggle title",
|
||||
"Write anything. Enter \"/\" for commands": "Write anything. Enter \"/\" for commands",
|
||||
"Names do not match": "Names do not match",
|
||||
"Today, {{time}}": "Today, {{time}}",
|
||||
"Yesterday, {{time}}": "Yesterday, {{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/es-ES/translation.json
Normal file
342
apps/client/public/locales/es-ES/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "Cuenta",
|
||||
"Active": "Activo",
|
||||
"Add": "Agregar",
|
||||
"Add group members": "Agregar miembros del grupo",
|
||||
"Add groups": "Agregar grupos",
|
||||
"Add members": "Agregar miembros",
|
||||
"Add to groups": "Agregar a grupos",
|
||||
"Add space members": "Agregar miembros al espacio",
|
||||
"Admin": "Administrador",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "¿Estás seguro de que deseas eliminar este grupo? Los miembros perderán acceso a los recursos a los que este grupo tiene acceso.",
|
||||
"Are you sure you want to delete this page?": "¿Está seguro de que desea eliminar esta página?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "¿Está seguro de que desea eliminar a este usuario del grupo? El usuario perderá acceso a los recursos a los que tiene acceso este grupo.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "¿Está seguro de que desea eliminar a este usuario del espacio? El usuario perderá todo acceso a este espacio.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "¿Está seguro de que desea restaurar esta versión? Cualquier cambio no versionado se perderá.",
|
||||
"Can become members of groups and spaces in workspace": "Pueden convertirse en miembros de grupos y espacios en el espacio de trabajo",
|
||||
"Can create and edit pages in space.": "Puede crear y editar páginas en el espacio.",
|
||||
"Can edit": "Puede editar",
|
||||
"Can manage workspace": "Puede gestionar el espacio de trabajo",
|
||||
"Can manage workspace but cannot delete it": "Puede gestionar el espacio de trabajo pero no puede eliminarlo",
|
||||
"Can view": "Puede ver",
|
||||
"Can view pages in space but not edit.": "Puede ver páginas en el espacio pero no editarlas.",
|
||||
"Cancel": "Cancelar",
|
||||
"Change email": "Cambiar correo electrónico",
|
||||
"Change password": "Cambiar contraseña",
|
||||
"Change photo": "Cambiar foto",
|
||||
"Choose a role": "Seleccione un rol",
|
||||
"Choose your preferred color scheme.": "Elige tu esquema de color preferido.",
|
||||
"Choose your preferred interface language.": "Elige tu idioma de interfaz preferido.",
|
||||
"Choose your preferred page width.": "Elige el ancho de página que prefieras.",
|
||||
"Confirm": "Confirmar",
|
||||
"Copy link": "Copiar enlace",
|
||||
"Create": "Crear",
|
||||
"Create group": "Crear grupo",
|
||||
"Create page": "Crear página",
|
||||
"Create space": "Crear espacio",
|
||||
"Create workspace": "Crear espacio de trabajo",
|
||||
"Current password": "Contraseña actual",
|
||||
"Dark": "Oscuro",
|
||||
"Date": "Fecha",
|
||||
"Delete": "Eliminar",
|
||||
"Delete group": "Eliminar grupo",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "¿Está seguro de que desea eliminar esta página? Esto eliminará sus dependientes y el historial de la página. Esta acción es irreversible.",
|
||||
"Description": "Descripción",
|
||||
"Details": "Detalles",
|
||||
"e.g ACME": "ej: ACME",
|
||||
"e.g ACME Inc": "ej: ACME Inc",
|
||||
"e.g Developers": "ej: Desarrolladores",
|
||||
"e.g Group for developers": "ej: Grupo para desarrolladores",
|
||||
"e.g product": "ej: producto",
|
||||
"e.g Product Team": "ej: Equipo de Producto",
|
||||
"e.g Sales": "ej: Ventas",
|
||||
"e.g Space for product team": "ej: Espacio para el equipo de producto",
|
||||
"e.g Space for sales team to collaborate": "ej: Espacio para que el equipo de ventas colabore",
|
||||
"Edit": "Editar",
|
||||
"Edit group": "Editar grupo",
|
||||
"Email": "Correo electrónico",
|
||||
"Enter a strong password": "Introduce una contraseña fuerte",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Ingrese direcciones de correo electrónico válidas separadas por coma o espacio [max: 50]",
|
||||
"enter valid emails addresses": "introduce direcciones de correo electrónico válidas",
|
||||
"Enter your current password": "Introduce tu contraseña actual",
|
||||
"enter your full name": "introduzca su nombre completo",
|
||||
"Enter your new password": "Ingrese su nueva contraseña",
|
||||
"Enter your new preferred email": "Introduce tu nuevo correo electrónico preferido",
|
||||
"Enter your password": "Introduce tu contraseña",
|
||||
"Error fetching page data.": "Error al obtener los datos de la página.",
|
||||
"Error loading page history.": "Error al cargar el historial de la página.",
|
||||
"Export": "Exportar",
|
||||
"Failed to create page": "No se pudo crear la página",
|
||||
"Failed to delete page": "No se pudo eliminar la página",
|
||||
"Failed to fetch recent pages": "Error al obtener las páginas recientes",
|
||||
"Failed to import pages": "No se pudieron importar las páginas",
|
||||
"Failed to load page. An error occurred.": "Error al cargar la página. Se produjo un error.",
|
||||
"Failed to update data": "No se pudo actualizar los datos",
|
||||
"Full access": "Acceso completo",
|
||||
"Full page width": "Ancho de página completa",
|
||||
"Full width": "Ancho completo",
|
||||
"General": "General",
|
||||
"Group": "Grupo",
|
||||
"Group description": "Descripción del grupo",
|
||||
"Group name": "Nombre del grupo",
|
||||
"Groups": "Grupos",
|
||||
"Has full access to space settings and pages.": "Tiene acceso completo a la configuración y páginas del espacio.",
|
||||
"Home": "Inicio",
|
||||
"Import pages": "Importar páginas",
|
||||
"Import pages & space settings": "Importar páginas y configuraciones del espacio",
|
||||
"Importing pages": "Importando páginas",
|
||||
"invalid invitation link": "enlace de invitación no válido",
|
||||
"Invitation signup": "Registro por invitación",
|
||||
"Invite by email": "Invitar por correo electrónico",
|
||||
"Invite members": "Invitar a miembros",
|
||||
"Invite new members": "Invitar a nuevos miembros",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Los miembros invitados que aún no han aceptado su invitación aparecerán aquí.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Los miembros invitados recibirán acceso a los espacios a los que los grupos pueden acceder",
|
||||
"Join the workspace": "Unirse al espacio de trabajo",
|
||||
"Language": "Idioma",
|
||||
"Light": "Ligero",
|
||||
"Link copied": "Enlace copiado",
|
||||
"Login": "Iniciar sesión",
|
||||
"Logout": "Cerrar sesión",
|
||||
"Manage Group": "Gestionar Grupo",
|
||||
"Manage members": "Gestionar miembros",
|
||||
"member": "miembro",
|
||||
"Member": "Miembro",
|
||||
"members": "miembros",
|
||||
"Members": "Miembros",
|
||||
"My preferences": "Mis preferencias",
|
||||
"My Profile": "Mi Perfil",
|
||||
"My profile": "Mi perfil",
|
||||
"Name": "Nombre",
|
||||
"New email": "Nuevo correo electrónico",
|
||||
"New page": "Nueva página",
|
||||
"New password": "Nueva contraseña",
|
||||
"No group found": "No se encontró grupo",
|
||||
"No page history saved yet.": "No hay historial de la página guardado aún.",
|
||||
"No pages yet": "No hay páginas todavía",
|
||||
"No results found...": "No se encontraron resultados...",
|
||||
"No user found": "No se encontró usuario",
|
||||
"Overview": "Visión general",
|
||||
"Owner": "Propietario",
|
||||
"page": "página",
|
||||
"Page deleted successfully": "Página eliminada con éxito",
|
||||
"Page history": "Historial de la página",
|
||||
"Page import is in progress. Please do not close this tab.": "La importación de la página está en curso. Por favor, no cierre esta pestaña.",
|
||||
"Pages": "Páginas",
|
||||
"pages": "páginas",
|
||||
"Password": "Contraseña",
|
||||
"Password changed successfully": "Contraseña cambiada con éxito",
|
||||
"Pending": "Pendiente",
|
||||
"Please confirm your action": "Por favor, confirme su acción",
|
||||
"Preferences": "Preferencias",
|
||||
"Print PDF": "Imprimir PDF",
|
||||
"Profile": "Perfil",
|
||||
"Recently updated": "Recientemente actualizado",
|
||||
"Remove": "Eliminar",
|
||||
"Remove group member": "Eliminar miembro del grupo",
|
||||
"Remove space member": "Eliminar miembro del espacio",
|
||||
"Restore": "Restaurar",
|
||||
"Role": "Rol",
|
||||
"Save": "Guardar",
|
||||
"Search": "Buscar",
|
||||
"Search for groups": "Buscar grupos",
|
||||
"Search for users": "Buscar usuarios",
|
||||
"Search for users and groups": "Buscar usuarios y grupos",
|
||||
"Search...": "Buscar...",
|
||||
"Select language": "Seleccionar idioma",
|
||||
"Select role": "Seleccionar rol",
|
||||
"Select role to assign to all invited members": "Seleccionar rol para asignar a todos los miembros invitados",
|
||||
"Select theme": "Seleccionar tema",
|
||||
"Send invitation": "Enviar invitación",
|
||||
"Settings": "Ajustes",
|
||||
"Setup workspace": "Configurar espacio de trabajo",
|
||||
"Sign In": "Iniciar sesión",
|
||||
"Sign Up": "Registrarse",
|
||||
"Slug": "Identificador",
|
||||
"Space": "Espacio",
|
||||
"Space description": "Descripción del espacio",
|
||||
"Space menu": "Menú de espacio",
|
||||
"Space name": "Nombre del espacio",
|
||||
"Space settings": "Configuración del espacio",
|
||||
"Space slug": "Identificador del espacio",
|
||||
"Spaces": "Espacios",
|
||||
"Spaces you belong to": "Espacios a los que perteneces",
|
||||
"No space found": "No se encontró espacio",
|
||||
"Search for spaces": "Buscar espacios",
|
||||
"Start typing to search...": "Empieza a escribir para buscar...",
|
||||
"Status": "Estado",
|
||||
"Successfully imported": "Importado con éxito",
|
||||
"Successfully restored": "Restaurado con éxito",
|
||||
"System settings": "Configuración del sistema",
|
||||
"Theme": "Tema",
|
||||
"To change your email, you have to enter your password and new email.": "Para cambiar tu correo electrónico, debes ingresar tu contraseña y nuevo correo electrónico.",
|
||||
"Toggle full page width": "Alternar el ancho de página completa",
|
||||
"Unable to import pages. Please try again.": "No se pueden importar las páginas. Por favor, inténtelo de nuevo.",
|
||||
"untitled": "sin título",
|
||||
"Untitled": "Sin título",
|
||||
"Updated successfully": "Actualizado con éxito",
|
||||
"User": "Usuario",
|
||||
"Workspace": "Espacio de trabajo",
|
||||
"Workspace Name": "Nombre del espacio de trabajo",
|
||||
"Workspace settings": "Configuración del espacio de trabajo",
|
||||
"You can change your password here.": "Puede cambiar su contraseña aquí.",
|
||||
"Your Email": "Su correo electrónico",
|
||||
"Your import is complete.": "Su importación está completa.",
|
||||
"Your name": "Tu nombre",
|
||||
"Your Name": "Tu Nombre",
|
||||
"Your password": "Tu contraseña",
|
||||
"Your password must be a minimum of 8 characters.": "Su contraseña debe tener un mínimo de 8 caracteres.",
|
||||
"Sidebar toggle": "Alternar barra lateral",
|
||||
"Comments": "Comentarios",
|
||||
"404 page not found": "404 página no encontrada",
|
||||
"Sorry, we can't find the page you are looking for.": "Lo sentimos, no podemos encontrar la página que buscas.",
|
||||
"Take me back to homepage": "Llévame de vuelta a la página de inicio",
|
||||
"Forgot password": "Olvidó la contraseña",
|
||||
"Forgot your password?": "¿Olvidó su contraseña?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Se ha enviado un enlace para restablecer la contraseña a tu correo electrónico. Por favor, revisa tu bandeja de entrada.",
|
||||
"Send reset link": "Enviar enlace de restablecimiento",
|
||||
"Password reset": "Restablecimiento de contraseña",
|
||||
"Your new password": "Tu nueva contraseña",
|
||||
"Set password": "Establecer contraseña",
|
||||
"Write a comment": "Escribir un comentario",
|
||||
"Reply...": "Responder...",
|
||||
"Error loading comments.": "Error al cargar comentarios.",
|
||||
"No comments yet.": "No hay comentarios todavía.",
|
||||
"Edit comment": "Editar comentario",
|
||||
"Delete comment": "Eliminar comentario",
|
||||
"Are you sure you want to delete this comment?": "¿Está seguro de que desea eliminar este comentario?",
|
||||
"Comment created successfully": "Comentario creado con éxito",
|
||||
"Error creating comment": "Error al crear comentario",
|
||||
"Comment updated successfully": "Comentario actualizado con éxito",
|
||||
"Failed to update comment": "No se pudo actualizar el comentario",
|
||||
"Comment deleted successfully": "Comentario eliminado con éxito",
|
||||
"Failed to delete comment": "No se pudo eliminar el comentario",
|
||||
"Comment resolved successfully": "Comentario resuelto con éxito",
|
||||
"Failed to resolve comment": "No se pudo resolver el comentario",
|
||||
"Revoke invitation": "Revocar invitación",
|
||||
"Revoke": "Revocar",
|
||||
"Don't": "No",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "¿Está seguro de que desea revocar esta invitación? El usuario no podrá unirse al espacio de trabajo.",
|
||||
"Resend invitation": "Reenviar invitación",
|
||||
"Anyone with this link can join this workspace.": "Cualquiera con este enlace puede unirse a este espacio de trabajo.",
|
||||
"Invite link": "Enlace de invitación",
|
||||
"Copy": "Copiar",
|
||||
"Copied": "Copiado",
|
||||
"Select a user": "Seleccionar un usuario",
|
||||
"Select a group": "Seleccionar un grupo",
|
||||
"Export all pages and attachments in this space.": "Exportar todas las páginas y archivos adjuntos en este espacio.",
|
||||
"Delete space": "Eliminar espacio",
|
||||
"Are you sure you want to delete this space?": "¿Está seguro de que desea eliminar este espacio?",
|
||||
"Delete this space with all its pages and data.": "Eliminar este espacio con todas sus páginas y datos.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Todas las páginas, comentarios, archivos adjuntos y permisos en este espacio se eliminarán de forma irreversible.",
|
||||
"Confirm space name": "Confirmar nombre del espacio",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Escribe el nombre del espacio <b>{{spaceName}}</b> para confirmar tu acción.",
|
||||
"Format": "Formato",
|
||||
"Include subpages": "Incluir subpáginas",
|
||||
"Include attachments": "Incluir adjuntos",
|
||||
"Select export format": "Seleccionar formato de exportación",
|
||||
"Export failed:": "Exportación fallida:",
|
||||
"export error": "error de exportación",
|
||||
"Export page": "Exportar página",
|
||||
"Export space": "Exportar espacio",
|
||||
"Export {{type}}": "Exportar {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "El archivo supera el límite de {{limit}} adjuntos",
|
||||
"Align left": "Alinear a la izquierda",
|
||||
"Align right": "Alinear a la derecha",
|
||||
"Align center": "Alinear al centro",
|
||||
"Merge cells": "Combinar celdas",
|
||||
"Split cell": "Dividir celda",
|
||||
"Delete column": "Eliminar columna",
|
||||
"Delete row": "Eliminar fila",
|
||||
"Add left column": "Agregar columna izquierda",
|
||||
"Add right column": "Agregar columna derecha",
|
||||
"Add row above": "Agregar fila arriba",
|
||||
"Add row below": "Agregar fila debajo",
|
||||
"Delete table": "Eliminar tabla",
|
||||
"Info": "Información",
|
||||
"Success": "Satisfactorio",
|
||||
"Warning": "Advertencia",
|
||||
"Danger": "Peligro",
|
||||
"Mermaid diagram error:": "Error en diagrama de Mermaid:",
|
||||
"Invalid Mermaid diagram": "Diagrama de Mermaid no válido",
|
||||
"Double-click to edit Draw.io diagram": "Doble clic para editar el diagrama de Draw.io",
|
||||
"Exit": "Salir",
|
||||
"Save & Exit": "Guardar y Salir",
|
||||
"Double-click to edit Excalidraw diagram": "Doble clic para editar el diagrama de Excalidraw",
|
||||
"Paste link": "Pegar enlace",
|
||||
"Edit link": "Editar enlace",
|
||||
"Remove link": "Eliminar enlace",
|
||||
"Add link": "Agregar enlace",
|
||||
"Please enter a valid url": "Por favor, ingrese una URL válida",
|
||||
"Empty equation": "Ecuación vacía",
|
||||
"Invalid equation": "Ecuación no válida",
|
||||
"Color": "Color",
|
||||
"Text color": "Color del texto",
|
||||
"Default": "Predeterminado",
|
||||
"Blue": "Azul",
|
||||
"Green": "Verde",
|
||||
"Purple": "Morado",
|
||||
"Red": "Rojo",
|
||||
"Yellow": "Amarillo",
|
||||
"Orange": "Naranja",
|
||||
"Pink": "Rosa",
|
||||
"Gray": "Gris",
|
||||
"Embed link": "Enlace adjunto",
|
||||
"Invalid {{provider}} embed link": "Enlace incrustado {{provider}} no válido",
|
||||
"Embed {{provider}}": "Incrustar {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Introduzca el enlace de {{provider}} para incrustar",
|
||||
"Bold": "Negrita",
|
||||
"Italic": "Cursiva",
|
||||
"Underline": "Subrayar",
|
||||
"Strike": "Tachar",
|
||||
"Code": "Código",
|
||||
"Comment": "Comentario",
|
||||
"Text": "Texto",
|
||||
"Heading 1": "Encabezado 1",
|
||||
"Heading 2": "Encabezado 2",
|
||||
"Heading 3": "Encabezado 3",
|
||||
"To-do List": "Lista de cosas por hacer",
|
||||
"Bullet List": "Lista con viñetas",
|
||||
"Numbered List": "Lista numerada",
|
||||
"Blockquote": "Cita en bloque",
|
||||
"Just start typing with plain text.": "Simplemente comienza a escribir con texto sin formato.",
|
||||
"Track tasks with a to-do list.": "Administra tareas con una lista de tareas pendientes.",
|
||||
"Big section heading.": "Gran encabezado de sección.",
|
||||
"Medium section heading.": "Encabezado de sección mediano.",
|
||||
"Small section heading.": "Pequeño encabezado de sección.",
|
||||
"Create a simple bullet list.": "Crear una lista con viñetas simple.",
|
||||
"Create a list with numbering.": "Crear una lista con numeración.",
|
||||
"Create block quote.": "Crear una cita en bloque.",
|
||||
"Insert code snippet.": "Insertar fragmento de código.",
|
||||
"Insert horizontal rule divider": "Insertar regla horizontal",
|
||||
"Upload any image from your device.": "Sube cualquier imagen desde tu dispositivo.",
|
||||
"Upload any video from your device.": "Sube cualquier video desde tu dispositivo.",
|
||||
"Upload any file from your device.": "Sube cualquier archivo desde tu dispositivo.",
|
||||
"Table": "Tabla",
|
||||
"Insert a table.": "Insertar una tabla.",
|
||||
"Insert collapsible block.": "Insertar bloque desplegable.",
|
||||
"Video": "Vídeo",
|
||||
"Divider": "Divisor",
|
||||
"Quote": "Cita",
|
||||
"Image": "Imagen",
|
||||
"File attachment": "Adjunto de archivo",
|
||||
"Toggle block": "Alternar bloque",
|
||||
"Callout": "Aviso",
|
||||
"Insert callout notice.": "Insertar aviso de llamada.",
|
||||
"Math inline": "Matemáticas en línea",
|
||||
"Insert inline math equation.": "Insertar ecuación matemática en línea.",
|
||||
"Math block": "Bloque de matemáticas",
|
||||
"Insert math equation": "Insertar ecuación matemática",
|
||||
"Mermaid diagram": "Diagrama de Mermaid",
|
||||
"Insert mermaid diagram": "Insertar diagrama de Mermaid",
|
||||
"Insert and design Drawio diagrams": "Insertar y diseñar diagramas Drawio",
|
||||
"Insert current date": "Insertar fecha actual",
|
||||
"Draw and sketch excalidraw diagrams": "Dibujar y esbozar diagramas de Excalidraw",
|
||||
"Multiple": "Múltiple",
|
||||
"Heading {{level}}": "Encabezado {{level}}",
|
||||
"Toggle title": "Alternar título",
|
||||
"Write anything. Enter \"/\" for commands": "Escribe cualquier cosa. Ingresa \"/\" para comandos",
|
||||
"Names do not match": "Los nombres no coinciden",
|
||||
"Today, {{time}}": "Hoy, {{time}}",
|
||||
"Yesterday, {{time}}": "Ayer, {{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/fr-FR/translation.json
Normal file
342
apps/client/public/locales/fr-FR/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "Compte",
|
||||
"Active": "Actif",
|
||||
"Add": "Ajouter",
|
||||
"Add group members": "Ajouter des membres au groupe",
|
||||
"Add groups": "Ajouter des groupes",
|
||||
"Add members": "Ajouter des membres",
|
||||
"Add to groups": "Ajouter aux groupes",
|
||||
"Add space members": "Ajouter des membres à l'espace",
|
||||
"Admin": "Admin",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Êtes-vous sûr de vouloir supprimer ce groupe ? Les membres perdront l'accès aux ressources auxquelles ce groupe a accès.",
|
||||
"Are you sure you want to delete this page?": "Êtes-vous sûr de vouloir supprimer cette page ?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Êtes-vous sûr de vouloir retirer cet utilisateur du groupe ? L'utilisateur perdra l'accès aux ressources auxquelles ce groupe a accès.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Êtes-vous sûr de vouloir retirer cet utilisateur de l'espace ? L'utilisateur perdra tout accès à cet espace.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Êtes-vous sûr de vouloir restaurer cette version ? Toutes les modifications non versionnées seront perdues.",
|
||||
"Can become members of groups and spaces in workspace": "Peut devenir membre de groupes et d'espaces dans l'espace de travail",
|
||||
"Can create and edit pages in space.": "Peut créer et modifier des pages dans l'espace.",
|
||||
"Can edit": "Peut modifier",
|
||||
"Can manage workspace": "Peut gérer l'espace de travail",
|
||||
"Can manage workspace but cannot delete it": "Peut gérer l'espace de travail mais ne peut pas le supprimer",
|
||||
"Can view": "Peut voir",
|
||||
"Can view pages in space but not edit.": "Peut voir les pages dans l'espace mais ne peut pas les modifier.",
|
||||
"Cancel": "Annuler",
|
||||
"Change email": "Changer l'email",
|
||||
"Change password": "Changer le mot de passe",
|
||||
"Change photo": "Changer la photo",
|
||||
"Choose a role": "Choisir un rôle",
|
||||
"Choose your preferred color scheme.": "Choisissez votre palette de couleurs préférée.",
|
||||
"Choose your preferred interface language.": "Choisissez votre langue d'interface préférée.",
|
||||
"Choose your preferred page width.": "Choisissez votre largeur de page préférée.",
|
||||
"Confirm": "Confirmer",
|
||||
"Copy link": "Copier le lien",
|
||||
"Create": "Créer",
|
||||
"Create group": "Créer groupe",
|
||||
"Create page": "Créer page",
|
||||
"Create space": "Créer espace",
|
||||
"Create workspace": "Créer espace de travail",
|
||||
"Current password": "Mot de passe actuel",
|
||||
"Dark": "Sombre",
|
||||
"Date": "Date",
|
||||
"Delete": "Supprimer",
|
||||
"Delete group": "Supprimer groupe",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Êtes-vous sûr de vouloir supprimer cette page ? Cela supprimera ses enfants et l'historique de la page. Cette action est irréversible.",
|
||||
"Description": "Description",
|
||||
"Details": "Détails",
|
||||
"e.g ACME": "par ex. ACME",
|
||||
"e.g ACME Inc": "par ex. ACME Inc",
|
||||
"e.g Developers": "par ex. Développeurs",
|
||||
"e.g Group for developers": "par ex. Groupe pour développeurs",
|
||||
"e.g product": "par ex. produit",
|
||||
"e.g Product Team": "par ex. Équipe Produit",
|
||||
"e.g Sales": "par ex. Ventes",
|
||||
"e.g Space for product team": "par ex. Espace pour l'équipe produit",
|
||||
"e.g Space for sales team to collaborate": "par ex. Espace pour l'équipe de vente pour collaborer",
|
||||
"Edit": "Modifier",
|
||||
"Edit group": "Modifier groupe",
|
||||
"Email": "Email",
|
||||
"Enter a strong password": "Entrez un mot de passe fort",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Entrez des adresses email valides séparées par une virgule ou un espace [max : 50]",
|
||||
"enter valid emails addresses": "entrez des adresses email valides",
|
||||
"Enter your current password": "Entrez votre mot de passe actuel",
|
||||
"enter your full name": "entrez votre nom complet",
|
||||
"Enter your new password": "Entrez votre nouveau mot de passe",
|
||||
"Enter your new preferred email": "Entrez votre nouvel email préféré",
|
||||
"Enter your password": "Entrez votre mot de passe",
|
||||
"Error fetching page data.": "Erreur lors de la récupération des données de la page.",
|
||||
"Error loading page history.": "Erreur lors du chargement de l'historique de la page.",
|
||||
"Export": "Exporter",
|
||||
"Failed to create page": "Échec de la création de la page",
|
||||
"Failed to delete page": "Échec de la suppression de la page",
|
||||
"Failed to fetch recent pages": "Échec de la récupération des pages récentes",
|
||||
"Failed to import pages": "Échec de l'importation des pages",
|
||||
"Failed to load page. An error occurred.": "Échec du chargement de la page. Une erreur s'est produite.",
|
||||
"Failed to update data": "Échec de la mise à jour des données",
|
||||
"Full access": "Accès complet",
|
||||
"Full page width": "Largeur de page complète",
|
||||
"Full width": "Largeur complète",
|
||||
"General": "Général",
|
||||
"Group": "Groupe",
|
||||
"Group description": "Description du groupe",
|
||||
"Group name": "Nom du groupe",
|
||||
"Groups": "Groupes",
|
||||
"Has full access to space settings and pages.": "A un accès complet aux paramètres de l'espace et aux pages.",
|
||||
"Home": "Accueil",
|
||||
"Import pages": "Importer des pages",
|
||||
"Import pages & space settings": "Importer des pages et paramètres de l'espace",
|
||||
"Importing pages": "Importation des pages",
|
||||
"invalid invitation link": "lien d'invitation invalide",
|
||||
"Invitation signup": "Inscription par invitation",
|
||||
"Invite by email": "Inviter par email",
|
||||
"Invite members": "Inviter des membres",
|
||||
"Invite new members": "Inviter de nouveaux membres",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Les membres invités qui n'ont pas encore accepté leur invitation apparaîtront ici.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Les membres invités auront accès aux espaces auxquels les groupes peuvent accéder",
|
||||
"Join the workspace": "Rejoindre l'espace de travail",
|
||||
"Language": "Langue",
|
||||
"Light": "Clair",
|
||||
"Link copied": "Lien copié",
|
||||
"Login": "Connexion",
|
||||
"Logout": "Déconnexion",
|
||||
"Manage Group": "Gérer le groupe",
|
||||
"Manage members": "Gérer les membres",
|
||||
"member": "membre",
|
||||
"Member": "Membre",
|
||||
"members": "membres",
|
||||
"Members": "Membres",
|
||||
"My preferences": "Mes préférences",
|
||||
"My Profile": "Mon Profil",
|
||||
"My profile": "Mon profil",
|
||||
"Name": "Nom",
|
||||
"New email": "Nouvel email",
|
||||
"New page": "Nouvelle page",
|
||||
"New password": "Nouveau mot de passe",
|
||||
"No group found": "Aucun groupe trouvé",
|
||||
"No page history saved yet.": "Aucun historique de la page enregistré pour l'instant.",
|
||||
"No pages yet": "Aucune page pour l'instant",
|
||||
"No results found...": "Aucun résultat trouvé...",
|
||||
"No user found": "Aucun utilisateur trouvé",
|
||||
"Overview": "Vue d'ensemble",
|
||||
"Owner": "Propriétaire",
|
||||
"page": "page",
|
||||
"Page deleted successfully": "Page supprimée avec succès",
|
||||
"Page history": "Historique de la page",
|
||||
"Page import is in progress. Please do not close this tab.": "L'importation de la page est en cours. Veuillez ne pas fermer cet onglet.",
|
||||
"Pages": "Pages",
|
||||
"pages": "pages",
|
||||
"Password": "Mot de passe",
|
||||
"Password changed successfully": "Mot de passe changé avec succès",
|
||||
"Pending": "En attente",
|
||||
"Please confirm your action": "Veuillez confirmer votre action",
|
||||
"Preferences": "Préférences",
|
||||
"Print PDF": "Imprimer PDF",
|
||||
"Profile": "Profil",
|
||||
"Recently updated": "Récemment mis à jour",
|
||||
"Remove": "Retirer",
|
||||
"Remove group member": "Retirer un membre du groupe",
|
||||
"Remove space member": "Retirer un membre de l'espace",
|
||||
"Restore": "Restaurer",
|
||||
"Role": "Rôle",
|
||||
"Save": "Enregistrer",
|
||||
"Search": "Rechercher",
|
||||
"Search for groups": "Rechercher des groupes",
|
||||
"Search for users": "Rechercher des utilisateurs",
|
||||
"Search for users and groups": "Rechercher des utilisateurs et des groupes",
|
||||
"Search...": "Rechercher...",
|
||||
"Select language": "Sélectionner la langue",
|
||||
"Select role": "Sélectionner un rôle",
|
||||
"Select role to assign to all invited members": "Sélectionner le rôle à attribuer à tous les membres invités",
|
||||
"Select theme": "Sélectionner le thème",
|
||||
"Send invitation": "Envoyer l'invitation",
|
||||
"Settings": "Paramètres",
|
||||
"Setup workspace": "Configurer l'espace de travail",
|
||||
"Sign In": "Se connecter",
|
||||
"Sign Up": "S'inscrire",
|
||||
"Slug": "Slug",
|
||||
"Space": "Espace",
|
||||
"Space description": "Description de l'espace",
|
||||
"Space menu": "Menu de l'espace",
|
||||
"Space name": "Nom de l'espace",
|
||||
"Space settings": "Paramètres de l'espace",
|
||||
"Space slug": "Slug de l'espace",
|
||||
"Spaces": "Espaces",
|
||||
"Spaces you belong to": "Espaces auxquels vous appartenez",
|
||||
"No space found": "Aucun espace trouvé",
|
||||
"Search for spaces": "Rechercher des espaces",
|
||||
"Start typing to search...": "Commencez à taper pour rechercher...",
|
||||
"Status": "Statut",
|
||||
"Successfully imported": "Importé avec succès",
|
||||
"Successfully restored": "Restauré avec succès",
|
||||
"System settings": "Paramètres système",
|
||||
"Theme": "Thème",
|
||||
"To change your email, you have to enter your password and new email.": "Pour changer votre email, vous devez entrer votre mot de passe et votre nouvel email.",
|
||||
"Toggle full page width": "Basculer sur la largeur complète de la page",
|
||||
"Unable to import pages. Please try again.": "Impossible d'importer les pages. Veuillez réessayer.",
|
||||
"untitled": "sans titre",
|
||||
"Untitled": "Sans titre",
|
||||
"Updated successfully": "Mis à jour avec succès",
|
||||
"User": "Utilisateur",
|
||||
"Workspace": "Espace de travail",
|
||||
"Workspace Name": "Nom de l'espace de travail",
|
||||
"Workspace settings": "Paramètres de l'espace de travail",
|
||||
"You can change your password here.": "Vous pouvez changer votre mot de passe ici.",
|
||||
"Your Email": "Votre Email",
|
||||
"Your import is complete.": "Votre importation est terminée.",
|
||||
"Your name": "Votre nom",
|
||||
"Your Name": "Votre Nom",
|
||||
"Your password": "Votre mot de passe",
|
||||
"Your password must be a minimum of 8 characters.": "Votre mot de passe doit contenir au moins 8 caractères.",
|
||||
"Sidebar toggle": "Bascule de la barre latérale",
|
||||
"Comments": "Commentaires",
|
||||
"404 page not found": "404 page non trouvée",
|
||||
"Sorry, we can't find the page you are looking for.": "Désolé, nous ne pouvons pas trouver la page que vous cherchez.",
|
||||
"Take me back to homepage": "Ramenez-moi à la page d'accueil",
|
||||
"Forgot password": "Mot de passe oublié",
|
||||
"Forgot your password?": "Mot de passe oublié?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Un lien de réinitialisation de mot de passe a été envoyé à votre e-mail. Veuillez vérifier votre boîte de réception.",
|
||||
"Send reset link": "Envoyer le lien de réinitialisation",
|
||||
"Password reset": "Réinitialisation du mot de passe",
|
||||
"Your new password": "Votre nouveau mot de passe",
|
||||
"Set password": "Définir le mot de passe",
|
||||
"Write a comment": "Écrire un commentaire",
|
||||
"Reply...": "Répondre...",
|
||||
"Error loading comments.": "Erreur lors du chargement des commentaires.",
|
||||
"No comments yet.": "Pas de commentaires pour l'instant.",
|
||||
"Edit comment": "Modifier le commentaire",
|
||||
"Delete comment": "Supprimer le commentaire",
|
||||
"Are you sure you want to delete this comment?": "Êtes-vous sûr de vouloir supprimer ce commentaire ?",
|
||||
"Comment created successfully": "Commentaire créé avec succès",
|
||||
"Error creating comment": "Erreur lors de la création du commentaire",
|
||||
"Comment updated successfully": "Commentaire mis à jour avec succès",
|
||||
"Failed to update comment": "Échec de la mise à jour du commentaire",
|
||||
"Comment deleted successfully": "Commentaire supprimé avec succès",
|
||||
"Failed to delete comment": "Échec de la suppression du commentaire",
|
||||
"Comment resolved successfully": "Commentaire résolu avec succès",
|
||||
"Failed to resolve comment": "Échec de la résolution du commentaire",
|
||||
"Revoke invitation": "Révoquer l'invitation",
|
||||
"Revoke": "Révoquer",
|
||||
"Don't": "Ne pas",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Êtes-vous sûr de vouloir révoquer cette invitation ? L'utilisateur ne pourra pas rejoindre l'espace de travail.",
|
||||
"Resend invitation": "Renvoyer l'invitation",
|
||||
"Anyone with this link can join this workspace.": "Toute personne ayant ce lien peut rejoindre cet espace de travail.",
|
||||
"Invite link": "Lien d'invitation",
|
||||
"Copy": "Copier",
|
||||
"Copied": "Copié",
|
||||
"Select a user": "Sélectionner un utilisateur",
|
||||
"Select a group": "Sélectionner un groupe",
|
||||
"Export all pages and attachments in this space.": "Exporter toutes les pages et pièces jointes dans cet espace.",
|
||||
"Delete space": "Supprimer l'espace",
|
||||
"Are you sure you want to delete this space?": "Êtes-vous sûr de vouloir supprimer cet espace ?",
|
||||
"Delete this space with all its pages and data.": "Supprimer cet espace avec toutes ses pages et données.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Toutes les pages, commentaires, pièces jointes et autorisations dans cet espace seront supprimés irréversiblement.",
|
||||
"Confirm space name": "Confirmer le nom de l'espace",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Tapez le nom de l'espace <b>{{spaceName}}</b> pour confirmer votre action.",
|
||||
"Format": "Format",
|
||||
"Include subpages": "Inclure les sous-pages",
|
||||
"Include attachments": "Inclure les pièces jointes",
|
||||
"Select export format": "Sélectionner le format d'exportation",
|
||||
"Export failed:": "Échec de l'exportation :",
|
||||
"export error": "exporter l'erreur",
|
||||
"Export page": "Exporter la page",
|
||||
"Export space": "Exporter l'espace",
|
||||
"Export {{type}}": "Exporter {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Le fichier dépasse la limite de {{limit}} pièces jointes",
|
||||
"Align left": "Aligner à gauche",
|
||||
"Align right": "Aligner à droite",
|
||||
"Align center": "Aligner au centre",
|
||||
"Merge cells": "Fusionner les cellules",
|
||||
"Split cell": "Diviser la cellule",
|
||||
"Delete column": "Supprimer la colonne",
|
||||
"Delete row": "Supprimer la ligne",
|
||||
"Add left column": "Ajouter colonne à gauche",
|
||||
"Add right column": "Ajouter colonne à droite",
|
||||
"Add row above": "Ajouter une ligne au-dessus",
|
||||
"Add row below": "Ajouter une ligne en dessous",
|
||||
"Delete table": "Supprimer le tableau",
|
||||
"Info": "Info",
|
||||
"Success": "Succès",
|
||||
"Warning": "Avertissement",
|
||||
"Danger": "Danger",
|
||||
"Mermaid diagram error:": "Erreur de diagramme Mermaid :",
|
||||
"Invalid Mermaid diagram": "Diagramme Mermaid invalide",
|
||||
"Double-click to edit Draw.io diagram": "Double-cliquez pour modifier le diagramme Draw.io",
|
||||
"Exit": "Quitter",
|
||||
"Save & Exit": "Enregistrer & Quitter",
|
||||
"Double-click to edit Excalidraw diagram": "Double-cliquez pour modifier le diagramme Excalidraw",
|
||||
"Paste link": "Coller le lien",
|
||||
"Edit link": "Modifier le lien",
|
||||
"Remove link": "Supprimer le lien",
|
||||
"Add link": "Ajouter un lien",
|
||||
"Please enter a valid url": "Veuillez entrer une URL valide",
|
||||
"Empty equation": "Équation vide",
|
||||
"Invalid equation": "Équation invalide",
|
||||
"Color": "Couleur",
|
||||
"Text color": "Couleur du texte",
|
||||
"Default": "Par défaut",
|
||||
"Blue": "Bleu",
|
||||
"Green": "Vert",
|
||||
"Purple": "Violet",
|
||||
"Red": "Rouge",
|
||||
"Yellow": "Jaune",
|
||||
"Orange": "Orange",
|
||||
"Pink": "Rose",
|
||||
"Gray": "Gris",
|
||||
"Embed link": "Intégrer un lien",
|
||||
"Invalid {{provider}} embed link": "Lien d'intégration {{provider}} non valide",
|
||||
"Embed {{provider}}": "Intégrer {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Entrez le lien {{provider}} à intégrer",
|
||||
"Bold": "Gras",
|
||||
"Italic": "Italique",
|
||||
"Underline": "Souligner",
|
||||
"Strike": "Barrer",
|
||||
"Code": "Code",
|
||||
"Comment": "Commentaire",
|
||||
"Text": "Texte",
|
||||
"Heading 1": "Titre 1",
|
||||
"Heading 2": "Titre 2",
|
||||
"Heading 3": "Titre 3",
|
||||
"To-do List": "Liste de tâches",
|
||||
"Bullet List": "Liste à puces",
|
||||
"Numbered List": "Liste numérotée",
|
||||
"Blockquote": "Bloc de citation",
|
||||
"Just start typing with plain text.": "Commencez simplement à taper avec du texte brut.",
|
||||
"Track tasks with a to-do list.": "Suivez les tâches avec une liste de tâches.",
|
||||
"Big section heading.": "Grand titre de section.",
|
||||
"Medium section heading.": "Titre de section moyen.",
|
||||
"Small section heading.": "Petit titre de section.",
|
||||
"Create a simple bullet list.": "Créez une simple liste à puces.",
|
||||
"Create a list with numbering.": "Créez une liste numérotée.",
|
||||
"Create block quote.": "Créez un bloc de citation.",
|
||||
"Insert code snippet.": "Insérez un extrait de code.",
|
||||
"Insert horizontal rule divider": "Insérer un séparateur de règle horizontale",
|
||||
"Upload any image from your device.": "Téléchargez n'importe quelle image depuis votre appareil.",
|
||||
"Upload any video from your device.": "Téléchargez n'importe quelle vidéo depuis votre appareil.",
|
||||
"Upload any file from your device.": "Téléchargez n'importe quel fichier depuis votre appareil.",
|
||||
"Table": "Tableau",
|
||||
"Insert a table.": "Insérez un tableau.",
|
||||
"Insert collapsible block.": "Insérer un bloc repliable.",
|
||||
"Video": "Vidéo",
|
||||
"Divider": "Diviseur",
|
||||
"Quote": "Citation",
|
||||
"Image": "Image",
|
||||
"File attachment": "Pièce jointe",
|
||||
"Toggle block": "Basculer le bloc",
|
||||
"Callout": "Appel",
|
||||
"Insert callout notice.": "Insérer un avis d'appel.",
|
||||
"Math inline": "Mathématiques en ligne",
|
||||
"Insert inline math equation.": "Insérez une équation mathématique en ligne.",
|
||||
"Math block": "Bloc mathématiques",
|
||||
"Insert math equation": "Insérer une équation mathématique",
|
||||
"Mermaid diagram": "Diagramme Mermaid",
|
||||
"Insert mermaid diagram": "Insérer un diagramme Mermaid",
|
||||
"Insert and design Drawio diagrams": "Insérer et concevoir des diagrammes Drawio",
|
||||
"Insert current date": "Insérer la date actuelle",
|
||||
"Draw and sketch excalidraw diagrams": "Dessiner et esquisser des diagrammes Excalidraw",
|
||||
"Multiple": "Multiple",
|
||||
"Heading {{level}}": "Titre {{level}}",
|
||||
"Toggle title": "Basculer le titre",
|
||||
"Write anything. Enter \"/\" for commands": "Écrivez n'importe quoi. Entrez \"/\" pour les commandes",
|
||||
"Names do not match": "Les noms ne correspondent pas",
|
||||
"Today, {{time}}": "Aujourd'hui, {{time}}",
|
||||
"Yesterday, {{time}}": "Hier, {{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/it-IT/translation.json
Normal file
342
apps/client/public/locales/it-IT/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "Account",
|
||||
"Active": "Attivo",
|
||||
"Add": "Aggiungi",
|
||||
"Add group members": "Aggiungi membri al gruppo",
|
||||
"Add groups": "Aggiungi gruppi",
|
||||
"Add members": "Aggiungi membri",
|
||||
"Add to groups": "Aggiungi ai gruppi",
|
||||
"Add space members": "Aggiungi membri allo spazio",
|
||||
"Admin": "Amministratore",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Sei sicuro di voler eliminare questo gruppo? I membri perderanno l'accesso alle risorse accessibili da questo gruppo.",
|
||||
"Are you sure you want to delete this page?": "Sei sicuro di voler eliminare questa pagina?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Sei sicuro di voler rimuovere questo utente dal gruppo? L'utente perderà l'accesso alle risorse accessibili da questo gruppo.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Sei sicuro di voler rimuovere questo utente dallo spazio? L'utente perderà tutti gli accessi a questo spazio.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Sei sicuro di voler ripristinare questa versione? Qualsiasi modifica non salvata come versione andrà persa.",
|
||||
"Can become members of groups and spaces in workspace": "Può diventare membro di gruppi e spazi nell'area di lavoro",
|
||||
"Can create and edit pages in space.": "Può creare e modificare le pagine nello spazio.",
|
||||
"Can edit": "Può modificare",
|
||||
"Can manage workspace": "Può gestire lo spazio di lavoro",
|
||||
"Can manage workspace but cannot delete it": "Può gestire lo spazio di lavoro ma non può eliminarlo",
|
||||
"Can view": "Può visualizzare",
|
||||
"Can view pages in space but not edit.": "Può visualizzare le pagine nello spazio ma non modificarle.",
|
||||
"Cancel": "Annulla",
|
||||
"Change email": "Cambia email",
|
||||
"Change password": "Cambia password",
|
||||
"Change photo": "Cambia foto",
|
||||
"Choose a role": "Scegli un ruolo",
|
||||
"Choose your preferred color scheme.": "Scegli il tuo schema di colori preferito.",
|
||||
"Choose your preferred interface language.": "Scegli la tua lingua preferita per l'interfaccia.",
|
||||
"Choose your preferred page width.": "Scegli la larghezza della pagina che preferisci.",
|
||||
"Confirm": "Conferma",
|
||||
"Copy link": "Copia link",
|
||||
"Create": "Crea",
|
||||
"Create group": "Crea gruppo",
|
||||
"Create page": "Crea pagina",
|
||||
"Create space": "Crea spazio",
|
||||
"Create workspace": "Crea spazio di lavoro",
|
||||
"Current password": "Password attuale",
|
||||
"Dark": "Scuro",
|
||||
"Date": "Data",
|
||||
"Delete": "Elimina",
|
||||
"Delete group": "Elimina gruppo",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sei sicuro di voler eliminare questa pagina? Verranno cancellate anche le sue sottopagine e la cronologia. Questa azione è irreversibile.",
|
||||
"Description": "Descrizione",
|
||||
"Details": "Dettagli",
|
||||
"e.g ACME": "ad es. ACME",
|
||||
"e.g ACME Inc": "es. ACME Inc",
|
||||
"e.g Developers": "es. Sviluppatori",
|
||||
"e.g Group for developers": "es. Gruppo per gli sviluppatori",
|
||||
"e.g product": "ad esempio prodotto",
|
||||
"e.g Product Team": "es. Team di Prodotto",
|
||||
"e.g Sales": "ad es. Vendite",
|
||||
"e.g Space for product team": "ad es. Spazio per il team di prodotto",
|
||||
"e.g Space for sales team to collaborate": "ad es. Spazio per il team di vendita per collaborare",
|
||||
"Edit": "Modifica",
|
||||
"Edit group": "Modifica gruppo",
|
||||
"Email": "Email",
|
||||
"Enter a strong password": "Inserisci una password sicura",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Inserisci indirizzi email validi separati da virgola o spazio [max: 50]",
|
||||
"enter valid emails addresses": "inserisci indirizzi email validi",
|
||||
"Enter your current password": "Inserisci la tua password attuale",
|
||||
"enter your full name": "inserisci il tuo nome completo",
|
||||
"Enter your new password": "Inserisci la tua nuova password",
|
||||
"Enter your new preferred email": "Inserisci la tua nuova email preferita",
|
||||
"Enter your password": "Inserisci la tua password",
|
||||
"Error fetching page data.": "Si è verificato un errore durante il recupero dei dati della pagina.",
|
||||
"Error loading page history.": "Si è verificato un errore durante il caricamento della cronologia della pagina.",
|
||||
"Export": "Esporta",
|
||||
"Failed to create page": "Impossibile creare pagina",
|
||||
"Failed to delete page": "Impossibile eliminare la pagina",
|
||||
"Failed to fetch recent pages": "Impossibile recuperare le pagine recenti",
|
||||
"Failed to import pages": "Impossibile importare le pagine",
|
||||
"Failed to load page. An error occurred.": "Il caricamento della pagina è fallito. Si è verificato un errore.",
|
||||
"Failed to update data": "Impossibile aggiornare i dati",
|
||||
"Full access": "Accesso completo",
|
||||
"Full page width": "Larghezza pagina intera",
|
||||
"Full width": "Larghezza intera",
|
||||
"General": "Generale",
|
||||
"Group": "Gruppo",
|
||||
"Group description": "Descrizione del gruppo",
|
||||
"Group name": "Nome del gruppo",
|
||||
"Groups": "Gruppi",
|
||||
"Has full access to space settings and pages.": "Ha pieno accesso alle impostazioni e alle pagine dello spazio.",
|
||||
"Home": "Casa",
|
||||
"Import pages": "Importa pagine",
|
||||
"Import pages & space settings": "Importa pagine e impostazioni dello spazio",
|
||||
"Importing pages": "Importazione pagine",
|
||||
"invalid invitation link": "link di invito non valido",
|
||||
"Invitation signup": "Iscrizione invito",
|
||||
"Invite by email": "Invita via email",
|
||||
"Invite members": "Invita membri",
|
||||
"Invite new members": "Invita nuovi membri",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "I membri invitati che non hanno ancora accettato il loro invito appariranno qui.",
|
||||
"Invited members will be granted access to spaces the groups can access": "I membri invitati avranno accesso agli spazi a cui i gruppi possono accedere",
|
||||
"Join the workspace": "Unisciti allo spazio di lavoro",
|
||||
"Language": "Lingua",
|
||||
"Light": "Chiaro",
|
||||
"Link copied": "Link copiato",
|
||||
"Login": "Login",
|
||||
"Logout": "Esci",
|
||||
"Manage Group": "Gestisci Gruppo",
|
||||
"Manage members": "Gestisci membri",
|
||||
"member": "membro",
|
||||
"Member": "Membro",
|
||||
"members": "membri",
|
||||
"Members": "Membri",
|
||||
"My preferences": "Le mie preferenze",
|
||||
"My Profile": "Il mio profilo",
|
||||
"My profile": "Il mio profilo",
|
||||
"Name": "Nome",
|
||||
"New email": "Nuova email",
|
||||
"New page": "Nuova pagina",
|
||||
"New password": "Nuova password",
|
||||
"No group found": "Nessun gruppo trovato",
|
||||
"No page history saved yet.": "Nessuna cronologia della pagina salvata.",
|
||||
"No pages yet": "Nessuna pagina ancora",
|
||||
"No results found...": "Nessun risultato trovato...",
|
||||
"No user found": "Nessun utente trovato",
|
||||
"Overview": "Panoramica",
|
||||
"Owner": "Proprietario",
|
||||
"page": "pagina",
|
||||
"Page deleted successfully": "Pagina eliminata con successo",
|
||||
"Page history": "Cronologia della pagina",
|
||||
"Page import is in progress. Please do not close this tab.": "L'importazione della pagina è in corso. Si prega di non chiudere questa scheda.",
|
||||
"Pages": "Pagine",
|
||||
"pages": "pagine",
|
||||
"Password": "Password",
|
||||
"Password changed successfully": "Password cambiata con successo",
|
||||
"Pending": "In sospeso",
|
||||
"Please confirm your action": "Si prega di confermare la propria azione",
|
||||
"Preferences": "Preferenze",
|
||||
"Print PDF": "Stampa PDF",
|
||||
"Profile": "Profilo",
|
||||
"Recently updated": "Aggiornato di recente",
|
||||
"Remove": "Rimuovi",
|
||||
"Remove group member": "Rimuovi membro dal gruppo",
|
||||
"Remove space member": "Rimuovi membro dallo spazio",
|
||||
"Restore": "Ripristina",
|
||||
"Role": "Ruolo",
|
||||
"Save": "Salva",
|
||||
"Search": "Cerca",
|
||||
"Search for groups": "Cerca gruppi",
|
||||
"Search for users": "Cerca un utente",
|
||||
"Search for users and groups": "Cerca utenti e gruppi",
|
||||
"Search...": "Cerca...",
|
||||
"Select language": "Seleziona lingua",
|
||||
"Select role": "Seleziona ruolo",
|
||||
"Select role to assign to all invited members": "Seleziona il ruolo da assegnare a tutti i membri invitati",
|
||||
"Select theme": "Seleziona tema",
|
||||
"Send invitation": "Invia invito",
|
||||
"Settings": "Impostazioni",
|
||||
"Setup workspace": "Imposta spazio di lavoro",
|
||||
"Sign In": "Accedi",
|
||||
"Sign Up": "Registrati",
|
||||
"Slug": "Identificatore",
|
||||
"Space": "Spazio",
|
||||
"Space description": "Descrizione dello spazio",
|
||||
"Space menu": "Menu spazio",
|
||||
"Space name": "Nome dello spazio",
|
||||
"Space settings": "Impostazioni dello spazio",
|
||||
"Space slug": "Lumaca spaziale",
|
||||
"Spaces": "Spazi",
|
||||
"Spaces you belong to": "Spazi a cui appartieni",
|
||||
"No space found": "Nessuno spazio trovato",
|
||||
"Search for spaces": "Cerca spazi",
|
||||
"Start typing to search...": "Inizia a digitare per cercare...",
|
||||
"Status": "Stato",
|
||||
"Successfully imported": "Importazione riuscita",
|
||||
"Successfully restored": "Ripristinato con successo",
|
||||
"System settings": "Impostazioni di sistema",
|
||||
"Theme": "Tema",
|
||||
"To change your email, you have to enter your password and new email.": "Per cambiare la tua email, devi inserire la tua password e la nuova email.",
|
||||
"Toggle full page width": "Attiva/disattiva larghezza pagina intera",
|
||||
"Unable to import pages. Please try again.": "Impossibile importare le pagine. Riprova.",
|
||||
"untitled": "senza titolo",
|
||||
"Untitled": "Senza titolo",
|
||||
"Updated successfully": "Aggiornato con successo",
|
||||
"User": "Utente",
|
||||
"Workspace": "Spazio di lavoro",
|
||||
"Workspace Name": "Nome dello spazio di lavoro",
|
||||
"Workspace settings": "Impostazioni dello spazio di lavoro",
|
||||
"You can change your password here.": "Puoi cambiare la tua password qui.",
|
||||
"Your Email": "La tua email",
|
||||
"Your import is complete.": "Il tuo importazione è completata.",
|
||||
"Your name": "Il tuo nome",
|
||||
"Your Name": "Il Tuo Nome",
|
||||
"Your password": "La tua password",
|
||||
"Your password must be a minimum of 8 characters.": "La tua password deve contenere almeno 8 caratteri.",
|
||||
"Sidebar toggle": "Attiva/disattiva barra laterale",
|
||||
"Comments": "Commenti",
|
||||
"404 page not found": "404 pagina non trovata",
|
||||
"Sorry, we can't find the page you are looking for.": "Siamo spiacenti, non riusciamo a trovare la pagina che stai cercando.",
|
||||
"Take me back to homepage": "Portami alla homepage",
|
||||
"Forgot password": "Hai dimenticato la password",
|
||||
"Forgot your password?": "Hai dimenticato la password?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Un link per il reset della password è stato inviato al tuo indirizzo email. Per favore, controlla la tua casella di posta.",
|
||||
"Send reset link": "Invia link di ripristino",
|
||||
"Password reset": "Reimposta password",
|
||||
"Your new password": "La tua nuova password",
|
||||
"Set password": "Imposta password",
|
||||
"Write a comment": "Scrivi un commento",
|
||||
"Reply...": "Rispondi...",
|
||||
"Error loading comments.": "Si è verificato un errore durante il caricamento dei commenti.",
|
||||
"No comments yet.": "Nessun commento ancora.",
|
||||
"Edit comment": "Modifica commento",
|
||||
"Delete comment": "Elimina commento",
|
||||
"Are you sure you want to delete this comment?": "Sei sicuro di voler eliminare questo commento?",
|
||||
"Comment created successfully": "Commento creato con successo",
|
||||
"Error creating comment": "Si è verificato un errore durante la creazione del commento",
|
||||
"Comment updated successfully": "Commento aggiornato con successo",
|
||||
"Failed to update comment": "Impossibile aggiornare il commento",
|
||||
"Comment deleted successfully": "Commento eliminato con successo",
|
||||
"Failed to delete comment": "Impossibile eliminare il commento",
|
||||
"Comment resolved successfully": "Commento risolto con successo",
|
||||
"Failed to resolve comment": "Impossibile risolvere il commento",
|
||||
"Revoke invitation": "Revoca invito",
|
||||
"Revoke": "Revoca",
|
||||
"Don't": "Non",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Sei sicuro di voler revocare questo invito? L'utente non potrà unirsi allo spazio di lavoro.",
|
||||
"Resend invitation": "Rispedisci invito",
|
||||
"Anyone with this link can join this workspace.": "Chiunque con questo link può unirsi a questo workspace.",
|
||||
"Invite link": "Link d'invito",
|
||||
"Copy": "Copia",
|
||||
"Copied": "Copiato",
|
||||
"Select a user": "Seleziona un utente",
|
||||
"Select a group": "Seleziona un gruppo",
|
||||
"Export all pages and attachments in this space.": "Esporta tutte le pagine e gli allegati in questo spazio.",
|
||||
"Delete space": "Elimina spazio",
|
||||
"Are you sure you want to delete this space?": "Sei sicuro di voler eliminare questo spazio?",
|
||||
"Delete this space with all its pages and data.": "Elimina questo spazio con tutte le sue pagine e i suoi dati.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Tutte le pagine, i commenti, gli allegati e i permessi in questo spazio verranno eliminati in modo irreversibile.",
|
||||
"Confirm space name": "Conferma nome spazio",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Digita il nome dello spazio <b>{{spaceName}}</b> per confermare la tua azione.",
|
||||
"Format": "Formato",
|
||||
"Include subpages": "Includi sottopagine",
|
||||
"Include attachments": "Includi allegati",
|
||||
"Select export format": "Seleziona formato di esportazione",
|
||||
"Export failed:": "Esportazione fallita:",
|
||||
"export error": "errore di esportazione",
|
||||
"Export page": "Esporta pagina",
|
||||
"Export space": "Esporta spazio",
|
||||
"Export {{type}}": "Esporta {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Il file supera il limite di allegati di {{limit}}",
|
||||
"Align left": "Allinea a sinistra",
|
||||
"Align right": "Allinea a destra",
|
||||
"Align center": "Allinea al centro",
|
||||
"Merge cells": "Unisci celle",
|
||||
"Split cell": "Dividi cella",
|
||||
"Delete column": "Elimina colonna",
|
||||
"Delete row": "Elimina riga",
|
||||
"Add left column": "Aggiungi colonna a sinistra",
|
||||
"Add right column": "Aggiungi colonna a destra",
|
||||
"Add row above": "Aggiungi riga sopra",
|
||||
"Add row below": "Aggiungi riga sotto",
|
||||
"Delete table": "Elimina tabella",
|
||||
"Info": "Informazioni",
|
||||
"Success": "Successo",
|
||||
"Warning": "Avviso",
|
||||
"Danger": "Pericolo",
|
||||
"Mermaid diagram error:": "Errore nel diagramma di Mermaid:",
|
||||
"Invalid Mermaid diagram": "Diagramma di Mermaid non valido",
|
||||
"Double-click to edit Draw.io diagram": "Doppio clic per modificare il diagramma Draw.io",
|
||||
"Exit": "Esci",
|
||||
"Save & Exit": "Salva ed esci",
|
||||
"Double-click to edit Excalidraw diagram": "Doppio clic per modificare il diagramma Excalidraw",
|
||||
"Paste link": "Incolla link",
|
||||
"Edit link": "Modifica link",
|
||||
"Remove link": "Rimuovi link",
|
||||
"Add link": "Aggiungi link",
|
||||
"Please enter a valid url": "Per favore inserisci un URL valido",
|
||||
"Empty equation": "Equazione vuota",
|
||||
"Invalid equation": "Equazione non valida",
|
||||
"Color": "Colore",
|
||||
"Text color": "Colore del testo",
|
||||
"Default": "Predefinito",
|
||||
"Blue": "Blu",
|
||||
"Green": "Verde",
|
||||
"Purple": "Viola",
|
||||
"Red": "Rosso",
|
||||
"Yellow": "Giallo",
|
||||
"Orange": "Arancione",
|
||||
"Pink": "Rosa",
|
||||
"Gray": "Grigio",
|
||||
"Embed link": "Incorpora collegamento",
|
||||
"Invalid {{provider}} embed link": "Link di incorporamento {{provider}} non valido",
|
||||
"Embed {{provider}}": "Incorpora {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Inserisci il link {{provider}} per incorporare",
|
||||
"Bold": "Grassetto",
|
||||
"Italic": "Corsivo",
|
||||
"Underline": "Sottolineato",
|
||||
"Strike": "Barrato",
|
||||
"Code": "Codice",
|
||||
"Comment": "Commento",
|
||||
"Text": "Testo",
|
||||
"Heading 1": "Intestazione 1",
|
||||
"Heading 2": "Intestazione 2",
|
||||
"Heading 3": "Intestazione 3",
|
||||
"To-do List": "Lista delle cose da fare",
|
||||
"Bullet List": "Elenco Puntato",
|
||||
"Numbered List": "Elenco Numerato",
|
||||
"Blockquote": "Blocco di citazione",
|
||||
"Just start typing with plain text.": "Inizia a digitare con testo semplice.",
|
||||
"Track tasks with a to-do list.": "Tieni traccia delle attività con una lista di cose da fare.",
|
||||
"Big section heading.": "Intestazione di una grande sezione.",
|
||||
"Medium section heading.": "Intestazione di sezione media.",
|
||||
"Small section heading.": "Piccolo titolo di sezione.",
|
||||
"Create a simple bullet list.": "Crea un semplice elenco puntato.",
|
||||
"Create a list with numbering.": "Crea un elenco numerato.",
|
||||
"Create block quote.": "Crea blocco citazione.",
|
||||
"Insert code snippet.": "Inserisci frammento di codice.",
|
||||
"Insert horizontal rule divider": "Inserisci divisore di regola orizzontale",
|
||||
"Upload any image from your device.": "Carica un'immagine dal tuo dispositivo.",
|
||||
"Upload any video from your device.": "Carica qualsiasi video dal tuo dispositivo.",
|
||||
"Upload any file from your device.": "Carica qualsiasi file dal tuo dispositivo.",
|
||||
"Table": "Tabella",
|
||||
"Insert a table.": "Inserisci una tabella.",
|
||||
"Insert collapsible block.": "Inserisci blocco comprimibile.",
|
||||
"Video": "Video",
|
||||
"Divider": "Divisore",
|
||||
"Quote": "Preventivo",
|
||||
"Image": "Immagine",
|
||||
"File attachment": "Allegato file",
|
||||
"Toggle block": "Attiva blocco",
|
||||
"Callout": "Avviso",
|
||||
"Insert callout notice.": "Inserisci avviso di richiamo.",
|
||||
"Math inline": "Matematica in linea",
|
||||
"Insert inline math equation.": "Inserisci equazione matematica in linea.",
|
||||
"Math block": "Blocco matematico",
|
||||
"Insert math equation": "Inserisci equazione matematica",
|
||||
"Mermaid diagram": "Diagramma di Mermaid",
|
||||
"Insert mermaid diagram": "Inserisci un diagramma di Mermaid",
|
||||
"Insert and design Drawio diagrams": "Inserisci e progetta diagrammi Drawio",
|
||||
"Insert current date": "Inserisci la data corrente",
|
||||
"Draw and sketch excalidraw diagrams": "Disegna e schizza diagrammi excalidraw",
|
||||
"Multiple": "Multiplo",
|
||||
"Heading {{level}}": "Intestazione {{level}}",
|
||||
"Toggle title": "Attiva/disattiva titolo",
|
||||
"Write anything. Enter \"/\" for commands": "Scrivi qualcosa. Digita \"/\" per i comandi",
|
||||
"Names do not match": "I nomi non corrispondono",
|
||||
"Today, {{time}}": "Oggi, {{time}}",
|
||||
"Yesterday, {{time}}": "Ieri, {{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/ja-JP/translation.json
Normal file
342
apps/client/public/locales/ja-JP/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "アカウント",
|
||||
"Active": "アクティブ",
|
||||
"Add": "追加",
|
||||
"Add group members": "グループメンバーを追加",
|
||||
"Add groups": "グループを追加",
|
||||
"Add members": "メンバーを追加",
|
||||
"Add to groups": "グループに追加",
|
||||
"Add space members": "スペースメンバーを追加",
|
||||
"Admin": "管理者",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "このグループを削除してもよろしいですか? メンバーはこのグループがアクセス権を持つリソースにアクセスできなくなります。",
|
||||
"Are you sure you want to delete this page?": "このページを削除してもよろしいですか?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "このユーザをグループから削除してもよろしいですか? ユーザはこのグループがアクセス権を持つリソースにアクセスできなくなります。",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "このユーザをスペースから削除してもよろしいですか? ユーザはこのスペースへのアクセス権をすべて失います。",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "このバージョンを復元してもよろしいですか? バージョン管理されていない変更は失われます。",
|
||||
"Can become members of groups and spaces in workspace": "ワークスペース内のグループやスペースのメンバーになることができます",
|
||||
"Can create and edit pages in space.": "スペース内のページを作成および編集できます。",
|
||||
"Can edit": "編集可能",
|
||||
"Can manage workspace": "ワークスペースを管理できます",
|
||||
"Can manage workspace but cannot delete it": "ワークスペースを管理できますが、削除はできません",
|
||||
"Can view": "閲覧可能",
|
||||
"Can view pages in space but not edit.": "スペース内のページを閲覧できますが、編集はできません。",
|
||||
"Cancel": "キャンセル",
|
||||
"Change email": "メールアドレスの変更",
|
||||
"Change password": "パスワードの変更",
|
||||
"Change photo": "画像の変更",
|
||||
"Choose a role": "ロールを選んでください",
|
||||
"Choose your preferred color scheme.": "お好みのカラースキームを選択してください。",
|
||||
"Choose your preferred interface language.": "お好みのインターフェース言語を選択してください。",
|
||||
"Choose your preferred page width.": "左右の余白を縮小する場合はオンにしてください。",
|
||||
"Confirm": "確認",
|
||||
"Copy link": "リンクをコピー",
|
||||
"Create": "新規作成",
|
||||
"Create group": "グループを作成",
|
||||
"Create page": "新規ページ",
|
||||
"Create space": "新規スペース",
|
||||
"Create workspace": "ワークスペースを作成",
|
||||
"Current password": "現在のパスワード",
|
||||
"Dark": "ダーク",
|
||||
"Date": "日付",
|
||||
"Delete": "削除",
|
||||
"Delete group": "グループを削除",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "このページを削除してもよろしいですか?この操作により、子ページおよびページ履歴が削除されます。この操作は元に戻せません。",
|
||||
"Description": "説明",
|
||||
"Details": "詳細",
|
||||
"e.g ACME": "例: 山田太郎",
|
||||
"e.g ACME Inc": "例: 株式会社サンプル",
|
||||
"e.g Developers": "例: エンジニア",
|
||||
"e.g Group for developers": "例: エンジニアグループ",
|
||||
"e.g product": "例: product",
|
||||
"e.g Product Team": "例: 製品チーム",
|
||||
"e.g Sales": "例: 営業",
|
||||
"e.g Space for product team": "例: 製品チームのスペース",
|
||||
"e.g Space for sales team to collaborate": "例: 営業チーム連携用スペース",
|
||||
"Edit": "編集",
|
||||
"Edit group": "グループを編集",
|
||||
"Email": "メールアドレス",
|
||||
"Enter a strong password": "強力なパスワードを入力してください",
|
||||
"Enter valid email addresses separated by comma or space max_50": "有効なメールアドレスをカンマまたはスペースで区切って入力してください(最大 50 個)",
|
||||
"enter valid emails addresses": "有効なメールアドレスを入力してください",
|
||||
"Enter your current password": "現在のパスワードを入力してください",
|
||||
"enter your full name": "氏名を入力してください",
|
||||
"Enter your new password": "新しいパスワードを入力してください",
|
||||
"Enter your new preferred email": "新しいメールアドレスを入力してください",
|
||||
"Enter your password": "パスワードを入力してください",
|
||||
"Error fetching page data.": "ページデータ取得中にエラーが発生しました。",
|
||||
"Error loading page history.": "ページ履歴の読み込み中にエラーが発生しました。",
|
||||
"Export": "エクスポート",
|
||||
"Failed to create page": "ページの作成に失敗しました",
|
||||
"Failed to delete page": "ページの削除に失敗しました",
|
||||
"Failed to fetch recent pages": "最近のページを取得できませんでした",
|
||||
"Failed to import pages": "ページのインポートに失敗しました",
|
||||
"Failed to load page. An error occurred.": "ページの読み込みに失敗しました。エラーが発生しました。",
|
||||
"Failed to update data": "データの更新に失敗しました",
|
||||
"Full access": "フルアクセス",
|
||||
"Full page width": "フルページ幅で表示",
|
||||
"Full width": "左右の余白を縮小",
|
||||
"General": "一般",
|
||||
"Group": "グループ",
|
||||
"Group description": "グループ説明",
|
||||
"Group name": "グループ名",
|
||||
"Groups": "グループ",
|
||||
"Has full access to space settings and pages.": "スペース設定とページにフルアクセスできます。",
|
||||
"Home": "ホーム",
|
||||
"Import pages": "ページをインポート",
|
||||
"Import pages & space settings": "ページとスペース設定をインポート",
|
||||
"Importing pages": "ページをインポートしています",
|
||||
"invalid invitation link": "招待リンクが間違っています",
|
||||
"Invitation signup": "招待登録",
|
||||
"Invite by email": "メールアドレスで招待する",
|
||||
"Invite members": "メンバーを招待する",
|
||||
"Invite new members": "新しいメンバーを招待する",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "招待をまだ承諾していないメンバーはここに表示されます。",
|
||||
"Invited members will be granted access to spaces the groups can access": "招待されたメンバーは、グループがアクセスできるスペースにアクセス権が付与されます",
|
||||
"Join the workspace": "ワークスペースに参加",
|
||||
"Language": "言語",
|
||||
"Light": "ライト",
|
||||
"Link copied": "リンクをコピーしました",
|
||||
"Login": "ログイン",
|
||||
"Logout": "ログアウト",
|
||||
"Manage Group": "グループを管理",
|
||||
"Manage members": "メンバーを管理",
|
||||
"member": "メンバー",
|
||||
"Member": "メンバー",
|
||||
"members": "メンバー",
|
||||
"Members": "メンバー",
|
||||
"My preferences": "個人設定",
|
||||
"My Profile": "プロフィール",
|
||||
"My profile": "プロフィール",
|
||||
"Name": "名前",
|
||||
"New email": "新しいメールアドレス",
|
||||
"New page": "新規ページ",
|
||||
"New password": "新しいパスワード",
|
||||
"No group found": "グループが見つかりません",
|
||||
"No page history saved yet.": "まだページの履歴が保存されていません。",
|
||||
"No pages yet": "ページがありません",
|
||||
"No results found...": "結果が見つかりませんでした...",
|
||||
"No user found": "ユーザがいません",
|
||||
"Overview": "概要",
|
||||
"Owner": "所有者",
|
||||
"page": "ページ",
|
||||
"Page deleted successfully": "ページが正常に削除されました",
|
||||
"Page history": "ページの履歴",
|
||||
"Page import is in progress. Please do not close this tab.": "ページのインポートが進行中です。このタブを閉じないでください。",
|
||||
"Pages": "ページ",
|
||||
"pages": "ページ",
|
||||
"Password": "パスワード",
|
||||
"Password changed successfully": "パスワードが正常に変更されました",
|
||||
"Pending": "保留中",
|
||||
"Please confirm your action": "アクションを確認してください",
|
||||
"Preferences": "設定",
|
||||
"Print PDF": "PDFを印刷",
|
||||
"Profile": "プロフィール",
|
||||
"Recently updated": "最近の更新",
|
||||
"Remove": "削除",
|
||||
"Remove group member": "グループメンバーを削除",
|
||||
"Remove space member": "スペースメンバーを削除",
|
||||
"Restore": "復元",
|
||||
"Role": "役割",
|
||||
"Save": "保存",
|
||||
"Search": "検索",
|
||||
"Search for groups": "グループを検索",
|
||||
"Search for users": "ユーザーを検索",
|
||||
"Search for users and groups": "ユーザーとグループを検索",
|
||||
"Search...": "検索する語句を入力",
|
||||
"Select language": "言語を選択",
|
||||
"Select role": "ロールを選択",
|
||||
"Select role to assign to all invited members": "招待されたすべてのメンバーに割り当てるロールを選択してください",
|
||||
"Select theme": "テーマを選択",
|
||||
"Send invitation": "招待を送る",
|
||||
"Settings": "設定",
|
||||
"Setup workspace": "ワークスペースを設定する",
|
||||
"Sign In": "サインイン",
|
||||
"Sign Up": "アカウント登録",
|
||||
"Slug": "Slug (URL用文字列)",
|
||||
"Space": "スペース",
|
||||
"Space description": "スペース説明",
|
||||
"Space menu": "スペースメニュー",
|
||||
"Space name": "スペース名",
|
||||
"Space settings": "スペース設定",
|
||||
"Space slug": "スペースのSlug (URL用文字列)",
|
||||
"Spaces": "スペース",
|
||||
"Spaces you belong to": "所属しているスペース",
|
||||
"No space found": "スペースが見つかりません",
|
||||
"Search for spaces": "スペースを検索",
|
||||
"Start typing to search...": "検索を開始するには入力してください...",
|
||||
"Status": "ステータス",
|
||||
"Successfully imported": "インポートに成功しました",
|
||||
"Successfully restored": "正常に復元されました",
|
||||
"System settings": "システム設定",
|
||||
"Theme": "テーマ",
|
||||
"To change your email, you have to enter your password and new email.": "メールアドレスを変更するには、パスワードと新しいメールアドレスを入力する必要があります。",
|
||||
"Toggle full page width": "ページ幅を切り替える",
|
||||
"Unable to import pages. Please try again.": "ページをインポートできません。もう一度お試しください。",
|
||||
"untitled": "無題",
|
||||
"Untitled": "無題",
|
||||
"Updated successfully": "正常に更新されました",
|
||||
"User": "ユーザー",
|
||||
"Workspace": "ワークスペース",
|
||||
"Workspace Name": "ワークスペース名",
|
||||
"Workspace settings": "ワークスペース設定",
|
||||
"You can change your password here.": "パスワードを変更できます。",
|
||||
"Your Email": "メールアドレス",
|
||||
"Your import is complete.": "インポートが完了しました。",
|
||||
"Your name": "名前",
|
||||
"Your Name": "名前",
|
||||
"Your password": "パスワード",
|
||||
"Your password must be a minimum of 8 characters.": "パスワードは最低 8 文字必要です。",
|
||||
"Sidebar toggle": "サイドバー切り替え",
|
||||
"Comments": "コメント",
|
||||
"404 page not found": "404 ページが見つかりません",
|
||||
"Sorry, we can't find the page you are looking for.": "お探しのページが見つかりません。",
|
||||
"Take me back to homepage": "ホームに戻る",
|
||||
"Forgot password": "パスワードを忘れた",
|
||||
"Forgot your password?": "パスワードを忘れましたか?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "パスワードリセットリンクがあなたのメールアドレスに送信されました。受信箱を確認してください。",
|
||||
"Send reset link": "リセットリンクを送る",
|
||||
"Password reset": "パスワードリセット",
|
||||
"Your new password": "新しいパスワード",
|
||||
"Set password": "パスワードを設定",
|
||||
"Write a comment": "コメントを書く",
|
||||
"Reply...": "返信...",
|
||||
"Error loading comments.": "コメントの読み込み中にエラーが発生しました。",
|
||||
"No comments yet.": "コメントがありません。",
|
||||
"Edit comment": "コメントを編集する",
|
||||
"Delete comment": "コメントを削除する",
|
||||
"Are you sure you want to delete this comment?": "このコメントを削除してもよろしいですか?",
|
||||
"Comment created successfully": "コメントが作成されました",
|
||||
"Error creating comment": "コメントの作成中にエラーが発生しました",
|
||||
"Comment updated successfully": "コメントが更新されました",
|
||||
"Failed to update comment": "コメントの更新に失敗しました",
|
||||
"Comment deleted successfully": "コメントが削除されました",
|
||||
"Failed to delete comment": "コメントの削除に失敗しました",
|
||||
"Comment resolved successfully": "コメントが解決されました",
|
||||
"Failed to resolve comment": "コメントの解決に失敗しました",
|
||||
"Revoke invitation": "招待を取り消す",
|
||||
"Revoke": "取り消す",
|
||||
"Don't": "取り消さない",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "この招待を取り消してもよろしいですか? ユーザはワークスペースに参加できなくなります。",
|
||||
"Resend invitation": "招待を再度送る",
|
||||
"Anyone with this link can join this workspace.": "このリンクを持っている人は誰でもこのワークスペースに参加できます。",
|
||||
"Invite link": "招待リンク",
|
||||
"Copy": "コピー",
|
||||
"Copied": "コピーしました",
|
||||
"Select a user": "ユーザを選択",
|
||||
"Select a group": "グループを選択",
|
||||
"Export all pages and attachments in this space.": "このスペースのすべてのページと添付ファイルをエクスポートします。",
|
||||
"Delete space": "スペースを削除",
|
||||
"Are you sure you want to delete this space?": "このスペースを削除してもよろしいですか?",
|
||||
"Delete this space with all its pages and data.": "このスペースおよびスペース内のすべてのページとデータを削除します。",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "このスペース内のすべてのページ、コメント、添付ファイル、および権限は完全に削除されます。",
|
||||
"Confirm space name": "スペース名を確認する",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "アクションを確認するためにスペース名 <b>{{spaceName}}</b> を入力してください。",
|
||||
"Format": "フォーマット",
|
||||
"Include subpages": "サブページを含める",
|
||||
"Include attachments": "添付ファイルを含める",
|
||||
"Select export format": "エクスポート形式を選択",
|
||||
"Export failed:": "エクスポートに失敗しました:",
|
||||
"export error": "エクスポートエラー",
|
||||
"Export page": "エクスポートページ",
|
||||
"Export space": "エクスポートスペース",
|
||||
"Export {{type}}": "{{type}}をエクスポート",
|
||||
"File exceeds the {{limit}} attachment limit": "ファイルが{{limit}}の添付制限を超えています",
|
||||
"Align left": "左揃え",
|
||||
"Align right": "右揃え",
|
||||
"Align center": "中央揃え",
|
||||
"Merge cells": "セルを結合",
|
||||
"Split cell": "セルを分割",
|
||||
"Delete column": "列を削除",
|
||||
"Delete row": "行を削除",
|
||||
"Add left column": "左側に列を追加",
|
||||
"Add right column": "右側の列を追加",
|
||||
"Add row above": "上に行を追加",
|
||||
"Add row below": "下に行を追加",
|
||||
"Delete table": "テーブルを削除",
|
||||
"Info": "情報",
|
||||
"Success": "成功",
|
||||
"Warning": "警告",
|
||||
"Danger": "危険",
|
||||
"Mermaid diagram error:": "Mermaid コードエラー",
|
||||
"Invalid Mermaid diagram": "無効な Mermaid コードです",
|
||||
"Double-click to edit Draw.io diagram": "ダブルクリックしてDraw.ioの図を編集",
|
||||
"Exit": "終了",
|
||||
"Save & Exit": "保存して終了",
|
||||
"Double-click to edit Excalidraw diagram": "ダブルクリックしてExcalidraw図を編集",
|
||||
"Paste link": "リンクを貼り付け",
|
||||
"Edit link": "リンクを編集",
|
||||
"Remove link": "リンクを削除",
|
||||
"Add link": "リンクを追加",
|
||||
"Please enter a valid url": "有効なURLを入力してください",
|
||||
"Empty equation": "空の数式です",
|
||||
"Invalid equation": "不正な数式です",
|
||||
"Color": "カラー",
|
||||
"Text color": "テキストカラー",
|
||||
"Default": "デフォルト",
|
||||
"Blue": "青色",
|
||||
"Green": "緑色",
|
||||
"Purple": "紫色",
|
||||
"Red": "赤色",
|
||||
"Yellow": "黄色",
|
||||
"Orange": "オレンジ色",
|
||||
"Pink": "ピンク色",
|
||||
"Gray": "灰色",
|
||||
"Embed link": "リンクを埋め込む",
|
||||
"Invalid {{provider}} embed link": "埋め込まれた {{provider}} のリンクは無効です",
|
||||
"Embed {{provider}}": "埋め込まれた {{provider}}",
|
||||
"Enter {{provider}} link to embed": "埋め込みたい {{provider}} のリンクを入力してください",
|
||||
"Bold": "太字",
|
||||
"Italic": "斜線",
|
||||
"Underline": "下線",
|
||||
"Strike": "打ち消し線",
|
||||
"Code": "コードブロック",
|
||||
"Comment": "コメント",
|
||||
"Text": "テキスト",
|
||||
"Heading 1": "見出し 1",
|
||||
"Heading 2": "見出し 2",
|
||||
"Heading 3": "見出し 3",
|
||||
"To-do List": "To-doリスト",
|
||||
"Bullet List": "箇条書きリスト",
|
||||
"Numbered List": "番号付きリスト",
|
||||
"Blockquote": "引用",
|
||||
"Just start typing with plain text.": "すぐに文章を書き始められます。",
|
||||
"Track tasks with a to-do list.": "Todoリストでタスクを追跡します。",
|
||||
"Big section heading.": "大きいフォントのセクション見出しです。",
|
||||
"Medium section heading.": "中くらいのフォントのセクション見出しです。",
|
||||
"Small section heading.": "小さいフォントのセクション見出しです。",
|
||||
"Create a simple bullet list.": "シンプルな箇条書きのリストを作成します。",
|
||||
"Create a list with numbering.": "番号付きのリストを作成します。",
|
||||
"Create block quote.": "引用文を作成します。",
|
||||
"Insert code snippet.": "コードスニペットを入力します。",
|
||||
"Insert horizontal rule divider": "水平線を挿入します。",
|
||||
"Upload any image from your device.": "画像をアップロードします。",
|
||||
"Upload any video from your device.": "動画をアップロードします。",
|
||||
"Upload any file from your device.": "ファイルをアップロードします。",
|
||||
"Table": "テーブル",
|
||||
"Insert a table.": "表を挿入します。",
|
||||
"Insert collapsible block.": "折りたたみ可能なブロックを挿入します。",
|
||||
"Video": "動画",
|
||||
"Divider": "区切り線",
|
||||
"Quote": "引用",
|
||||
"Image": "画像",
|
||||
"File attachment": "ファイル添付",
|
||||
"Toggle block": "ブロックを切り替える",
|
||||
"Callout": "コールアウト",
|
||||
"Insert callout notice.": "コールアウトブロックを挿入します。",
|
||||
"Math inline": "インライン数式",
|
||||
"Insert inline math equation.": "インライン数式を挿入します。",
|
||||
"Math block": "数式ブロック",
|
||||
"Insert math equation": "数式を挿入します",
|
||||
"Mermaid diagram": "Mermaidコード",
|
||||
"Insert mermaid diagram": "Mermaidコードを記述して図を挿入します",
|
||||
"Insert and design Drawio diagrams": "Drawioの図を挿入してデザインします",
|
||||
"Insert current date": "今日の日付を挿入します",
|
||||
"Draw and sketch excalidraw diagrams": "Excalidrawの図を埋め込みます",
|
||||
"Multiple": "複数",
|
||||
"Heading {{level}}": "見出し {{level}}",
|
||||
"Toggle title": "タイトルの表示/非表示を切り替える",
|
||||
"Write anything. Enter \"/\" for commands": "文字を入力するか、「/」でコマンドを呼び出します",
|
||||
"Names do not match": "名前が一致しません",
|
||||
"Today, {{time}}": "今日、{{time}}",
|
||||
"Yesterday, {{time}}": "昨日、{{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/ko-KR/translation.json
Normal file
342
apps/client/public/locales/ko-KR/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "계정",
|
||||
"Active": "활성",
|
||||
"Add": "추가",
|
||||
"Add group members": "팀에 사용자 추가",
|
||||
"Add groups": "팀 생성",
|
||||
"Add members": "사용자 추가",
|
||||
"Add to groups": "팀에 추가",
|
||||
"Add space members": "Space에 사용자 추가",
|
||||
"Admin": "관리자",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "이 팀을 삭제하시겠습니까? 해당 팀에 속한 사용자들은 이 팀이 가진 모든 권한을 잃게 됩니다.",
|
||||
"Are you sure you want to delete this page?": "이 페이지를 삭제하시겠습니까?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "이 사용자를 팀에서 제거하시겠습니까? 사용자는 이 팀이 가진 모든 권한을 잃게 됩니다.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "이 사용자를 Space에서 제거하시겠습니까? 사용자는 이 Space에 대한 모든 접근 권한을 잃게 됩니다.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "이 버전으로 복원하시겠습니까? 저장되지 않은 모든 변경사항이 손실됩니다.",
|
||||
"Can become members of groups and spaces in workspace": "Workspace 내 팀 및 Space의 사용자가 될 수 있습니다.",
|
||||
"Can create and edit pages in space.": "Space에 페이지를 생성하고 편집할 수 있습니다.",
|
||||
"Can edit": "편집할 수 있음",
|
||||
"Can manage workspace": "Workspace를 관리할 수 있음",
|
||||
"Can manage workspace but cannot delete it": "Workspace를 관리할 수 있지만, 삭제는 불가능.",
|
||||
"Can view": "볼 수 있음",
|
||||
"Can view pages in space but not edit.": "Space의 페이지를 볼 수 있지만, 편집은 불가능.",
|
||||
"Cancel": "취소",
|
||||
"Change email": "이메일 변경",
|
||||
"Change password": "비밀번호 변경",
|
||||
"Change photo": "사진 변경",
|
||||
"Choose a role": "역할 선택",
|
||||
"Choose your preferred color scheme.": "선호하는 배경 색을 선택하세요.",
|
||||
"Choose your preferred interface language.": "선호하는 인터페이스 언어를 선택하세요.",
|
||||
"Choose your preferred page width.": "선호하는 페이지 너비를 선택하세요.",
|
||||
"Confirm": "확인",
|
||||
"Copy link": "링크 복사",
|
||||
"Create": "생성",
|
||||
"Create group": "팀 생성",
|
||||
"Create page": "페이지 생성",
|
||||
"Create space": "Space 생성",
|
||||
"Create workspace": "Workspace 생성",
|
||||
"Current password": "현재 비밀번호",
|
||||
"Dark": "어두운",
|
||||
"Date": "날짜",
|
||||
"Delete": "삭제",
|
||||
"Delete group": "팀 삭제",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "이 페이지를 삭제하시겠습니까? 하위 페이지와 페이지 기록이 모두 삭제됩니다. 이 작업은 되돌릴 수 없습니다.",
|
||||
"Description": "설명",
|
||||
"Details": "세부사항",
|
||||
"e.g ACME": "예: ACME",
|
||||
"e.g ACME Inc": "예: ACME Inc",
|
||||
"e.g Developers": "예: 개발자",
|
||||
"e.g Group for developers": "예: 개발자를 위한 팀",
|
||||
"e.g product": "예: 제품",
|
||||
"e.g Product Team": "예: 제품 팀",
|
||||
"e.g Sales": "예: 영업",
|
||||
"e.g Space for product team": "예: 제품 팀을 위한 Space",
|
||||
"e.g Space for sales team to collaborate": "예: 영업 팀의 Space",
|
||||
"Edit": "편집",
|
||||
"Edit group": "팀 편집",
|
||||
"Email": "이메일",
|
||||
"Enter a strong password": "강력한 비밀번호를 입력하세요",
|
||||
"Enter valid email addresses separated by comma or space max_50": "유효한 이메일 주소를 쉼표나 공백으로 구분하여 입력하세요 [최대: 50]",
|
||||
"enter valid emails addresses": "유효한 이메일 주소를 입력하세요",
|
||||
"Enter your current password": "현재 비밀번호를 입력하세요",
|
||||
"enter your full name": "전체 이름을 입력하세요",
|
||||
"Enter your new password": "새 비밀번호를 입력하세요",
|
||||
"Enter your new preferred email": "새로운 이메일을 입력하세요",
|
||||
"Enter your password": "비밀번호를 입력하세요",
|
||||
"Error fetching page data.": "페이지 데이터 불러오기 오류.",
|
||||
"Error loading page history.": "페이지 기록 불러오기 오류.",
|
||||
"Export": "내보내기",
|
||||
"Failed to create page": "페이지 생성 실패",
|
||||
"Failed to delete page": "페이지 삭제 실패",
|
||||
"Failed to fetch recent pages": "최근 페이지 불러오기 실패",
|
||||
"Failed to import pages": "페이지 가져오기 실패",
|
||||
"Failed to load page. An error occurred.": "페이지 불러오기 실패. 오류가 발생했습니다.",
|
||||
"Failed to update data": "데이터 갱신 실패",
|
||||
"Full access": "전체 권한",
|
||||
"Full page width": "전체 페이지 너비",
|
||||
"Full width": "전체 너비",
|
||||
"General": "일반",
|
||||
"Group": "팀",
|
||||
"Group description": "팀 설명",
|
||||
"Group name": "팀 이름",
|
||||
"Groups": "팀",
|
||||
"Has full access to space settings and pages.": "Space 설정과 페이지에 대한 전체 접근 권한이 있습니다.",
|
||||
"Home": "홈",
|
||||
"Import pages": "페이지 가져오기",
|
||||
"Import pages & space settings": "페이지 및 Space 설정 가져오기",
|
||||
"Importing pages": "페이지 가져오기 중",
|
||||
"invalid invitation link": "유효하지 않은 초대 링크",
|
||||
"Invitation signup": "초대 가입",
|
||||
"Invite by email": "이메일로 초대",
|
||||
"Invite members": "사용자 초대",
|
||||
"Invite new members": "새 사용자 초대",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "초대를 아직 수락하지 않은 초대된 사용자가 여기에 표시됩니다.",
|
||||
"Invited members will be granted access to spaces the groups can access": "초대된 사용자는 팀이 접근할 수 있는 Space에 대한 접근 권한을 받게 됩니다",
|
||||
"Join the workspace": "Workspace 참여",
|
||||
"Language": "언어",
|
||||
"Light": "밝은",
|
||||
"Link copied": "링크 복사됨",
|
||||
"Login": "로그인",
|
||||
"Logout": "로그아웃",
|
||||
"Manage Group": "팀 관리",
|
||||
"Manage members": "사용자 관리",
|
||||
"member": "사용자",
|
||||
"Member": "사용자",
|
||||
"members": "사용자들",
|
||||
"Members": "사용자들",
|
||||
"My preferences": "내 설정",
|
||||
"My Profile": "내 프로필",
|
||||
"My profile": "내 프로필",
|
||||
"Name": "이름",
|
||||
"New email": "새 이메일",
|
||||
"New page": "새 페이지",
|
||||
"New password": "새 비밀번호",
|
||||
"No group found": "팀을 찾을 수 없음",
|
||||
"No page history saved yet.": "아직 저장된 페이지 기록이 없습니다.",
|
||||
"No pages yet": "아직 페이지가 없습니다",
|
||||
"No results found...": "결과를 찾을 수 없습니다...",
|
||||
"No user found": "사용자를 찾을 수 없음",
|
||||
"Overview": "개요",
|
||||
"Owner": "소유자",
|
||||
"page": "페이지",
|
||||
"Page deleted successfully": "페이지 삭제 완료",
|
||||
"Page history": "페이지 기록",
|
||||
"Page import is in progress. Please do not close this tab.": "페이지 가져오기가 진행 중입니다. 이 탭을 닫지 마세요.",
|
||||
"Pages": "페이지",
|
||||
"pages": "페이지",
|
||||
"Password": "비밀번호",
|
||||
"Password changed successfully": "비밀번호 변경 완료",
|
||||
"Pending": "대기 중",
|
||||
"Please confirm your action": "작업을 확인해 주세요",
|
||||
"Preferences": "설정",
|
||||
"Print PDF": "PDF로 인쇄",
|
||||
"Profile": "프로필",
|
||||
"Recently updated": "최근 업데이트",
|
||||
"Remove": "제거",
|
||||
"Remove group member": "팀에서 사용자 제거",
|
||||
"Remove space member": "Space에서 사용자 제거",
|
||||
"Restore": "복원",
|
||||
"Role": "역할",
|
||||
"Save": "저장",
|
||||
"Search": "검색",
|
||||
"Search for groups": "팀 검색",
|
||||
"Search for users": "사용자 검색",
|
||||
"Search for users and groups": "사용자 및 팀 검색",
|
||||
"Search...": "검색...",
|
||||
"Select language": "언어 선택",
|
||||
"Select role": "역할 선택",
|
||||
"Select role to assign to all invited members": "초대된 모든 사용자에게 할당할 역할 선택",
|
||||
"Select theme": "배경 선택",
|
||||
"Send invitation": "초대 보내기",
|
||||
"Settings": "설정",
|
||||
"Setup workspace": "Workspace 설정",
|
||||
"Sign In": "로그인",
|
||||
"Sign Up": "회원 가입",
|
||||
"Slug": "고유 경로",
|
||||
"Space": "Space",
|
||||
"Space description": "Space 설명",
|
||||
"Space menu": "Space 메뉴",
|
||||
"Space name": "Space 이름",
|
||||
"Space settings": "Space 설정",
|
||||
"Space slug": "Space의 고유 경로",
|
||||
"Spaces": "Space",
|
||||
"Spaces you belong to": "소속된 Space",
|
||||
"No space found": "Space을 찾을 수 없음",
|
||||
"Search for spaces": "Space 검색",
|
||||
"Start typing to search...": "검색하려면 입력을 시작하세요...",
|
||||
"Status": "상태",
|
||||
"Successfully imported": "가져오기 완료",
|
||||
"Successfully restored": "복원 완료",
|
||||
"System settings": "시스템 설정",
|
||||
"Theme": "배경",
|
||||
"To change your email, you have to enter your password and new email.": "이메일을 변경하려면 현재 비밀번호와 새 이메일을 입력해야 합니다.",
|
||||
"Toggle full page width": "전체 페이지 너비 전환",
|
||||
"Unable to import pages. Please try again.": "페이지를 가져올 수 없습니다. 다시 시도해주세요.",
|
||||
"untitled": "제목 없음",
|
||||
"Untitled": "제목 없음",
|
||||
"Updated successfully": "업데이트 완료",
|
||||
"User": "사용자",
|
||||
"Workspace": "Workspace",
|
||||
"Workspace Name": "Workspce 이름",
|
||||
"Workspace settings": "Workspace 설정",
|
||||
"You can change your password here.": "여기서 비밀번호를 변경할 수 있습니다.",
|
||||
"Your Email": "이메일",
|
||||
"Your import is complete.": "가져오기가 완료되었습니다.",
|
||||
"Your name": "이름",
|
||||
"Your Name": "이름",
|
||||
"Your password": "비밀번호",
|
||||
"Your password must be a minimum of 8 characters.": "비밀번호는 최소 8자 이상이어야 합니다.",
|
||||
"Sidebar toggle": "사이드바 전환",
|
||||
"Comments": "댓글",
|
||||
"404 page not found": "404 페이지를 찾을 수 없음",
|
||||
"Sorry, we can't find the page you are looking for.": "죄송합니다. 페이지를 찾을 수 없습니다.",
|
||||
"Take me back to homepage": "홈페이지로 돌아가기",
|
||||
"Forgot password": "비밀번호 찾기",
|
||||
"Forgot your password?": "비밀번호를 잊으셨나요?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "비밀번호 재설정 링크가 이메일로 전송되었습니다. 받은 편지함을 확인해주세요.",
|
||||
"Send reset link": "재설정 링크 보내기",
|
||||
"Password reset": "비밀번호 재설정",
|
||||
"Your new password": "새 비밀번호",
|
||||
"Set password": "비밀번호 설정",
|
||||
"Write a comment": "댓글 작성",
|
||||
"Reply...": "답글...",
|
||||
"Error loading comments.": "댓글 불러오기 오류.",
|
||||
"No comments yet.": "아직 댓글이 없습니다.",
|
||||
"Edit comment": "댓글 수정",
|
||||
"Delete comment": "댓글 삭제",
|
||||
"Are you sure you want to delete this comment?": "이 댓글을 삭제하시겠습니까?",
|
||||
"Comment created successfully": "댓글 생성 완료",
|
||||
"Error creating comment": "댓글 생성 오류",
|
||||
"Comment updated successfully": "댓글 업데이트 완료",
|
||||
"Failed to update comment": "댓글 업데이트 실패",
|
||||
"Comment deleted successfully": "댓글 삭제 완료",
|
||||
"Failed to delete comment": "댓글 삭제 실패",
|
||||
"Comment resolved successfully": "댓글 처리 완료",
|
||||
"Failed to resolve comment": "댓글 처리 실패",
|
||||
"Revoke invitation": "초대 취소",
|
||||
"Revoke": "취소",
|
||||
"Don't": "하지 않음",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "이 초대를 취소하시겠습니까? 사용자가 Workspace에 참여할 수 없게 됩니다.",
|
||||
"Resend invitation": "초대 재전송",
|
||||
"Anyone with this link can join this workspace.": "이 링크를 가진 모든 사람이 Workspace에 참여할 수 있습니다.",
|
||||
"Invite link": "초대 링크",
|
||||
"Copy": "복사",
|
||||
"Copied": "복사됨",
|
||||
"Select a user": "사용자 선택",
|
||||
"Select a group": "팀 선택",
|
||||
"Export all pages and attachments in this space.": "이 Space의 모든 페이지와 첨부파일을 내보냅니다.",
|
||||
"Delete space": "Space 삭제",
|
||||
"Are you sure you want to delete this space?": "이 Space을 삭제하시겠습니까?",
|
||||
"Delete this space with all its pages and data.": "이 Space의 모든 페이지와 데이터를 삭제합니다.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "이 Space의 모든 페이지, 댓글, 첨부파일 및 권한이 영구적으로 삭제됩니다.",
|
||||
"Confirm space name": "Space 이름 확인",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "작업을 진행하려면 Space 이름 <b>{{spaceName}}</b>을 입력하세요.",
|
||||
"Format": "형식",
|
||||
"Include subpages": "하위 페이지 포함",
|
||||
"Include attachments": "첨부파일 포함",
|
||||
"Select export format": "내보내기 형식 선택",
|
||||
"Export failed:": "내보내기 실패:",
|
||||
"export error": "내보내기 오류",
|
||||
"Export page": "페이지 내보내기",
|
||||
"Export space": "Space 내보내기",
|
||||
"Export {{type}}": "{{type}} 내보내기",
|
||||
"File exceeds the {{limit}} attachment limit": "첨부 파일 크기 제한 {{limit}}을 초과했습니다",
|
||||
"Align left": "왼쪽 정렬",
|
||||
"Align right": "오른쪽 정렬",
|
||||
"Align center": "가운데 정렬",
|
||||
"Merge cells": "셀 병합",
|
||||
"Split cell": "셀 분할",
|
||||
"Delete column": "열 삭제",
|
||||
"Delete row": "행 삭제",
|
||||
"Add left column": "왼쪽 열 추가",
|
||||
"Add right column": "오른쪽 열 추가",
|
||||
"Add row above": "위에 행 추가",
|
||||
"Add row below": "아래에 행 추가",
|
||||
"Delete table": "테이블 삭제",
|
||||
"Info": "정보",
|
||||
"Success": "완료",
|
||||
"Warning": "경고",
|
||||
"Danger": "위험",
|
||||
"Mermaid diagram error:": "Mermaid diagram 오류:",
|
||||
"Invalid Mermaid diagram": "잘못된 Mermaid diagram",
|
||||
"Double-click to edit Draw.io diagram": "Draw.io diagram을 편집하려면 더블 클릭하세요",
|
||||
"Exit": "나가기",
|
||||
"Save & Exit": "저장 후 나가기",
|
||||
"Double-click to edit Excalidraw diagram": "Excalidraw diagram을 편집하려면 더블 클릭하세요",
|
||||
"Paste link": "링크 붙여넣기",
|
||||
"Edit link": "링크 편집",
|
||||
"Remove link": "링크 제거",
|
||||
"Add link": "링크 추가",
|
||||
"Please enter a valid url": "유효한 URL을 입력하세요",
|
||||
"Empty equation": "빈 수식",
|
||||
"Invalid equation": "잘못된 수식",
|
||||
"Color": "색상",
|
||||
"Text color": "텍스트 색상",
|
||||
"Default": "기본값",
|
||||
"Blue": "파란색",
|
||||
"Green": "초록색",
|
||||
"Purple": "보라색",
|
||||
"Red": "빨간색",
|
||||
"Yellow": "노란색",
|
||||
"Orange": "주황색",
|
||||
"Pink": "분홍색",
|
||||
"Gray": "회색",
|
||||
"Embed link": "임베드 링크",
|
||||
"Invalid {{provider}} embed link": "잘못된 {{provider}} 임베드 링크",
|
||||
"Embed {{provider}}": "{{provider}} 임베드",
|
||||
"Enter {{provider}} link to embed": "임베드를 할 {{provider}} 링크 입력",
|
||||
"Bold": "굵게",
|
||||
"Italic": "기울임",
|
||||
"Underline": "밑줄",
|
||||
"Strike": "취소선",
|
||||
"Code": "코드",
|
||||
"Comment": "댓글",
|
||||
"Text": "텍스트",
|
||||
"Heading 1": "제목 1",
|
||||
"Heading 2": "제목 2",
|
||||
"Heading 3": "제목 3",
|
||||
"To-do List": "할 일 목록",
|
||||
"Bullet List": "글머리 기호 목록",
|
||||
"Numbered List": "번호 매기기 목록",
|
||||
"Blockquote": "인용구",
|
||||
"Just start typing with plain text.": "일반 텍스트로 입력을 시작하세요.",
|
||||
"Track tasks with a to-do list.": "할 일 목록으로 작업을 추적하세요.",
|
||||
"Big section heading.": "대제목.",
|
||||
"Medium section heading.": "중제목.",
|
||||
"Small section heading.": "소제목.",
|
||||
"Create a simple bullet list.": "글머리 기호 만들기.",
|
||||
"Create a list with numbering.": "숫자 목록 만들기.",
|
||||
"Create block quote.": "인용구 만들기.",
|
||||
"Insert code snippet.": "코드 블록 삽입.",
|
||||
"Insert horizontal rule divider": "가로 구분선 삽입",
|
||||
"Upload any image from your device.": "기기에서 이미지를 업로드하세요.",
|
||||
"Upload any video from your device.": "기기에서 비디오를 업로드하세요.",
|
||||
"Upload any file from your device.": "기기에서 파일을 업로드하세요.",
|
||||
"Table": "테이블",
|
||||
"Insert a table.": "테이블 삽입.",
|
||||
"Insert collapsible block.": "접을 수 있는 블록 삽입.",
|
||||
"Video": "비디오",
|
||||
"Divider": "구분선",
|
||||
"Quote": "인용",
|
||||
"Image": "이미지",
|
||||
"File attachment": "파일 첨부",
|
||||
"Toggle block": "블록 토글",
|
||||
"Callout": "경고 상자",
|
||||
"Insert callout notice.": "돋보이는 글을 작성하기.",
|
||||
"Math inline": "수식",
|
||||
"Insert inline math equation.": "수식 삽입.",
|
||||
"Math block": "수식 블록",
|
||||
"Insert math equation": "수식 삽입",
|
||||
"Mermaid diagram": "Mermaid diagram",
|
||||
"Insert mermaid diagram": "Mermaid diagram 삽입",
|
||||
"Insert and design Drawio diagrams": "Drawio diagram 삽입 및 디자인",
|
||||
"Insert current date": "현재 날짜 삽입",
|
||||
"Draw and sketch excalidraw diagrams": "Excalidraw diagram 그리기 및 스케치",
|
||||
"Multiple": "복제",
|
||||
"Heading {{level}}": "제목 {{level}}",
|
||||
"Toggle title": "제목 토글",
|
||||
"Write anything. Enter \"/\" for commands": "아무거나 입력하세요. 명령어를 사용하려면 \"/\"를 입력하세요",
|
||||
"Names do not match": "이름이 일치하지 않습니다",
|
||||
"Today, {{time}}": "오늘, {{time}}",
|
||||
"Yesterday, {{time}}": "어제, {{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/pt-BR/translation.json
Normal file
342
apps/client/public/locales/pt-BR/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "Conta",
|
||||
"Active": "Ativo",
|
||||
"Add": "Adicionar",
|
||||
"Add group members": "Adicionar membros ao grupo",
|
||||
"Add groups": "Adicionar grupos",
|
||||
"Add members": "Adicionar membros",
|
||||
"Add to groups": "Adicionar aos grupos",
|
||||
"Add space members": "Adicionar membros do espaço",
|
||||
"Admin": "Administrador",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Tem certeza de que deseja excluir este grupo? Os membros perderão acesso aos recursos que este grupo possui.",
|
||||
"Are you sure you want to delete this page?": "Tem certeza de que deseja excluir esta página?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Tem certeza de que deseja remover este usuário do grupo? O usuário perderá acesso aos recursos que este grupo possui.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Tem certeza de que deseja remover este usuário do espaço? O usuário perderá todo acesso a este espaço.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Tem certeza de que deseja restaurar esta versão? Quaisquer alterações não versionadas serão perdidas.",
|
||||
"Can become members of groups and spaces in workspace": "Pode se tornar membro de grupos e espaços no workspace",
|
||||
"Can create and edit pages in space.": "Pode criar e editar páginas no espaço.",
|
||||
"Can edit": "Pode editar",
|
||||
"Can manage workspace": "Pode gerenciar o workspace",
|
||||
"Can manage workspace but cannot delete it": "Pode gerenciar o workspace, mas não pode excluí-lo",
|
||||
"Can view": "Pode visualizar",
|
||||
"Can view pages in space but not edit.": "Pode visualizar páginas no espaço, mas não editar.",
|
||||
"Cancel": "Cancelar",
|
||||
"Change email": "Alterar email",
|
||||
"Change password": "Alterar senha",
|
||||
"Change photo": "Alterar foto",
|
||||
"Choose a role": "Escolha um papel",
|
||||
"Choose your preferred color scheme.": "Escolha seu esquema de cores preferido.",
|
||||
"Choose your preferred interface language.": "Escolha o idioma da interface.",
|
||||
"Choose your preferred page width.": "Escolha a largura preferida da página.",
|
||||
"Confirm": "Confirmar",
|
||||
"Copy link": "Copiar link",
|
||||
"Create": "Criar",
|
||||
"Create group": "Criar grupo",
|
||||
"Create page": "Criar página",
|
||||
"Create space": "Criar espaço",
|
||||
"Create workspace": "Criar workspace",
|
||||
"Current password": "Senha atual",
|
||||
"Dark": "Escuro",
|
||||
"Date": "Data",
|
||||
"Delete": "Excluir",
|
||||
"Delete group": "Excluir grupo",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Você tem certeza que quer deletar essa página? Isso irá deletar todas as páginas filhas e to o histórico. Esta ação é irreversível.",
|
||||
"Description": "Descrição",
|
||||
"Details": "Detalhes",
|
||||
"e.g ACME": "ex.: ACME",
|
||||
"e.g ACME Inc": "ex.: ACME Inc",
|
||||
"e.g Developers": "ex.: Desenvolvedores",
|
||||
"e.g Group for developers": "ex.: Grupo para desenvolvedores",
|
||||
"e.g product": "ex.: produto",
|
||||
"e.g Product Team": "ex.: Equipe de Produto",
|
||||
"e.g Sales": "ex.: Vendas",
|
||||
"e.g Space for product team": "ex.: Espaço para a equipe de produto",
|
||||
"e.g Space for sales team to collaborate": "ex.: Espaço para a equipe de vendas colaborar",
|
||||
"Edit": "Editar",
|
||||
"Edit group": "Editar grupo",
|
||||
"Email": "Email",
|
||||
"Enter a strong password": "Insira uma senha forte",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Insira endereços de email válidos separados por vírgula ou espaço [máx: 50]",
|
||||
"enter valid emails addresses": "insira endereços de email válidos",
|
||||
"Enter your current password": "Insira sua senha atual",
|
||||
"enter your full name": "insira seu nome completo",
|
||||
"Enter your new password": "Insira sua nova senha",
|
||||
"Enter your new preferred email": "Insira seu novo email preferido",
|
||||
"Enter your password": "Insira sua senha",
|
||||
"Error fetching page data.": "Erro ao buscar dados da página.",
|
||||
"Error loading page history.": "Erro ao carregar o histórico da página.",
|
||||
"Export": "Exportar",
|
||||
"Failed to create page": "Falha ao criar página",
|
||||
"Failed to delete page": "Falha ao excluir página",
|
||||
"Failed to fetch recent pages": "Falha ao buscar páginas recentes",
|
||||
"Failed to import pages": "Falha ao importar páginas",
|
||||
"Failed to load page. An error occurred.": "Falha ao carregar página. Ocorreu um erro.",
|
||||
"Failed to update data": "Falha ao atualizar dados",
|
||||
"Full access": "Acesso total",
|
||||
"Full page width": "Usar largura total da página",
|
||||
"Full width": "Largura total",
|
||||
"General": "Geral",
|
||||
"Group": "Grupo",
|
||||
"Group description": "Descrição do grupo",
|
||||
"Group name": "Nome do grupo",
|
||||
"Groups": "Grupos",
|
||||
"Has full access to space settings and pages.": "Tem acesso total às configurações do espaço e às páginas.",
|
||||
"Home": "Início",
|
||||
"Import pages": "Importar páginas",
|
||||
"Import pages & space settings": "Importar páginas e configurações de espaço",
|
||||
"Importing pages": "Importando páginas",
|
||||
"invalid invitation link": "link de convite inválido",
|
||||
"Invitation signup": "Cadastro por convite",
|
||||
"Invite by email": "Convidar por email",
|
||||
"Invite members": "Convidar membros",
|
||||
"Invite new members": "Convidar novos membros",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Membros convidados que ainda não aceitaram o convite aparecerão aqui.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Os membros convidados terão acesso aos espaços que os grupos podem acessar",
|
||||
"Join the workspace": "Entrar no workspace",
|
||||
"Language": "Idioma",
|
||||
"Light": "Claro",
|
||||
"Link copied": "Link copiado",
|
||||
"Login": "Entrar",
|
||||
"Logout": "Sair",
|
||||
"Manage Group": "Gerenciar Grupo",
|
||||
"Manage members": "Gerenciar membros",
|
||||
"member": "membro",
|
||||
"Member": "Membro",
|
||||
"members": "membros",
|
||||
"Members": "Membros",
|
||||
"My preferences": "Minhas preferências",
|
||||
"My Profile": "Meu Perfil",
|
||||
"My profile": "Meu perfil",
|
||||
"Name": "Nome",
|
||||
"New email": "Novo email",
|
||||
"New page": "Nova página",
|
||||
"New password": "Nova senha",
|
||||
"No group found": "Nenhum grupo encontrado",
|
||||
"No page history saved yet.": "Nenhum histórico de página salvo ainda.",
|
||||
"No pages yet": "Nenhuma página ainda",
|
||||
"No results found...": "Nenhum resultado encontrado...",
|
||||
"No user found": "Nenhum usuário encontrado",
|
||||
"Overview": "Visão geral",
|
||||
"Owner": "Proprietário",
|
||||
"page": "página",
|
||||
"Page deleted successfully": "Página excluída com sucesso",
|
||||
"Page history": "Histórico da página",
|
||||
"Page import is in progress. Please do not close this tab.": "A importação da página está em andamento. Por favor, não feche esta aba.",
|
||||
"Pages": "Páginas",
|
||||
"pages": "páginas",
|
||||
"Password": "Senha",
|
||||
"Password changed successfully": "Senha alterada com sucesso",
|
||||
"Pending": "Pendente",
|
||||
"Please confirm your action": "Por favor, confirme sua ação",
|
||||
"Preferences": "Preferências",
|
||||
"Print PDF": "Imprimir PDF",
|
||||
"Profile": "Perfil",
|
||||
"Recently updated": "Atualizado recentemente",
|
||||
"Remove": "Remover",
|
||||
"Remove group member": "Remover membro do grupo",
|
||||
"Remove space member": "Remover membro do espaço",
|
||||
"Restore": "Restaurar",
|
||||
"Role": "Função",
|
||||
"Save": "Salvar",
|
||||
"Search": "Buscar",
|
||||
"Search for groups": "Buscar grupos",
|
||||
"Search for users": "Buscar usuários",
|
||||
"Search for users and groups": "Buscar usuários e grupos",
|
||||
"Search...": "Buscar...",
|
||||
"Select language": "Selecionar idioma",
|
||||
"Select role": "Selecionar função",
|
||||
"Select role to assign to all invited members": "Selecione a função para atribuir a todos os membros convidados",
|
||||
"Select theme": "Selecionar tema",
|
||||
"Send invitation": "Enviar convite",
|
||||
"Settings": "Configurações",
|
||||
"Setup workspace": "Configurar workspace",
|
||||
"Sign In": "Entrar",
|
||||
"Sign Up": "Registrar-se",
|
||||
"Slug": "Slug",
|
||||
"Space": "Espaço",
|
||||
"Space description": "Descrição do espaço",
|
||||
"Space menu": "Menu do espaço",
|
||||
"Space name": "Nome do espaço",
|
||||
"Space settings": "Configurações do espaço",
|
||||
"Space slug": "Slug do espaço",
|
||||
"Spaces": "Espaços",
|
||||
"Spaces you belong to": "Espaços aos quais você pertence",
|
||||
"No space found": "Nenhum espaço encontrado",
|
||||
"Search for spaces": "Pesquisar espaços",
|
||||
"Start typing to search...": "Comece a digitar para buscar...",
|
||||
"Status": "Estado",
|
||||
"Successfully imported": "Importado com sucesso",
|
||||
"Successfully restored": "Restaurado com sucesso",
|
||||
"System settings": "Configurações do sistema",
|
||||
"Theme": "Tema",
|
||||
"To change your email, you have to enter your password and new email.": "Para alterar seu email, você precisa inserir sua senha e o novo email.",
|
||||
"Toggle full page width": "Alternar para largura total da página",
|
||||
"Unable to import pages. Please try again.": "Não foi possível importar as páginas. Por favor, tente novamente.",
|
||||
"untitled": "sem título",
|
||||
"Untitled": "Sem título",
|
||||
"Updated successfully": "Atualizado com sucesso",
|
||||
"User": "Usuário",
|
||||
"Workspace": "Espaço de Trabalho",
|
||||
"Workspace Name": "Nome do Workspace",
|
||||
"Workspace settings": "Configurações do workspace",
|
||||
"You can change your password here.": "Você pode alterar sua senha aqui.",
|
||||
"Your Email": "Seu email",
|
||||
"Your import is complete.": "Sua importação está concluída.",
|
||||
"Your name": "Seu nome",
|
||||
"Your Name": "Seu Nome",
|
||||
"Your password": "Sua senha",
|
||||
"Your password must be a minimum of 8 characters.": "Sua senha deve ter no mínimo 8 caracteres.",
|
||||
"Sidebar toggle": "Interruptor do painel lateral",
|
||||
"Comments": "Comentários",
|
||||
"404 page not found": "Erro 404: Página não encontrada",
|
||||
"Sorry, we can't find the page you are looking for.": "Desculpe, não conseguimos encontrar a página que você está procurando.",
|
||||
"Take me back to homepage": "Leve-me de volta para a página inicial",
|
||||
"Forgot password": "Esqueci a senha",
|
||||
"Forgot your password?": "Esqueceu sua senha?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Um link de redefinição de senha foi enviado para o seu email. Por favor, verifique sua caixa de entrada.",
|
||||
"Send reset link": "Enviar link de recuperação",
|
||||
"Password reset": "Resetar a senha",
|
||||
"Your new password": "Sua nova senha",
|
||||
"Set password": "Definir a senha",
|
||||
"Write a comment": "Escreva um comentário",
|
||||
"Reply...": "Responder...",
|
||||
"Error loading comments.": "Erro ao carregar comentários.",
|
||||
"No comments yet.": "Ainda sem comentários.",
|
||||
"Edit comment": "Editar comentário",
|
||||
"Delete comment": "Excluir comentário",
|
||||
"Are you sure you want to delete this comment?": "Você tem certeza de que deseja excluir este comentário?",
|
||||
"Comment created successfully": "Comentário criado com sucesso",
|
||||
"Error creating comment": "Erro ao criar comentário",
|
||||
"Comment updated successfully": "Comentário atualizado com sucesso",
|
||||
"Failed to update comment": "Falha ao atualizar comentário",
|
||||
"Comment deleted successfully": "Comentário excluído com sucesso",
|
||||
"Failed to delete comment": "Falha ao excluir comentário",
|
||||
"Comment resolved successfully": "Comentário resolvido com sucesso",
|
||||
"Failed to resolve comment": "Falha ao resolver comentário",
|
||||
"Revoke invitation": "Cancelar o convite",
|
||||
"Revoke": "Anular",
|
||||
"Don't": "Não",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Tem certeza de que deseja revogar este convite? O usuário não poderá participar do espaço de trabalho.",
|
||||
"Resend invitation": "Reenviar convite",
|
||||
"Anyone with this link can join this workspace.": "Qualquer um com este link pode participar deste espaço de trabalho.",
|
||||
"Invite link": "Link do convite",
|
||||
"Copy": "Copiar",
|
||||
"Copied": "Copiado",
|
||||
"Select a user": "Selecione um usuário",
|
||||
"Select a group": "Selecione um grupo",
|
||||
"Export all pages and attachments in this space.": "Exportar todas as páginas e anexos deste espaço.",
|
||||
"Delete space": "Excluir Espaço",
|
||||
"Are you sure you want to delete this space?": "Tem certeza de que deseja excluir este espaço?",
|
||||
"Delete this space with all its pages and data.": "Excluir este espaço com todas as suas páginas e dados.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Todas as páginas, comentários, anexos e permissões neste espaço serão excluídos de forma irreversível.",
|
||||
"Confirm space name": "Confirme o nome do espaço",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Digite o nome do espaço <b>{{spaceName}}</b> para confirmar sua ação.",
|
||||
"Format": "Formato",
|
||||
"Include subpages": "Incluir subpáginas",
|
||||
"Include attachments": "Incluir anexos",
|
||||
"Select export format": "Selecionado o formato para exportação",
|
||||
"Export failed:": "Falha ao exportar:",
|
||||
"export error": "erro de exportação",
|
||||
"Export page": "Exportar página",
|
||||
"Export space": "Exportar espaço",
|
||||
"Export {{type}}": "Exportar para {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "O arquivo excede o limite de anexos {{limit}}",
|
||||
"Align left": "Alinhar à esquerda",
|
||||
"Align right": "Alinhar à direita",
|
||||
"Align center": "Alinhar ao centro",
|
||||
"Merge cells": "Mesclar células",
|
||||
"Split cell": "Dividir célula",
|
||||
"Delete column": "Excluir coluna",
|
||||
"Delete row": "Excluir linha",
|
||||
"Add left column": "Adicionar coluna à esquerda",
|
||||
"Add right column": "Adicionar coluna à direita",
|
||||
"Add row above": "Adicionar linha acima",
|
||||
"Add row below": "Adicionar linha abaixo",
|
||||
"Delete table": "Excluir tabela",
|
||||
"Info": "Informação",
|
||||
"Success": "Sucesso",
|
||||
"Warning": "Aviso",
|
||||
"Danger": "Perigo",
|
||||
"Mermaid diagram error:": "Erro no diagrama Mermaid:",
|
||||
"Invalid Mermaid diagram": "Diagrama Mermaid inválido",
|
||||
"Double-click to edit Draw.io diagram": "Clique duas vezes para editar o diagrama Draw.io",
|
||||
"Exit": "Sair",
|
||||
"Save & Exit": "Salvar e Sair",
|
||||
"Double-click to edit Excalidraw diagram": "Clique duas vezes para editar o diagrama Excalidraw",
|
||||
"Paste link": "Colar link",
|
||||
"Edit link": "Editar link",
|
||||
"Remove link": "Remover link",
|
||||
"Add link": "Adicionar link",
|
||||
"Please enter a valid url": "Por favor, insira uma URL válida",
|
||||
"Empty equation": "Equação vazia",
|
||||
"Invalid equation": "Equação inválida",
|
||||
"Color": "Cor",
|
||||
"Text color": "Cor do texto",
|
||||
"Default": "Padrão",
|
||||
"Blue": "Azul",
|
||||
"Green": "Verde",
|
||||
"Purple": "Violeta",
|
||||
"Red": "Vermelho",
|
||||
"Yellow": "Amarelo",
|
||||
"Orange": "Laranja",
|
||||
"Pink": "Rosa",
|
||||
"Gray": "Cinza",
|
||||
"Embed link": "Link embutido",
|
||||
"Invalid {{provider}} embed link": "Link de incorporação {{provider}} inválido",
|
||||
"Embed {{provider}}": "Incorporar {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Digite o link do {{provider}} para incorporar",
|
||||
"Bold": "Negrito",
|
||||
"Italic": "Itálico",
|
||||
"Underline": "Sublinhado",
|
||||
"Strike": "Tracejado",
|
||||
"Code": "Código",
|
||||
"Comment": "Comentário",
|
||||
"Text": "Texto",
|
||||
"Heading 1": "Título 1",
|
||||
"Heading 2": "Título 2",
|
||||
"Heading 3": "Título 3",
|
||||
"To-do List": "Lista de Tarefas",
|
||||
"Bullet List": "Lista de Pontos",
|
||||
"Numbered List": "Lista Numerada",
|
||||
"Blockquote": "Bloco de Citação",
|
||||
"Just start typing with plain text.": "Basta começar a digita.",
|
||||
"Track tasks with a to-do list.": "Acompanhe tarefas com uma lista de tarefas.",
|
||||
"Big section heading.": "Título de seção grande.",
|
||||
"Medium section heading.": "Título de seção média.",
|
||||
"Small section heading.": "Título de seção pequena.",
|
||||
"Create a simple bullet list.": "Crie uma lista simples com marcadores.",
|
||||
"Create a list with numbering.": "Crie uma lista com numeração.",
|
||||
"Create block quote.": "Crie uma citação em bloco.",
|
||||
"Insert code snippet.": "Insira um trecho de código.",
|
||||
"Insert horizontal rule divider": "Insira um divisor horizontal",
|
||||
"Upload any image from your device.": "Envie qualquer imagem do seu dispositivo.",
|
||||
"Upload any video from your device.": "Envie qualquer vídeo do seu dispositivo.",
|
||||
"Upload any file from your device.": "Envie qualquer arquivo do seu dispositivo.",
|
||||
"Table": "Tabela",
|
||||
"Insert a table.": "Insira uma tabela.",
|
||||
"Insert collapsible block.": "Insira um bloco colapsável.",
|
||||
"Video": "Vídeo",
|
||||
"Divider": "Divisor",
|
||||
"Quote": "Citação",
|
||||
"Image": "Imagem",
|
||||
"File attachment": "Anexo de arquivo",
|
||||
"Toggle block": "Bloco colapsável",
|
||||
"Callout": "Aviso",
|
||||
"Insert callout notice.": "Insira um aviso.",
|
||||
"Math inline": "Matemática inline",
|
||||
"Insert inline math equation.": "Insira uma equação matemática inline.",
|
||||
"Math block": "Bloco de matemática",
|
||||
"Insert math equation": "Insira uma equação matemática",
|
||||
"Mermaid diagram": "Diagrama Mermaid",
|
||||
"Insert mermaid diagram": "Insira um diagrama Mermaid",
|
||||
"Insert and design Drawio diagrams": "Insira e projete diagramas Drawio",
|
||||
"Insert current date": "Insira a data atual",
|
||||
"Draw and sketch excalidraw diagrams": "Desenhe e esboce diagramas Excalidraw",
|
||||
"Multiple": "Múltiplo",
|
||||
"Heading {{level}}": "Título {{level}}",
|
||||
"Toggle title": "Alternar título",
|
||||
"Write anything. Enter \"/\" for commands": "Escreva qualquer coisa. Digite \"/\" para comandos",
|
||||
"Names do not match": "Os nomes não coincidem",
|
||||
"Today, {{time}}": "Hoje, {{time}}",
|
||||
"Yesterday, {{time}}": "Ontem, {{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/ru-RU/translation.json
Normal file
342
apps/client/public/locales/ru-RU/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "Аккаунт",
|
||||
"Active": "Активный",
|
||||
"Add": "Добавить",
|
||||
"Add group members": "Добавить участников группы",
|
||||
"Add groups": "Добавить группы",
|
||||
"Add members": "Добавить участников",
|
||||
"Add to groups": "Добавить в группы",
|
||||
"Add space members": "Добавить участников пространства",
|
||||
"Admin": "Администратор",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Вы уверены, что хотите удалить эту группу? Участники потеряют доступ к материалам, к которым у этой группы есть доступ.",
|
||||
"Are you sure you want to delete this page?": "Вы уверены, что хотите удалить эту страницу?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Вы уверены, что хотите удалить этого пользователя из группы? Пользователь потеряет доступ к материалам, к которым у этой группы есть доступ.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Вы уверены, что хотите удалить этого пользователя из пространства? Пользователь потеряет весь доступ к этому пространству.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Вы уверены, что хотите восстановить эту версию? Все не зафиксированные изменения будут потеряны.",
|
||||
"Can become members of groups and spaces in workspace": "Могут становиться участниками групп и пространств в рабочем пространстве",
|
||||
"Can create and edit pages in space.": "Может создавать и редактировать страницы в пространстве.",
|
||||
"Can edit": "Может изменять",
|
||||
"Can manage workspace": "Может управлять рабочим пространством",
|
||||
"Can manage workspace but cannot delete it": "Может управлять рабочим пространством, но не может его удалить",
|
||||
"Can view": "Может просматривать",
|
||||
"Can view pages in space but not edit.": "Может просматривать страницы в пространстве, но не может их редактировать.",
|
||||
"Cancel": "Отменить",
|
||||
"Change email": "Изменить электронную почту",
|
||||
"Change password": "Изменить пароль",
|
||||
"Change photo": "Изменить фото",
|
||||
"Choose a role": "Выберите роль",
|
||||
"Choose your preferred color scheme.": "Выберите предпочитаемую цветовую схему.",
|
||||
"Choose your preferred interface language.": "Выберите предпочитаемый язык интерфейса.",
|
||||
"Choose your preferred page width.": "Выберите предпочитаемую ширину страницы.",
|
||||
"Confirm": "Подтвердить",
|
||||
"Copy link": "Копировать ссылку",
|
||||
"Create": "Создать",
|
||||
"Create group": "Создать группу",
|
||||
"Create page": "Создать страницу",
|
||||
"Create space": "Создать пространство",
|
||||
"Create workspace": "Создать рабочее пространство",
|
||||
"Current password": "Текущий пароль",
|
||||
"Dark": "Темная",
|
||||
"Date": "Дата",
|
||||
"Delete": "Удалить",
|
||||
"Delete group": "Удалить группу",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Вы уверены, что хотите удалить эту страницу? Это удалит её дочерние страницы, а также историю страницы. Это действие необратимо.",
|
||||
"Description": "Описание",
|
||||
"Details": "Подробности",
|
||||
"e.g ACME": "например, ACME",
|
||||
"e.g ACME Inc": "например, ACME Inc",
|
||||
"e.g Developers": "например, Разработчики",
|
||||
"e.g Group for developers": "например, Группа для разработчиков",
|
||||
"e.g product": "например, продукт",
|
||||
"e.g Product Team": "например, Продуктовая команда",
|
||||
"e.g Sales": "например, Продажи",
|
||||
"e.g Space for product team": "например, Пространство для продуктовой команды",
|
||||
"e.g Space for sales team to collaborate": "например, Пространство для совместной работы команды продаж",
|
||||
"Edit": "Редактировать",
|
||||
"Edit group": "Редактировать группу",
|
||||
"Email": "Электронная почта",
|
||||
"Enter a strong password": "Введите надёжный пароль",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Введите действительные адреса электронной почты, разделенные запятой или пробелом [макс: 50]",
|
||||
"enter valid emails addresses": "введите действительные адреса электронной почты",
|
||||
"Enter your current password": "Введите ваш текущий пароль",
|
||||
"enter your full name": "введите ваше полное имя",
|
||||
"Enter your new password": "Введите ваш новый пароль",
|
||||
"Enter your new preferred email": "Введите ваш новый предпочитаемый адрес электронной почты",
|
||||
"Enter your password": "Введите ваш пароль",
|
||||
"Error fetching page data.": "Ошибка при загрузке данных страницы.",
|
||||
"Error loading page history.": "Ошибка при загрузке истории страницы.",
|
||||
"Export": "Экспорт",
|
||||
"Failed to create page": "Не удалось создать страницу",
|
||||
"Failed to delete page": "Не удалось удалить страницу",
|
||||
"Failed to fetch recent pages": "Не удалось получить недавние страницы",
|
||||
"Failed to import pages": "Не удалось импортировать страницы",
|
||||
"Failed to load page. An error occurred.": "Не удалось загрузить страницу. Произошла ошибка.",
|
||||
"Failed to update data": "Не удалось обновить данные",
|
||||
"Full access": "Полный доступ",
|
||||
"Full page width": "Ширина на всю страницу",
|
||||
"Full width": "Во всю ширину",
|
||||
"General": "Основные",
|
||||
"Group": "Группа",
|
||||
"Group description": "Описание группы",
|
||||
"Group name": "Название группы",
|
||||
"Groups": "Группы",
|
||||
"Has full access to space settings and pages.": "Имеет полный доступ к настройкам пространства и страницам.",
|
||||
"Home": "Главная",
|
||||
"Import pages": "Импортировать страницы",
|
||||
"Import pages & space settings": "Импорт страниц и настройки пространства",
|
||||
"Importing pages": "Импортирование страниц",
|
||||
"invalid invitation link": "ссылка на приглашение недействительна",
|
||||
"Invitation signup": "Регистрация по приглашению",
|
||||
"Invite by email": "Пригласить по электронной почте",
|
||||
"Invite members": "Пригласить участников",
|
||||
"Invite new members": "Пригласить новых участников",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Приглашённые участники, которые ещё не приняли приглашение, появятся здесь.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Приглашённые участники получат доступ к пространствам, доступ к которым есть у группы",
|
||||
"Join the workspace": "Присоединиться к рабочему пространству",
|
||||
"Language": "Язык",
|
||||
"Light": "Светлая",
|
||||
"Link copied": "Ссылка скопирована",
|
||||
"Login": "Войти",
|
||||
"Logout": "Выйти",
|
||||
"Manage Group": "Управление группой",
|
||||
"Manage members": "Управление участниками",
|
||||
"member": "участник",
|
||||
"Member": "Участник",
|
||||
"members": "участники",
|
||||
"Members": "Участники",
|
||||
"My preferences": "Мои настройки",
|
||||
"My Profile": "Мой Профиль",
|
||||
"My profile": "Мой профиль",
|
||||
"Name": "Имя",
|
||||
"New email": "Новый электронный адрес",
|
||||
"New page": "Новая страница",
|
||||
"New password": "Новый пароль",
|
||||
"No group found": "Группа не найдена",
|
||||
"No page history saved yet.": "История страниц ещё не сохранена.",
|
||||
"No pages yet": "Страниц пока нет",
|
||||
"No results found...": "Результаты не найдены...",
|
||||
"No user found": "Пользователь не найден",
|
||||
"Overview": "Обзор",
|
||||
"Owner": "Владелец",
|
||||
"page": "страница",
|
||||
"Page deleted successfully": "Страница успешно удалена",
|
||||
"Page history": "История страницы",
|
||||
"Page import is in progress. Please do not close this tab.": "Импорт страницы в процессе. Пожалуйста, не закрывайте эту вкладку.",
|
||||
"Pages": "Страницы",
|
||||
"pages": "страницы",
|
||||
"Password": "Пароль",
|
||||
"Password changed successfully": "Пароль успешно изменён",
|
||||
"Pending": "В ожидании",
|
||||
"Please confirm your action": "Пожалуйста, подтвердите ваше действие",
|
||||
"Preferences": "Внешний вид",
|
||||
"Print PDF": "Печать PDF",
|
||||
"Profile": "Профиль",
|
||||
"Recently updated": "Обновлено недавно",
|
||||
"Remove": "Удалить",
|
||||
"Remove group member": "Удалить участника группы",
|
||||
"Remove space member": "Удалить участника пространства",
|
||||
"Restore": "Восстановить",
|
||||
"Role": "Роль",
|
||||
"Save": "Сохранить",
|
||||
"Search": "Поиск",
|
||||
"Search for groups": "Поиск групп",
|
||||
"Search for users": "Поиск пользователей",
|
||||
"Search for users and groups": "Поиск пользователей и групп",
|
||||
"Search...": "Поиск...",
|
||||
"Select language": "Выберите язык",
|
||||
"Select role": "Выберите роль",
|
||||
"Select role to assign to all invited members": "Выберите роль для всех приглашённых участников",
|
||||
"Select theme": "Выберите тему",
|
||||
"Send invitation": "Отправить приглашение",
|
||||
"Settings": "Настройки",
|
||||
"Setup workspace": "Настроить рабочее пространство",
|
||||
"Sign In": "Вход",
|
||||
"Sign Up": "Регистрация",
|
||||
"Slug": "Slug",
|
||||
"Space": "Пространство",
|
||||
"Space description": "Описание пространства",
|
||||
"Space menu": "Меню пространства",
|
||||
"Space name": "Название пространства",
|
||||
"Space settings": "Настройки пространства",
|
||||
"Space slug": "Slug пространства",
|
||||
"Spaces": "Пространства",
|
||||
"Spaces you belong to": "Пространства, к которым вы принадлежите",
|
||||
"No space found": "Пространства не найдены",
|
||||
"Search for spaces": "Поиск пространств",
|
||||
"Start typing to search...": "Начните вводить для поиска...",
|
||||
"Status": "Статус",
|
||||
"Successfully imported": "Успешно импортировано",
|
||||
"Successfully restored": "Успешно восстановлено",
|
||||
"System settings": "Системные настройки",
|
||||
"Theme": "Тема",
|
||||
"To change your email, you have to enter your password and new email.": "Чтобы изменить электронную почту, вам нужно ввести пароль и новый адрес.",
|
||||
"Toggle full page width": "Переключить ширину на всю страницу",
|
||||
"Unable to import pages. Please try again.": "Не удалось импортировать страницы. Пожалуйста, попробуйте ещё раз.",
|
||||
"untitled": "без названия",
|
||||
"Untitled": "Без названия",
|
||||
"Updated successfully": "Обновлено успешно",
|
||||
"User": "Пользователь",
|
||||
"Workspace": "Рабочее пространство",
|
||||
"Workspace Name": "Имя рабочего пространства",
|
||||
"Workspace settings": "Настройки рабочего пространства",
|
||||
"You can change your password here.": "Вы можете изменить свой пароль здесь.",
|
||||
"Your Email": "Ваш адрес электронной почты",
|
||||
"Your import is complete.": "Ваш импорт завершен.",
|
||||
"Your name": "Ваше имя",
|
||||
"Your Name": "Ваше Имя",
|
||||
"Your password": "Ваш пароль",
|
||||
"Your password must be a minimum of 8 characters.": "Ваш пароль должен содержать минимум 8 символов.",
|
||||
"Sidebar toggle": "Переключить боковую панель",
|
||||
"Comments": "Комментарии",
|
||||
"404 page not found": "404 страница не найдена",
|
||||
"Sorry, we can't find the page you are looking for.": "К сожалению, мы не можем найти страницу, которую вы ищете.",
|
||||
"Take me back to homepage": "Вернуться на главную страницу",
|
||||
"Forgot password": "Забыли пароль",
|
||||
"Forgot your password?": "Забыли пароль?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Ссылка для сброса пароля была отправлена на ваш электронный адрес. Пожалуйста, проверьте входящие сообщения.",
|
||||
"Send reset link": "Отправить ссылку для сброса",
|
||||
"Password reset": "Сброс пароля",
|
||||
"Your new password": "Ваш новый пароль",
|
||||
"Set password": "Установить пароль",
|
||||
"Write a comment": "Написать комментарий",
|
||||
"Reply...": "Ответить...",
|
||||
"Error loading comments.": "Ошибка при загрузке комментариев.",
|
||||
"No comments yet.": "Комментариев пока нет.",
|
||||
"Edit comment": "Редактировать комментарий",
|
||||
"Delete comment": "Удалить комментарий",
|
||||
"Are you sure you want to delete this comment?": "Вы уверены, что хотите удалить этот комментарий?",
|
||||
"Comment created successfully": "Комментарий успешно создан",
|
||||
"Error creating comment": "Ошибка при создании комментария",
|
||||
"Comment updated successfully": "Комментарий успешно обновлён",
|
||||
"Failed to update comment": "Не удалось обновить комментарий",
|
||||
"Comment deleted successfully": "Комментарий успешно удалён",
|
||||
"Failed to delete comment": "Не удалось удалить комментарий",
|
||||
"Comment resolved successfully": "Комментарий успешно разрешён",
|
||||
"Failed to resolve comment": "Не удалось разрешить комментарий",
|
||||
"Revoke invitation": "Отозвать приглашение",
|
||||
"Revoke": "Отозвать",
|
||||
"Don't": "Нет",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Вы уверены, что хотите отозвать это приглашение? Пользователь не сможет присоединиться к рабочему пространству.",
|
||||
"Resend invitation": "Отправить приглашение повторно",
|
||||
"Anyone with this link can join this workspace.": "Любой, у кого есть эта ссылка, может присоединиться к этому рабочему пространству.",
|
||||
"Invite link": "Ссылка для приглашения",
|
||||
"Copy": "Копировать",
|
||||
"Copied": "Скопировано",
|
||||
"Select a user": "Выберите пользователя",
|
||||
"Select a group": "Выберите группу",
|
||||
"Export all pages and attachments in this space.": "Экспортировать все страницы и вложения в этом пространстве.",
|
||||
"Delete space": "Удалить пространство",
|
||||
"Are you sure you want to delete this space?": "Вы уверены, что хотите удалить это пространство?",
|
||||
"Delete this space with all its pages and data.": "Удалить это пространство со всеми его страницами и данными.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Все страницы, комментарии, вложения и разрешения в этом пространстве будут удалены безвозвратно.",
|
||||
"Confirm space name": "Подтвердите название пространства",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Введите название пространства <b>{{spaceName}}</b>, чтобы подтвердить ваше действие.",
|
||||
"Format": "Формат",
|
||||
"Include subpages": "Включить вложенные страницы",
|
||||
"Include attachments": "Включить вложения",
|
||||
"Select export format": "Выберите формат экспорта",
|
||||
"Export failed:": "Экспортирование не удалось:",
|
||||
"export error": "ошибка экспорта",
|
||||
"Export page": "Экспорт страницы",
|
||||
"Export space": "Экспорт пространства",
|
||||
"Export {{type}}": "Экспорт {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Файл превышает лимит вложений {{limit}}",
|
||||
"Align left": "По левому краю",
|
||||
"Align right": "По правому краю",
|
||||
"Align center": "По центру",
|
||||
"Merge cells": "Объединить ячейки",
|
||||
"Split cell": "Разделить ячейку",
|
||||
"Delete column": "Удалить столбец",
|
||||
"Delete row": "Удалить строку",
|
||||
"Add left column": "Добавить левый столбец",
|
||||
"Add right column": "Добавить правый столбец",
|
||||
"Add row above": "Добавить строку выше",
|
||||
"Add row below": "Добавить строку ниже",
|
||||
"Delete table": "Удалить таблицу",
|
||||
"Info": "Информация",
|
||||
"Success": "Успешно",
|
||||
"Warning": "Предупреждение",
|
||||
"Danger": "Важно",
|
||||
"Mermaid diagram error:": "Ошибка диаграммы Mermaid:",
|
||||
"Invalid Mermaid diagram": "Недопустимая диаграмма Mermaid",
|
||||
"Double-click to edit Draw.io diagram": "Кликните дважды для редактирования диаграммы Draw.io",
|
||||
"Exit": "Выйти",
|
||||
"Save & Exit": "Сохранить и выйти",
|
||||
"Double-click to edit Excalidraw diagram": "Кликните дважды для редактирования диаграммы Excalidraw",
|
||||
"Paste link": "Вставить ссылку",
|
||||
"Edit link": "Редактировать ссылку",
|
||||
"Remove link": "Удалить ссылку",
|
||||
"Add link": "Добавить ссылку",
|
||||
"Please enter a valid url": "Пожалуйста, введите корректный url",
|
||||
"Empty equation": "Пустое выражение",
|
||||
"Invalid equation": "Недопустимое уравнение",
|
||||
"Color": "Цвет",
|
||||
"Text color": "Цвет текста",
|
||||
"Default": "По умолчанию",
|
||||
"Blue": "Синий",
|
||||
"Green": "Зелёный",
|
||||
"Purple": "Фиолетовый",
|
||||
"Red": "Красный",
|
||||
"Yellow": "Жёлтый",
|
||||
"Orange": "Оранжевый",
|
||||
"Pink": "Розовый",
|
||||
"Gray": "Серый",
|
||||
"Embed link": "Встроенная ссылка",
|
||||
"Invalid {{provider}} embed link": "Неверная ссылка для встраивания {{provider}}",
|
||||
"Embed {{provider}}": "Встроить {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Введите ссылку для встраивания {{provider}}",
|
||||
"Bold": "Жирный",
|
||||
"Italic": "Курсив",
|
||||
"Underline": "Подчёркнутый",
|
||||
"Strike": "Перечёркнутый",
|
||||
"Code": "Код",
|
||||
"Comment": "Комментарий",
|
||||
"Text": "Текст",
|
||||
"Heading 1": "Заголовок 1",
|
||||
"Heading 2": "Заголовок 2",
|
||||
"Heading 3": "Заголовок 3",
|
||||
"To-do List": "Список дел",
|
||||
"Bullet List": "Маркированный список",
|
||||
"Numbered List": "Нумерованный список",
|
||||
"Blockquote": "Блок цитирования",
|
||||
"Just start typing with plain text.": "Просто начните печатать обычный текст.",
|
||||
"Track tasks with a to-do list.": "Отследить задачи с помощью списка дел.",
|
||||
"Big section heading.": "Большой заголовок раздела.",
|
||||
"Medium section heading.": "Средний заголовок раздела.",
|
||||
"Small section heading.": "Маленький заголовок раздела.",
|
||||
"Create a simple bullet list.": "Создать простой маркированный список.",
|
||||
"Create a list with numbering.": "Создать нумерованный список.",
|
||||
"Create block quote.": "Создать блок цитирования.",
|
||||
"Insert code snippet.": "Вставить фрагмент кода.",
|
||||
"Insert horizontal rule divider": "Вставить горизонтальный разделитель",
|
||||
"Upload any image from your device.": "Загрузить любое изображение с вашего устройства.",
|
||||
"Upload any video from your device.": "Загрузить любое видео с вашего устройства.",
|
||||
"Upload any file from your device.": "Загрузить любой файл с вашего устройства.",
|
||||
"Table": "Таблица",
|
||||
"Insert a table.": "Вставить таблицу.",
|
||||
"Insert collapsible block.": "Вставить сворачиваемый блок.",
|
||||
"Video": "Видео",
|
||||
"Divider": "Разделитель",
|
||||
"Quote": "Цитата",
|
||||
"Image": "Изображение",
|
||||
"File attachment": "Прикрепленный файл",
|
||||
"Toggle block": "Переключить блок",
|
||||
"Callout": "Выноска",
|
||||
"Insert callout notice.": "Вставить выноску с сообщением.",
|
||||
"Math inline": "Формула в строке",
|
||||
"Insert inline math equation.": "Вставить математическое выражение в строку.",
|
||||
"Math block": "Блок формул",
|
||||
"Insert math equation": "Вставить математическое выражение",
|
||||
"Mermaid diagram": "Диаграмма Mermaid",
|
||||
"Insert mermaid diagram": "Вставить диаграмму Mermaid",
|
||||
"Insert and design Drawio diagrams": "Вставьте и редактируйте диаграммы Draw.io",
|
||||
"Insert current date": "Вставить текущую дату",
|
||||
"Draw and sketch excalidraw diagrams": "Создайте и рисуйте диаграммы Excalidraw",
|
||||
"Multiple": "Несколько",
|
||||
"Heading {{level}}": "Заголовок {{level}}",
|
||||
"Toggle title": "Переключить заголовок",
|
||||
"Write anything. Enter \"/\" for commands": "Пишите что угодно. Введите \"/\" для выбора команд",
|
||||
"Names do not match": "Названия не совпадают",
|
||||
"Today, {{time}}": "Сегодня, {{time}}",
|
||||
"Yesterday, {{time}}": "Вчера, {{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/zh-CN/translation.json
Normal file
342
apps/client/public/locales/zh-CN/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "账户",
|
||||
"Active": "活跃",
|
||||
"Add": "添加",
|
||||
"Add group members": "添加群组成员",
|
||||
"Add groups": "添加群组",
|
||||
"Add members": "添加成员",
|
||||
"Add to groups": "添加到群组",
|
||||
"Add space members": "添加空间成员",
|
||||
"Admin": "管理员",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "您确定要删除这个群组吗?成员将失去对该群组可访问资源的访问权限。",
|
||||
"Are you sure you want to delete this page?": "您确定要删除这个页面吗?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "您确定要从群组中移除这个用户吗?该用户将失去对该群组可访问资源的访问权限。",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "您确定要从空间中移除这个用户吗?该用户将失去对这个空间的所有访问权限。",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "您确定要恢复此版本吗?任何未版本化的更改将会丢失。",
|
||||
"Can become members of groups and spaces in workspace": "可以成为工作区中群组和空间的成员",
|
||||
"Can create and edit pages in space.": "能够在空间中创建和编辑页面",
|
||||
"Can edit": "可以编辑",
|
||||
"Can manage workspace": "可以管理工作区",
|
||||
"Can manage workspace but cannot delete it": "可以管理工作区但不能删除它",
|
||||
"Can view": "可以查看",
|
||||
"Can view pages in space but not edit.": "能够在空间中读取页面,但不允许编辑",
|
||||
"Cancel": "取消",
|
||||
"Change email": "更改电子邮箱",
|
||||
"Change password": "更改密码",
|
||||
"Change photo": "更改照片",
|
||||
"Choose a role": "选择一个角色",
|
||||
"Choose your preferred color scheme.": "选择您喜欢的配色方案。",
|
||||
"Choose your preferred interface language.": "选择您喜欢的界面语言。",
|
||||
"Choose your preferred page width.": "选择您喜欢的页面宽度。",
|
||||
"Confirm": "确认",
|
||||
"Copy link": "复制链接",
|
||||
"Create": "创建",
|
||||
"Create group": "创建群组",
|
||||
"Create page": "创建页面",
|
||||
"Create space": "创建空间",
|
||||
"Create workspace": "创建工作空间",
|
||||
"Current password": "当前密码",
|
||||
"Dark": "深色",
|
||||
"Date": "日期",
|
||||
"Delete": "删除",
|
||||
"Delete group": "删除群组",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "您确定要删除这个页面吗?这将删除其子页面和页面历史记录。此操作不可逆。",
|
||||
"Description": "描述",
|
||||
"Details": "详情",
|
||||
"e.g ACME": "例如:ACME",
|
||||
"e.g ACME Inc": "例如:ACME Inc",
|
||||
"e.g Developers": "例如:开发人员",
|
||||
"e.g Group for developers": "例如:开发人员群组",
|
||||
"e.g product": "例如:product",
|
||||
"e.g Product Team": "例如:产品团队",
|
||||
"e.g Sales": "例如:销售",
|
||||
"e.g Space for product team": "例如:产品团队的空间",
|
||||
"e.g Space for sales team to collaborate": "例如:销售团队协作的空间",
|
||||
"Edit": "编辑",
|
||||
"Edit group": "编辑群组",
|
||||
"Email": "电子邮箱",
|
||||
"Enter a strong password": "输入一个强密码",
|
||||
"Enter valid email addresses separated by comma or space max_50": "输入有效的电子邮箱地址,用逗号或空格分隔 [最多:50个]",
|
||||
"enter valid emails addresses": "输入有效的电子邮箱地址",
|
||||
"Enter your current password": "输入您的当前密码",
|
||||
"enter your full name": "输入您的全名",
|
||||
"Enter your new password": "输入您的新密码",
|
||||
"Enter your new preferred email": "输入您新的首选电子邮箱",
|
||||
"Enter your password": "输入您的密码",
|
||||
"Error fetching page data.": "获取页面数据时出错。",
|
||||
"Error loading page history.": "加载页面历史时出错。",
|
||||
"Export": "导出",
|
||||
"Failed to create page": "创建页面失败",
|
||||
"Failed to delete page": "删除页面失败",
|
||||
"Failed to fetch recent pages": "获取最近页面失败",
|
||||
"Failed to import pages": "导入页面失败",
|
||||
"Failed to load page. An error occurred.": "页面加载失败。发生了一个错误。",
|
||||
"Failed to update data": "数据更新失败",
|
||||
"Full access": "完全访问",
|
||||
"Full page width": "全页宽度",
|
||||
"Full width": "全宽",
|
||||
"General": "常规",
|
||||
"Group": "群组",
|
||||
"Group description": "群组描述",
|
||||
"Group name": "群组名称",
|
||||
"Groups": "群组",
|
||||
"Has full access to space settings and pages.": "能够更改全部空间设置和页面",
|
||||
"Home": "首页",
|
||||
"Import pages": "导入页面",
|
||||
"Import pages & space settings": "导入页面和空间设置",
|
||||
"Importing pages": "正在导入页面",
|
||||
"invalid invitation link": "无效的邀请链接",
|
||||
"Invitation signup": "邀请注册",
|
||||
"Invite by email": "通过电子邮箱邀请",
|
||||
"Invite members": "邀请成员",
|
||||
"Invite new members": "邀请新成员",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "尚未接受邀请的成员将显示在这里。",
|
||||
"Invited members will be granted access to spaces the groups can access": "被邀请的成员将被授予访问群组可以访问的空间的权限",
|
||||
"Join the workspace": "加入工作空间",
|
||||
"Language": "语言",
|
||||
"Light": "浅色",
|
||||
"Link copied": "链接已复制",
|
||||
"Login": "登录",
|
||||
"Logout": "退出登录",
|
||||
"Manage Group": "管理群组",
|
||||
"Manage members": "管理成员",
|
||||
"member": "成员",
|
||||
"Member": "成员",
|
||||
"members": "成员",
|
||||
"Members": "成员",
|
||||
"My preferences": "我的偏好设置",
|
||||
"My Profile": "我的个人资料",
|
||||
"My profile": "我的个人资料",
|
||||
"Name": "名称",
|
||||
"New email": "新电子邮箱",
|
||||
"New page": "新建页面",
|
||||
"New password": "新密码",
|
||||
"No group found": "未找到群组",
|
||||
"No page history saved yet.": "尚未保存页面历史。",
|
||||
"No pages yet": "暂无页面",
|
||||
"No results found...": "未找到结果...",
|
||||
"No user found": "未找到用户",
|
||||
"Overview": "概览",
|
||||
"Owner": "所有者",
|
||||
"page": "个页面",
|
||||
"Page deleted successfully": "页面已成功删除",
|
||||
"Page history": "页面历史",
|
||||
"Page import is in progress. Please do not close this tab.": "页面导入正在进行中。请不要关闭此标签页。",
|
||||
"Pages": "页面",
|
||||
"pages": "个页面",
|
||||
"Password": "密码",
|
||||
"Password changed successfully": "密码更改成功",
|
||||
"Pending": "待定",
|
||||
"Please confirm your action": "请确认您的操作",
|
||||
"Preferences": "偏好设置",
|
||||
"Print PDF": "打印 PDF",
|
||||
"Profile": "个人资料",
|
||||
"Recently updated": "最近更新",
|
||||
"Remove": "移除",
|
||||
"Remove group member": "移除群组成员",
|
||||
"Remove space member": "移除空间成员",
|
||||
"Restore": "恢复",
|
||||
"Role": "角色",
|
||||
"Save": "保存",
|
||||
"Search": "搜索",
|
||||
"Search for groups": "搜索群组",
|
||||
"Search for users": "搜索用户",
|
||||
"Search for users and groups": "搜索用户和群组",
|
||||
"Search...": "搜索...",
|
||||
"Select language": "选择语言",
|
||||
"Select role": "选择角色",
|
||||
"Select role to assign to all invited members": "选择要分配给所有被邀请成员的角色",
|
||||
"Select theme": "选择主题",
|
||||
"Send invitation": "发送邀请",
|
||||
"Settings": "设置",
|
||||
"Setup workspace": "设置工作空间",
|
||||
"Sign In": "登录",
|
||||
"Sign Up": "注册",
|
||||
"Slug": "短链接",
|
||||
"Space": "空间",
|
||||
"Space description": "空间描述",
|
||||
"Space menu": "空间菜单",
|
||||
"Space name": "空间名称",
|
||||
"Space settings": "空间设置",
|
||||
"Space slug": "空间短链接",
|
||||
"Spaces": "空间",
|
||||
"Spaces you belong to": "您所属的空间",
|
||||
"No space found": "未找到空间",
|
||||
"Search for spaces": "搜索空间",
|
||||
"Start typing to search...": "开始输入以搜索...",
|
||||
"Status": "状态",
|
||||
"Successfully imported": "成功导入",
|
||||
"Successfully restored": "恢复成功",
|
||||
"System settings": "系统设置",
|
||||
"Theme": "主题",
|
||||
"To change your email, you have to enter your password and new email.": "要更改您的电子邮箱,您需要输入密码和新的电子邮箱地址。",
|
||||
"Toggle full page width": "切换全页宽度",
|
||||
"Unable to import pages. Please try again.": "无法导入页面。请重试。",
|
||||
"untitled": "无标题",
|
||||
"Untitled": "无标题",
|
||||
"Updated successfully": "更新成功",
|
||||
"User": "用户",
|
||||
"Workspace": "工作区",
|
||||
"Workspace Name": "工作空间名称",
|
||||
"Workspace settings": "工作区设置",
|
||||
"You can change your password here.": "您可以在这里更改密码。",
|
||||
"Your Email": "您的电子邮箱",
|
||||
"Your import is complete.": "导入已完成。",
|
||||
"Your name": "您的姓名",
|
||||
"Your Name": "您的姓名",
|
||||
"Your password": "您的密码",
|
||||
"Your password must be a minimum of 8 characters.": "您的密码必须至少包含8个字符。",
|
||||
"Sidebar toggle": "切换侧边栏",
|
||||
"Comments": "评论",
|
||||
"404 page not found": "404 页面未找到",
|
||||
"Sorry, we can't find the page you are looking for.": "抱歉,我们无法找到你所需要的页面",
|
||||
"Take me back to homepage": "回到主页",
|
||||
"Forgot password": "忘记密码",
|
||||
"Forgot your password?": "忘记密码了吗?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "密码重置链接已经发送到您的邮箱,请检查收件箱",
|
||||
"Send reset link": "发送重置链接",
|
||||
"Password reset": "重置密码",
|
||||
"Your new password": "您的新密码",
|
||||
"Set password": "设置密码",
|
||||
"Write a comment": "编写评论",
|
||||
"Reply...": "回复...",
|
||||
"Error loading comments.": "加载评论时出错",
|
||||
"No comments yet.": "目前还没有评论",
|
||||
"Edit comment": "编辑评论",
|
||||
"Delete comment": "删除评论",
|
||||
"Are you sure you want to delete this comment?": "你确定要删除这条评论吗?",
|
||||
"Comment created successfully": "成功创建评论",
|
||||
"Error creating comment": "创建评论时出错",
|
||||
"Comment updated successfully": "评论更新成功",
|
||||
"Failed to update comment": "更新评论失败",
|
||||
"Comment deleted successfully": "成功删除评论",
|
||||
"Failed to delete comment": "删除评论失败",
|
||||
"Comment resolved successfully": "成功标记评论为解决",
|
||||
"Failed to resolve comment": "标记评论为解决失败",
|
||||
"Revoke invitation": "撤回邀请",
|
||||
"Revoke": "撤销",
|
||||
"Don't": "不要",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "你确定要撤回这个邀请吗?此用户将不能再加入此工作空间。",
|
||||
"Resend invitation": "重新发送邀请",
|
||||
"Anyone with this link can join this workspace.": "任何拥有此连接的人都可以加入此工作区",
|
||||
"Invite link": "邀请链接",
|
||||
"Copy": "复制",
|
||||
"Copied": "已复制",
|
||||
"Select a user": "选择一个用户",
|
||||
"Select a group": "选择一个组",
|
||||
"Export all pages and attachments in this space.": "导出当前空间的所有页面和附件",
|
||||
"Delete space": "删除空间",
|
||||
"Are you sure you want to delete this space?": "您确定要删除这个空间吗?",
|
||||
"Delete this space with all its pages and data.": "删除空间及其所有页面和数据",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "此空间中的所有页面、评论、附件和权限将被不可逆转地删除。",
|
||||
"Confirm space name": "确认空间名称",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "输入空间名称<b>{{spaceName}}</b>以确认您的操作。",
|
||||
"Format": "格式",
|
||||
"Include subpages": "包括子页面",
|
||||
"Include attachments": "包括附件",
|
||||
"Select export format": "选择导出格式",
|
||||
"Export failed:": "导出失败:",
|
||||
"export error": "导出出错",
|
||||
"Export page": "导出页面",
|
||||
"Export space": "导出空间",
|
||||
"Export {{type}}": "导出为 {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "文件超出了 {{limit}} 类型附件限制",
|
||||
"Align left": "靠左对齐",
|
||||
"Align right": "靠右对齐",
|
||||
"Align center": "居中对齐",
|
||||
"Merge cells": "合并单元格",
|
||||
"Split cell": "分割单元格",
|
||||
"Delete column": "删除整列",
|
||||
"Delete row": "删除整行",
|
||||
"Add left column": "在左侧添加列",
|
||||
"Add right column": "在右侧添加列",
|
||||
"Add row above": "在上方添加行",
|
||||
"Add row below": "在下方插入行",
|
||||
"Delete table": "删除表格",
|
||||
"Info": "信息",
|
||||
"Success": "成功",
|
||||
"Warning": "警告",
|
||||
"Danger": "危险",
|
||||
"Mermaid diagram error:": "Mermaid 图表错误:",
|
||||
"Invalid Mermaid diagram": "无效的 Mermaid 图表",
|
||||
"Double-click to edit Draw.io diagram": "双击以编辑 Draw.io 图表",
|
||||
"Exit": "退出",
|
||||
"Save & Exit": "保存并退出",
|
||||
"Double-click to edit Excalidraw diagram": "双击以编辑 Excalidraw 图表",
|
||||
"Paste link": "粘贴链接",
|
||||
"Edit link": "编辑链接",
|
||||
"Remove link": "移除链接",
|
||||
"Add link": "添加链接",
|
||||
"Please enter a valid url": "请输入一个合法的 URL",
|
||||
"Empty equation": "空白公式",
|
||||
"Invalid equation": "无效的公式",
|
||||
"Color": "颜色",
|
||||
"Text color": "文字颜色",
|
||||
"Default": "默认",
|
||||
"Blue": "蓝色",
|
||||
"Green": "绿色",
|
||||
"Purple": "紫色",
|
||||
"Red": "红色",
|
||||
"Yellow": "黄色",
|
||||
"Orange": "橙色",
|
||||
"Pink": "粉色",
|
||||
"Gray": "灰色",
|
||||
"Embed link": "嵌入链接",
|
||||
"Invalid {{provider}} embed link": "无效的 {{provider}} 嵌入链接",
|
||||
"Embed {{provider}}": "嵌入 {{provider}}",
|
||||
"Enter {{provider}} link to embed": "输入 {{provider}} 链接来嵌入",
|
||||
"Bold": "粗体",
|
||||
"Italic": "斜体",
|
||||
"Underline": "下划线",
|
||||
"Strike": "删除线",
|
||||
"Code": "代码",
|
||||
"Comment": "评论",
|
||||
"Text": "文字",
|
||||
"Heading 1": "1 级标题",
|
||||
"Heading 2": "2 级标题",
|
||||
"Heading 3": "3 级标题",
|
||||
"To-do List": "代办列表",
|
||||
"Bullet List": "无需列表",
|
||||
"Numbered List": "有序列表",
|
||||
"Blockquote": "引用块",
|
||||
"Just start typing with plain text.": "只需开始键入纯文本",
|
||||
"Track tasks with a to-do list.": "使用代办列表跟踪任务",
|
||||
"Big section heading.": "大标题",
|
||||
"Medium section heading.": "中标题",
|
||||
"Small section heading.": "小标题",
|
||||
"Create a simple bullet list.": "创建一个简单的无序列表",
|
||||
"Create a list with numbering.": "创建一个有序列表",
|
||||
"Create block quote.": "创建引用块",
|
||||
"Insert code snippet.": "插入代码片段",
|
||||
"Insert horizontal rule divider": "插入水平分割线",
|
||||
"Upload any image from your device.": "从设备上传任何图像",
|
||||
"Upload any video from your device.": "从设备上传任何视频",
|
||||
"Upload any file from your device.": "从设备上传任何文件",
|
||||
"Table": "表格",
|
||||
"Insert a table.": "插入一个表格",
|
||||
"Insert collapsible block.": "插入一个折叠块",
|
||||
"Video": "视频",
|
||||
"Divider": "分割线",
|
||||
"Quote": "引用",
|
||||
"Image": "图像",
|
||||
"File attachment": "文件附件",
|
||||
"Toggle block": "切换块",
|
||||
"Callout": "标注块",
|
||||
"Insert callout notice.": "插入标注提示块",
|
||||
"Math inline": "行内公式",
|
||||
"Insert inline math equation.": "插入行内公式",
|
||||
"Math block": "公式块",
|
||||
"Insert math equation": "插入数学公式",
|
||||
"Mermaid diagram": "Mermaid 图表",
|
||||
"Insert mermaid diagram": "插入 Mermaid 图表",
|
||||
"Insert and design Drawio diagrams": "插入并设计 Draw.io 图表",
|
||||
"Insert current date": "插入当前日期",
|
||||
"Draw and sketch excalidraw diagrams": "绘制 Excalidraw 图表",
|
||||
"Multiple": "多个",
|
||||
"Heading {{level}}": "{{level}} 级标题",
|
||||
"Toggle title": "切换标题",
|
||||
"Write anything. Enter \"/\" for commands": "开始编写内容,输入 \"/\" 以使用指令",
|
||||
"Names do not match": "名称不匹配",
|
||||
"Today, {{time}}": "今天,{{time}}",
|
||||
"Yesterday, {{time}}": "昨天,{{time}}"
|
||||
}
|
||||
@ -10,51 +10,18 @@ import Groups from "@/pages/settings/group/groups";
|
||||
import GroupInfo from "./pages/settings/group/group-info";
|
||||
import Spaces from "@/pages/settings/space/spaces.tsx";
|
||||
import { Error404 } from "@/components/ui/error-404.tsx";
|
||||
import { useQuerySubscription } from "@/features/websocket/use-query-subscription.ts";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { socketAtom } from "@/features/websocket/atoms/socket-atom.ts";
|
||||
import { useTreeSocket } from "@/features/websocket/use-tree-socket.ts";
|
||||
import { useEffect } from "react";
|
||||
import { io } from "socket.io-client";
|
||||
import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom.ts";
|
||||
import { SOCKET_URL } from "@/features/websocket/types";
|
||||
import AccountPreferences from "@/pages/settings/account/account-preferences.tsx";
|
||||
import SpaceHome from "@/pages/space/space-home.tsx";
|
||||
import PageRedirect from "@/pages/page/page-redirect.tsx";
|
||||
import Layout from "@/components/layouts/global/layout.tsx";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import InviteSignup from "@/pages/auth/invite-signup.tsx";
|
||||
import ForgotPassword from "@/pages/auth/forgot-password.tsx";
|
||||
import PasswordReset from "./pages/auth/password-reset";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function App() {
|
||||
const [, setSocket] = useAtom(socketAtom);
|
||||
const authToken = useAtomValue(authTokensAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authToken?.accessToken) {
|
||||
return;
|
||||
}
|
||||
const newSocket = io(SOCKET_URL, {
|
||||
transports: ["websocket"],
|
||||
auth: {
|
||||
token: authToken.accessToken,
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
setSocket(newSocket);
|
||||
|
||||
newSocket.on("connect", () => {
|
||||
console.log("ws connected");
|
||||
});
|
||||
|
||||
return () => {
|
||||
console.log("ws disconnected");
|
||||
newSocket.disconnect();
|
||||
};
|
||||
}, [authToken?.accessToken]);
|
||||
|
||||
useQuerySubscription();
|
||||
useTreeSocket();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -63,6 +30,8 @@ export default function App() {
|
||||
<Route path={"/login"} element={<LoginPage />} />
|
||||
<Route path={"/invites/:invitationId"} element={<InviteSignup />} />
|
||||
<Route path={"/setup/register"} element={<SetupWorkspace />} />
|
||||
<Route path={"/forgot-password"} element={<ForgotPassword />} />
|
||||
<Route path={"/password-reset"} element={<PasswordReset />} />
|
||||
|
||||
<Route path={"/p/:pageSlug"} element={<PageRedirect />} />
|
||||
|
||||
@ -74,7 +43,7 @@ export default function App() {
|
||||
path={"/s/:spaceSlug/p/:pageSlug"}
|
||||
element={
|
||||
<ErrorBoundary
|
||||
fallback={<>Failed to load page. An error occurred.</>}
|
||||
fallback={<>{t("Failed to load page. An error occurred.")}</>}
|
||||
>
|
||||
<Page />
|
||||
</ErrorBoundary>
|
||||
|
||||
153
apps/client/src/components/common/export-modal.tsx
Normal file
153
apps/client/src/components/common/export-modal.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
Group,
|
||||
Text,
|
||||
Select,
|
||||
Switch,
|
||||
Divider,
|
||||
} from "@mantine/core";
|
||||
import { exportPage } from "@/features/page/services/page-service.ts";
|
||||
import { useState } from "react";
|
||||
import { ExportFormat } from "@/features/page/types/page.types.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { exportSpace } from "@/features/space/services/space-service";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ExportModalProps {
|
||||
id: string;
|
||||
type: "space" | "page";
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function ExportModal({
|
||||
id,
|
||||
type,
|
||||
open,
|
||||
onClose,
|
||||
}: ExportModalProps) {
|
||||
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
|
||||
const [includeChildren, setIncludeChildren] = useState<boolean>(false);
|
||||
const [includeAttachments, setIncludeAttachments] = useState<boolean>(true);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
if (type === "page") {
|
||||
await exportPage({ pageId: id, format, includeChildren });
|
||||
}
|
||||
if (type === "space") {
|
||||
await exportSpace({ spaceId: id, format, includeAttachments });
|
||||
}
|
||||
setIncludeChildren(false);
|
||||
setIncludeAttachments(true);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
message: "Export failed:" + err.response?.data.message,
|
||||
color: "red",
|
||||
});
|
||||
console.error("export error", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (format: ExportFormat) => {
|
||||
setFormat(format);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal.Root
|
||||
opened={open}
|
||||
onClose={onClose}
|
||||
size={500}
|
||||
padding="xl"
|
||||
yOffset="10vh"
|
||||
xOffset={0}
|
||||
mah={400}
|
||||
>
|
||||
<Modal.Overlay />
|
||||
<Modal.Content style={{ overflow: "hidden" }}>
|
||||
<Modal.Header py={0}>
|
||||
<Modal.Title fw={500}>Export {type}</Modal.Title>
|
||||
<Modal.CloseButton />
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<div>
|
||||
<Text size="md">{t("Format")}</Text>
|
||||
</div>
|
||||
<ExportFormatSelection format={format} onChange={handleChange} />
|
||||
</Group>
|
||||
|
||||
{type === "page" && (
|
||||
<>
|
||||
<Divider my="sm" />
|
||||
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<div>
|
||||
<Text size="md">{t("Include subpages")}</Text>
|
||||
</div>
|
||||
<Switch
|
||||
onChange={(event) =>
|
||||
setIncludeChildren(event.currentTarget.checked)
|
||||
}
|
||||
checked={includeChildren}
|
||||
/>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "space" && (
|
||||
<>
|
||||
<Divider my="sm" />
|
||||
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<div>
|
||||
<Text size="md">{t("Include attachments")}</Text>
|
||||
</div>
|
||||
<Switch
|
||||
onChange={(event) =>
|
||||
setIncludeAttachments(event.currentTarget.checked)
|
||||
}
|
||||
checked={includeAttachments}
|
||||
/>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Group justify="center" mt="md">
|
||||
<Button onClick={onClose} variant="default">
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleExport}>{t("Export")}</Button>
|
||||
</Group>
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
</Modal.Root>
|
||||
);
|
||||
}
|
||||
|
||||
interface ExportFormatSelection {
|
||||
format: ExportFormat;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Select
|
||||
data={[
|
||||
{ value: "markdown", label: "Markdown" },
|
||||
{ value: "html", label: "HTML" },
|
||||
]}
|
||||
defaultValue={format}
|
||||
onChange={onChange}
|
||||
styles={{ wrapper: { maxWidth: 120 } }}
|
||||
comboboxProps={{ width: "120" }}
|
||||
allowDeselect={false}
|
||||
withCheckIcon={false}
|
||||
aria-label={t("Select export format")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
19
apps/client/src/components/common/no-table-results.tsx
Normal file
19
apps/client/src/components/common/no-table-results.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Table, Text } from "@mantine/core";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface NoTableResultsProps {
|
||||
colSpan: number;
|
||||
}
|
||||
export default function NoTableResults({ colSpan }: NoTableResultsProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={colSpan}>
|
||||
<Text fw={500} c="dimmed" ta="center">
|
||||
{t("No results found...")}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
}
|
||||
44
apps/client/src/components/common/paginate.tsx
Normal file
44
apps/client/src/components/common/paginate.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { Button, Group } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface PagePaginationProps {
|
||||
currentPage: number;
|
||||
hasPrevPage: boolean;
|
||||
hasNextPage: boolean;
|
||||
onPageChange: (newPage: number) => void;
|
||||
}
|
||||
|
||||
export default function Paginate({
|
||||
currentPage,
|
||||
hasPrevPage,
|
||||
hasNextPage,
|
||||
onPageChange,
|
||||
}: PagePaginationProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!hasPrevPage && !hasNextPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Group mt="md">
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-sm"
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={!hasPrevPage}
|
||||
>
|
||||
{t("Prev")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-sm"
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={!hasNextPage}
|
||||
>
|
||||
{t("Next")}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@ -1,70 +1,86 @@
|
||||
import { Text, Group, UnstyledButton, Badge, Table } from "@mantine/core";
|
||||
import { Link } from "react-router-dom";
|
||||
import PageListSkeleton from "@/components/ui/page-list-skeleton.tsx";
|
||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||
import { formattedDate } from "@/lib/time.ts";
|
||||
import { useRecentChangesQuery } from "@/features/page/queries/page-query.ts";
|
||||
import { IconFileDescription } from "@tabler/icons-react";
|
||||
import { getSpaceUrl } from "@/lib/config.ts";
|
||||
import {
|
||||
Text,
|
||||
Group,
|
||||
UnstyledButton,
|
||||
Badge,
|
||||
Table,
|
||||
ActionIcon,
|
||||
} from '@mantine/core';
|
||||
import {Link} from 'react-router-dom';
|
||||
import PageListSkeleton from '@/components/ui/page-list-skeleton.tsx';
|
||||
import { buildPageUrl } from '@/features/page/page.utils.ts';
|
||||
import { formattedDate } from '@/lib/time.ts';
|
||||
import { useRecentChangesQuery } from '@/features/page/queries/page-query.ts';
|
||||
import { IconFileDescription } from '@tabler/icons-react';
|
||||
import { getSpaceUrl } from '@/lib/config.ts';
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
spaceId?: string;
|
||||
}
|
||||
export default function RecentChanges({ spaceId }: Props) {
|
||||
const { data: pages, isLoading, isError } = useRecentChangesQuery(spaceId);
|
||||
|
||||
export default function RecentChanges({spaceId}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const {data: pages, isLoading, isError} = useRecentChangesQuery(spaceId);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageListSkeleton />;
|
||||
return <PageListSkeleton/>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <Text>Failed to fetch recent pages</Text>;
|
||||
return <Text>{t("Failed to fetch recent pages")}</Text>;
|
||||
}
|
||||
|
||||
return pages && pages.items.length > 0 ? (
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<Table.Tbody>
|
||||
{pages.items.map((page) => (
|
||||
<Table.Tr key={page.id}>
|
||||
<Table.Td>
|
||||
<UnstyledButton
|
||||
component={Link}
|
||||
to={buildPageUrl(page?.space.slug, page.slugId, page.title)}
|
||||
>
|
||||
<Group wrap="nowrap">
|
||||
{page.icon || <IconFileDescription size={18} />}
|
||||
|
||||
<Text fw={500} size="md" lineClamp={1}>
|
||||
{page.title || "Untitled"}
|
||||
</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Table.Td>
|
||||
{!spaceId && (
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<Table.Tbody>
|
||||
{pages.items.map((page) => (
|
||||
<Table.Tr key={page.id}>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
color="blue"
|
||||
variant="light"
|
||||
<UnstyledButton
|
||||
component={Link}
|
||||
to={getSpaceUrl(page?.space.slug)}
|
||||
style={{ cursor: "pointer" }}
|
||||
to={buildPageUrl(page?.space.slug, page.slugId, page.title)}
|
||||
>
|
||||
{page?.space.name}
|
||||
</Badge>
|
||||
<Group wrap="nowrap">
|
||||
{page.icon || (
|
||||
<ActionIcon variant='transparent' color='gray' size={18}>
|
||||
<IconFileDescription size={18}/>
|
||||
</ActionIcon>
|
||||
)}
|
||||
|
||||
<Text fw={500} size="md" lineClamp={1}>
|
||||
{page.title || t("Untitled")}
|
||||
</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Table.Td>
|
||||
)}
|
||||
<Table.Td>
|
||||
<Text c="dimmed" size="xs" fw={500}>
|
||||
{formattedDate(page.updatedAt)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
{!spaceId && (
|
||||
<Table.Td>
|
||||
<Badge
|
||||
color="blue"
|
||||
variant="light"
|
||||
component={Link}
|
||||
to={getSpaceUrl(page?.space.slug)}
|
||||
style={{cursor: 'pointer'}}
|
||||
>
|
||||
{page?.space.name}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
)}
|
||||
<Table.Td>
|
||||
<Text c="dimmed" style={{whiteSpace: 'nowrap'}} size="xs" fw={500}>
|
||||
{formattedDate(page.updatedAt)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
) : (
|
||||
<Text size="md" ta="center">
|
||||
No pages yet
|
||||
{t("No pages yet")}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
37
apps/client/src/components/common/search-input.tsx
Normal file
37
apps/client/src/components/common/search-input.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { TextInput, Group } from "@mantine/core";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface SearchInputProps {
|
||||
placeholder?: string;
|
||||
debounceDelay?: number;
|
||||
onSearch: (value: string) => void;
|
||||
}
|
||||
|
||||
export function SearchInput({
|
||||
placeholder,
|
||||
debounceDelay = 500,
|
||||
onSearch,
|
||||
}: SearchInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState("");
|
||||
const [debouncedValue] = useDebouncedValue(value, debounceDelay);
|
||||
|
||||
useEffect(() => {
|
||||
onSearch(debouncedValue);
|
||||
}, [debouncedValue, onSearch]);
|
||||
|
||||
return (
|
||||
<Group mb="sm">
|
||||
<TextInput
|
||||
size="sm"
|
||||
placeholder={placeholder || t("Search...")}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
32
apps/client/src/components/icons/airtable-icon.tsx
Normal file
32
apps/client/src/components/icons/airtable-icon.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { rem } from '@mantine/core';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function AirtableIcon({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 215"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path
|
||||
fill="#ffbf00"
|
||||
d="M114.259 2.701 18.86 42.176c-5.305 2.195-5.25 9.73.089 11.847l95.797 37.989a35.544 35.544 0 0 0 26.208 0l95.799-37.99c5.337-2.115 5.393-9.65.086-11.846L141.442 2.7a35.549 35.549 0 0 0-27.183 0"
|
||||
/>
|
||||
<path
|
||||
fill="#26b5f8"
|
||||
d="M136.35 112.757v94.902c0 4.514 4.55 7.605 8.746 5.942l106.748-41.435a6.39 6.39 0 0 0 4.035-5.941V71.322c0-4.514-4.551-7.604-8.747-5.941l-106.748 41.434a6.392 6.392 0 0 0-4.035 5.942"
|
||||
/>
|
||||
<path
|
||||
fill="#ed3049"
|
||||
d="m111.423 117.654-31.68 15.296-3.217 1.555L9.65 166.548C5.411 168.593 0 165.504 0 160.795V71.72c0-1.704.874-3.175 2.046-4.283a7.266 7.266 0 0 1 1.618-1.213c1.598-.959 3.878-1.215 5.816-.448l101.41 40.18c5.155 2.045 5.56 9.268.533 11.697"
|
||||
/>
|
||||
<path
|
||||
fillOpacity={0.25}
|
||||
d="m111.423 117.654-31.68 15.296L2.045 67.438a7.266 7.266 0 0 1 1.618-1.213c1.598-.959 3.878-1.215 5.816-.448l101.41 40.18c5.155 2.045 5.56 9.268.533 11.697"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
23
apps/client/src/components/icons/figma-icon.tsx
Normal file
23
apps/client/src/components/icons/figma-icon.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { rem } from '@mantine/core';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function FigmaIcon({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<g fill="none" fillRule="evenodd" transform="translate(4)">
|
||||
<circle cx={12} cy={12} r={4} fill="#19bcfe" />
|
||||
<path fill="#09cf83" d="M4 24a4 4 0 0 0 4-4v-4H4a4 4 0 1 0 0 8z" />
|
||||
<path fill="#a259ff" d="M4 16h4V8H4a4 4 0 1 0 0 8z" />
|
||||
<path fill="#f24e1e" d="M4 8h4V0H4a4 4 0 1 0 0 8z" />
|
||||
<path fill="#ff7262" d="M12 8H8V0h4a4 4 0 1 1 0 8z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
17
apps/client/src/components/icons/framer-icon.tsx
Normal file
17
apps/client/src/components/icons/framer-icon.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { rem } from '@mantine/core';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function FramerIcon({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path d="M4 0h16v8h-8zm0 8h8l8 8H4zm0 8h8v8z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
24
apps/client/src/components/icons/google-drive-icon.tsx
Normal file
24
apps/client/src/components/icons/google-drive-icon.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { rem } from '@mantine/core';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function GoogleDriveIcon({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 87.3 78"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da" />
|
||||
<path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#00ac47" />
|
||||
<path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z"
|
||||
fill="#ea4335" />
|
||||
<path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d" />
|
||||
<path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc" />
|
||||
<path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z"
|
||||
fill="#ffba00" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
23
apps/client/src/components/icons/google-sheets-icon.tsx
Normal file
23
apps/client/src/components/icons/google-sheets-icon.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { rem } from '@mantine/core';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function GoogleSheetsIcon({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 48 48"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path fill="#43a047" d="M37,45H11c-1.657,0-3-1.343-3-3V6c0-1.657,1.343-3,3-3h19l10,10v29C40,43.657,38.657,45,37,45z"/>
|
||||
<path fill="#c8e6c9" d="M40 13L30 13 30 3z"/>
|
||||
<path fill="#2e7d32" d="M30 13L40 23 40 13z"/>
|
||||
<path
|
||||
fill="#e8f5e9"
|
||||
d="M31,23H17h-2v2v2v2v2v2v2v2h18v-2v-2v-2v-2v-2v-2v-2H31z M17,25h4v2h-4V25z M17,29h4v2h-4V29z M17,33h4v2h-4V33z M31,35h-8v-2h8V35z M31,31h-8v-2h8V31z M31,27h-8v-2h8V27z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
20
apps/client/src/components/icons/icon-drawio.tsx
Normal file
20
apps/client/src/components/icons/icon-drawio.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { rem } from '@mantine/core';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
function IconDrawio({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="#F08705"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path d="M19.69 13.419h-2.527l-2.667-4.555a1.292 1.292 0 001.035-1.28V4.16c0-.725-.576-1.312-1.302-1.312H9.771c-.726 0-1.312.576-1.312 1.301v3.435c0 .619.426 1.152 1.034 1.28l-2.666 4.555H4.309c-.725 0-1.312.576-1.312 1.301v3.435c0 .725.576 1.312 1.302 1.312h4.458c.726 0 1.312-.576 1.312-1.302v-3.434c0-.726-.576-1.312-1.301-1.312h-.437l2.645-4.523h2.059l2.656 4.523h-.438c-.725 0-1.312.576-1.312 1.301v3.435c0 .725.576 1.312 1.302 1.312H19.7c.726 0 1.312-.576 1.312-1.302v-3.434c0-.726-.576-1.312-1.301-1.312zM24 22.976c0 .565-.459 1.024-1.013 1.024H1.024A1.022 1.022 0 010 22.987V1.024C0 .459.459 0 1.013 0h21.963C23.541 0 24 .459 24 1.013z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default IconDrawio;
|
||||
20
apps/client/src/components/icons/icon-excalidraw.tsx
Normal file
20
apps/client/src/components/icons/icon-excalidraw.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { rem } from "@mantine/core";
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
function IconExcalidraw({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="#6965DB"
|
||||
viewBox="0 0 24 24"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path d="M23.943 19.806a.196.196 0 00-.168-.034c-1.26-1.855-2.873-3.61-4.419-5.315l-.252-.284c-.001-.073-.067-.12-.134-.15l-.084-.084c-.05-.1-.169-.167-.286-.1-.47.234-.907.585-1.327.919-.554.434-1.109.87-1.63 1.354a5.058 5.058 0 00-.588.618c-.084.117-.017.217.084.267-.37.368-.74.736-1.109 1.12a.19.19 0 00-.05.134c0 .05.033.1.067.117l.655.502v.016c.924.92 2.554 2.173 4.285 3.527.251.201.52.402.773.602.117.134.234.285.335.418.05.066.169.084.236.033.033.034.084.067.118.1a.24.24 0 00.1.034.153.153 0 00.135-.066.237.237 0 00.033-.1c.017 0 .017.016.034.016a.192.192 0 00.134-.05l3.058-3.327c.12-.116.014-.267 0-.267zm-7.628-.134l-1.546-1.17-.15-.1c-.035-.017-.068-.05-.102-.067l-.117-.1c.66-.66 1.33-1.308 2-1.956-.488.484-1.463 1.906-1.261 2.373.002 0 .018.042.067.084l1.11.936zm4.1 3.126l-1.277-.97a26.906 26.906 0 00-1.58-1.504c.69.518 1.277.97 1.361 1.053.673.585.638.485 1.093.87l.554.4c-.074.103-.151.148-.151.151zm.336.25l-.034-.016a.913.913 0 00.152-.117zM.587 3.476c.034.217.085.435.118.636.201 1.103.403 2.106.772 2.858l.152.568c.05.217.134.485.219.552a66.769 66.769 0 003.578 2.942.177.177 0 00.219 0s0 .016.016.016a.153.153 0 00.118.05.191.191 0 00.134-.05c1.798-1.989 3.142-3.627 4.1-4.998.068-.066.084-.167.084-.25.067-.067.118-.151.185-.201.067-.067.067-.184 0-.235l-.017-.016c0-.033-.017-.084-.05-.1-.42-.401-.722-.685-1.042-.986a93.555 93.555 0 01-2.352-2.273c-.017-.017-.034-.034-.067-.034-.336-.117-1.025-.234-1.882-.385-1.277-.216-3.008-.517-4.57-.986 0 0-.101 0-.118.017l-.05.05C.05.714.022.707 0 .718c.017.1.017.167.05.284 0 .033.068.301.068.334zm7.191 4.78l-.033.034a.036.036 0 01.033-.034zM6.553 2.238c.101.1.521.502.622.585-.437-.2-1.529-.702-2.034-.869.505.1 1.194.201 1.412.284zM.79 1.403c.252.434.454 1.939.655 3.41-.118-.469-.201-.936-.302-1.372C.992 2.673.84 1.988.638 1.386c.124 0 .152.021.152.017zm-.286-.369c0-.016 0-.033-.017-.033.085 0 .135.017.202.05 0 .006-.145-.017-.185-.017zm23.17-.217c.017-.066-.336-.367-.219-.384.253-.017.253-.401 0-.401-.335.017-.688.1-1.008.15-.587.117-1.192.234-1.78.367a79.696 79.696 0 00-3.949.937c-.403.117-.857.2-1.243.401-.135.067-.118.2-.05.284-.034.017-.051.017-.085.034-.117.017-.218.034-.335.05-.102.017-.152.1-.135.2 0 .017.017.05.017.067-.706.936-1.496 1.923-2.353 2.976-.84.969-1.73 1.989-2.62 3.042-2.84 3.31-6.05 7.07-9.594 10.38a.161.161 0 000 .234c.016.016.033.033.05.033-.05.05-.101.085-.152.134-.033.034-.05.067-.05.1a.364.364 0 00-.067.084c-.067.067-.067.184.017.234.067.066.185.066.235-.017.017-.017.017-.033.033-.033a.265.265 0 01.37 0c.202.217.404.435.588.618l-.42-.35c-.067-.067-.184-.05-.234.016-.068.066-.051.184.016.234l4.469 3.727c.034.034.067.034.118.034a.15.15 0 00.117-.05l.101-.1c.017.016.05.016.067.016.05 0 .084-.016.118-.05 6.049-6.05 10.922-10.614 16.5-14.693.05-.033.067-.1.067-.15.067 0 .118-.05.15-.117 1.026-3.125 1.228-5.9 1.295-7.27 0-.059.016-.038.016-.068.017-.033.017-.05.017-.05a.978.978 0 00-.067-.619zm-10.82 4.915c.268-.301.537-.619.806-.903-1.73 2.273-4.603 5.766-8.67 9.929 2.773-3.059 5.562-6.218 7.864-9.026zM5.14 23.466c-.016-.017-.016-.017 0-.017zm2.504-2.156c.135-.15.27-.284.42-.434 0 0 0 .016.017.016-.224.198-.433.418-.437.418zm.69-.668c.099-.1.14-.173.284-.318.992-1.02 2.017-2.04 3.059-3.076l.016-.016c.252-.2.555-.418.824-.619a228.063 228.063 0 00-4.184 4.029zM14.852 3.91c-.554.719-1.176 1.671-1.697 2.423-1.646 2.374-6.94 8.174-7.057 8.274a1189.647 1189.647 0 01-4.839 4.597l-.1.1c-.085-.1-.085-.25.016-.334C8.652 11.966 13.19 6.133 15.021 3.576c-.05.116-.084.216-.168.334zm2.906 3.427c-.671-.386-.99-.987-.806-1.572l.05-.2a.775.775 0 01.085-.167 1.9 1.9 0 01.756-.703c.016 0 .033 0 .05-.016-.017-.034-.017-.084-.017-.134.017-.1.085-.167.202-.167.202 0 .824.184 1.059.384.067.05.134.117.202.184.084.1.218.268.285.401.034.017.067.184.118.268.033.134.067.284.05.418-.017.016 0 .116-.017.116a1.605 1.605 0 01-.218.619c-.03.03.006.012-.05.067a1.22 1.22 0 01-.32.334 1.49 1.49 0 01-1.26.234 2.191 2.191 0 00-.169-.066zm4.37 1.403c0 .017-.017.05 0 .067-.034 0-.05.017-.085.034a109.886 109.886 0 00-3.915 3.025c1.11-.986 2.218-1.989 3.378-2.975.336-.301.571-.686.638-1.12l.168-1.003v-.033c.085-.201.404-.118.353.1-.004-.001-.173.795-.537 1.905z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default IconExcalidraw;
|
||||
20
apps/client/src/components/icons/icon-mermaid.tsx
Normal file
20
apps/client/src/components/icons/icon-mermaid.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { rem } from "@mantine/core";
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
function IconMermaid({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="#FF3670"
|
||||
viewBox="0 0 24 24"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path d="M23.99 2.115A12.223 12.223 0 0012 10.149 12.223 12.223 0 00.01 2.115a12.23 12.23 0 005.32 10.604 6.562 6.562 0 012.845 5.423v3.754h7.65v-3.754a6.561 6.561 0 012.844-5.423 12.223 12.223 0 005.32-10.604z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default IconMermaid;
|
||||
11
apps/client/src/components/icons/index.ts
Normal file
11
apps/client/src/components/icons/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export { AirtableIcon } from "./airtable-icon.tsx";
|
||||
export { FigmaIcon } from "./figma-icon.tsx";
|
||||
export { TypeformIcon } from "./typeform-icon.tsx";
|
||||
export { VimeoIcon } from "./vimeo-icon.tsx";
|
||||
export { MiroIcon } from "./miro-icon.tsx";
|
||||
export { GoogleDriveIcon } from "./google-drive-icon.tsx";
|
||||
export { GoogleSheetsIcon } from "./google-sheets-icon.tsx";
|
||||
export { FramerIcon } from "./framer-icon.tsx";
|
||||
export { LoomIcon } from "./loom-icon.tsx";
|
||||
export { YoutubeIcon } from "./youtube-icon.tsx";
|
||||
|
||||
19
apps/client/src/components/icons/loom-icon.tsx
Normal file
19
apps/client/src/components/icons/loom-icon.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { rem } from '@mantine/core';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function LoomIcon({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="#625DF5"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path
|
||||
d="M24 10.665h-7.018l6.078-3.509-1.335-2.312-6.078 3.509 3.508-6.077L16.843.94l-3.508 6.077V0h-2.67v7.018L7.156.94 4.844 2.275l3.509 6.077-6.078-3.508L.94 7.156l6.078 3.509H0v2.67h7.017L.94 16.844l1.335 2.313 6.077-3.508-3.509 6.077 2.312 1.335 3.509-6.078V24h2.67v-7.017l3.508 6.077 2.312-1.335-3.509-6.078 6.078 3.509 1.335-2.313-6.077-3.508h7.017v-2.67H24zm-12 4.966a3.645 3.645 0 1 1 0-7.29 3.645 3.645 0 0 1 0 7.29z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
18
apps/client/src/components/icons/miro-icon.tsx
Normal file
18
apps/client/src/components/icons/miro-icon.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { rem } from '@mantine/core';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function MiroIcon({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path
|
||||
d="M17.392 0H13.9L17 4.808 10.444 0H6.949l3.102 6.3L3.494 0H0l3.05 8.131L0 24h3.494L10.05 6.985 6.949 24h3.494L17 5.494 13.899 24h3.493L24 3.672 17.392 0z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
18
apps/client/src/components/icons/typeform-icon.tsx
Normal file
18
apps/client/src/components/icons/typeform-icon.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { rem } from '@mantine/core';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function TypeformIcon({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path
|
||||
d="M15.502 13.035c-.5 0-.756-.411-.756-.917 0-.505.252-.894.756-.894.513 0 .756.407.756.894-.004.515-.261.917-.756.917Zm-4.888-1.81c.292 0 .414.17.414.317 0 .357-.365.514-1.126.536 0-.442.253-.854.712-.854Zm-3.241 1.81c-.473 0-.67-.384-.67-.917 0-.527.202-.894.67-.894.477 0 .702.38.702.894 0 .537-.234.917-.702.917Zm-3.997-2.334h-.738l1.224 2.808c-.234.519-.36.648-.522.648-.171 0-.333-.138-.45-.259l-.324.43c.22.232.522.366.832.366.387 0 .685-.224.856-.626l1.413-3.371h-.725l-.738 2.012-.828-2.008Zm19.553.523c.36 0 .432.246.432.823v1.516H24v-1.914c0-.689-.473-.988-.91-.988-.386 0-.742.241-.94.688a.901.901 0 0 0-.891-.688c-.365 0-.73.232-.927.666v-.626h-.64v2.857h.64v-1.22c0-.617.324-1.114.765-1.114.36 0 .427.246.427.823v1.516h.64l-.005-1.225c0-.617.329-1.114.77-1.114Zm-5.1-.523h-.324v2.857h.639v-1.095c0-.693.306-1.163.76-1.163.118 0 .217.005.325.05l.099-.676c-.081-.009-.153-.018-.225-.018-.45 0-.774.309-.964.707V10.7h-.31Zm-2.327-.045c-.846 0-1.418.644-1.418 1.458 0 .845.58 1.475 1.418 1.475.85 0 1.431-.648 1.431-1.475-.004-.818-.594-1.458-1.431-1.458Zm-4.852 2.38c-.333 0-.581-.17-.685-.515.847-.036 1.675-.242 1.675-.988 0-.43-.423-.872-1.03-.872-.82 0-1.374.666-1.374 1.457 0 .828.545 1.476 1.36 1.476.567 0 .927-.228 1.21-.559l-.31-.42c-.329.335-.531.42-.846.42Zm-3.151-2.38c-.324 0-.648.188-.774.483v-.438h-.64v3.98h.64v-1.422c.135.205.445.34.72.34.85 0 1.3-.631 1.3-1.48-.004-.841-.445-1.463-1.246-1.463Zm-4.483-1.1H0v.622h1.18v3.38h.67v-3.38h1.166v-.622Zm9.502 1.145h-.383v.572h.383v2.285h.639v-2.285h.621v-.572h-.621v-.447c0-.286.117-.385.382-.385.1 0 .19.027.311.068l.144-.537c-.117-.067-.351-.094-.504-.094-.612 0-.972.367-.972 1.002v.393Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
19
apps/client/src/components/icons/vimeo-icon.tsx
Normal file
19
apps/client/src/components/icons/vimeo-icon.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { rem } from '@mantine/core';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function VimeoIcon({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="#1AB7EA"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path
|
||||
d="M23.9765 6.4168c-.105 2.338-1.739 5.5429-4.894 9.6088-3.2679 4.247-6.0258 6.3699-8.2898 6.3699-1.409 0-2.578-1.294-3.553-3.881l-1.9179-7.1138c-.719-2.584-1.488-3.878-2.312-3.878-.179 0-.806.378-1.8809 1.132l-1.129-1.457a315.06 315.06 0 003.501-3.1279c1.579-1.368 2.765-2.085 3.5539-2.159 1.867-.18 3.016 1.1 3.447 3.838.465 2.953.789 4.789.971 5.5069.5389 2.45 1.1309 3.674 1.7759 3.674.502 0 1.256-.796 2.265-2.385 1.004-1.589 1.54-2.797 1.612-3.628.144-1.371-.395-2.061-1.614-2.061-.574 0-1.167.121-1.777.391 1.186-3.8679 3.434-5.7568 6.7619-5.6368 2.4729.06 3.6279 1.664 3.4929 4.7969z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
19
apps/client/src/components/icons/youtube-icon.tsx
Normal file
19
apps/client/src/components/icons/youtube-icon.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { rem } from '@mantine/core';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function YoutubeIcon({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="#FF0000"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path
|
||||
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@ -1,20 +1,22 @@
|
||||
import { Group, Text } from "@mantine/core";
|
||||
import {Group, Text, Tooltip} from "@mantine/core";
|
||||
import classes from "./app-header.module.css";
|
||||
import React from "react";
|
||||
import TopMenu from "@/components/layouts/global/top-menu.tsx";
|
||||
import { Link } from "react-router-dom";
|
||||
import {Link} from "react-router-dom";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import { useAtom } from "jotai/index";
|
||||
import {useAtom} from "jotai/index";
|
||||
import {
|
||||
desktopSidebarAtom,
|
||||
mobileSidebarAtom,
|
||||
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
|
||||
import {useToggleSidebar} from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
|
||||
import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const links = [{ link: APP_ROUTE.HOME, label: "Home" }];
|
||||
const links = [{link: APP_ROUTE.HOME, label: "Home"}];
|
||||
|
||||
export function AppHeader() {
|
||||
const { t } = useTranslation();
|
||||
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
||||
const toggleMobile = useToggleSidebar(mobileSidebarAtom);
|
||||
|
||||
@ -25,7 +27,7 @@ export function AppHeader() {
|
||||
|
||||
const items = links.map((link) => (
|
||||
<Link key={link.label} to={link.link} className={classes.link}>
|
||||
{link.label}
|
||||
{t(link.label)}
|
||||
</Link>
|
||||
));
|
||||
|
||||
@ -35,28 +37,35 @@ export function AppHeader() {
|
||||
<Group wrap="nowrap">
|
||||
{!isHomeRoute && (
|
||||
<>
|
||||
<SidebarToggle
|
||||
aria-label="sidebar toggle"
|
||||
opened={mobileOpened}
|
||||
onClick={toggleMobile}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
<Tooltip label={t("Sidebar toggle")}>
|
||||
|
||||
<SidebarToggle
|
||||
aria-label="sidebar toggle"
|
||||
opened={desktopOpened}
|
||||
onClick={toggleDesktop}
|
||||
visibleFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
<SidebarToggle
|
||||
aria-label={t("Sidebar toggle")}
|
||||
opened={mobileOpened}
|
||||
onClick={toggleMobile}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={t("Sidebar toggle")}>
|
||||
<SidebarToggle
|
||||
aria-label={t("Sidebar toggle")}
|
||||
opened={desktopOpened}
|
||||
onClick={toggleDesktop}
|
||||
visibleFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text
|
||||
size="lg"
|
||||
fw={600}
|
||||
style={{ cursor: "pointer", userSelect: "none" }}
|
||||
style={{cursor: "pointer", userSelect: "none"}}
|
||||
component={Link}
|
||||
to="/home"
|
||||
>
|
||||
Docmost
|
||||
</Text>
|
||||
@ -67,7 +76,7 @@ export function AppHeader() {
|
||||
</Group>
|
||||
|
||||
<Group px={"xl"}>
|
||||
<TopMenu />
|
||||
<TopMenu/>
|
||||
</Group>
|
||||
</Group>
|
||||
</>
|
||||
|
||||
@ -14,3 +14,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.resizeHandle {
|
||||
width: 3px;
|
||||
cursor: col-resize;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
&:hover, &:active {
|
||||
width: 5px;
|
||||
background: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-5))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -3,9 +3,11 @@ import CommentList from "@/features/comment/components/comment-list.tsx";
|
||||
import { useAtom } from "jotai";
|
||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import React, { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Aside() {
|
||||
const [{ tab }] = useAtom(asideStateAtom);
|
||||
const { t } = useTranslation();
|
||||
|
||||
let title: string;
|
||||
let component: ReactNode;
|
||||
@ -25,7 +27,7 @@ export default function Aside() {
|
||||
{component && (
|
||||
<>
|
||||
<Text mb="md" fw={500}>
|
||||
{title}
|
||||
{t(title)}
|
||||
</Text>
|
||||
|
||||
<ScrollArea
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { AppShell, Container } from "@mantine/core";
|
||||
import React from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import SettingsSidebar from "@/components/settings/settings-sidebar.tsx";
|
||||
import { useAtom } from "jotai";
|
||||
import {
|
||||
asideStateAtom,
|
||||
desktopSidebarAtom,
|
||||
mobileSidebarAtom,
|
||||
mobileSidebarAtom, sidebarWidthAtom,
|
||||
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx";
|
||||
import { AppHeader } from "@/components/layouts/global/app-header.tsx";
|
||||
@ -21,6 +21,46 @@ export default function GlobalAppShell({
|
||||
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
||||
const [desktopOpened] = useAtom(desktopSidebarAtom);
|
||||
const [{ isAsideOpen }] = useAtom(asideStateAtom);
|
||||
const [sidebarWidth, setSidebarWidth] = useAtom(sidebarWidthAtom);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const sidebarRef = useRef(null);
|
||||
|
||||
const startResizing = React.useCallback((mouseDownEvent) => {
|
||||
mouseDownEvent.preventDefault();
|
||||
setIsResizing(true);
|
||||
}, []);
|
||||
|
||||
const stopResizing = React.useCallback(() => {
|
||||
setIsResizing(false);
|
||||
}, []);
|
||||
|
||||
const resize = React.useCallback(
|
||||
(mouseMoveEvent) => {
|
||||
if (isResizing) {
|
||||
const newWidth = mouseMoveEvent.clientX - sidebarRef.current.getBoundingClientRect().left;
|
||||
if (newWidth < 220) {
|
||||
setSidebarWidth(220);
|
||||
return;
|
||||
}
|
||||
if (newWidth > 600) {
|
||||
setSidebarWidth(600);
|
||||
return;
|
||||
}
|
||||
setSidebarWidth(newWidth);
|
||||
}
|
||||
},
|
||||
[isResizing]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
//https://codesandbox.io/p/sandbox/kz9de
|
||||
window.addEventListener("mousemove", resize);
|
||||
window.addEventListener("mouseup", stopResizing);
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", resize);
|
||||
window.removeEventListener("mouseup", stopResizing);
|
||||
};
|
||||
}, [resize, stopResizing]);
|
||||
|
||||
const location = useLocation();
|
||||
const isSettingsRoute = location.pathname.startsWith("/settings");
|
||||
@ -33,7 +73,7 @@ export default function GlobalAppShell({
|
||||
header={{ height: 45 }}
|
||||
navbar={
|
||||
!isHomeRoute && {
|
||||
width: 300,
|
||||
width: isSpaceRoute ? sidebarWidth : 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: {
|
||||
mobile: !mobileOpened,
|
||||
@ -54,7 +94,8 @@ export default function GlobalAppShell({
|
||||
<AppHeader />
|
||||
</AppShell.Header>
|
||||
{!isHomeRoute && (
|
||||
<AppShell.Navbar className={classes.navbar} withBorder={false}>
|
||||
<AppShell.Navbar className={classes.navbar} withBorder={false} ref={sidebarRef}>
|
||||
<div className={classes.resizeHandle} onMouseDown={startResizing} />
|
||||
{isSpaceRoute && <SpaceSidebar />}
|
||||
{isSettingsRoute && <SettingsSidebar />}
|
||||
</AppShell.Navbar>
|
||||
|
||||
@ -19,3 +19,5 @@ export const asideStateAtom = atom<AsideStateType>({
|
||||
tab: "",
|
||||
isAsideOpen: false,
|
||||
});
|
||||
|
||||
export const sidebarWidthAtom = atomWithWebStorage<number>('sidebarWidth', 300);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Avatar, Group, Menu, rem, UnstyledButton, Text } from "@mantine/core";
|
||||
import { Group, Menu, UnstyledButton, Text } from "@mantine/core";
|
||||
import {
|
||||
IconBrush,
|
||||
IconChevronDown,
|
||||
IconLogout,
|
||||
IconSettings,
|
||||
@ -12,8 +13,10 @@ import { Link } from "react-router-dom";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import useAuth from "@/features/auth/hooks/use-auth.ts";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function TopMenu() {
|
||||
const { t } = useTranslation();
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const { logout } = useAuth();
|
||||
|
||||
@ -35,48 +38,35 @@ export default function TopMenu() {
|
||||
variant="filled"
|
||||
size="sm"
|
||||
/>
|
||||
<Text fw={500} size="sm" lh={1} mr={3}>
|
||||
<Text fw={500} size="sm" lh={1} mr={3} lineClamp={1}>
|
||||
{workspace.name}
|
||||
</Text>
|
||||
<IconChevronDown
|
||||
style={{ width: rem(12), height: rem(12) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
<IconChevronDown size={16} />
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>Workspace</Menu.Label>
|
||||
<Menu.Label>{t("Workspace")}</Menu.Label>
|
||||
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
to={APP_ROUTE.SETTINGS.WORKSPACE.GENERAL}
|
||||
leftSection={
|
||||
<IconSettings
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
leftSection={<IconSettings size={16} />}
|
||||
>
|
||||
Workspace settings
|
||||
{t("Workspace settings")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
to={APP_ROUTE.SETTINGS.WORKSPACE.MEMBERS}
|
||||
leftSection={
|
||||
<IconUsers
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
leftSection={<IconUsers size={16} />}
|
||||
>
|
||||
Manage members
|
||||
{t("Manage members")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Label>Account</Menu.Label>
|
||||
<Menu.Label>{t("Account")}</Menu.Label>
|
||||
<Menu.Item component={Link} to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<CustomAvatar
|
||||
@ -85,11 +75,11 @@ export default function TopMenu() {
|
||||
name={user.name}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div style={{width: 190}}>
|
||||
<Text size="sm" fw={500} lineClamp={1}>
|
||||
{user.name}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
<Text size="xs" c="dimmed" truncate="end">
|
||||
{user.email}
|
||||
</Text>
|
||||
</div>
|
||||
@ -98,28 +88,23 @@ export default function TopMenu() {
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}
|
||||
leftSection={
|
||||
<IconUserCircle
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
leftSection={<IconUserCircle size={16} />}
|
||||
>
|
||||
My profile
|
||||
{t("My profile")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
to={APP_ROUTE.SETTINGS.ACCOUNT.PREFERENCES}
|
||||
leftSection={<IconBrush size={16} />}
|
||||
>
|
||||
{t("My preferences")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item
|
||||
onClick={logout}
|
||||
leftSection={
|
||||
<IconLogout
|
||||
style={{ width: rem(16), height: rem(16) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Logout
|
||||
<Menu.Item onClick={logout} leftSection={<IconLogout size={16} />}>
|
||||
{t("Logout")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
} from "@tabler/icons-react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import classes from "./settings.module.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface DataItem {
|
||||
label: string;
|
||||
@ -51,6 +52,7 @@ const groupedData: DataGroup[] = [
|
||||
];
|
||||
|
||||
export default function SettingsSidebar() {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const [active, setActive] = useState(location.pathname);
|
||||
const navigate = useNavigate();
|
||||
@ -62,7 +64,7 @@ export default function SettingsSidebar() {
|
||||
const menuItems = groupedData.map((group) => (
|
||||
<div key={group.heading}>
|
||||
<Text c="dimmed" className={classes.linkHeader}>
|
||||
{group.heading}
|
||||
{t(group.heading)}
|
||||
</Text>
|
||||
{group.items.map((item) => (
|
||||
<Link
|
||||
@ -72,7 +74,7 @@ export default function SettingsSidebar() {
|
||||
to={item.path}
|
||||
>
|
||||
<item.icon className={classes.linkIcon} stroke={2} />
|
||||
<span>{item.label}</span>
|
||||
<span>{t(item.label)}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
@ -89,10 +91,22 @@ export default function SettingsSidebar() {
|
||||
>
|
||||
<IconArrowLeft stroke={2} />
|
||||
</ActionIcon>
|
||||
<Text fw={500}>Settings</Text>
|
||||
<Text fw={500}>{t("Settings")}</Text>
|
||||
</Group>
|
||||
|
||||
<ScrollArea w="100%">{menuItems}</ScrollArea>
|
||||
<div className={classes.version}>
|
||||
<Text
|
||||
className={classes.version}
|
||||
size="sm"
|
||||
c="dimmed"
|
||||
component="a"
|
||||
href="https://github.com/docmost/docmost/releases"
|
||||
target="_blank"
|
||||
>
|
||||
v{APP_VERSION}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -57,3 +57,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.version {
|
||||
padding-left: var(--mantine-spacing-xs) ;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import data from "@emoji-mart/data";
|
||||
import Picker from "@emoji-mart/react";
|
||||
import React, { ReactNode, useState } from "react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Popover,
|
||||
Button,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { useClickOutside, useDisclosure, useWindowEvent } from "@mantine/hooks";
|
||||
import { Suspense } from "react";
|
||||
const Picker = React.lazy(() => import("@emoji-mart/react"));
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface EmojiPickerInterface {
|
||||
onEmojiSelect: (emoji: any) => void;
|
||||
@ -22,8 +23,26 @@ function EmojiPicker({
|
||||
removeEmojiAction,
|
||||
readOnly,
|
||||
}: EmojiPickerInterface) {
|
||||
const { t } = useTranslation();
|
||||
const [opened, handlers] = useDisclosure(false);
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const [target, setTarget] = useState<HTMLElement | null>(null);
|
||||
const [dropdown, setDropdown] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
useClickOutside(
|
||||
() => handlers.close(),
|
||||
["mousedown", "touchstart"],
|
||||
[dropdown, target],
|
||||
);
|
||||
|
||||
// We need this because the default Mantine popover closeOnEscape does not work
|
||||
useWindowEvent("keydown", (event) => {
|
||||
if (opened && event.key === "Escape") {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
handlers.close();
|
||||
}
|
||||
});
|
||||
|
||||
const handleEmojiSelect = (emoji) => {
|
||||
onEmojiSelect(emoji);
|
||||
@ -42,20 +61,23 @@ function EmojiPicker({
|
||||
width={332}
|
||||
position="bottom"
|
||||
disabled={readOnly}
|
||||
closeOnEscape={true}
|
||||
>
|
||||
<Popover.Target>
|
||||
<Popover.Target ref={setTarget}>
|
||||
<ActionIcon c="gray" variant="transparent" onClick={handlers.toggle}>
|
||||
{icon}
|
||||
</ActionIcon>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown bg="000" style={{ border: "none" }}>
|
||||
<Picker
|
||||
data={data}
|
||||
onEmojiSelect={handleEmojiSelect}
|
||||
perLine={8}
|
||||
skinTonePosition="search"
|
||||
theme={colorScheme}
|
||||
/>
|
||||
<Popover.Dropdown bg="000" style={{ border: "none" }} ref={setDropdown}>
|
||||
<Suspense fallback={null}>
|
||||
<Picker
|
||||
data={async () => (await import("@emoji-mart/data")).default}
|
||||
onEmojiSelect={handleEmojiSelect}
|
||||
perLine={8}
|
||||
skinTonePosition="search"
|
||||
theme={colorScheme}
|
||||
/>
|
||||
</Suspense>
|
||||
<Button
|
||||
variant="default"
|
||||
c="gray"
|
||||
@ -68,7 +90,7 @@ function EmojiPicker({
|
||||
}}
|
||||
onClick={handleRemoveEmoji}
|
||||
>
|
||||
Remove
|
||||
{t("Remove")}
|
||||
</Button>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
|
||||
@ -1,19 +1,28 @@
|
||||
import { Title, Text, Button, Container, Group } from "@mantine/core";
|
||||
import classes from "./error-404.module.css";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function Error404() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Container className={classes.root}>
|
||||
<Title className={classes.title}>404 Page Not Found</Title>
|
||||
<Text c="dimmed" size="lg" ta="center" className={classes.description}>
|
||||
Sorry, we can't find the page you are looking for.
|
||||
</Text>
|
||||
<Group justify="center">
|
||||
<Button component={Link} to={"/home"} variant="subtle" size="md">
|
||||
Take me back to homepage
|
||||
</Button>
|
||||
</Group>
|
||||
</Container>
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("404 page not found")} - Docmost</title>
|
||||
</Helmet>
|
||||
<Container className={classes.root}>
|
||||
<Title className={classes.title}>{t("404 page not found")}</Title>
|
||||
<Text c="dimmed" size="lg" ta="center" className={classes.description}>
|
||||
{t("Sorry, we can't find the page you are looking for.")}
|
||||
</Text>
|
||||
<Group justify="center">
|
||||
<Button component={Link} to={"/home"} variant="subtle" size="md">
|
||||
{t("Take me back to homepage")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import React, { forwardRef } from "react";
|
||||
import { IconCheck, IconChevronDown } from "@tabler/icons-react";
|
||||
import { Group, Text, Menu, Button } from "@mantine/core";
|
||||
import { IRoleData } from "@/lib/types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface RoleButtonProps extends React.ComponentPropsWithoutRef<"button"> {
|
||||
name: string;
|
||||
@ -23,7 +24,7 @@ const RoleButton = forwardRef<HTMLButtonElement, RoleButtonProps>(
|
||||
),
|
||||
);
|
||||
|
||||
interface SpaceRoleMenuProps {
|
||||
interface RoleMenuProps {
|
||||
roles: IRoleData[];
|
||||
roleName: string;
|
||||
onChange?: (value: string) => void;
|
||||
@ -35,11 +36,13 @@ export default function RoleSelectMenu({
|
||||
roleName,
|
||||
onChange,
|
||||
disabled,
|
||||
}: SpaceRoleMenuProps) {
|
||||
}: RoleMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Menu withArrow>
|
||||
<Menu.Target>
|
||||
<RoleButton name={roleName} disabled={disabled} />
|
||||
<RoleButton name={t(roleName)} disabled={disabled} />
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
@ -50,9 +53,9 @@ export default function RoleSelectMenu({
|
||||
>
|
||||
<Group flex="1" gap="xs">
|
||||
<div>
|
||||
<Text size="sm">{item.label}</Text>
|
||||
<Text size="sm">{t(item.label)}</Text>
|
||||
<Text size="xs" opacity={0.65}>
|
||||
{item.description}
|
||||
{t(item.description)}
|
||||
</Text>
|
||||
</div>
|
||||
{item.label === roleName && <IconCheck size={20} />}
|
||||
|
||||
@ -1,15 +1,9 @@
|
||||
import React from "react";
|
||||
import {
|
||||
IconLayoutSidebarRightCollapse,
|
||||
IconLayoutSidebarRightExpand,
|
||||
IconLayoutSidebarRightExpand
|
||||
} from "@tabler/icons-react";
|
||||
import {
|
||||
ActionIcon,
|
||||
BoxProps,
|
||||
ElementProps,
|
||||
MantineColor,
|
||||
MantineSize,
|
||||
} from "@mantine/core";
|
||||
import React from "react";
|
||||
import { ActionIcon, BoxProps, ElementProps, MantineColor, MantineSize } from "@mantine/core";
|
||||
|
||||
export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
|
||||
size?: MantineSize | `compact-${MantineSize}` | (string & {});
|
||||
@ -17,18 +11,18 @@ export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
|
||||
opened?: boolean;
|
||||
}
|
||||
|
||||
export default function SidebarToggle({
|
||||
opened,
|
||||
size = "sm",
|
||||
...others
|
||||
}: SidebarToggleProps) {
|
||||
return (
|
||||
<ActionIcon size={size} {...others} variant="subtle" color="gray">
|
||||
{opened ? (
|
||||
<IconLayoutSidebarRightExpand />
|
||||
) : (
|
||||
<IconLayoutSidebarRightCollapse />
|
||||
)}
|
||||
</ActionIcon>
|
||||
);
|
||||
}
|
||||
const SidebarToggle = React.forwardRef<HTMLButtonElement, SidebarToggleProps>(
|
||||
({ opened, size = "sm", ...others }, ref) => {
|
||||
return (
|
||||
<ActionIcon size={size} {...others} variant="subtle" color="gray" ref={ref}>
|
||||
{opened ? (
|
||||
<IconLayoutSidebarRightExpand />
|
||||
) : (
|
||||
<IconLayoutSidebarRightCollapse />
|
||||
)}
|
||||
</ActionIcon>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default SidebarToggle;
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import Cookies from "js-cookie";
|
||||
import { createJSONStorage, atomWithStorage } from "jotai/utils";
|
||||
import { ITokens } from '../types/auth.types';
|
||||
|
||||
|
||||
const cookieStorage = createJSONStorage<ITokens>(() => {
|
||||
const cookieStorage = createJSONStorage<any>(() => {
|
||||
return {
|
||||
getItem: () => Cookies.get("authTokens"),
|
||||
setItem: (key, value) => Cookies.set(key, value),
|
||||
setItem: (key, value) => Cookies.set(key, value, { expires: 30 }),
|
||||
removeItem: (key) => Cookies.remove(key),
|
||||
};
|
||||
});
|
||||
|
||||
export const authTokensAtom = atomWithStorage<ITokens | null>("authTokens", null, cookieStorage);
|
||||
export const authTokensAtom = atomWithStorage<any | null>(
|
||||
"authTokens",
|
||||
null,
|
||||
cookieStorage,
|
||||
);
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
import { useState } from "react";
|
||||
import * as z from "zod";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import useAuth from "@/features/auth/hooks/use-auth";
|
||||
import { IForgotPassword } from "@/features/auth/types/auth.types";
|
||||
import { Box, Button, Container, Text, TextInput, Title } from "@mantine/core";
|
||||
import classes from "./auth.module.css";
|
||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, { message: "Email is required" })
|
||||
.email({ message: "Invalid email address" }),
|
||||
});
|
||||
|
||||
export function ForgotPasswordForm() {
|
||||
const { t } = useTranslation();
|
||||
const { forgotPassword, isLoading } = useAuth();
|
||||
const [isTokenSent, setIsTokenSent] = useState<boolean>(false);
|
||||
useRedirectIfAuthenticated();
|
||||
|
||||
const form = useForm<IForgotPassword>({
|
||||
validate: zodResolver(formSchema),
|
||||
initialValues: {
|
||||
email: "",
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(data: IForgotPassword) {
|
||||
if (await forgotPassword(data)) {
|
||||
setIsTokenSent(true);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container size={420} my={40} className={classes.container}>
|
||||
<Box p="xl" mt={200}>
|
||||
<Title order={2} ta="center" fw={500} mb="md">
|
||||
{t("Forgot password")}
|
||||
</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
{!isTokenSent && (
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder="email@example.com"
|
||||
variant="filled"
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isTokenSent && (
|
||||
<Text>
|
||||
{t(
|
||||
"A password reset link has been sent to your email. Please check your inbox.",
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!isTokenSent && (
|
||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||
{t("Send reset link")}
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@ -17,15 +17,17 @@ import useAuth from "@/features/auth/hooks/use-auth";
|
||||
import classes from "@/features/auth/components/auth.module.css";
|
||||
import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-query.ts";
|
||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2),
|
||||
name: z.string().trim().min(1),
|
||||
password: z.string().min(8),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
export function InviteSignUpForm() {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@ -55,7 +57,7 @@ export function InviteSignUpForm() {
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <div>invalid invitation link</div>;
|
||||
return <div>{t("invalid invitation link")}</div>;
|
||||
}
|
||||
|
||||
if (!invitation) {
|
||||
@ -66,7 +68,7 @@ export function InviteSignUpForm() {
|
||||
<Container size={420} my={40} className={classes.container}>
|
||||
<Box p="xl" mt={200}>
|
||||
<Title order={2} ta="center" fw={500} mb="md">
|
||||
Join the workspace
|
||||
{t("Join the workspace")}
|
||||
</Title>
|
||||
|
||||
<Stack align="stretch" justify="center" gap="xl">
|
||||
@ -74,8 +76,8 @@ export function InviteSignUpForm() {
|
||||
<TextInput
|
||||
id="name"
|
||||
type="text"
|
||||
label="Name"
|
||||
placeholder="enter your full name"
|
||||
label={t("Name")}
|
||||
placeholder={t("enter your full name")}
|
||||
variant="filled"
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
@ -83,7 +85,7 @@ export function InviteSignUpForm() {
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
label={t("Email")}
|
||||
value={invitation.email}
|
||||
disabled
|
||||
variant="filled"
|
||||
@ -91,14 +93,14 @@ export function InviteSignUpForm() {
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Your password"
|
||||
label={t("Password")}
|
||||
placeholder={t("Your password")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||
Sign Up
|
||||
{t("Sign Up")}
|
||||
</Button>
|
||||
</form>
|
||||
</Stack>
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import * as React from "react";
|
||||
import * as z from "zod";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import useAuth from "@/features/auth/hooks/use-auth";
|
||||
@ -10,9 +9,13 @@ import {
|
||||
Button,
|
||||
PasswordInput,
|
||||
Box,
|
||||
Anchor,
|
||||
} from "@mantine/core";
|
||||
import classes from "./auth.module.css";
|
||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z
|
||||
@ -23,6 +26,7 @@ const formSchema = z.object({
|
||||
});
|
||||
|
||||
export function LoginForm() {
|
||||
const { t } = useTranslation();
|
||||
const { signIn, isLoading } = useAuth();
|
||||
useRedirectIfAuthenticated();
|
||||
|
||||
@ -42,30 +46,40 @@ export function LoginForm() {
|
||||
<Container size={420} my={40} className={classes.container}>
|
||||
<Box p="xl" mt={200}>
|
||||
<Title order={2} ta="center" fw={500} mb="md">
|
||||
Login
|
||||
{t("Login")}
|
||||
</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
label={t("Email")}
|
||||
placeholder="email@example.com"
|
||||
variant="filled"
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Your password"
|
||||
label={t("Password")}
|
||||
placeholder={t("Your password")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
|
||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||
Sign In
|
||||
{t("Sign In")}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<Anchor
|
||||
to={APP_ROUTE.AUTH.FORGOT_PASSWORD}
|
||||
component={Link}
|
||||
underline="never"
|
||||
size="sm"
|
||||
>
|
||||
{t("Forgot your password?")}
|
||||
</Anchor>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
import * as z from "zod";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import useAuth from "@/features/auth/hooks/use-auth";
|
||||
import { IPasswordReset } from "@/features/auth/types/auth.types";
|
||||
import { Box, Button, Container, PasswordInput, Title } from "@mantine/core";
|
||||
import classes from "./auth.module.css";
|
||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
newPassword: z
|
||||
.string()
|
||||
.min(8, { message: "Password must contain at least 8 characters" }),
|
||||
});
|
||||
|
||||
interface PasswordResetFormProps {
|
||||
resetToken?: string;
|
||||
}
|
||||
|
||||
export function PasswordResetForm({ resetToken }: PasswordResetFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const { passwordReset, isLoading } = useAuth();
|
||||
useRedirectIfAuthenticated();
|
||||
|
||||
const form = useForm<IPasswordReset>({
|
||||
validate: zodResolver(formSchema),
|
||||
initialValues: {
|
||||
newPassword: "",
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(data: IPasswordReset) {
|
||||
await passwordReset({
|
||||
token: resetToken,
|
||||
newPassword: data.newPassword,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Container size={420} my={40} className={classes.container}>
|
||||
<Box p="xl" mt={200}>
|
||||
<Title order={2} ta="center" fw={500} mb="md">
|
||||
{t("Password reset")}
|
||||
</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<PasswordInput
|
||||
label={t("New password")}
|
||||
placeholder={t("Your new password")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("newPassword")}
|
||||
/>
|
||||
|
||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||
{t("Set password")}
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@ -13,10 +13,11 @@ import {
|
||||
import { ISetupWorkspace } from "@/features/auth/types/auth.types";
|
||||
import useAuth from "@/features/auth/hooks/use-auth";
|
||||
import classes from "@/features/auth/components/auth.module.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
workspaceName: z.string().min(2).max(60),
|
||||
name: z.string().min(2).max(60),
|
||||
workspaceName: z.string().trim().min(3).max(50),
|
||||
name: z.string().min(1).max(50),
|
||||
email: z
|
||||
.string()
|
||||
.min(1, { message: "email is required" })
|
||||
@ -25,6 +26,7 @@ const formSchema = z.object({
|
||||
});
|
||||
|
||||
export function SetupWorkspaceForm() {
|
||||
const { t } = useTranslation();
|
||||
const { setupWorkspace, isLoading } = useAuth();
|
||||
// useRedirectIfAuthenticated();
|
||||
|
||||
@ -46,15 +48,15 @@ export function SetupWorkspaceForm() {
|
||||
<Container size={420} my={40} className={classes.container}>
|
||||
<Box p="xl" mt={200}>
|
||||
<Title order={2} ta="center" fw={500} mb="md">
|
||||
Create workspace
|
||||
{t("Create workspace")}
|
||||
</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<TextInput
|
||||
id="workspaceName"
|
||||
type="text"
|
||||
label="Workspace Name"
|
||||
placeholder="e.g ACME Inc"
|
||||
label={t("Workspace Name")}
|
||||
placeholder={t("e.g ACME Inc")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("workspaceName")}
|
||||
@ -63,8 +65,8 @@ export function SetupWorkspaceForm() {
|
||||
<TextInput
|
||||
id="name"
|
||||
type="text"
|
||||
label="Your Name"
|
||||
placeholder="enter your full name"
|
||||
label={t("Your Name")}
|
||||
placeholder={t("enter your full name")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("name")}
|
||||
@ -73,7 +75,7 @@ export function SetupWorkspaceForm() {
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
label="Your Email"
|
||||
label={t("Your Email")}
|
||||
placeholder="email@example.com"
|
||||
variant="filled"
|
||||
mt="md"
|
||||
@ -81,14 +83,14 @@ export function SetupWorkspaceForm() {
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Enter a strong password"
|
||||
label={t("Password")}
|
||||
placeholder={t("Enter a strong password")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||
Setup workspace
|
||||
{t("Setup workspace")}
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
|
||||
@ -1,40 +1,45 @@
|
||||
import { useState } from "react";
|
||||
import { login, setupWorkspace } from "@/features/auth/services/auth-service";
|
||||
import {
|
||||
forgotPassword,
|
||||
login,
|
||||
logout,
|
||||
passwordReset,
|
||||
setupWorkspace,
|
||||
verifyUserToken,
|
||||
} from "@/features/auth/services/auth-service";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAtom } from "jotai";
|
||||
import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
|
||||
import {
|
||||
IForgotPassword,
|
||||
ILogin,
|
||||
IRegister,
|
||||
IPasswordReset,
|
||||
ISetupWorkspace,
|
||||
IVerifyUserToken,
|
||||
} from "@/features/auth/types/auth.types";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { IAcceptInvite } from "@/features/workspace/types/workspace.types.ts";
|
||||
import { acceptInvitation } from "@/features/workspace/services/workspace-service.ts";
|
||||
import Cookies from "js-cookie";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import { RESET } from "jotai/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function useAuth() {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [, setCurrentUser] = useAtom(currentUserAtom);
|
||||
const [authToken, setAuthToken] = useAtom(authTokensAtom);
|
||||
|
||||
const handleSignIn = async (data: ILogin) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const res = await login(data);
|
||||
await login(data);
|
||||
setIsLoading(false);
|
||||
setAuthToken(res.tokens);
|
||||
|
||||
navigate(APP_ROUTE.HOME);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
setIsLoading(false);
|
||||
console.log(err);
|
||||
notifications.show({
|
||||
message: err.response?.data.message,
|
||||
color: "red",
|
||||
@ -46,12 +51,8 @@ export default function useAuth() {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const res = await acceptInvitation(data);
|
||||
await acceptInvitation(data);
|
||||
setIsLoading(false);
|
||||
|
||||
console.log(res);
|
||||
setAuthToken(res.tokens);
|
||||
|
||||
navigate(APP_ROUTE.HOME);
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
@ -68,9 +69,6 @@ export default function useAuth() {
|
||||
try {
|
||||
const res = await setupWorkspace(data);
|
||||
setIsLoading(false);
|
||||
|
||||
setAuthToken(res.tokens);
|
||||
|
||||
navigate(APP_ROUTE.HOME);
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
@ -81,42 +79,75 @@ export default function useAuth() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleIsAuthenticated = async () => {
|
||||
if (!authToken) {
|
||||
return false;
|
||||
}
|
||||
const handlePasswordReset = async (data: IPasswordReset) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const accessToken = authToken.accessToken;
|
||||
const payload = jwtDecode(accessToken);
|
||||
|
||||
// true if jwt is active
|
||||
const now = Date.now().valueOf() / 1000;
|
||||
return payload.exp >= now;
|
||||
await passwordReset(data);
|
||||
setIsLoading(false);
|
||||
navigate(APP_ROUTE.HOME);
|
||||
notifications.show({
|
||||
message: t("Password reset was successful"),
|
||||
});
|
||||
} catch (err) {
|
||||
console.log("invalid jwt token", err);
|
||||
return false;
|
||||
setIsLoading(false);
|
||||
notifications.show({
|
||||
message: err.response?.data.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const hasTokens = (): boolean => {
|
||||
return !!authToken;
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
setAuthToken(null);
|
||||
setCurrentUser(null);
|
||||
Cookies.remove("authTokens");
|
||||
navigate(APP_ROUTE.AUTH.LOGIN);
|
||||
setCurrentUser(RESET);
|
||||
await logout();
|
||||
window.location.replace(APP_ROUTE.AUTH.LOGIN);
|
||||
};
|
||||
|
||||
const handleForgotPassword = async (data: IForgotPassword) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await forgotPassword(data);
|
||||
setIsLoading(false);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
setIsLoading(false);
|
||||
notifications.show({
|
||||
message: err.response?.data.message,
|
||||
color: "red",
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyUserToken = async (data: IVerifyUserToken) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await verifyUserToken(data);
|
||||
setIsLoading(false);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
setIsLoading(false);
|
||||
notifications.show({
|
||||
message: err.response?.data.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
signIn: handleSignIn,
|
||||
invitationSignup: handleInvitationSignUp,
|
||||
setupWorkspace: handleSetupWorkspace,
|
||||
isAuthenticated: handleIsAuthenticated,
|
||||
forgotPassword: handleForgotPassword,
|
||||
passwordReset: handlePasswordReset,
|
||||
verifyUserToken: handleVerifyUserToken,
|
||||
logout: handleLogout,
|
||||
hasTokens,
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,19 +1,15 @@
|
||||
import { useEffect } from "react";
|
||||
import useCurrentUser from "@/features/user/hooks/use-current-user.ts";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import useAuth from "@/features/auth/hooks/use-auth.ts";
|
||||
|
||||
export function useRedirectIfAuthenticated() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { data, isLoading } = useCurrentUser();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
const validAuth = await isAuthenticated();
|
||||
if (validAuth) {
|
||||
navigate("/home");
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, [isAuthenticated]);
|
||||
if (data && data?.user) {
|
||||
navigate(APP_ROUTE.HOME);
|
||||
}
|
||||
}, [isLoading, data]);
|
||||
}
|
||||
|
||||
28
apps/client/src/features/auth/queries/auth-query.tsx
Normal file
28
apps/client/src/features/auth/queries/auth-query.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { useQuery, UseQueryResult } from "@tanstack/react-query";
|
||||
import { getCollabToken, verifyUserToken } from "../services/auth-service";
|
||||
import { ICollabToken, IVerifyUserToken } from "../types/auth.types";
|
||||
|
||||
export function useVerifyUserTokenQuery(
|
||||
verify: IVerifyUserToken,
|
||||
): UseQueryResult<any, Error> {
|
||||
return useQuery({
|
||||
queryKey: ["verify-token", verify],
|
||||
queryFn: () => verifyUserToken(verify),
|
||||
enabled: !!verify.token,
|
||||
staleTime: 0,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCollabToken(): UseQueryResult<ICollabToken, Error> {
|
||||
return useQuery({
|
||||
queryKey: ["collab-token"],
|
||||
queryFn: () => getCollabToken(),
|
||||
staleTime: 24 * 60 * 60 * 1000, //24hrs
|
||||
refetchInterval: 20 * 60 * 60 * 1000, //20hrs
|
||||
retry: 10,
|
||||
retryDelay: (retryAttempt) => {
|
||||
// Exponential backoff: 5s, 10s, 20s, etc.
|
||||
return 5000 * Math.pow(2, retryAttempt - 1);
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -1,22 +1,21 @@
|
||||
import api from "@/lib/api-client";
|
||||
import {
|
||||
IChangePassword,
|
||||
ICollabToken,
|
||||
IForgotPassword,
|
||||
ILogin,
|
||||
IRegister,
|
||||
IPasswordReset,
|
||||
ISetupWorkspace,
|
||||
ITokenResponse,
|
||||
IVerifyUserToken,
|
||||
} from "@/features/auth/types/auth.types";
|
||||
|
||||
export async function login(data: ILogin): Promise<ITokenResponse> {
|
||||
const req = await api.post<ITokenResponse>("/auth/login", data);
|
||||
return req.data;
|
||||
export async function login(data: ILogin): Promise<void> {
|
||||
await api.post<void>("/auth/login", data);
|
||||
}
|
||||
|
||||
/*
|
||||
export async function register(data: IRegister): Promise<ITokenResponse> {
|
||||
const req = await api.post<ITokenResponse>("/auth/register", data);
|
||||
return req.data;
|
||||
}*/
|
||||
export async function logout(): Promise<void> {
|
||||
await api.post<void>("/auth/logout");
|
||||
}
|
||||
|
||||
export async function changePassword(
|
||||
data: IChangePassword,
|
||||
@ -27,7 +26,24 @@ export async function changePassword(
|
||||
|
||||
export async function setupWorkspace(
|
||||
data: ISetupWorkspace,
|
||||
): Promise<ITokenResponse> {
|
||||
const req = await api.post<ITokenResponse>("/auth/setup", data);
|
||||
): Promise<any> {
|
||||
const req = await api.post<any>("/auth/setup", data);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function forgotPassword(data: IForgotPassword): Promise<void> {
|
||||
await api.post<void>("/auth/forgot-password", data);
|
||||
}
|
||||
|
||||
export async function passwordReset(data: IPasswordReset): Promise<void> {
|
||||
await api.post<void>("/auth/password-reset", data);
|
||||
}
|
||||
|
||||
export async function verifyUserToken(data: IVerifyUserToken): Promise<any> {
|
||||
return api.post<any>("/auth/verify-token", data);
|
||||
}
|
||||
|
||||
export async function getCollabToken(): Promise<ICollabToken> {
|
||||
const req = await api.post<ICollabToken>("/auth/collab-token");
|
||||
return req.data;
|
||||
}
|
||||
|
||||
@ -16,16 +16,25 @@ export interface ISetupWorkspace {
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ITokens {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export interface ITokenResponse {
|
||||
tokens: ITokens;
|
||||
}
|
||||
|
||||
export interface IChangePassword {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
export interface IForgotPassword {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface IPasswordReset {
|
||||
token?: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
export interface IVerifyUserToken {
|
||||
token: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ICollabToken {
|
||||
token: string;
|
||||
}
|
||||
|
||||
@ -1,14 +1,32 @@
|
||||
import { Button, Group } from '@mantine/core';
|
||||
import { Button, Group } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CommentActionsProps = {
|
||||
onSave: () => void;
|
||||
isLoading?: boolean;
|
||||
onCancel?: () => void;
|
||||
isCommentEditor?: boolean;
|
||||
};
|
||||
|
||||
function CommentActions({ onSave, isLoading }: CommentActionsProps) {
|
||||
function CommentActions({
|
||||
onSave,
|
||||
isLoading,
|
||||
onCancel,
|
||||
isCommentEditor,
|
||||
}: CommentActionsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group justify="flex-end" pt={2} wrap="nowrap">
|
||||
<Button size="compact-sm" loading={isLoading} onClick={onSave}>Save</Button>
|
||||
<Group justify="flex-end" pt="sm" wrap="nowrap">
|
||||
{isCommentEditor && (
|
||||
<Button size="compact-sm" variant="default" onClick={onCancel}>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button size="compact-sm" loading={isLoading} onClick={onSave}>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import { useCreateCommentMutation } from "@/features/comment/queries/comment-que
|
||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface CommentDialogProps {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
@ -21,6 +22,7 @@ interface CommentDialogProps {
|
||||
}
|
||||
|
||||
function CommentDialog({ editor, pageId }: CommentDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [comment, setComment] = useState("");
|
||||
const [, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
||||
const [, setActiveCommentId] = useAtom(activeCommentIdAtom);
|
||||
@ -107,7 +109,7 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) {
|
||||
|
||||
<CommentEditor
|
||||
onUpdate={handleCommentEditorChange}
|
||||
placeholder="Write a comment"
|
||||
placeholder={t("Write a comment")}
|
||||
editable={true}
|
||||
autofocus={true}
|
||||
/>
|
||||
|
||||
@ -7,6 +7,7 @@ import classes from "./comment.module.css";
|
||||
import { useFocusWithin } from "@mantine/hooks";
|
||||
import clsx from "clsx";
|
||||
import { forwardRef, useEffect, useImperativeHandle } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface CommentEditorProps {
|
||||
defaultContent?: any;
|
||||
@ -27,6 +28,7 @@ const CommentEditor = forwardRef(
|
||||
}: CommentEditorProps,
|
||||
ref,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const { ref: focusRef, focused } = useFocusWithin();
|
||||
|
||||
const commentEditor = useEditor({
|
||||
@ -36,7 +38,7 @@ const CommentEditor = forwardRef(
|
||||
dropcursor: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: placeholder || "Reply...",
|
||||
placeholder: placeholder || t("Reply..."),
|
||||
}),
|
||||
Underline,
|
||||
Link,
|
||||
@ -46,6 +48,8 @@ const CommentEditor = forwardRef(
|
||||
},
|
||||
content: defaultContent,
|
||||
editable,
|
||||
immediatelyRender: true,
|
||||
shouldRerenderOnTransaction: false,
|
||||
autofocus: (autofocus && "end") || false,
|
||||
});
|
||||
|
||||
|
||||
@ -24,7 +24,6 @@ function CommentListItem({ comment }: CommentListItemProps) {
|
||||
const { hovered, ref } = useHover();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const editor = useAtomValue(pageEditorAtom);
|
||||
const [content, setContent] = useState<string>(comment.content);
|
||||
const updateCommentMutation = useUpdateCommentMutation();
|
||||
@ -59,6 +58,9 @@ function CommentListItem({ comment }: CommentListItemProps) {
|
||||
function handleEditToggle() {
|
||||
setIsEditing(true);
|
||||
}
|
||||
function cancelEdit() {
|
||||
setIsEditing(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box ref={ref} pb="xs">
|
||||
@ -116,6 +118,8 @@ function CommentListItem({ comment }: CommentListItemProps) {
|
||||
<CommentActions
|
||||
onSave={handleUpdateComment}
|
||||
isLoading={isLoading}
|
||||
onCancel={cancelEdit}
|
||||
isCommentEditor={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -6,7 +6,6 @@ import {
|
||||
useCommentsQuery,
|
||||
useCreateCommentMutation,
|
||||
} from "@/features/comment/queries/comment-query";
|
||||
|
||||
import CommentEditor from "@/features/comment/components/comment-editor";
|
||||
import CommentActions from "@/features/comment/components/comment-actions";
|
||||
import { useFocusWithin } from "@mantine/hooks";
|
||||
@ -14,8 +13,10 @@ import { IComment } from "@/features/comment/types/comment.types.ts";
|
||||
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
||||
import { IPagination } from "@/lib/types.ts";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function CommentList() {
|
||||
const { t } = useTranslation();
|
||||
const { pageSlug } = useParams();
|
||||
const { data: page } = usePageQuery({ pageId: extractPageSlugId(pageSlug) });
|
||||
const {
|
||||
@ -79,11 +80,11 @@ function CommentList() {
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <div>Error loading comments.</div>;
|
||||
return <div>{t("Error loading comments.")}</div>;
|
||||
}
|
||||
|
||||
if (!comments || comments.items.length === 0) {
|
||||
return <>No comments yet.</>;
|
||||
return <>{t("No comments yet.")}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { ActionIcon, Menu } from '@mantine/core';
|
||||
import { IconDots, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { ActionIcon, Menu } from "@mantine/core";
|
||||
import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CommentMenuProps = {
|
||||
onEditComment: () => void;
|
||||
@ -8,34 +9,35 @@ type CommentMenuProps = {
|
||||
};
|
||||
|
||||
function CommentMenu({ onEditComment, onDeleteComment }: CommentMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
//@ts-ignore
|
||||
const openDeleteModal = () =>
|
||||
modals.openConfirmModal({
|
||||
title: 'Are you sure you want to delete this comment?',
|
||||
title: t("Are you sure you want to delete this comment?"),
|
||||
centered: true,
|
||||
labels: { confirm: 'Delete', cancel: 'Cancel' },
|
||||
confirmProps: { color: 'red' },
|
||||
labels: { confirm: t("Delete"), cancel: t("Cancel") },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: onDeleteComment,
|
||||
});
|
||||
|
||||
return (
|
||||
<Menu shadow="md" width={200}>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="default" style={{ border: 'none' }}>
|
||||
<ActionIcon variant="default" style={{ border: "none" }}>
|
||||
<IconDots size={20} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item onClick={onEditComment}
|
||||
leftSection={<IconEdit size={14} />}>
|
||||
Edit comment
|
||||
<Menu.Item onClick={onEditComment} leftSection={<IconEdit size={14} />}>
|
||||
{t("Edit comment")}
|
||||
</Menu.Item>
|
||||
<Menu.Item leftSection={<IconTrash size={14} />}
|
||||
onClick={openDeleteModal}
|
||||
<Menu.Item
|
||||
leftSection={<IconTrash size={14} />}
|
||||
onClick={openDeleteModal}
|
||||
>
|
||||
Delete comment
|
||||
{t("Delete comment")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@ -1,34 +1,44 @@
|
||||
import { ActionIcon } from '@mantine/core';
|
||||
import { IconCircleCheck } from '@tabler/icons-react';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { useResolveCommentMutation } from '@/features/comment/queries/comment-query';
|
||||
import { ActionIcon } from "@mantine/core";
|
||||
import { IconCircleCheck } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { useResolveCommentMutation } from "@/features/comment/queries/comment-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function ResolveComment({ commentId, pageId, resolvedAt }) {
|
||||
const { t } = useTranslation();
|
||||
const resolveCommentMutation = useResolveCommentMutation();
|
||||
|
||||
const isResolved = resolvedAt != null;
|
||||
const iconColor = isResolved ? 'green' : 'gray';
|
||||
const iconColor = isResolved ? "green" : "gray";
|
||||
|
||||
//@ts-ignore
|
||||
const openConfirmModal = () =>
|
||||
modals.openConfirmModal({
|
||||
title: 'Are you sure you want to resolve this comment thread?',
|
||||
title: t("Are you sure you want to resolve this comment thread?"),
|
||||
centered: true,
|
||||
labels: { confirm: 'Confirm', cancel: 'Cancel' },
|
||||
labels: { confirm: t("Confirm"), cancel: t("Cancel") },
|
||||
onConfirm: handleResolveToggle,
|
||||
});
|
||||
|
||||
const handleResolveToggle = async () => {
|
||||
try {
|
||||
await resolveCommentMutation.mutateAsync({ commentId, resolved: !isResolved });
|
||||
await resolveCommentMutation.mutateAsync({
|
||||
commentId,
|
||||
resolved: !isResolved,
|
||||
});
|
||||
//TODO: remove comment mark
|
||||
// Remove comment thread from state on resolve
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle resolved state:', error);
|
||||
console.error("Failed to toggle resolved state:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionIcon onClick={openConfirmModal} variant="default" style={{ border: 'none' }}>
|
||||
<ActionIcon
|
||||
onClick={openConfirmModal}
|
||||
variant="default"
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
<IconCircleCheck size={20} stroke={2} color={iconColor} />
|
||||
</ActionIcon>
|
||||
);
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
} from "@/features/comment/types/comment.types";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { IPagination } from "@/lib/types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const RQ_KEY = (pageId: string) => ["comments", pageId];
|
||||
|
||||
@ -25,7 +26,6 @@ export function useCommentsQuery(
|
||||
params: ICommentParams,
|
||||
): UseQueryResult<IPagination<IComment>, Error> {
|
||||
return useQuery({
|
||||
// eslint-disable-next-line @tanstack/query/exhaustive-deps
|
||||
queryKey: RQ_KEY(params.pageId),
|
||||
queryFn: () => getPageComments(params),
|
||||
enabled: !!params.pageId,
|
||||
@ -34,6 +34,7 @@ export function useCommentsQuery(
|
||||
|
||||
export function useCreateCommentMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation<IComment, Error, Partial<IComment>>({
|
||||
mutationFn: (data) => createComment(data),
|
||||
@ -46,28 +47,37 @@ export function useCreateCommentMutation() {
|
||||
//}
|
||||
|
||||
queryClient.refetchQueries({ queryKey: RQ_KEY(data.pageId) });
|
||||
notifications.show({ message: "Comment created successfully" });
|
||||
notifications.show({ message: t("Comment created successfully") });
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({ message: "Error creating comment", color: "red" });
|
||||
notifications.show({
|
||||
message: t("Error creating comment"),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateCommentMutation() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation<IComment, Error, Partial<IComment>>({
|
||||
mutationFn: (data) => updateComment(data),
|
||||
onSuccess: (data) => {
|
||||
notifications.show({ message: "Comment updated successfully" });
|
||||
notifications.show({ message: t("Comment updated successfully") });
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({ message: "Failed to update comment", color: "red" });
|
||||
notifications.show({
|
||||
message: t("Failed to update comment"),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteCommentMutation(pageId?: string) {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (commentId: string) => deleteComment(commentId),
|
||||
@ -87,16 +97,20 @@ export function useDeleteCommentMutation(pageId?: string) {
|
||||
});
|
||||
}
|
||||
|
||||
notifications.show({ message: "Comment deleted successfully" });
|
||||
notifications.show({ message: t("Comment deleted successfully") });
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({ message: "Failed to delete comment", color: "red" });
|
||||
notifications.show({
|
||||
message: t("Failed to delete comment"),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useResolveCommentMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: IResolveComment) => resolveComment(data),
|
||||
@ -115,11 +129,11 @@ export function useResolveCommentMutation() {
|
||||
queryClient.setQueryData(RQ_KEY(data.pageId), updatedComments);
|
||||
}*/
|
||||
|
||||
notifications.show({ message: "Comment resolved successfully" });
|
||||
notifications.show({ message: t("Comment resolved successfully") });
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
message: "Failed to resolve comment",
|
||||
message: t("Failed to resolve comment"),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { atom } from 'jotai';
|
||||
import { Editor } from '@tiptap/core';
|
||||
import { atom } from "jotai";
|
||||
import { Editor } from "@tiptap/core";
|
||||
|
||||
export const pageEditorAtom = atom<Editor | null>(null);
|
||||
|
||||
export const titleEditorAtom = atom<Editor | null>(null);
|
||||
|
||||
export const yjsConnectionStatusAtom = atom<string>("");
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { Group, Text, Paper, ActionIcon } from "@mantine/core";
|
||||
import { getFileUrl } from "@/lib/config.ts";
|
||||
import { IconDownload, IconPaperclip } from "@tabler/icons-react";
|
||||
import { useHover } from "@mantine/hooks";
|
||||
import { formatBytes } from "@/lib";
|
||||
|
||||
export default function AttachmentView(props: NodeViewProps) {
|
||||
const { node, selected } = props;
|
||||
const { url, name, size } = node.attrs;
|
||||
const { hovered, ref } = useHover();
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
<Paper withBorder p="4px" ref={ref} data-drag-handle>
|
||||
<Group
|
||||
justify="space-between"
|
||||
gap="xl"
|
||||
style={{ cursor: "pointer" }}
|
||||
wrap="nowrap"
|
||||
h={25}
|
||||
>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<IconPaperclip size={20} />
|
||||
|
||||
<Text component="span" size="md" truncate="end">
|
||||
{name}
|
||||
</Text>
|
||||
|
||||
<Text component="span" size="sm" c="dimmed" inline>
|
||||
{formatBytes(size)}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{selected || hovered ? (
|
||||
<a href={getFileUrl(url)} target="_blank">
|
||||
<ActionIcon variant="default" aria-label="download file">
|
||||
<IconDownload size={18} />
|
||||
</ActionIcon>
|
||||
</a>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Group>
|
||||
</Paper>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import { handleAttachmentUpload } from "@docmost/editor-ext";
|
||||
import { uploadFile } from "@/features/page/services/page-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { getFileUploadSizeLimit } from "@/lib/config.ts";
|
||||
import { formatBytes } from "@/lib";
|
||||
import i18n from "@/i18n.ts";
|
||||
|
||||
export const uploadAttachmentAction = handleAttachmentUpload({
|
||||
onUpload: async (file: File, pageId: string): Promise<any> => {
|
||||
try {
|
||||
return await uploadFile(file, pageId);
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
color: "red",
|
||||
message: err?.response.data.message,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
validateFn: (file) => {
|
||||
if (file.type.includes("image/") || file.type.includes("video/")) {
|
||||
return false;
|
||||
}
|
||||
if (file.size > getFileUploadSizeLimit()) {
|
||||
notifications.show({
|
||||
color: "red",
|
||||
message: i18n.t("File exceeds the {{limit}} attachment limit", {
|
||||
limit: formatBytes(getFileUploadSizeLimit()),
|
||||
}),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
@ -18,14 +18,16 @@ import classes from "./bubble-menu.module.css";
|
||||
import { ActionIcon, rem, Tooltip } from "@mantine/core";
|
||||
import { ColorSelector } from "./color-selector";
|
||||
import { NodeSelector } from "./node-selector";
|
||||
import { TextAlignmentSelector } from "./text-alignment-selector";
|
||||
import {
|
||||
draftCommentIdAtom,
|
||||
showCommentPopupAtom,
|
||||
} from "@/features/comment/atoms/comment-atom";
|
||||
import { useAtom } from "jotai";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { v7 as uuid7 } from "uuid";
|
||||
import { isCellSelection, isTextSelected } from "@docmost/editor-ext";
|
||||
import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface BubbleMenuItem {
|
||||
name: string;
|
||||
@ -39,6 +41,7 @@ type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children" | "editor"> & {
|
||||
};
|
||||
|
||||
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
||||
const [, setDraftCommentId] = useAtom(draftCommentIdAtom);
|
||||
const showCommentPopupRef = useRef(showCommentPopup);
|
||||
@ -49,31 +52,31 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
|
||||
const items: BubbleMenuItem[] = [
|
||||
{
|
||||
name: "bold",
|
||||
name: "Bold",
|
||||
isActive: () => props.editor.isActive("bold"),
|
||||
command: () => props.editor.chain().focus().toggleBold().run(),
|
||||
icon: IconBold,
|
||||
},
|
||||
{
|
||||
name: "italic",
|
||||
name: "Italic",
|
||||
isActive: () => props.editor.isActive("italic"),
|
||||
command: () => props.editor.chain().focus().toggleItalic().run(),
|
||||
icon: IconItalic,
|
||||
},
|
||||
{
|
||||
name: "underline",
|
||||
name: "Underline",
|
||||
isActive: () => props.editor.isActive("underline"),
|
||||
command: () => props.editor.chain().focus().toggleUnderline().run(),
|
||||
icon: IconUnderline,
|
||||
},
|
||||
{
|
||||
name: "strike",
|
||||
name: "Strike",
|
||||
isActive: () => props.editor.isActive("strike"),
|
||||
command: () => props.editor.chain().focus().toggleStrike().run(),
|
||||
icon: IconStrikethrough,
|
||||
},
|
||||
{
|
||||
name: "code",
|
||||
name: "Code",
|
||||
isActive: () => props.editor.isActive("code"),
|
||||
command: () => props.editor.chain().focus().toggleCode().run(),
|
||||
icon: IconCode,
|
||||
@ -81,10 +84,10 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
];
|
||||
|
||||
const commentItem: BubbleMenuItem = {
|
||||
name: "comment",
|
||||
name: "Comment",
|
||||
isActive: () => props.editor.isActive("comment"),
|
||||
command: () => {
|
||||
const commentId = uuidv4();
|
||||
const commentId = uuid7();
|
||||
|
||||
props.editor.chain().focus().setCommentDecoration().run();
|
||||
setDraftCommentId(commentId);
|
||||
@ -115,6 +118,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
moveTransition: "transform 0.15s ease-out",
|
||||
onHide: () => {
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsTextAlignmentOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
},
|
||||
@ -122,6 +126,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
};
|
||||
|
||||
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
|
||||
const [isTextAlignmentSelectorOpen, setIsTextAlignmentOpen] = useState(false);
|
||||
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
|
||||
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
||||
|
||||
@ -133,18 +138,32 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
isOpen={isNodeSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
||||
setIsTextAlignmentOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextAlignmentSelector
|
||||
editor={props.editor}
|
||||
isOpen={isTextAlignmentSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsTextAlignmentOpen(!isTextAlignmentSelectorOpen);
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<ActionIcon.Group>
|
||||
{items.map((item, index) => (
|
||||
<Tooltip key={index} label={item.name} withArrow>
|
||||
<Tooltip key={index} label={t(item.name)} withArrow>
|
||||
<ActionIcon
|
||||
key={index}
|
||||
variant="default"
|
||||
size="lg"
|
||||
radius="0"
|
||||
aria-label={item.name}
|
||||
aria-label={t(item.name)}
|
||||
className={clsx({ [classes.active]: item.isActive() })}
|
||||
style={{ border: "none" }}
|
||||
onClick={item.command}
|
||||
@ -160,6 +179,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
isOpen={isLinkSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsLinkSelectorOpen(!isLinkSelectorOpen);
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsTextAlignmentOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -168,6 +190,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
isOpen={isColorSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsColorSelectorOpen(!isColorSelectorOpen);
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsTextAlignmentOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -175,7 +200,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
variant="default"
|
||||
size="lg"
|
||||
radius="0"
|
||||
aria-label={commentItem.name}
|
||||
aria-label={t(commentItem.name)}
|
||||
style={{ border: "none" }}
|
||||
onClick={commentItem.command}
|
||||
>
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface BubbleColorMenuItem {
|
||||
name: string;
|
||||
@ -106,6 +107,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const activeColorItem = TEXT_COLORS.find(({ color }) =>
|
||||
editor.isActive("textStyle", { color }),
|
||||
);
|
||||
@ -117,7 +119,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
return (
|
||||
<Popover width={200} opened={isOpen} withArrow>
|
||||
<Popover.Target>
|
||||
<Tooltip label="Text color" withArrow>
|
||||
<Tooltip label={t("Text color")} withArrow>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
@ -136,8 +138,8 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
<Popover.Dropdown>
|
||||
{/* make mah responsive */}
|
||||
<ScrollArea.Autosize type="scroll" mah="400">
|
||||
<Text span c="dimmed" inherit>
|
||||
COLOR
|
||||
<Text span c="dimmed" tt="uppercase" inherit>
|
||||
{t("Color")}
|
||||
</Text>
|
||||
|
||||
<Button.Group orientation="vertical">
|
||||
@ -155,7 +157,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
}
|
||||
onClick={() => {
|
||||
editor.commands.unsetColor();
|
||||
name !== "Default" &&
|
||||
name !== t("Default") &&
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
@ -165,7 +167,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
}}
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
{name}
|
||||
{t(name)}
|
||||
</Button>
|
||||
))}
|
||||
</Button.Group>
|
||||
|
||||
@ -3,6 +3,7 @@ import { IconLink } from "@tabler/icons-react";
|
||||
import { ActionIcon, Popover, Tooltip } from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface LinkSelectorProps {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
@ -15,6 +16,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const onLink = useCallback(
|
||||
(url: string) => {
|
||||
setIsOpen(false);
|
||||
@ -32,7 +34,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
||||
withArrow
|
||||
>
|
||||
<Popover.Target>
|
||||
<Tooltip label="Add link" withArrow>
|
||||
<Tooltip label={t("Add link")} withArrow>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
} from "@tabler/icons-react";
|
||||
import { Popover, Button, ScrollArea } from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface NodeSelectorProps {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
@ -33,6 +34,8 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const items: BubbleMenuItem[] = [
|
||||
{
|
||||
name: "Text",
|
||||
@ -114,7 +117,7 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
rightSection={<IconChevronDown size={16} />}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{activeItem?.name}
|
||||
{t(activeItem?.name)}
|
||||
</Button>
|
||||
</Popover.Target>
|
||||
|
||||
@ -137,7 +140,7 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
}}
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
{item.name}
|
||||
{t(item.name)}
|
||||
</Button>
|
||||
))}
|
||||
</Button.Group>
|
||||
|
||||
@ -0,0 +1,107 @@
|
||||
import React, { Dispatch, FC, SetStateAction } from "react";
|
||||
import {
|
||||
IconAlignCenter,
|
||||
IconAlignJustified,
|
||||
IconAlignLeft,
|
||||
IconAlignRight,
|
||||
IconCheck,
|
||||
IconChevronDown,
|
||||
} from "@tabler/icons-react";
|
||||
import { Popover, Button, ScrollArea, rem } from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface TextAlignmentProps {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export interface BubbleMenuItem {
|
||||
name: string;
|
||||
icon: React.ElementType;
|
||||
command: () => void;
|
||||
isActive: () => boolean;
|
||||
}
|
||||
|
||||
export const TextAlignmentSelector: FC<TextAlignmentProps> = ({
|
||||
editor,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const items: BubbleMenuItem[] = [
|
||||
{
|
||||
name: "Align left",
|
||||
isActive: () => editor.isActive({ textAlign: "left" }),
|
||||
command: () => editor.chain().focus().setTextAlign("left").run(),
|
||||
icon: IconAlignLeft,
|
||||
},
|
||||
{
|
||||
name: "Align center",
|
||||
isActive: () => editor.isActive({ textAlign: "center" }),
|
||||
command: () => editor.chain().focus().setTextAlign("center").run(),
|
||||
icon: IconAlignCenter,
|
||||
},
|
||||
{
|
||||
name: "Align right",
|
||||
isActive: () => editor.isActive({ textAlign: "right" }),
|
||||
command: () => editor.chain().focus().setTextAlign("right").run(),
|
||||
icon: IconAlignRight,
|
||||
},
|
||||
{
|
||||
name: "Justify",
|
||||
isActive: () => editor.isActive({ textAlign: "justify" }),
|
||||
command: () => editor.chain().focus().setTextAlign("justify").run(),
|
||||
icon: IconAlignJustified,
|
||||
},
|
||||
];
|
||||
|
||||
const activeItem = items.filter((item) => item.isActive()).pop() ?? {
|
||||
name: "Multiple",
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover opened={isOpen} withArrow>
|
||||
<Popover.Target>
|
||||
<Button
|
||||
variant="default"
|
||||
style={{ border: "none", height: "34px" }}
|
||||
px="5"
|
||||
radius="0"
|
||||
rightSection={<IconChevronDown size={16} />}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<IconAlignLeft style={{ width: rem(16) }} stroke={2} />
|
||||
</Button>
|
||||
</Popover.Target>
|
||||
|
||||
<Popover.Dropdown>
|
||||
<ScrollArea.Autosize type="scroll" mah={400}>
|
||||
<Button.Group orientation="vertical">
|
||||
{items.map((item, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="default"
|
||||
leftSection={<item.icon size={16} />}
|
||||
rightSection={
|
||||
activeItem.name === item.name && <IconCheck size={16} />
|
||||
}
|
||||
justify="left"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
item.command();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
{t(item.name)}
|
||||
</Button>
|
||||
))}
|
||||
</Button.Group>
|
||||
</ScrollArea.Autosize>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@ -17,8 +17,10 @@ import {
|
||||
IconInfoCircleFilled,
|
||||
} from "@tabler/icons-react";
|
||||
import { CalloutType } from "@docmost/editor-ext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const shouldShow = useCallback(
|
||||
({ state }: ShouldShowProps) => {
|
||||
if (!state) {
|
||||
@ -71,11 +73,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
<ActionIcon.Group className="actionIconGroup">
|
||||
<Tooltip position="top" label="Info">
|
||||
<Tooltip position="top" label={t("Info")}>
|
||||
<ActionIcon
|
||||
onClick={() => setCalloutType("info")}
|
||||
size="lg"
|
||||
aria-label="Info"
|
||||
aria-label={t("Info")}
|
||||
variant={
|
||||
editor.isActive("callout", { type: "info" }) ? "light" : "default"
|
||||
}
|
||||
@ -84,11 +86,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Success">
|
||||
<Tooltip position="top" label={t("Success")}>
|
||||
<ActionIcon
|
||||
onClick={() => setCalloutType("success")}
|
||||
size="lg"
|
||||
aria-label="Success"
|
||||
aria-label={t("Success")}
|
||||
variant={
|
||||
editor.isActive("callout", { type: "success" })
|
||||
? "light"
|
||||
@ -99,11 +101,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Warning">
|
||||
<Tooltip position="top" label={t("Warning")}>
|
||||
<ActionIcon
|
||||
onClick={() => setCalloutType("warning")}
|
||||
size="lg"
|
||||
aria-label="Warning"
|
||||
aria-label={t("Warning")}
|
||||
variant={
|
||||
editor.isActive("callout", { type: "warning" })
|
||||
? "light"
|
||||
@ -114,11 +116,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Danger">
|
||||
<Tooltip position="top" label={t("Danger")}>
|
||||
<ActionIcon
|
||||
onClick={() => setCalloutType("danger")}
|
||||
size="lg"
|
||||
aria-label="Danger"
|
||||
aria-label={t("Danger")}
|
||||
variant={
|
||||
editor.isActive("callout", { type: "danger" })
|
||||
? "light"
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import {
|
||||
Editor,
|
||||
NodeViewContent,
|
||||
NodeViewProps,
|
||||
NodeViewWrapper,
|
||||
} from "@tiptap/react";
|
||||
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import {
|
||||
IconAlertTriangleFilled,
|
||||
IconCircleCheckFilled,
|
||||
|
||||
@ -0,0 +1,103 @@
|
||||
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { ActionIcon, CopyButton, Group, Select, Tooltip } from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||
import classes from "./code-block.module.css";
|
||||
import React from "react";
|
||||
import { Suspense } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const MermaidView = React.lazy(
|
||||
() => import("@/features/editor/components/code-block/mermaid-view.tsx"),
|
||||
);
|
||||
|
||||
export default function CodeBlockView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node, updateAttributes, extension, editor, getPos } = props;
|
||||
const { language } = node.attrs;
|
||||
const [languageValue, setLanguageValue] = useState<string | null>(
|
||||
language || null,
|
||||
);
|
||||
const [isSelected, setIsSelected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const updateSelection = () => {
|
||||
const { state } = editor;
|
||||
const { from, to } = state.selection;
|
||||
// Check if the selection intersects with the node's range
|
||||
const isNodeSelected =
|
||||
(from >= getPos() && from < getPos() + node.nodeSize) ||
|
||||
(to > getPos() && to <= getPos() + node.nodeSize);
|
||||
setIsSelected(isNodeSelected);
|
||||
};
|
||||
|
||||
editor.on("selectionUpdate", updateSelection);
|
||||
return () => {
|
||||
editor.off("selectionUpdate", updateSelection);
|
||||
};
|
||||
}, [editor, getPos(), node.nodeSize]);
|
||||
|
||||
function changeLanguage(language: string) {
|
||||
setLanguageValue(language);
|
||||
updateAttributes({
|
||||
language: language,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="codeBlock">
|
||||
<Group
|
||||
justify="flex-end"
|
||||
contentEditable={false}
|
||||
className={classes.menuGroup}
|
||||
>
|
||||
<Select
|
||||
placeholder="auto"
|
||||
checkIconPosition="right"
|
||||
data={extension.options.lowlight.listLanguages().sort()}
|
||||
value={languageValue}
|
||||
onChange={changeLanguage}
|
||||
searchable
|
||||
style={{ maxWidth: "130px" }}
|
||||
classNames={{ input: classes.selectInput }}
|
||||
disabled={!editor.isEditable}
|
||||
/>
|
||||
|
||||
<CopyButton value={node?.textContent} timeout={2000}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip
|
||||
label={copied ? t("Copied") : t("Copy")}
|
||||
withArrow
|
||||
position="right"
|
||||
>
|
||||
<ActionIcon
|
||||
color={copied ? "teal" : "gray"}
|
||||
variant="subtle"
|
||||
onClick={copy}
|
||||
>
|
||||
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</CopyButton>
|
||||
</Group>
|
||||
|
||||
<pre
|
||||
spellCheck="false"
|
||||
hidden={
|
||||
((language === "mermaid" && !editor.isEditable) ||
|
||||
(language === "mermaid" && !isSelected)) &&
|
||||
node.textContent.length > 0
|
||||
}
|
||||
>
|
||||
<NodeViewContent as="code" className={`language-${language}`} />
|
||||
</pre>
|
||||
|
||||
{language === "mermaid" && (
|
||||
<Suspense fallback={null}>
|
||||
<MermaidView props={props} />
|
||||
</Suspense>
|
||||
)}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
.selectInput {
|
||||
height: 25px;
|
||||
min-height: 25px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: light-dark(var(--mantine-color-red-8), var(--mantine-color-red-7));
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-gray-8));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mermaid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.menuGroup {
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
import { NodeViewProps } from "@tiptap/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import mermaid from "mermaid";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import classes from "./code-block.module.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
suppressErrorRendering: true,
|
||||
});
|
||||
|
||||
interface MermaidViewProps {
|
||||
props: NodeViewProps;
|
||||
}
|
||||
|
||||
export default function MermaidView({ props }: MermaidViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node } = props;
|
||||
const [preview, setPreview] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const id = `mermaid-${uuidv4()}`;
|
||||
|
||||
if (node.textContent.length > 0) {
|
||||
mermaid
|
||||
.render(id, node.textContent)
|
||||
.then((item) => {
|
||||
setPreview(item.svg);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (props.editor.isEditable) {
|
||||
setPreview(
|
||||
`<div class="${classes.error}">${t("Mermaid diagram error:")} ${err}</div>`,
|
||||
);
|
||||
} else {
|
||||
setPreview(
|
||||
`<div class="${classes.error}">${t("Invalid Mermaid diagram")}</div>`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [node.textContent]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes.mermaid}
|
||||
contentEditable={false}
|
||||
dangerouslySetInnerHTML={{ __html: preview }}
|
||||
></div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
import type { EditorView } from "@tiptap/pm/view";
|
||||
import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx";
|
||||
import { uploadVideoAction } from "@/features/editor/components/video/upload-video-action.tsx";
|
||||
import { uploadAttachmentAction } from "../attachment/upload-attachment-action";
|
||||
import { createMentionAction } from "@/features/editor/components/link/internal-link-paste.ts";
|
||||
import { Slice } from "@tiptap/pm/model";
|
||||
import { INTERNAL_LINK_REGEX } from "@/lib/constants.ts";
|
||||
|
||||
export const handlePaste = (
|
||||
view: EditorView,
|
||||
event: ClipboardEvent,
|
||||
pageId: string,
|
||||
creatorId?: string,
|
||||
) => {
|
||||
const clipboardData = event.clipboardData.getData("text/plain");
|
||||
|
||||
if (INTERNAL_LINK_REGEX.test(clipboardData)) {
|
||||
// we have to do this validation here to allow the default link extension to takeover if needs be
|
||||
event.preventDefault();
|
||||
const url = clipboardData.trim();
|
||||
const { from: pos, empty } = view.state.selection;
|
||||
const match = INTERNAL_LINK_REGEX.exec(url);
|
||||
const currentPageMatch = INTERNAL_LINK_REGEX.exec(window.location.href);
|
||||
|
||||
// pasted link must be from the same workspace/domain and must not be on a selection
|
||||
if (!empty || match[2] !== window.location.host) {
|
||||
// allow the default link extension to handle this
|
||||
return false;
|
||||
}
|
||||
|
||||
// for now, we only support internal links from the same space
|
||||
// compare space name
|
||||
if (currentPageMatch[4].toLowerCase() !== match[4].toLowerCase()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
createMentionAction(url, view, pos, creatorId);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.clipboardData?.files.length) {
|
||||
event.preventDefault();
|
||||
const [file] = Array.from(event.clipboardData.files);
|
||||
const pos = view.state.selection.from;
|
||||
|
||||
if (file) {
|
||||
uploadImageAction(file, view, pos, pageId);
|
||||
uploadVideoAction(file, view, pos, pageId);
|
||||
uploadAttachmentAction(file, view, pos, pageId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const handleFileDrop = (
|
||||
view: EditorView,
|
||||
event: DragEvent,
|
||||
moved: boolean,
|
||||
pageId: string,
|
||||
) => {
|
||||
if (!moved && event.dataTransfer?.files.length) {
|
||||
event.preventDefault();
|
||||
const [file] = Array.from(event.dataTransfer.files);
|
||||
const coordinates = view.posAtCoords({
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
});
|
||||
// here we deduct 1 from the pos or else the image will create an extra node
|
||||
if (file) {
|
||||
uploadImageAction(file, view, coordinates?.pos ?? 0 - 1, pageId);
|
||||
uploadVideoAction(file, view, coordinates?.pos ?? 0 - 1, pageId);
|
||||
uploadAttachmentAction(file, view, coordinates?.pos ?? 0 - 1, pageId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@ -1,45 +0,0 @@
|
||||
import type { EditorView } from "@tiptap/pm/view";
|
||||
import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx";
|
||||
import { uploadVideoAction } from "@/features/editor/components/video/upload-video-action.tsx";
|
||||
|
||||
export const handleFilePaste = (
|
||||
view: EditorView,
|
||||
event: ClipboardEvent,
|
||||
pageId: string,
|
||||
) => {
|
||||
if (event.clipboardData?.files.length) {
|
||||
event.preventDefault();
|
||||
const [file] = Array.from(event.clipboardData.files);
|
||||
const pos = view.state.selection.from;
|
||||
|
||||
if (file) {
|
||||
uploadImageAction(file, view, pos, pageId);
|
||||
uploadVideoAction(file, view, pos, pageId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const handleFileDrop = (
|
||||
view: EditorView,
|
||||
event: DragEvent,
|
||||
moved: boolean,
|
||||
pageId: string,
|
||||
) => {
|
||||
if (!moved && event.dataTransfer?.files.length) {
|
||||
event.preventDefault();
|
||||
const [file] = Array.from(event.dataTransfer.files);
|
||||
const coordinates = view.posAtCoords({
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
});
|
||||
// here we deduct 1 from the pos or else the image will create an extra node
|
||||
if (file) {
|
||||
uploadImageAction(file, view, coordinates?.pos ?? 0 - 1, pageId);
|
||||
uploadVideoAction(file, view, coordinates?.pos ?? 0 - 1, pageId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@ -1,28 +1,34 @@
|
||||
import React, { memo, useCallback, useState } from "react";
|
||||
import { Slider } from "@mantine/core";
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { Slider } from '@mantine/core';
|
||||
|
||||
export type ImageWidthProps = {
|
||||
onChange: (value: number) => void;
|
||||
value: number;
|
||||
width?: string;
|
||||
};
|
||||
|
||||
export const NodeWidthResize = memo(({ onChange, value }: ImageWidthProps) => {
|
||||
export const NodeWidthResize = memo(({ onChange, value, width }: ImageWidthProps) => {
|
||||
const [currentValue, setCurrentValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValue: number) => {
|
||||
onChange(newValue);
|
||||
},
|
||||
[onChange],
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Slider
|
||||
p={"sm"}
|
||||
p={'sm'}
|
||||
min={10}
|
||||
value={currentValue}
|
||||
onChange={setCurrentValue}
|
||||
onChangeEnd={handleChange}
|
||||
w={width || 100}
|
||||
label={(value) => `${value}%`}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -0,0 +1,82 @@
|
||||
import {
|
||||
BubbleMenu as BaseBubbleMenu,
|
||||
findParentNode,
|
||||
posToDOMRect,
|
||||
} from '@tiptap/react';
|
||||
import { useCallback } from 'react';
|
||||
import { sticky } from 'tippy.js';
|
||||
import { Node as PMNode } from 'prosemirror-model';
|
||||
import {
|
||||
EditorMenuProps,
|
||||
ShouldShowProps,
|
||||
} from '@/features/editor/components/table/types/types.ts';
|
||||
import { NodeWidthResize } from '@/features/editor/components/common/node-width-resize.tsx';
|
||||
|
||||
export function DrawioMenu({ editor }: EditorMenuProps) {
|
||||
const shouldShow = useCallback(
|
||||
({ state }: ShouldShowProps) => {
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return editor.isActive('drawio') && editor.getAttributes('drawio')?.src;
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
const getReferenceClientRect = useCallback(() => {
|
||||
const { selection } = editor.state;
|
||||
const predicate = (node: PMNode) => node.type.name === 'drawio';
|
||||
const parent = findParentNode(predicate)(selection);
|
||||
|
||||
if (parent) {
|
||||
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
||||
return dom.getBoundingClientRect();
|
||||
}
|
||||
|
||||
return posToDOMRect(editor.view, selection.from, selection.to);
|
||||
}, [editor]);
|
||||
|
||||
const onWidthChange = useCallback(
|
||||
(value: number) => {
|
||||
editor.commands.updateAttributes('drawio', { width: `${value}%` });
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey={`drawio-menu}`}
|
||||
updateDelay={0}
|
||||
tippyOptions={{
|
||||
getReferenceClientRect,
|
||||
offset: [0, 8],
|
||||
zIndex: 99,
|
||||
popperOptions: {
|
||||
modifiers: [{ name: 'flip', enabled: false }],
|
||||
},
|
||||
plugins: [sticky],
|
||||
sticky: 'popper',
|
||||
}}
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{editor.getAttributes('drawio')?.width && (
|
||||
<NodeWidthResize
|
||||
onChange={onWidthChange}
|
||||
value={parseInt(editor.getAttributes('drawio').width)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</BaseBubbleMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export default DrawioMenu;
|
||||
@ -0,0 +1,184 @@
|
||||
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Card,
|
||||
Image,
|
||||
Modal,
|
||||
Text,
|
||||
useComputedColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { useRef, useState } from "react";
|
||||
import { uploadFile } from "@/features/page/services/page-service.ts";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { getDrawioUrl, getFileUrl } from "@/lib/config.ts";
|
||||
import {
|
||||
DrawIoEmbed,
|
||||
DrawIoEmbedRef,
|
||||
EventExit,
|
||||
EventSave,
|
||||
} from "react-drawio";
|
||||
import { IAttachment } from "@/lib/types";
|
||||
import { decodeBase64ToSvgString, svgStringToFile } from "@/lib/utils";
|
||||
import clsx from "clsx";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function DrawioView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node, updateAttributes, editor, selected } = props;
|
||||
const { src, title, width, attachmentId } = node.attrs;
|
||||
const drawioRef = useRef<DrawIoEmbedRef>(null);
|
||||
const [initialXML, setInitialXML] = useState<string>("");
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const computedColorScheme = useComputedColorScheme();
|
||||
|
||||
const handleOpen = async () => {
|
||||
if (!editor.isEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (src) {
|
||||
const url = getFileUrl(src);
|
||||
const request = await fetch(url, {
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
});
|
||||
const blob = await request.blob();
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(blob);
|
||||
reader.onloadend = () => {
|
||||
const base64data = (reader.result || "") as string;
|
||||
setInitialXML(base64data);
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
open();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (data: EventSave) => {
|
||||
const svgString = decodeBase64ToSvgString(data.xml);
|
||||
|
||||
const fileName = "diagram.drawio.svg";
|
||||
const drawioSVGFile = await svgStringToFile(svgString, fileName);
|
||||
|
||||
const pageId = editor.storage?.pageId;
|
||||
|
||||
let attachment: IAttachment = null;
|
||||
|
||||
if (attachmentId) {
|
||||
attachment = await uploadFile(drawioSVGFile, pageId, attachmentId);
|
||||
} else {
|
||||
attachment = await uploadFile(drawioSVGFile, pageId);
|
||||
}
|
||||
|
||||
updateAttributes({
|
||||
src: `/files/${attachment.id}/${attachment.fileName}?t=${new Date(attachment.updatedAt).getTime()}`,
|
||||
title: attachment.fileName,
|
||||
size: attachment.fileSize,
|
||||
attachmentId: attachment.id,
|
||||
});
|
||||
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
<Modal.Root opened={opened} onClose={close} fullScreen>
|
||||
<Modal.Overlay />
|
||||
<Modal.Content style={{ overflow: "hidden" }}>
|
||||
<Modal.Body>
|
||||
<div style={{ height: "100vh" }}>
|
||||
<DrawIoEmbed
|
||||
ref={drawioRef}
|
||||
xml={initialXML}
|
||||
baseUrl={getDrawioUrl()}
|
||||
urlParameters={{
|
||||
ui: computedColorScheme === "light" ? "kennedy" : "dark",
|
||||
spin: true,
|
||||
libraries: true,
|
||||
saveAndExit: true,
|
||||
noSaveBtn: true,
|
||||
}}
|
||||
onSave={(data: EventSave) => {
|
||||
// If the save is triggered by another event, then do nothing
|
||||
if (data.parentEvent !== "save") {
|
||||
return;
|
||||
}
|
||||
handleSave(data);
|
||||
}}
|
||||
onClose={(data: EventExit) => {
|
||||
// If the exit is triggered by another event, then do nothing
|
||||
if (data.parentEvent) {
|
||||
return;
|
||||
}
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
</Modal.Root>
|
||||
|
||||
{src ? (
|
||||
<div style={{ position: "relative" }}>
|
||||
<Image
|
||||
onClick={(e) => e.detail === 2 && handleOpen()}
|
||||
radius="md"
|
||||
fit="contain"
|
||||
w={width}
|
||||
src={getFileUrl(src)}
|
||||
alt={title}
|
||||
className={clsx(
|
||||
selected ? "ProseMirror-selectednode" : "",
|
||||
"alignCenter",
|
||||
)}
|
||||
/>
|
||||
|
||||
{selected && (
|
||||
<ActionIcon
|
||||
onClick={handleOpen}
|
||||
variant="default"
|
||||
color="gray"
|
||||
mx="xs"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
right: 8,
|
||||
}}
|
||||
>
|
||||
<IconEdit size={18} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Card
|
||||
radius="md"
|
||||
onClick={(e) => e.detail === 2 && handleOpen()}
|
||||
p="xs"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
withBorder
|
||||
className={clsx(selected ? "ProseMirror-selectednode" : "")}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<ActionIcon variant="transparent" color="gray">
|
||||
<IconEdit size={18} />
|
||||
</ActionIcon>
|
||||
|
||||
<Text component="span" size="lg" c="dimmed">
|
||||
{t("Double-click to edit Draw.io diagram")}
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
132
apps/client/src/features/editor/components/embed/embed-view.tsx
Normal file
132
apps/client/src/features/editor/components/embed/embed-view.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { useMemo } from "react";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
ActionIcon,
|
||||
AspectRatio,
|
||||
Button,
|
||||
Card,
|
||||
FocusTrap,
|
||||
Group,
|
||||
Popover,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import { z } from "zod";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import {
|
||||
getEmbedProviderById,
|
||||
getEmbedUrlAndProvider,
|
||||
} from "@/features/editor/components/embed/providers.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import i18n from "i18next";
|
||||
|
||||
const schema = z.object({
|
||||
url: z
|
||||
.string()
|
||||
.trim()
|
||||
.url({ message: i18n.t("Please enter a valid url") }),
|
||||
});
|
||||
|
||||
export default function EmbedView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node, selected, updateAttributes } = props;
|
||||
const { src, provider } = node.attrs;
|
||||
|
||||
const embedUrl = useMemo(() => {
|
||||
if (src) {
|
||||
return getEmbedUrlAndProvider(src).embedUrl;
|
||||
}
|
||||
return null;
|
||||
}, [src]);
|
||||
|
||||
const embedForm = useForm<{ url: string }>({
|
||||
initialValues: {
|
||||
url: "",
|
||||
},
|
||||
validate: zodResolver(schema),
|
||||
});
|
||||
|
||||
async function onSubmit(data: { url: string }) {
|
||||
if (provider) {
|
||||
const embedProvider = getEmbedProviderById(provider);
|
||||
if (embedProvider.regex.test(data.url)) {
|
||||
updateAttributes({ src: data.url });
|
||||
} else {
|
||||
notifications.show({
|
||||
message: t("Invalid {{provider}} embed link", {
|
||||
provider: embedProvider.name,
|
||||
}),
|
||||
position: "top-right",
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
{embedUrl ? (
|
||||
<>
|
||||
<AspectRatio ratio={16 / 9}>
|
||||
<iframe
|
||||
src={embedUrl}
|
||||
allow="encrypted-media"
|
||||
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
||||
allowFullScreen
|
||||
frameBorder="0"
|
||||
></iframe>
|
||||
</AspectRatio>
|
||||
</>
|
||||
) : (
|
||||
<Popover width={300} position="bottom" withArrow shadow="md">
|
||||
<Popover.Target>
|
||||
<Card
|
||||
radius="md"
|
||||
p="xs"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
withBorder
|
||||
className={clsx(selected ? "ProseMirror-selectednode" : "")}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<ActionIcon variant="transparent" color="gray">
|
||||
<IconEdit size={18} />
|
||||
</ActionIcon>
|
||||
|
||||
<Text component="span" size="lg" c="dimmed">
|
||||
{t("Embed {{provider}}", {
|
||||
provider: getEmbedProviderById(provider).name,
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown bg="var(--mantine-color-body)">
|
||||
<form onSubmit={embedForm.onSubmit(onSubmit)}>
|
||||
<FocusTrap active={true}>
|
||||
<TextInput
|
||||
placeholder={t("Enter {{provider}} link to embed", {
|
||||
provider: getEmbedProviderById(provider).name,
|
||||
})}
|
||||
key={embedForm.key("url")}
|
||||
{...embedForm.getInputProps("url")}
|
||||
data-autofocus
|
||||
/>
|
||||
</FocusTrap>
|
||||
|
||||
<Group justify="center" mt="xs">
|
||||
<Button type="submit">{t("Embed link")}</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
)}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
129
apps/client/src/features/editor/components/embed/providers.ts
Normal file
129
apps/client/src/features/editor/components/embed/providers.ts
Normal file
@ -0,0 +1,129 @@
|
||||
export interface IEmbedProvider {
|
||||
id: string;
|
||||
name: string;
|
||||
regex: RegExp;
|
||||
getEmbedUrl: (match: RegExpMatchArray, url?: string) => string;
|
||||
}
|
||||
|
||||
export const embedProviders: IEmbedProvider[] = [
|
||||
{
|
||||
id: 'loom',
|
||||
name: 'Loom',
|
||||
regex: /^https?:\/\/(?:www\.)?loom\.com\/(?:share|embed)\/([\da-zA-Z]+)\/?/,
|
||||
getEmbedUrl: (match, url) => {
|
||||
if(url.includes("/embed/")){
|
||||
return url;
|
||||
}
|
||||
return `https://loom.com/embed/${match[1]}`;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'airtable',
|
||||
name: 'Airtable',
|
||||
regex: /^https:\/\/(www.)?airtable.com\/([a-zA-Z0-9]{2,})\/.*/,
|
||||
getEmbedUrl: (match, url: string) => {
|
||||
const path = url.split('airtable.com/');
|
||||
if(url.includes("/embed/")){
|
||||
return url;
|
||||
}
|
||||
return `https://airtable.com/embed/${path[1]}`;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'figma',
|
||||
name: 'Figma',
|
||||
regex: /^https:\/\/[\w\.-]+\.?figma.com\/(file|proto|board|design|slides|deck)\/([0-9a-zA-Z]{22,128})/,
|
||||
getEmbedUrl: (match, url: string) => {
|
||||
return `https://www.figma.com/embed?url=${url}&embed_host=docmost`;
|
||||
}
|
||||
},
|
||||
{
|
||||
'id': 'typeform',
|
||||
name: 'Typeform',
|
||||
regex: /^(https?:)?(\/\/)?[\w\.]+\.typeform\.com\/to\/.+/,
|
||||
getEmbedUrl: (match, url: string) => {
|
||||
return url;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'miro',
|
||||
name: 'Miro',
|
||||
regex: /^https:\/\/(www\.)?miro\.com\/app\/board\/([\w-]+=)/,
|
||||
getEmbedUrl: (match, url) => {
|
||||
if(url.includes("/live-embed/")){
|
||||
return url;
|
||||
}
|
||||
return `https://miro.com/app/live-embed/${match[2]}?embedMode=view_only_without_ui&autoplay=true&embedSource=docmost`;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'youtube',
|
||||
name: 'YouTube',
|
||||
regex: /^((?:https?:)?\/\/)?((?:www|m|music)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/,
|
||||
getEmbedUrl: (match, url) => {
|
||||
if (url.includes("/embed/")){
|
||||
return url;
|
||||
}
|
||||
return `https://www.youtube-nocookie.com/embed/${match[5]}`;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'vimeo',
|
||||
name: 'Vimeo',
|
||||
regex: /^(https:)?\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)/,
|
||||
getEmbedUrl: (match) => {
|
||||
return `https://player.vimeo.com/video/${match[4]}`;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'framer',
|
||||
name: 'Framer',
|
||||
regex: /^https:\/\/(www\.)?framer\.com\/embed\/([\w-]+)/,
|
||||
getEmbedUrl: (match, url: string) => {
|
||||
return url;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'gdrive',
|
||||
name: 'Google Drive',
|
||||
regex: /^((?:https?:)?\/\/)?((?:www|m)\.)?(drive\.google\.com)\/file\/d\/([a-zA-Z0-9_-]+)\/.*$/,
|
||||
getEmbedUrl: (match) => {
|
||||
return `https://drive.google.com/file/d/${match[4]}/preview`;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'gsheets',
|
||||
name: 'Google Sheets',
|
||||
regex: /^((?:https?:)?\/\/)?((?:www|m)\.)?(docs\.google\.com)\/spreadsheets\/d\/e\/([a-zA-Z0-9_-]+)\/.*$/,
|
||||
getEmbedUrl: (match, url: string) => {
|
||||
return url
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export function getEmbedProviderById(id: string) {
|
||||
return embedProviders.find(provider => provider.id.toLowerCase() === id.toLowerCase());
|
||||
}
|
||||
|
||||
export interface IEmbedResult {
|
||||
embedUrl: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export function getEmbedUrlAndProvider(url: string): IEmbedResult {
|
||||
for (const provider of embedProviders) {
|
||||
const match = url.match(provider.regex);
|
||||
if (match) {
|
||||
return {
|
||||
embedUrl: provider.getEmbedUrl(match, url),
|
||||
provider: provider.name.toLowerCase()
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
embedUrl: url,
|
||||
provider: 'iframe',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,82 @@
|
||||
import {
|
||||
BubbleMenu as BaseBubbleMenu,
|
||||
findParentNode,
|
||||
posToDOMRect,
|
||||
} from '@tiptap/react';
|
||||
import { useCallback } from 'react';
|
||||
import { sticky } from 'tippy.js';
|
||||
import { Node as PMNode } from 'prosemirror-model';
|
||||
import {
|
||||
EditorMenuProps,
|
||||
ShouldShowProps,
|
||||
} from '@/features/editor/components/table/types/types.ts';
|
||||
import { NodeWidthResize } from '@/features/editor/components/common/node-width-resize.tsx';
|
||||
|
||||
export function ExcalidrawMenu({ editor }: EditorMenuProps) {
|
||||
const shouldShow = useCallback(
|
||||
({ state }: ShouldShowProps) => {
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return editor.isActive('excalidraw') && editor.getAttributes('excalidraw')?.src;
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
const getReferenceClientRect = useCallback(() => {
|
||||
const { selection } = editor.state;
|
||||
const predicate = (node: PMNode) => node.type.name === 'excalidraw';
|
||||
const parent = findParentNode(predicate)(selection);
|
||||
|
||||
if (parent) {
|
||||
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
|
||||
return dom.getBoundingClientRect();
|
||||
}
|
||||
|
||||
return posToDOMRect(editor.view, selection.from, selection.to);
|
||||
}, [editor]);
|
||||
|
||||
const onWidthChange = useCallback(
|
||||
(value: number) => {
|
||||
editor.commands.updateAttributes('excalidraw', { width: `${value}%` });
|
||||
},
|
||||
[editor]
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey={`excalidraw-menu}`}
|
||||
updateDelay={0}
|
||||
tippyOptions={{
|
||||
getReferenceClientRect,
|
||||
offset: [0, 8],
|
||||
zIndex: 99,
|
||||
popperOptions: {
|
||||
modifiers: [{ name: 'flip', enabled: false }],
|
||||
},
|
||||
plugins: [sticky],
|
||||
sticky: 'popper',
|
||||
}}
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{editor.getAttributes('excalidraw')?.width && (
|
||||
<NodeWidthResize
|
||||
onChange={onWidthChange}
|
||||
value={parseInt(editor.getAttributes('excalidraw').width)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</BaseBubbleMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExcalidrawMenu;
|
||||
@ -0,0 +1,215 @@
|
||||
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Image,
|
||||
Text,
|
||||
useComputedColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
import { uploadFile } from "@/features/page/services/page-service.ts";
|
||||
import { svgStringToFile } from "@/lib";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { getFileUrl } from "@/lib/config.ts";
|
||||
import { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types/types";
|
||||
import { IAttachment } from "@/lib/types";
|
||||
import ReactClearModal from "react-clear-modal";
|
||||
import clsx from "clsx";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import { lazy } from "react";
|
||||
import { Suspense } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Excalidraw = lazy(() =>
|
||||
import("@excalidraw/excalidraw").then((module) => ({
|
||||
default: module.Excalidraw,
|
||||
})),
|
||||
);
|
||||
|
||||
export default function ExcalidrawView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node, updateAttributes, editor, selected } = props;
|
||||
const { src, title, width, attachmentId } = node.attrs;
|
||||
|
||||
const [excalidrawAPI, setExcalidrawAPI] =
|
||||
useState<ExcalidrawImperativeAPI>(null);
|
||||
const [excalidrawData, setExcalidrawData] = useState<any>(null);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const computedColorScheme = useComputedColorScheme();
|
||||
|
||||
const handleOpen = async () => {
|
||||
if (!editor.isEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (src) {
|
||||
const url = getFileUrl(src);
|
||||
const request = await fetch(url, {
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
const { loadFromBlob } = await import("@excalidraw/excalidraw");
|
||||
|
||||
const data = await loadFromBlob(await request.blob(), null, null);
|
||||
setExcalidrawData(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
open();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { exportToSvg } = await import("@excalidraw/excalidraw");
|
||||
|
||||
const svg = await exportToSvg({
|
||||
elements: excalidrawAPI?.getSceneElements(),
|
||||
appState: {
|
||||
exportEmbedScene: true,
|
||||
exportWithDarkMode: false,
|
||||
},
|
||||
files: excalidrawAPI?.getFiles(),
|
||||
});
|
||||
|
||||
const serializer = new XMLSerializer();
|
||||
let svgString = serializer.serializeToString(svg);
|
||||
|
||||
svgString = svgString.replace(
|
||||
/https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g,
|
||||
"https://unpkg.com/@excalidraw/excalidraw@latest",
|
||||
);
|
||||
|
||||
const fileName = "diagram.excalidraw.svg";
|
||||
const excalidrawSvgFile = await svgStringToFile(svgString, fileName);
|
||||
|
||||
const pageId = editor.storage?.pageId;
|
||||
|
||||
let attachment: IAttachment = null;
|
||||
if (attachmentId) {
|
||||
attachment = await uploadFile(excalidrawSvgFile, pageId, attachmentId);
|
||||
} else {
|
||||
attachment = await uploadFile(excalidrawSvgFile, pageId);
|
||||
}
|
||||
|
||||
updateAttributes({
|
||||
src: `/files/${attachment.id}/${attachment.fileName}?t=${new Date(attachment.updatedAt).getTime()}`,
|
||||
title: attachment.fileName,
|
||||
size: attachment.fileSize,
|
||||
attachmentId: attachment.id,
|
||||
});
|
||||
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper>
|
||||
<ReactClearModal
|
||||
style={{
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
padding: 0,
|
||||
zIndex: 200,
|
||||
}}
|
||||
isOpen={opened}
|
||||
onRequestClose={close}
|
||||
disableCloseOnBgClick={true}
|
||||
contentProps={{
|
||||
style: {
|
||||
padding: 0,
|
||||
width: "90vw",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Group
|
||||
justify="flex-end"
|
||||
wrap="nowrap"
|
||||
bg="var(--mantine-color-body)"
|
||||
p="xs"
|
||||
>
|
||||
<Button onClick={handleSave} size={"compact-sm"}>
|
||||
{t("Save & Exit")}
|
||||
</Button>
|
||||
<Button onClick={close} color="red" size={"compact-sm"}>
|
||||
{t("Exit")}
|
||||
</Button>
|
||||
</Group>
|
||||
<div style={{ height: "90vh" }}>
|
||||
<Suspense fallback={null}>
|
||||
<Excalidraw
|
||||
excalidrawAPI={(api) => setExcalidrawAPI(api)}
|
||||
initialData={{
|
||||
...excalidrawData,
|
||||
scrollToContent: true,
|
||||
}}
|
||||
theme={computedColorScheme}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</ReactClearModal>
|
||||
|
||||
{src ? (
|
||||
<div style={{ position: "relative" }}>
|
||||
<Image
|
||||
onClick={(e) => e.detail === 2 && handleOpen()}
|
||||
radius="md"
|
||||
fit="contain"
|
||||
w={width}
|
||||
src={getFileUrl(src)}
|
||||
alt={title}
|
||||
className={clsx(
|
||||
selected ? "ProseMirror-selectednode" : "",
|
||||
"alignCenter",
|
||||
)}
|
||||
/>
|
||||
|
||||
{selected && (
|
||||
<ActionIcon
|
||||
onClick={handleOpen}
|
||||
variant="default"
|
||||
color="gray"
|
||||
mx="xs"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
right: 8,
|
||||
}}
|
||||
>
|
||||
<IconEdit size={18} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Card
|
||||
radius="md"
|
||||
onClick={(e) => e.detail === 2 && handleOpen()}
|
||||
p="xs"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
withBorder
|
||||
className={clsx(selected ? "ProseMirror-selectednode" : "")}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<ActionIcon variant="transparent" color="gray">
|
||||
<IconEdit size={18} />
|
||||
</ActionIcon>
|
||||
|
||||
<Text component="span" size="lg" c="dimmed">
|
||||
{t("Double-click to edit Excalidraw diagram")}
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
@ -17,8 +17,10 @@ import {
|
||||
IconLayoutAlignRight,
|
||||
} from "@tabler/icons-react";
|
||||
import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function ImageMenu({ editor }: EditorMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const shouldShow = useCallback(
|
||||
({ state }: ShouldShowProps) => {
|
||||
if (!state) {
|
||||
@ -96,11 +98,11 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
<ActionIcon.Group className="actionIconGroup">
|
||||
<Tooltip position="top" label="Align image left">
|
||||
<Tooltip position="top" label={t("Align left")}>
|
||||
<ActionIcon
|
||||
onClick={alignImageLeft}
|
||||
size="lg"
|
||||
aria-label="Align image left"
|
||||
aria-label={t("Align left")}
|
||||
variant={
|
||||
editor.isActive("image", { align: "left" }) ? "light" : "default"
|
||||
}
|
||||
@ -109,11 +111,11 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Align image center">
|
||||
<Tooltip position="top" label={t("Align center")}>
|
||||
<ActionIcon
|
||||
onClick={alignImageCenter}
|
||||
size="lg"
|
||||
aria-label="Align image center"
|
||||
aria-label={t("Align center")}
|
||||
variant={
|
||||
editor.isActive("image", { align: "center" })
|
||||
? "light"
|
||||
@ -124,11 +126,11 @@ export function ImageMenu({ editor }: EditorMenuProps) {
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Align image right">
|
||||
<Tooltip position="top" label={t("Align right")}>
|
||||
<ActionIcon
|
||||
onClick={alignImageRight}
|
||||
size="lg"
|
||||
aria-label="Align image right"
|
||||
aria-label={t("Align right")}
|
||||
variant={
|
||||
editor.isActive("image", { align: "right" }) ? "light" : "default"
|
||||
}
|
||||
|
||||
@ -2,32 +2,28 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { useMemo } from "react";
|
||||
import { Image } from "@mantine/core";
|
||||
import { getFileUrl } from "@/lib/config.ts";
|
||||
import clsx from "clsx";
|
||||
|
||||
export default function ImageView(props: NodeViewProps) {
|
||||
const { node, selected } = props;
|
||||
const { src, width, align, title } = node.attrs;
|
||||
|
||||
const flexJustifyContent = useMemo(() => {
|
||||
if (align === "center") return "center";
|
||||
if (align === "right") return "flex-end";
|
||||
return "flex-start";
|
||||
const alignClass = useMemo(() => {
|
||||
if (align === "left") return "alignLeft";
|
||||
if (align === "right") return "alignRight";
|
||||
if (align === "center") return "alignCenter";
|
||||
return "alignCenter";
|
||||
}, [align]);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper
|
||||
style={{
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
justifyContent: flexJustifyContent,
|
||||
}}
|
||||
>
|
||||
<NodeViewWrapper>
|
||||
<Image
|
||||
radius="md"
|
||||
fit="contain"
|
||||
w={width}
|
||||
src={getFileUrl(src)}
|
||||
alt={title}
|
||||
className={selected ? "ProseMirror-selectednode" : ""}
|
||||
className={clsx(selected ? "ProseMirror-selectednode" : "", alignClass)}
|
||||
/>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
import { handleImageUpload } from "@docmost/editor-ext";
|
||||
import { uploadFile } from "@/features/page/services/page-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { getFileUploadSizeLimit } from "@/lib/config.ts";
|
||||
import { formatBytes } from "@/lib";
|
||||
import i18n from "@/i18n.ts";
|
||||
|
||||
export const uploadImageAction = handleImageUpload({
|
||||
onUpload: async (file: File, pageId: string): Promise<any> => {
|
||||
try {
|
||||
return await uploadFile(file, pageId);
|
||||
} catch (err) {
|
||||
console.error("failed to upload image", err);
|
||||
notifications.show({
|
||||
color: "red",
|
||||
message: err?.response.data.message,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
@ -14,8 +21,13 @@ export const uploadImageAction = handleImageUpload({
|
||||
if (!file.type.includes("image/")) {
|
||||
return false;
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 20) {
|
||||
//error("File size too big (max 20MB).");
|
||||
if (file.size > getFileUploadSizeLimit()) {
|
||||
notifications.show({
|
||||
color: "red",
|
||||
message: i18n.t("File exceeds the {{limit}} attachment limit", {
|
||||
limit: formatBytes(getFileUploadSizeLimit()),
|
||||
}),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
import { EditorView } from "@tiptap/pm/view";
|
||||
import { getPageById } from "@/features/page/services/page-service.ts";
|
||||
import { IPage } from "@/features/page/types/page.types.ts";
|
||||
import { v7 } from "uuid";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
|
||||
export type LinkFn = (
|
||||
url: string,
|
||||
view: EditorView,
|
||||
pos: number,
|
||||
creatorId: string,
|
||||
) => void;
|
||||
|
||||
export interface InternalLinkOptions {
|
||||
validateFn: (url: string, view: EditorView) => boolean;
|
||||
onResolveLink: (linkedPageId: string, creatorId: string) => Promise<any>;
|
||||
}
|
||||
|
||||
export const handleInternalLink =
|
||||
({ validateFn, onResolveLink }: InternalLinkOptions): LinkFn =>
|
||||
async (url: string, view, pos, creatorId) => {
|
||||
const validated = validateFn(url, view);
|
||||
if (!validated) return;
|
||||
|
||||
const linkedPageId = extractPageSlugId(url);
|
||||
|
||||
await onResolveLink(linkedPageId, creatorId).then(
|
||||
(page: IPage) => {
|
||||
const { schema } = view.state;
|
||||
|
||||
const node = schema.nodes.mention.create({
|
||||
id: v7(),
|
||||
label: page.title || "Untitled",
|
||||
entityType: "page",
|
||||
entityId: page.id,
|
||||
slugId: page.slugId,
|
||||
creatorId: creatorId,
|
||||
});
|
||||
|
||||
if (!node) return;
|
||||
|
||||
const transaction = view.state.tr.replaceWith(pos, pos, node);
|
||||
view.dispatch(transaction);
|
||||
},
|
||||
() => {
|
||||
// on failure, insert as normal link
|
||||
const { schema } = view.state;
|
||||
|
||||
const transaction = view.state.tr.insertText(url, pos);
|
||||
transaction.addMark(
|
||||
pos,
|
||||
pos + url.length,
|
||||
schema.marks.link.create({ href: url }),
|
||||
);
|
||||
|
||||
view.dispatch(transaction);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const createMentionAction = handleInternalLink({
|
||||
onResolveLink: async (linkedPageId: string): Promise<any> => {
|
||||
// eslint-disable-next-line no-useless-catch
|
||||
try {
|
||||
return await getPageById({ pageId: linkedPageId });
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
validateFn: (url: string, view: EditorView) => {
|
||||
// validation is already done on the paste handler
|
||||
return true;
|
||||
},
|
||||
});
|
||||
@ -3,11 +3,13 @@ import { Button, Group, TextInput } from "@mantine/core";
|
||||
import { IconLink } from "@tabler/icons-react";
|
||||
import { useLinkEditorState } from "@/features/editor/components/link/use-link-editor-state.tsx";
|
||||
import { LinkEditorPanelProps } from "@/features/editor/components/link/types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const LinkEditorPanel = ({
|
||||
onSetLink,
|
||||
initialUrl,
|
||||
}: LinkEditorPanelProps) => {
|
||||
const { t } = useTranslation();
|
||||
const state = useLinkEditorState({
|
||||
onSetLink,
|
||||
initialUrl,
|
||||
@ -20,12 +22,12 @@ export const LinkEditorPanel = ({
|
||||
<TextInput
|
||||
leftSection={<IconLink size={16} />}
|
||||
variant="filled"
|
||||
placeholder="Paste link"
|
||||
placeholder={t("Paste link")}
|
||||
value={state.url}
|
||||
onChange={state.onChange}
|
||||
/>
|
||||
<Button p={"xs"} type="submit" disabled={!state.isValidUrl}>
|
||||
Save
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
|
||||
@ -7,6 +7,8 @@ import {
|
||||
Flex,
|
||||
} from "@mantine/core";
|
||||
import { IconLinkOff, IconPencil } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classes from "./link.module.css";
|
||||
|
||||
export type LinkPreviewPanelProps = {
|
||||
url: string;
|
||||
@ -19,6 +21,8 @@ export const LinkPreviewPanel = ({
|
||||
onEdit,
|
||||
url,
|
||||
}: LinkPreviewPanelProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card withBorder radius="md" padding="xs" bg="var(--mantine-color-body)">
|
||||
@ -28,12 +32,7 @@ export const LinkPreviewPanel = ({
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
inherit
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
className={classes.link}
|
||||
>
|
||||
{url}
|
||||
</Anchor>
|
||||
@ -42,13 +41,13 @@ export const LinkPreviewPanel = ({
|
||||
<Flex align="center">
|
||||
<Divider mx={4} orientation="vertical" />
|
||||
|
||||
<Tooltip label="Edit link">
|
||||
<Tooltip label={t("Edit link")}>
|
||||
<ActionIcon onClick={onEdit} variant="subtle" color="gray">
|
||||
<IconPencil size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Remove link">
|
||||
<Tooltip label={t("Remove link")}>
|
||||
<ActionIcon onClick={onClear} variant="subtle" color="red">
|
||||
<IconLinkOff size={16} />
|
||||
</ActionIcon>
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
.link {
|
||||
color: light-dark(var(--mantine-color-dark-4), var(--mantine-color-dark-1));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@ -8,8 +8,10 @@ import classes from "./math.module.css";
|
||||
import { v4 } from "uuid";
|
||||
import { IconTrashX } from "@tabler/icons-react";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function MathBlockView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node, updateAttributes, editor, getPos } = props;
|
||||
const mathResultContainer = useRef<HTMLDivElement>(null);
|
||||
const mathPreviewContainer = useRef<HTMLDivElement>(null);
|
||||
@ -94,9 +96,9 @@ export default function MathBlockView(props: NodeViewProps) {
|
||||
></div>
|
||||
{((isEditing && !preview?.trim().length) ||
|
||||
(!isEditing && !node.attrs.text.trim().length)) && (
|
||||
<div>Empty equation</div>
|
||||
<div>{t("Empty equation")}</div>
|
||||
)}
|
||||
{error && <div>Invalid equation</div>}
|
||||
{error && <div>{t("Invalid equation")}</div>}
|
||||
</NodeViewWrapper>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
|
||||
@ -6,8 +6,10 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { Popover, Textarea } from "@mantine/core";
|
||||
import classes from "./math.module.css";
|
||||
import { v4 } from "uuid";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function MathInlineView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node, updateAttributes, editor, getPos } = props;
|
||||
const mathResultContainer = useRef<HTMLDivElement>(null);
|
||||
const mathPreviewContainer = useRef<HTMLDivElement>(null);
|
||||
@ -38,7 +40,7 @@ export default function MathInlineView(props: NodeViewProps) {
|
||||
renderMath(preview || "", mathPreviewContainer.current);
|
||||
} else if (preview !== null) {
|
||||
queueMicrotask(() => {
|
||||
updateAttributes({ text: preview });
|
||||
updateAttributes({ text: preview.trim() });
|
||||
});
|
||||
}
|
||||
}, [preview, isEditing]);
|
||||
@ -84,9 +86,9 @@ export default function MathInlineView(props: NodeViewProps) {
|
||||
></div>
|
||||
{((isEditing && !preview?.trim().length) ||
|
||||
(!isEditing && !node.attrs.text.trim().length)) && (
|
||||
<div>Empty equation</div>
|
||||
<div>{t("Empty equation")}</div>
|
||||
)}
|
||||
{error && <div>Invalid equation</div>}
|
||||
{error && <div>{t("Invalid equation")}</div>}
|
||||
</NodeViewWrapper>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown p={"xs"}>
|
||||
@ -97,7 +99,7 @@ export default function MathInlineView(props: NodeViewProps) {
|
||||
ref={textAreaRef}
|
||||
draggable={false}
|
||||
classNames={{ input: classes.textInput }}
|
||||
value={preview?.trim() ?? ""}
|
||||
value={preview ?? ""}
|
||||
placeholder={"E = mc^2"}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape" || (e.key === "Enter" && !e.shiftKey)) {
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
transition: background-color 0.2s;
|
||||
padding: 0 0.25rem;
|
||||
margin: 0 0.1rem;
|
||||
user-select: none;
|
||||
|
||||
&.empty {
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-gray-4));
|
||||
@ -17,10 +18,6 @@
|
||||
color: light-dark(var(--mantine-color-red-8), var(--mantine-color-red-7));
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-gray-8));
|
||||
}
|
||||
|
||||
&:not(.error, .empty) * {
|
||||
font-family: KaTeX_Main, Times New Roman, serif;
|
||||
}
|
||||
}
|
||||
|
||||
.mathBlock {
|
||||
@ -33,7 +30,8 @@
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
margin: 0 0.1rem;
|
||||
overflow-x: scroll;
|
||||
overflow-x: auto;
|
||||
user-select: none;
|
||||
|
||||
.textInput {
|
||||
width: 400px;
|
||||
@ -52,10 +50,4 @@
|
||||
color: light-dark(var(--mantine-color-red-8), var(--mantine-color-red-7));
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-gray-8));
|
||||
}
|
||||
|
||||
&:not(.error, .empty) * {
|
||||
font-family: KaTeX_Main, Times New Roman, serif;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,273 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useSearchSuggestionsQuery } from "@/features/search/queries/search-query.ts";
|
||||
import {
|
||||
ActionIcon,
|
||||
Group,
|
||||
Paper,
|
||||
ScrollArea,
|
||||
Text,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import clsx from "clsx";
|
||||
import classes from "./mention.module.css";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import { IconFileDescription } from "@tabler/icons-react";
|
||||
import { useSpaceQuery } from "@/features/space/queries/space-query.ts";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { v7 as uuid7 } from "uuid";
|
||||
import { useAtom } from "jotai";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import {
|
||||
MentionListProps,
|
||||
MentionSuggestionItem,
|
||||
} from "@/features/editor/components/mention/mention.type.ts";
|
||||
|
||||
const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(1);
|
||||
const viewportRef = useRef<HTMLDivElement>(null);
|
||||
const { spaceSlug } = useParams();
|
||||
const { data: space } = useSpaceQuery(spaceSlug);
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const [renderItems, setRenderItems] = useState<MentionSuggestionItem[]>([]);
|
||||
|
||||
const { data: suggestion, isLoading } = useSearchSuggestionsQuery({
|
||||
query: props.query,
|
||||
includeUsers: true,
|
||||
includePages: true,
|
||||
spaceId: space.id,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (suggestion && !isLoading) {
|
||||
let items: MentionSuggestionItem[] = [];
|
||||
|
||||
if (suggestion?.users?.length > 0) {
|
||||
items.push({ entityType: "header", label: "Users" });
|
||||
|
||||
items = items.concat(
|
||||
suggestion.users.map((user) => ({
|
||||
id: uuid7(),
|
||||
label: user.name,
|
||||
entityType: "user",
|
||||
entityId: user.id,
|
||||
avatarUrl: user.avatarUrl,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
if (suggestion?.pages?.length > 0) {
|
||||
items.push({ entityType: "header", label: "Pages" });
|
||||
items = items.concat(
|
||||
suggestion.pages.map((page) => ({
|
||||
id: uuid7(),
|
||||
label: page.title || "Untitled",
|
||||
entityType: "page",
|
||||
entityId: page.id,
|
||||
slugId: page.slugId,
|
||||
icon: page.icon,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
setRenderItems(items);
|
||||
// update editor storage
|
||||
props.editor.storage.mentionItems = items;
|
||||
}
|
||||
}, [suggestion, isLoading]);
|
||||
|
||||
const selectItem = useCallback(
|
||||
(index: number) => {
|
||||
const item = renderItems?.[index];
|
||||
if (item) {
|
||||
if (item.entityType === "user") {
|
||||
props.command({
|
||||
id: item.id,
|
||||
label: item.label,
|
||||
entityType: "user",
|
||||
entityId: item.entityId,
|
||||
creatorId: currentUser?.user.id,
|
||||
});
|
||||
}
|
||||
if (item.entityType === "page") {
|
||||
props.command({
|
||||
id: item.id,
|
||||
label: item.label || "Untitled",
|
||||
entityType: "page",
|
||||
entityId: item.entityId,
|
||||
slugId: item.slugId,
|
||||
creatorId: currentUser?.user.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[renderItems],
|
||||
);
|
||||
|
||||
const upHandler = () => {
|
||||
if (!renderItems.length) return;
|
||||
|
||||
let newIndex = selectedIndex;
|
||||
|
||||
do {
|
||||
newIndex = (newIndex + renderItems.length - 1) % renderItems.length;
|
||||
} while (renderItems[newIndex].entityType === "header");
|
||||
setSelectedIndex(newIndex);
|
||||
};
|
||||
|
||||
const downHandler = () => {
|
||||
if (!renderItems.length) return;
|
||||
let newIndex = selectedIndex;
|
||||
do {
|
||||
newIndex = (newIndex + 1) % renderItems.length;
|
||||
} while (renderItems[newIndex].entityType === "header");
|
||||
setSelectedIndex(newIndex);
|
||||
};
|
||||
|
||||
const enterHandler = () => {
|
||||
if (!renderItems.length) return;
|
||||
if (renderItems[selectedIndex].entityType !== "header") {
|
||||
selectItem(selectedIndex);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIndex(1);
|
||||
}, [suggestion]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onKeyDown: ({ event }) => {
|
||||
if (event.key === "ArrowUp") {
|
||||
upHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
downHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "Enter") {
|
||||
// don't trap the enter button if there are no items to render
|
||||
if (renderItems.length === 0) {
|
||||
return false;
|
||||
}
|
||||
enterHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
}));
|
||||
|
||||
// if no results and enter what to do?
|
||||
|
||||
useEffect(() => {
|
||||
viewportRef.current
|
||||
?.querySelector(`[data-item-index="${selectedIndex}"]`)
|
||||
?.scrollIntoView({ block: "nearest" });
|
||||
}, [selectedIndex]);
|
||||
|
||||
if (renderItems.length === 0) {
|
||||
return (
|
||||
<Paper shadow="md" p="xs" withBorder>
|
||||
No results
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper id="mention" shadow="md" p="xs" withBorder>
|
||||
<ScrollArea.Autosize
|
||||
viewportRef={viewportRef}
|
||||
mah={350}
|
||||
w={320}
|
||||
scrollbarSize={8}
|
||||
>
|
||||
{renderItems?.map((item, index) => {
|
||||
if (item.entityType === "header") {
|
||||
return (
|
||||
<div key={`${item.label}-${index}`}>
|
||||
<Text c="dimmed" mb={4} tt="uppercase">
|
||||
{item.label}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
} else if (item.entityType === "user") {
|
||||
return (
|
||||
<UnstyledButton
|
||||
data-item-index={index}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
className={clsx(classes.menuBtn, {
|
||||
[classes.selectedItem]: index === selectedIndex,
|
||||
})}
|
||||
>
|
||||
<Group>
|
||||
<CustomAvatar
|
||||
size={"sm"}
|
||||
avatarUrl={item.avatarUrl}
|
||||
name={item.label}
|
||||
/>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="sm" fw={500}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
);
|
||||
} else if (item.entityType === "page") {
|
||||
return (
|
||||
<UnstyledButton
|
||||
data-item-index={index}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
className={clsx(classes.menuBtn, {
|
||||
[classes.selectedItem]: index === selectedIndex,
|
||||
})}
|
||||
>
|
||||
<Group>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
component="div"
|
||||
aria-label={item.label}
|
||||
>
|
||||
{item.icon || (
|
||||
<ActionIcon
|
||||
component="span"
|
||||
variant="transparent"
|
||||
color="gray"
|
||||
size={18}
|
||||
>
|
||||
<IconFileDescription size={18} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</ActionIcon>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="sm" fw={500}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</ScrollArea.Autosize>
|
||||
</Paper>
|
||||
);
|
||||
});
|
||||
|
||||
export default MentionList;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user