Compare commits

...

83 Commits

Author SHA1 Message Date
6b41538b60 v0.8.2 2025-02-21 13:16:16 +00:00
496f5d7384 pin s3 package to 3.701.0 2025-02-21 13:15:19 +00:00
32c7a16d06 fix: accept invitation password hashing (#773) 2025-02-21 12:48:25 +00:00
64ecef09bc upgrade to NestJS 11 (#766)
* upgrade to nest 11

* update dependencies
2025-02-20 21:17:03 +00:00
3e5cb92621 v0.8.1 2025-02-18 16:59:27 +00:00
fd5ad2f576 fix signup email 2025-02-18 16:26:16 +00:00
74a5360561 v0.8.0 2025-02-18 11:14:16 +00:00
7580e8d1fe fix pagination limit 2025-02-15 14:15:39 +00:00
f92d63261d Implement space member search (#731)
* Hide pagination buttons if there is nothing to paginate
* Create reusable hook for search and pagination
2025-02-15 14:14:30 +00:00
4d51986250 update dependences (#729) 2025-02-14 16:59:19 +00:00
e209aaa272 feat: internal page links and mentions (#604)
* Work on mentions

* fix: properly parse page slug

* fix editor suggestion bugs

* mentions must start with whitespace

* add icon to page mention render

* feat: backlinks - WIP

* UI - WIP

* permissions check
* use FTS for page suggestion

* cleanup

* WIP

* page title fallback

* feat: handle internal link paste

* link styling

* WIP

* Switch back to LIKE operator for search suggestion

* WIP
* scope to workspaceId
* still create link for pages not found

* select necessary columns

* cleanups
2025-02-14 15:36:44 +00:00
0ef6b1978a feat: UI pagination and members search (#724)
* feat: pagination (UI)

* Fixes

* feat: add search to member list page

* responsiveness
2025-02-13 23:28:00 +00:00
ae842f94d0 * fix: popover does not close when clicking outside in SwitchSpace. (#720) 2025-02-12 16:14:21 +00:00
7121771f92 fix workspace setup 2025-02-12 15:33:08 +00:00
040d6625df fix: enforce 32-character minimum length for APP_SECRET (#702)
* Enforce 32 characters minimum APP_SECRET length

* update APP_SECRET comment
2025-02-06 17:46:32 +00:00
33ddd92198 * fix codeblock tab-size (#703)
* hide codeblock menu group during printing
2025-02-06 17:43:31 +00:00
54e8d60840 New language options (es-ES, it-IT, ja-JP, ko-KR, ru-RU) (#701) 2025-02-06 16:47:41 +00:00
db986038c2 New Crowdin updates (#659)
New translations
2025-02-06 16:44:48 +00:00
de0b5f0046 feat: add text alignment (#667)
* feat: text alignment

* fix text case

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-02-06 16:24:36 +00:00
638b811857 fix import 2025-02-03 21:51:09 +00:00
d775a61c95 fix client side env variable refresh (#695) 2025-02-03 21:47:38 +00:00
0f74f03264 fix name extraction from email 2025-01-30 22:13:23 +00:00
f8b93ce93f fix: switch space bug (#692) 2025-01-30 21:14:11 +00:00
85d18b8cc8 Set default language on invitation signup (#691)
* Default language selection to en-US if locale is undefined (Client)
2025-01-30 13:25:10 +00:00
4d9fe6f804 Fix invitation signup redirect (#690)
* Fix invitation signup redirect
2025-01-30 12:54:02 +00:00
85159a2c95 * fix 401 redirect in auth routes (#674)
* fix config getter
2025-01-26 14:01:08 +00:00
990612793f refactor: switch to HttpOnly cookie (#660)
* Switch to httpOnly cookie
* create endpoint to retrieve temporary collaboration token

* cleanups
2025-01-22 22:11:11 +00:00
f2235fd2a2 update katex (#658) 2025-01-22 18:08:31 +00:00
2044cbb21c fix translation
* fix filesize formatting
2025-01-16 15:29:09 +00:00
3d52b82cd4 v0.7.0 2025-01-16 13:15:39 +00:00
89a2dd602b fix punycode DeprecationWarning error (#631) 2025-01-16 13:13:14 +00:00
3cb954db69 fix: editor improvements (#583)
* delete unused component

* return page prosemirror content

* prefetch pages

* use prosemirro json content on editor

* cache page query with id and slug as key

* Show notice on collaboration disconnection

* enable scroll while typing

* enable immediatelyRender

* avoid image break in PDF print

* Comment editor rendering props
2025-01-16 12:48:35 +00:00
71cfe3cd8e fix: add cancel button for editing comments (#580)
* fix: add cancel button for editing comments

* cleanup

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-01-15 16:37:57 +00:00
f7efb6c2c9 Fix: Ensure only one emoji list appears (#572)
* Fix: Ensure only one emoji list appears

* fix: refactor logic

* remove unused file node-id-atoms

* small fix

* align with Mantine UI

* close emoji picker on escape

* translate string

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2025-01-15 16:07:26 +00:00
59b514fa26 New Crowdin updates (#630)
* New translations translation.json
2025-01-15 14:18:05 +00:00
0c1f9304f4 New Crowdin updates (#627)
New translations translation.json
2025-01-15 14:12:48 +00:00
e876214eeb fix: embed provider name in error message 2025-01-11 22:25:51 +00:00
5fece5fc68 feat: google sheets embed (#615) 2025-01-11 22:22:06 +00:00
f3dbf7cc5d feat: add new languages to selection (#626)
* Add new languages to selection

* more translations
2025-01-11 22:11:31 +00:00
f7ac6bb4bb New Crowdin updates (#605)
* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (Spanish)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (English)

* New translations translation.json (Portuguese, Brazilian)

* New translations translation.json (French)

* New translations translation.json (German)

* New translations translation.json (Chinese Simplified)

* New translations translation.json (Portuguese, Brazilian)
2025-01-11 15:38:32 +00:00
1f5ffe7f9d Fix empty translation value 2025-01-11 15:34:27 +00:00
95715421c6 fix: move markdown clipboard extension to client app 2025-01-04 21:25:05 +00:00
f5bc99b449 fix: link paste handler (#609)
* feat: support pasting markdown

* fix link paste handler
2025-01-04 20:47:49 +00:00
287b833838 feat: support pasting markdown (#606) 2025-01-04 16:57:36 +00:00
0cbbcb8eb1 Update Crowdin configuration file 2025-01-04 13:22:31 +00:00
670ee64179 Support I18n (#243)
* feat: support i18n

* feat: wip support i18n

* feat: complete space translation

* feat: complete page translation

* feat: update space translation

* feat: update workspace translation

* feat: update group translation

* feat: update workspace translation

* feat: update page translation

* feat: update user translation

* chore: update pnpm-lock

* feat: add query translation

* refactor: merge to single file

* chore: remove necessary code

* feat: save language to BE

* fix: only load current language

* feat: save language to locale column

* fix: cleanups

* add language menu to preferences page

* new translations

* translate editor

* Translate editor placeholders

* translate space selection component

---------

Co-authored-by: Philip Okugbe <phil@docmost.com>
Co-authored-by: Philip Okugbe <16838612+Philipinho@users.noreply.github.com>
2025-01-04 13:17:17 +00:00
290b7d9d94 v0.6.2 2024-12-14 20:39:19 +00:00
2503bfd3a2 fix: prevent CDNs from caching attachments (#562) 2024-12-14 19:55:49 +00:00
f48d6dd60b fix: don't throw error while parsing auth tokens 2024-12-12 14:29:25 +00:00
1302b1b602 v0.6.1 2024-12-11 14:55:06 +00:00
89a3f4cfc2 v0.6.1 2024-12-11 14:54:19 +00:00
e48b1c0dae fix: markdown math import (#529)
* fix: markdown math block import

* fix: block and inline math import

* cleanup
2024-12-09 15:08:25 +00:00
4a2a5a7a4d fix: postgres and redis url validation (#548) 2024-12-09 14:56:15 +00:00
532001fd82 chore: fix linting (#544)
* fix: eslint (server)

* fix: eslint (client)

* commit package lock file

* fix linting
2024-12-09 14:51:31 +00:00
e6bf4cdd6c fix: fix markdown import file button (#542) 2024-12-06 12:26:55 +00:00
a9a4a26db5 fix export controller reference in module 2024-11-30 20:42:31 +00:00
ede5633415 fix export fileName 2024-11-30 20:40:53 +00:00
a25cf84671 fix: add spaceId 2024-11-30 20:29:13 +00:00
a37d558bac v0.6.0 2024-11-30 20:06:44 +00:00
ddb0f9225f fix: uuid7 for commentId (#524) 2024-11-30 20:04:50 +00:00
c717847ca8 chore: update packages (#507) 2024-11-30 19:54:04 +00:00
fe83557767 feat: space export (#506)
* wip

* Space export
* option to export pages with children
* include attachments in exports
* unified export UI

* cleanup

* fix: change export icon

* add export button to space settings

* cleanups

* export name
2024-11-30 19:47:22 +00:00
9fa432dba9 feat: support tab key in code block (#523) 2024-11-30 14:40:05 +00:00
c6aaefecbd fix: clear local cache on logout (#519) 2024-11-28 20:35:53 +00:00
311d81bc71 fix wrong tree sync bug (#514) 2024-11-28 19:39:38 +00:00
f178e6654f fix: properly support redis db (#517) 2024-11-28 18:54:28 +00:00
ca186f3c0e fix: return direct embed urls if present (#516) 2024-11-28 18:53:49 +00:00
a16d5d1bf4 feat: websocket rooms (#515) 2024-11-28 18:53:29 +00:00
d97baf5824 add env variable (#513) 2024-11-28 18:48:25 +00:00
8349d8271c fix: allow space in inline math (#508) 2024-11-28 18:48:08 +00:00
2e6d16dbc3 fix: full width bug on smaller screens (#518) 2024-11-28 18:44:42 +00:00
4107793e73 fix: disable user-select 2024-11-28 15:55:10 +00:00
a1b6ac7f3e fix: close space selection popover onClickOutside (#485) 2024-11-27 02:32:12 +00:00
dd0319a14d fix: index imported content (#495) 2024-11-20 13:36:36 +00:00
8194c7d42d fix: focus editor on bottom click (#484) 2024-11-13 20:00:25 +00:00
d01ced078b * Reduce code block font-size
* Make inline code more distinctive
2024-11-13 11:36:55 -08:00
da9c971050 fix breadcrumb clipping (#457) 2024-11-13 19:15:37 +00:00
4e7af507c6 fix tree dnd 2024-11-06 19:29:12 -08:00
f7426a0b45 fix: use clsx 2024-11-01 10:09:52 +00:00
b85b34d6b1 feat: resizable sidebar (#452)
* feat: resizable sidebar

* only expand space sidebar
2024-11-01 10:05:03 +00:00
e064e58f79 Fix sidebar responsivity (#453)
* navbar height fix. has to be cleaned up
* use parent height for tree
* cleanups
2024-11-01 09:41:23 +00:00
4f1a97ceb9 Revert "fix: prevent default browser save behavior (#450)" (#451)
This reverts commit d07338861b.
2024-10-30 12:23:31 +00:00
d07338861b fix: prevent default browser save behavior (#450) 2024-10-30 11:41:23 +00:00
262 changed files with 16412 additions and 8259 deletions

View File

@ -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
@ -40,3 +40,5 @@ SMTP_IGNORETLS=false
# Postmark driver config
POSTMARK_TOKEN=
# for custom drawio server
DRAWIO_URL=

View File

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

View 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",
},
},
);

View File

@ -1,73 +1,77 @@
{
"name": "client",
"private": true,
"version": "0.5.0",
"version": "0.8.2",
"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",
"@excalidraw/excalidraw": "^0.17.6",
"@mantine/core": "^7.12.2",
"@mantine/form": "^7.12.2",
"@mantine/hooks": "^7.12.2",
"@mantine/modals": "^7.12.2",
"@mantine/notifications": "^7.12.2",
"@mantine/spotlight": "^7.12.2",
"@tabler/icons-react": "^3.14.0",
"@tanstack/react-query": "^5.53.2",
"axios": "^1.7.7",
"@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",
"file-saver": "^2.0.5",
"jotai": "^2.9.3",
"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.11",
"lowlight": "^3.1.0",
"mermaid": "^11.0.2",
"katex": "0.16.21",
"lowlight": "^3.2.0",
"mermaid": "^11.4.1",
"react": "^18.3.1",
"react-arborist": "^3.4.0",
"react-clear-modal": "^2.0.9",
"react-arborist": "3.4.0",
"react-clear-modal": "^2.0.11",
"react-dom": "^18.3.1",
"react-drawio": "^0.2.0",
"react-error-boundary": "^4.0.13",
"react-drawio": "^1.0.1",
"react-error-boundary": "^4.1.2",
"react-helmet-async": "^2.0.5",
"react-router-dom": "^6.26.1",
"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.12",
"tiptap-extension-global-drag-handle": "^0.1.16",
"zod": "^3.23.8"
},
"devDependencies": {
"@tanstack/eslint-plugin-query": "^5.53.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": "22.5.2",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^9.9.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.11",
"@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.43",
"postcss": "^8.4.49",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.3.3",
"typescript": "^5.5.4",
"vite": "^5.4.8"
"prettier": "^3.4.1",
"typescript": "^5.7.2",
"typescript-eslint": "^8.17.0",
"vite": "^6.1.0"
}
}

View 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}}"
}

View 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}}"
}

View 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}}"
}

View 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}}"
}

View 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}}"
}

View 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}}"
}

View 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}}"
}

View 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}}"
}

View 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}}"
}

View 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}}"
}

View File

@ -10,14 +10,6 @@ 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";
@ -26,37 +18,10 @@ 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 (
<>
@ -78,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>

View 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")}
/>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -8,17 +8,19 @@ import {
} 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 { 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 { t } = useTranslation();
const {data: pages, isLoading, isError} = useRecentChangesQuery(spaceId);
if (isLoading) {
@ -26,7 +28,7 @@ export default function RecentChanges({spaceId}: Props) {
}
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 ? (
@ -48,7 +50,7 @@ export default function RecentChanges({spaceId}: Props) {
)}
<Text fw={500} size="md" lineClamp={1}>
{page.title || 'Untitled'}
{page.title || t("Untitled")}
</Text>
</Group>
</UnstyledButton>
@ -78,7 +80,7 @@ export default function RecentChanges({spaceId}: Props) {
</Table.ScrollContainer>
) : (
<Text size="md" ta="center">
No pages yet
{t("No pages yet")}
</Text>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -4,6 +4,7 @@ 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";

View File

@ -11,10 +11,12 @@ import {
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.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"}];
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,10 +37,10 @@ export function AppHeader() {
<Group wrap="nowrap">
{!isHomeRoute && (
<>
<Tooltip label="Sidebar toggle">
<Tooltip label={t("Sidebar toggle")}>
<SidebarToggle
aria-label="Sidebar toggle"
aria-label={t("Sidebar toggle")}
opened={mobileOpened}
onClick={toggleMobile}
hiddenFrom="sm"
@ -46,9 +48,9 @@ export function AppHeader() {
/>
</Tooltip>
<Tooltip label="Sidebar toggle">
<Tooltip label={t("Sidebar toggle")}>
<SidebarToggle
aria-label="Sidebar toggle"
aria-label={t("Sidebar toggle")}
opened={desktopOpened}
onClick={toggleDesktop}
visibleFrom="sm"

View File

@ -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))
}
}

View File

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

View File

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

View File

@ -19,3 +19,5 @@ export const asideStateAtom = atom<AsideStateType>({
tab: "",
isAsideOpen: false,
});
export const sidebarWidthAtom = atomWithWebStorage<number>('sidebarWidth', 300);

View File

@ -13,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();
@ -36,7 +38,7 @@ 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 size={16} />
@ -44,14 +46,14 @@ export default function TopMenu() {
</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 size={16} />}
>
Workspace settings
{t("Workspace settings")}
</Menu.Item>
<Menu.Item
@ -59,12 +61,12 @@ export default function TopMenu() {
to={APP_ROUTE.SETTINGS.WORKSPACE.MEMBERS}
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
@ -88,7 +90,7 @@ export default function TopMenu() {
to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}
leftSection={<IconUserCircle size={16} />}
>
My profile
{t("My profile")}
</Menu.Item>
<Menu.Item
@ -96,13 +98,13 @@ export default function TopMenu() {
to={APP_ROUTE.SETTINGS.ACCOUNT.PREFERENCES}
leftSection={<IconBrush size={16} />}
>
My preferences
{t("My preferences")}
</Menu.Item>
<Menu.Divider />
<Menu.Item onClick={logout} leftSection={<IconLogout size={16} />}>
Logout
{t("Logout")}
</Menu.Item>
</Menu.Dropdown>
</Menu>

View File

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

View File

@ -1,14 +1,14 @@
import React, { ReactNode } from 'react';
import React, { ReactNode, useState } from "react";
import {
ActionIcon,
Popover,
Button,
useMantineColorScheme,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { Suspense } from 'react';
const Picker = React.lazy(() => import('@emoji-mart/react'));
} from "@mantine/core";
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;
@ -23,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);
@ -43,16 +61,17 @@ 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' }}>
<Popover.Dropdown bg="000" style={{ border: "none" }} ref={setDropdown}>
<Suspense fallback={null}>
<Picker
data={async () => (await import('@emoji-mart/data')).default}
data={async () => (await import("@emoji-mart/data")).default}
onEmojiSelect={handleEmojiSelect}
perLine={8}
skinTonePosition="search"
@ -64,14 +83,14 @@ function EmojiPicker({
c="gray"
size="xs"
style={{
position: 'absolute',
position: "absolute",
zIndex: 2,
bottom: '1rem',
right: '1rem',
bottom: "1rem",
right: "1rem",
}}
onClick={handleRemoveEmoji}
>
Remove
{t("Remove")}
</Button>
</Popover.Dropdown>
</Popover>

View File

@ -2,21 +2,24 @@ 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 (
<>
<Helmet>
<title>404 page not found - Docmost</title>
<title>{t("404 page not found")} - Docmost</title>
</Helmet>
<Container className={classes.root}>
<Title className={classes.title}>404 Page Not Found</Title>
<Title className={classes.title}>{t("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.
{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">
Take me back to homepage
{t("Take me back to homepage")}
</Button>
</Group>
</Container>

View File

@ -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;
@ -36,10 +37,12 @@ export default function RoleSelectMenu({
onChange,
disabled,
}: 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} />}

View File

@ -1,8 +1,7 @@
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, { expires: 30 }),
@ -10,7 +9,7 @@ const cookieStorage = createJSONStorage<ITokens>(() => {
};
});
export const authTokensAtom = atomWithStorage<ITokens | null>(
export const authTokensAtom = atomWithStorage<any | null>(
"authTokens",
null,
cookieStorage,

View File

@ -6,6 +6,7 @@ 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
@ -15,6 +16,7 @@ const formSchema = z.object({
});
export function ForgotPasswordForm() {
const { t } = useTranslation();
const { forgotPassword, isLoading } = useAuth();
const [isTokenSent, setIsTokenSent] = useState<boolean>(false);
useRedirectIfAuthenticated();
@ -36,7 +38,7 @@ export function ForgotPasswordForm() {
<Container size={420} my={40} className={classes.container}>
<Box p="xl" mt={200}>
<Title order={2} ta="center" fw={500} mb="md">
Forgot password
{t("Forgot password")}
</Title>
<form onSubmit={form.onSubmit(onSubmit)}>
@ -53,14 +55,15 @@ export function ForgotPasswordForm() {
{isTokenSent && (
<Text>
A password reset link has been sent to your email. Please check
your inbox.
{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}>
Send reset link
{t("Send reset link")}
</Button>
)}
</form>

View File

@ -17,6 +17,7 @@ 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().trim().min(1),
@ -26,6 +27,7 @@ const formSchema = z.object({
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>

View File

@ -9,13 +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
@ -26,6 +26,7 @@ const formSchema = z.object({
});
export function LoginForm() {
const { t } = useTranslation();
const { signIn, isLoading } = useAuth();
useRedirectIfAuthenticated();
@ -45,29 +46,29 @@ 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>
@ -77,7 +78,7 @@ export function LoginForm() {
underline="never"
size="sm"
>
Forgot your password?
{t("Forgot your password?")}
</Anchor>
</Box>
</Container>

View File

@ -2,16 +2,10 @@ 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,
Text,
Title,
} from "@mantine/core";
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
@ -24,6 +18,7 @@ interface PasswordResetFormProps {
}
export function PasswordResetForm({ resetToken }: PasswordResetFormProps) {
const { t } = useTranslation();
const { passwordReset, isLoading } = useAuth();
useRedirectIfAuthenticated();
@ -37,28 +32,28 @@ export function PasswordResetForm({ resetToken }: PasswordResetFormProps) {
async function onSubmit(data: IPasswordReset) {
await passwordReset({
token: resetToken,
newPassword: data.newPassword
})
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">
Password reset
{t("Password reset")}
</Title>
<form onSubmit={form.onSubmit(onSubmit)}>
<PasswordInput
label="New password"
placeholder="Your new password"
label={t("New password")}
placeholder={t("Your new password")}
variant="filled"
mt="md"
{...form.getInputProps("newPassword")}
/>
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
Set password
{t("Set password")}
</Button>
</form>
</Box>

View File

@ -13,6 +13,7 @@ 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().trim().min(3).max(50),
@ -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>

View File

@ -2,13 +2,13 @@ import { useState } from "react";
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,
@ -20,29 +20,26 @@ import {
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",
@ -54,11 +51,8 @@ export default function useAuth() {
setIsLoading(true);
try {
const res = await acceptInvitation(data);
await acceptInvitation(data);
setIsLoading(false);
setAuthToken(res.tokens);
navigate(APP_ROUTE.HOME);
} catch (err) {
setIsLoading(false);
@ -75,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);
@ -92,14 +83,11 @@ export default function useAuth() {
setIsLoading(true);
try {
const res = await passwordReset(data);
await passwordReset(data);
setIsLoading(false);
setAuthToken(res.tokens);
navigate(APP_ROUTE.HOME);
notifications.show({
message: "Password reset was successful",
message: t("Password reset was successful"),
});
} catch (err) {
setIsLoading(false);
@ -110,33 +98,10 @@ export default function useAuth() {
}
};
const handleIsAuthenticated = async () => {
if (!authToken) {
return false;
}
try {
const accessToken = authToken.accessToken;
const payload = jwtDecode(accessToken);
// true if jwt is active
const now = Date.now().valueOf() / 1000;
return payload.exp >= now;
} catch (err) {
console.log("invalid jwt token", err);
return false;
}
};
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) => {
@ -179,12 +144,10 @@ export default function useAuth() {
signIn: handleSignIn,
invitationSignup: handleInvitationSignUp,
setupWorkspace: handleSetupWorkspace,
isAuthenticated: handleIsAuthenticated,
forgotPassword: handleForgotPassword,
passwordReset: handlePasswordReset,
verifyUserToken: handleVerifyUserToken,
logout: handleLogout,
hasTokens,
isLoading,
};
}

View File

@ -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]);
}

View File

@ -1,14 +1,28 @@
import { useQuery, UseQueryResult } from "@tanstack/react-query";
import { verifyUserToken } from "../services/auth-service";
import { IVerifyUserToken } from "../types/auth.types";
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,
});
}
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);
},
});
}

View File

@ -1,51 +1,49 @@
import api from "@/lib/api-client";
import {
IChangePassword,
ICollabToken,
IForgotPassword,
ILogin,
IPasswordReset,
IRegister,
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
data: IChangePassword,
): Promise<IChangePassword> {
const req = await api.post<IChangePassword>("/auth/change-password", data);
return req.data;
}
export async function setupWorkspace(
data: ISetupWorkspace
): Promise<ITokenResponse> {
const req = await api.post<ITokenResponse>("/auth/setup", data);
data: ISetupWorkspace,
): 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<any>("/auth/forgot-password", data);
await api.post<void>("/auth/forgot-password", data);
}
export async function passwordReset(
data: IPasswordReset
): Promise<ITokenResponse> {
const req = await api.post<any>("/auth/password-reset", data);
return req.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;
}

View File

@ -16,15 +16,6 @@ export interface ISetupWorkspace {
password: string;
}
export interface ITokens {
accessToken: string;
refreshToken: string;
}
export interface ITokenResponse {
tokens: ITokens;
}
export interface IChangePassword {
oldPassword: string;
newPassword: string;
@ -43,3 +34,7 @@ export interface IVerifyUserToken {
token: string;
type: string;
}
export interface ICollabToken {
token: string;
}

View File

@ -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>
);
}

View File

@ -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}
/>

View File

@ -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,
});

View File

@ -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}
/>
</>
)}

View File

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

View File

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

View File

@ -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>
);

View File

@ -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",
});
},

View File

@ -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>("");

View File

@ -1,8 +1,9 @@
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 { 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> => {
@ -23,7 +24,9 @@ export const uploadAttachmentAction = handleAttachmentUpload({
if (file.size > getFileUploadSizeLimit()) {
notifications.show({
color: "red",
message: `File exceeds the ${formatBytes(getFileUploadSizeLimit())} attachment limit`,
message: i18n.t("File exceeds the {{limit}} attachment limit", {
limit: formatBytes(getFileUploadSizeLimit()),
}),
});
return false;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
);
};

View File

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

View File

@ -1,21 +1,22 @@
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 MermaidView from "@/features/editor/components/code-block/mermaid-view.tsx";
import classes from './code-block.module.css';
import React from 'react';
import { Suspense } from 'react';
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')
() => 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
language || null,
);
const [isSelected, setIsSelected] = useState(false);
@ -30,9 +31,9 @@ export default function CodeBlockView(props: NodeViewProps) {
setIsSelected(isNodeSelected);
};
editor.on('selectionUpdate', updateSelection);
editor.on("selectionUpdate", updateSelection);
return () => {
editor.off('selectionUpdate', updateSelection);
editor.off("selectionUpdate", updateSelection);
};
}, [editor, getPos(), node.nodeSize]);
@ -45,7 +46,11 @@ export default function CodeBlockView(props: NodeViewProps) {
return (
<NodeViewWrapper className="codeBlock">
<Group justify="flex-end" contentEditable={false}>
<Group
justify="flex-end"
contentEditable={false}
className={classes.menuGroup}
>
<Select
placeholder="auto"
checkIconPosition="right"
@ -53,7 +58,7 @@ export default function CodeBlockView(props: NodeViewProps) {
value={languageValue}
onChange={changeLanguage}
searchable
style={{ maxWidth: '130px' }}
style={{ maxWidth: "130px" }}
classNames={{ input: classes.selectInput }}
disabled={!editor.isEditable}
/>
@ -61,12 +66,12 @@ export default function CodeBlockView(props: NodeViewProps) {
<CopyButton value={node?.textContent} timeout={2000}>
{({ copied, copy }) => (
<Tooltip
label={copied ? 'Copied' : 'Copy'}
label={copied ? t("Copied") : t("Copy")}
withArrow
position="right"
>
<ActionIcon
color={copied ? 'teal' : 'gray'}
color={copied ? "teal" : "gray"}
variant="subtle"
onClick={copy}
>
@ -80,15 +85,15 @@ export default function CodeBlockView(props: NodeViewProps) {
<pre
spellCheck="false"
hidden={
((language === 'mermaid' && !editor.isEditable) ||
(language === 'mermaid' && !isSelected)) &&
((language === "mermaid" && !editor.isEditable) ||
(language === "mermaid" && !isSelected)) &&
node.textContent.length > 0
}
>
<NodeViewContent as="code" className={`language-${language}`} />
</pre>
{language === 'mermaid' && (
{language === "mermaid" && (
<Suspense fallback={null}>
<MermaidView props={props} />
</Suspense>

View File

@ -16,3 +16,9 @@
align-items: center;
justify-content: center;
}
.menuGroup {
@media print {
display: none;
}
}

View File

@ -3,6 +3,7 @@ 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,
@ -14,6 +15,7 @@ interface MermaidViewProps {
}
export default function MermaidView({ props }: MermaidViewProps) {
const { t } = useTranslation();
const { node } = props;
const [preview, setPreview] = useState<string>("");
@ -29,11 +31,11 @@ export default function MermaidView({ props }: MermaidViewProps) {
.catch((err) => {
if (props.editor.isEditable) {
setPreview(
`<div class="${classes.error}">Mermaid diagram error: ${err}</div>`,
`<div class="${classes.error}">${t("Mermaid diagram error:")} ${err}</div>`,
);
} else {
setPreview(
`<div class="${classes.error}">Invalid Mermaid Diagram</div>`,
`<div class="${classes.error}">${t("Invalid Mermaid diagram")}</div>`,
);
}
});

View File

@ -2,12 +2,42 @@ 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 handleFilePaste = (
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);

View File

@ -1,25 +1,34 @@
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 { getFileUrl } from '@/lib/config.ts';
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';
} 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 [initialXML, setInitialXML] = useState<string>("");
const [opened, { open, close }] = useDisclosure(false);
const computedColorScheme = useComputedColorScheme();
@ -32,15 +41,15 @@ export default function DrawioView(props: NodeViewProps) {
if (src) {
const url = getFileUrl(src);
const request = await fetch(url, {
credentials: 'include',
cache: 'no-store',
credentials: "include",
cache: "no-store",
});
const blob = await request.blob();
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => {
let base64data = (reader.result || '') as string;
const base64data = (reader.result || "") as string;
setInitialXML(base64data);
};
}
@ -54,7 +63,7 @@ export default function DrawioView(props: NodeViewProps) {
const handleSave = async (data: EventSave) => {
const svgString = decodeBase64ToSvgString(data.xml);
const fileName = 'diagram.drawio.svg';
const fileName = "diagram.drawio.svg";
const drawioSVGFile = await svgStringToFile(svgString, fileName);
const pageId = editor.storage?.pageId;
@ -81,14 +90,15 @@ export default function DrawioView(props: NodeViewProps) {
<NodeViewWrapper>
<Modal.Root opened={opened} onClose={close} fullScreen>
<Modal.Overlay />
<Modal.Content style={{ overflow: 'hidden' }}>
<Modal.Content style={{ overflow: "hidden" }}>
<Modal.Body>
<div style={{ height: '100vh' }}>
<div style={{ height: "100vh" }}>
<DrawIoEmbed
ref={drawioRef}
xml={initialXML}
baseUrl={getDrawioUrl()}
urlParameters={{
ui: computedColorScheme === 'light' ? 'kennedy' : 'dark',
ui: computedColorScheme === "light" ? "kennedy" : "dark",
spin: true,
libraries: true,
saveAndExit: true,
@ -96,7 +106,7 @@ export default function DrawioView(props: NodeViewProps) {
}}
onSave={(data: EventSave) => {
// If the save is triggered by another event, then do nothing
if (data.parentEvent !== 'save') {
if (data.parentEvent !== "save") {
return;
}
handleSave(data);
@ -115,7 +125,7 @@ export default function DrawioView(props: NodeViewProps) {
</Modal.Root>
{src ? (
<div style={{ position: 'relative' }}>
<div style={{ position: "relative" }}>
<Image
onClick={(e) => e.detail === 2 && handleOpen()}
radius="md"
@ -124,8 +134,8 @@ export default function DrawioView(props: NodeViewProps) {
src={getFileUrl(src)}
alt={title}
className={clsx(
selected ? 'ProseMirror-selectednode' : '',
'alignCenter'
selected ? "ProseMirror-selectednode" : "",
"alignCenter",
)}
/>
@ -136,7 +146,7 @@ export default function DrawioView(props: NodeViewProps) {
color="gray"
mx="xs"
style={{
position: 'absolute',
position: "absolute",
top: 8,
right: 8,
}}
@ -151,20 +161,20 @@ export default function DrawioView(props: NodeViewProps) {
onClick={(e) => e.detail === 2 && handleOpen()}
p="xs"
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
withBorder
className={clsx(selected ? 'ProseMirror-selectednode' : '')}
className={clsx(selected ? "ProseMirror-selectednode" : "")}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ display: "flex", alignItems: "center" }}>
<ActionIcon variant="transparent" color="gray">
<IconEdit size={18} />
</ActionIcon>
<Text component="span" size="lg" c="dimmed">
Double-click to edit drawio diagram
{t("Double-click to edit Draw.io diagram")}
</Text>
</div>
</Card>

View File

@ -1,22 +1,37 @@
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 {
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
getEmbedUrlAndProvider,
} from "@/features/editor/components/embed/providers.ts";
import { notifications } from '@mantine/notifications';
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
import i18n from "i18next";
const schema = z.object({
url: z
.string().trim().url({ message: 'please enter a valid url' }),
.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;
@ -41,9 +56,11 @@ export default function EmbedView(props: NodeViewProps) {
updateAttributes({ src: data.url });
} else {
notifications.show({
message: `Invalid ${provider} embed link`,
position: 'top-right',
color: 'red'
message: t("Invalid {{provider}} embed link", {
provider: embedProvider.name,
}),
position: "top-right",
color: "red",
});
}
}
@ -62,7 +79,6 @@ export default function EmbedView(props: NodeViewProps) {
frameBorder="0"
></iframe>
</AspectRatio>
</>
) : (
<Popover width={300} position="bottom" withArrow shadow="md">
@ -71,20 +87,22 @@ export default function EmbedView(props: NodeViewProps) {
radius="md"
p="xs"
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
withBorder
className={clsx(selected ? 'ProseMirror-selectednode' : '')}
className={clsx(selected ? "ProseMirror-selectednode" : "")}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ display: "flex", alignItems: "center" }}>
<ActionIcon variant="transparent" color="gray">
<IconEdit size={18} />
</ActionIcon>
<Text component="span" size="lg" c="dimmed">
Embed {getEmbedProviderById(provider).name}
{t("Embed {{provider}}", {
provider: getEmbedProviderById(provider).name,
})}
</Text>
</div>
</Card>
@ -92,15 +110,18 @@ export default function EmbedView(props: NodeViewProps) {
<Popover.Dropdown bg="var(--mantine-color-body)">
<form onSubmit={embedForm.onSubmit(onSubmit)}>
<FocusTrap active={true}>
<TextInput placeholder={`Enter ${getEmbedProviderById(provider).name} link to embed`}
key={embedForm.key('url')}
{... embedForm.getInputProps('url')}
data-autofocus
<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">Embed link</Button>
<Button type="submit">{t("Embed link")}</Button>
</Group>
</form>
</Popover.Dropdown>

View File

@ -10,7 +10,10 @@ export const embedProviders: IEmbedProvider[] = [
id: 'loom',
name: 'Loom',
regex: /^https?:\/\/(?:www\.)?loom\.com\/(?:share|embed)\/([\da-zA-Z]+)\/?/,
getEmbedUrl: (match) => {
getEmbedUrl: (match, url) => {
if(url.includes("/embed/")){
return url;
}
return `https://loom.com/embed/${match[1]}`;
}
},
@ -20,6 +23,9 @@ export const embedProviders: IEmbedProvider[] = [
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]}`;
}
},
@ -43,7 +49,10 @@ export const embedProviders: IEmbedProvider[] = [
id: 'miro',
name: 'Miro',
regex: /^https:\/\/(www\.)?miro\.com\/app\/board\/([\w-]+=)/,
getEmbedUrl: (match) => {
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`;
}
},
@ -51,7 +60,10 @@ export const embedProviders: IEmbedProvider[] = [
id: 'youtube',
name: 'YouTube',
regex: /^((?:https?:)?\/\/)?((?:www|m|music)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/,
getEmbedUrl: (match) => {
getEmbedUrl: (match, url) => {
if (url.includes("/embed/")){
return url;
}
return `https://www.youtube-nocookie.com/embed/${match[5]}`;
}
},
@ -79,6 +91,14 @@ export const embedProviders: IEmbedProvider[] = [
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) {

View File

@ -1,4 +1,4 @@
import { NodeViewProps, NodeViewWrapper } from '@tiptap/react';
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import {
ActionIcon,
Button,
@ -7,27 +7,29 @@ import {
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';
} 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) => ({
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;
@ -46,11 +48,11 @@ export default function ExcalidrawView(props: NodeViewProps) {
if (src) {
const url = getFileUrl(src);
const request = await fetch(url, {
credentials: 'include',
cache: 'no-store',
credentials: "include",
cache: "no-store",
});
const { loadFromBlob } = await import('@excalidraw/excalidraw');
const { loadFromBlob } = await import("@excalidraw/excalidraw");
const data = await loadFromBlob(await request.blob(), null, null);
setExcalidrawData(data);
@ -67,7 +69,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
return;
}
const { exportToSvg } = await import('@excalidraw/excalidraw');
const { exportToSvg } = await import("@excalidraw/excalidraw");
const svg = await exportToSvg({
elements: excalidrawAPI?.getSceneElements(),
@ -83,10 +85,10 @@ export default function ExcalidrawView(props: NodeViewProps) {
svgString = svgString.replace(
/https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g,
'https://unpkg.com/@excalidraw/excalidraw@latest'
"https://unpkg.com/@excalidraw/excalidraw@latest",
);
const fileName = 'diagram.excalidraw.svg';
const fileName = "diagram.excalidraw.svg";
const excalidrawSvgFile = await svgStringToFile(svgString, fileName);
const pageId = editor.storage?.pageId;
@ -112,7 +114,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
<NodeViewWrapper>
<ReactClearModal
style={{
backgroundColor: 'rgba(0, 0, 0, 0.5)',
backgroundColor: "rgba(0, 0, 0, 0.5)",
padding: 0,
zIndex: 200,
}}
@ -122,7 +124,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
contentProps={{
style: {
padding: 0,
width: '90vw',
width: "90vw",
},
}}
>
@ -132,14 +134,14 @@ export default function ExcalidrawView(props: NodeViewProps) {
bg="var(--mantine-color-body)"
p="xs"
>
<Button onClick={handleSave} size={'compact-sm'}>
Save & Exit
<Button onClick={handleSave} size={"compact-sm"}>
{t("Save & Exit")}
</Button>
<Button onClick={close} color="red" size={'compact-sm'}>
Exit
<Button onClick={close} color="red" size={"compact-sm"}>
{t("Exit")}
</Button>
</Group>
<div style={{ height: '90vh' }}>
<div style={{ height: "90vh" }}>
<Suspense fallback={null}>
<Excalidraw
excalidrawAPI={(api) => setExcalidrawAPI(api)}
@ -154,7 +156,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
</ReactClearModal>
{src ? (
<div style={{ position: 'relative' }}>
<div style={{ position: "relative" }}>
<Image
onClick={(e) => e.detail === 2 && handleOpen()}
radius="md"
@ -163,8 +165,8 @@ export default function ExcalidrawView(props: NodeViewProps) {
src={getFileUrl(src)}
alt={title}
className={clsx(
selected ? 'ProseMirror-selectednode' : '',
'alignCenter'
selected ? "ProseMirror-selectednode" : "",
"alignCenter",
)}
/>
@ -175,7 +177,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
color="gray"
mx="xs"
style={{
position: 'absolute',
position: "absolute",
top: 8,
right: 8,
}}
@ -190,20 +192,20 @@ export default function ExcalidrawView(props: NodeViewProps) {
onClick={(e) => e.detail === 2 && handleOpen()}
p="xs"
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
withBorder
className={clsx(selected ? 'ProseMirror-selectednode' : '')}
className={clsx(selected ? "ProseMirror-selectednode" : "")}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div style={{ display: "flex", alignItems: "center" }}>
<ActionIcon variant="transparent" color="gray">
<IconEdit size={18} />
</ActionIcon>
<Text component="span" size="lg" c="dimmed">
Double-click to edit Excalidraw diagram
{t("Double-click to edit Excalidraw diagram")}
</Text>
</div>
</Card>

View File

@ -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"
}

View File

@ -1,8 +1,9 @@
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 { 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> => {
@ -23,7 +24,9 @@ export const uploadImageAction = handleImageUpload({
if (file.size > getFileUploadSizeLimit()) {
notifications.show({
color: "red",
message: `File exceeds the ${formatBytes(getFileUploadSizeLimit())} attachment limit`,
message: i18n.t("File exceeds the {{limit}} attachment limit", {
limit: formatBytes(getFileUploadSizeLimit()),
}),
});
return false;
}

View File

@ -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;
},
});

View File

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

View File

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

View File

@ -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;
}

View File

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

View File

@ -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)) {

View File

@ -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));
@ -30,6 +31,7 @@
transition: background-color 0.2s;
margin: 0 0.1rem;
overflow-x: auto;
user-select: none;
.textInput {
width: 400px;

View File

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

View File

@ -0,0 +1,113 @@
import { ReactRenderer, useEditor } from "@tiptap/react";
import tippy from "tippy.js";
import MentionList from "@/features/editor/components/mention/mention-list.tsx";
function getWhitespaceCount(query: string) {
const matches = query?.match(/([\s]+)/g);
return matches?.length || 0;
}
const mentionRenderItems = () => {
let component: ReactRenderer | null = null;
let popup: any | null = null;
return {
onStart: (props: {
editor: ReturnType<typeof useEditor>;
clientRect: DOMRect;
query: string;
}) => {
// query must not start with a whitespace
if (props.query.charAt(0) === ' '){
return;
}
// don't render component if space between the search query words is greater than 4
const whitespaceCount = getWhitespaceCount(props.query);
if (whitespaceCount > 4) {
return;
}
component = new ReactRenderer(MentionList, {
props,
editor: props.editor,
});
if (!props.clientRect) {
return;
}
// @ts-ignore
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: "manual",
placement: "bottom-start",
});
},
onUpdate: (props: {
editor: ReturnType<typeof useEditor>;
clientRect: DOMRect;
query: string;
}) => {
// query must not start with a whitespace
if (props.query.charAt(0) === ' '){
component?.destroy();
return;
}
// only update component if popup is not destroyed
if (!popup?.[0].state.isDestroyed) {
component?.updateProps(props);
}
if (!props || !props.clientRect) {
return;
}
const whitespaceCount = getWhitespaceCount(props.query);
// destroy component if space is greater 3 without a match
if (
whitespaceCount > 3 &&
props.editor.storage.mentionItems.length === 0
) {
popup?.[0]?.destroy();
component?.destroy();
return;
}
popup &&
!popup?.[0].state.isDestroyed &&
popup?.[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown: (props: { event: KeyboardEvent }) => {
if (props.event.key)
if (
props.event.key === "Escape" ||
(props.event.key === "Enter" && !popup?.[0].state.isShown)
) {
popup?.[0].destroy();
component?.destroy();
return false;
}
return (component?.ref as any)?.onKeyDown(props);
},
onExit: () => {
if (popup && !popup?.[0].state.isDestroyed) {
popup[0].destroy();
}
if (component) {
component.destroy();
}
},
};
};
export default mentionRenderItems;

View File

@ -0,0 +1,56 @@
import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import { ActionIcon, Anchor, Text } from "@mantine/core";
import { IconFileDescription } from "@tabler/icons-react";
import { Link, useParams } from "react-router-dom";
import { usePageQuery } from "@/features/page/queries/page-query.ts";
import { buildPageUrl } from "@/features/page/page.utils.ts";
import classes from "./mention.module.css";
export default function MentionView(props: NodeViewProps) {
const { node } = props;
const { label, entityType, entityId, slugId } = node.attrs;
const { spaceSlug } = useParams();
const {
data: page,
isLoading,
isError,
} = usePageQuery({ pageId: entityType === "page" ? slugId : null });
return (
<NodeViewWrapper style={{ display: "inline" }}>
{entityType === "user" && (
<Text className={classes.userMention} component="span">
@{label}
</Text>
)}
{entityType === "page" && (
<Anchor
component={Link}
fw={500}
to={buildPageUrl(spaceSlug, slugId, label)}
underline="never"
className={classes.pageMentionLink}
>
{page?.icon ? (
<span style={{ marginRight: "4px" }}>{page.icon}</span>
) : (
<ActionIcon
variant="transparent"
color="gray"
component="span"
size={18}
style={{ verticalAlign: "text-bottom" }}
>
<IconFileDescription size={18} />
</ActionIcon>
)}
<span className={classes.pageMentionText}>
{page?.title || label}
</span>
</Anchor>
)}
</NodeViewWrapper>
);
}

View File

@ -0,0 +1,58 @@
.pageMentionLink {
color: light-dark(
var(--mantine-color-dark-4),
var(--mantine-color-dark-1)
) !important;
}
.pageMentionText {
@mixin light {
border-bottom: 0.05em solid var(--mantine-color-dark-0);
}
@mixin dark {
border-bottom: 0.05em solid var(--mantine-color-dark-2);
}
}
.userMention {
background-color: light-dark(
var(--mantine-color-gray-1),
var(--mantine-color-dark-6)
);
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-1));
font-weight: 500;
border-radius: 0.4rem;
box-decoration-break: clone;
padding: 0.1rem 0.3rem;
cursor: pointer;
&::after {
content: "\200B";
}
}
.menuBtn {
width: 100%;
padding: 4px;
margin-bottom: 2px;
color: var(--mantine-color-text);
border-radius: var(--mantine-radius-sm);
&:hover {
@mixin light {
background: var(--mantine-color-gray-2);
}
@mixin dark {
background: var(--mantine-color-gray-light);
}
}
}
.selectedItem {
@mixin light {
background: var(--mantine-color-gray-2);
}
@mixin dark {
background: var(--mantine-color-gray-light);
}
}

View File

@ -0,0 +1,28 @@
import { Editor, Range } from "@tiptap/core";
export interface MentionListProps {
query: string;
command: any;
items: [];
range: Range;
text: string;
editor: Editor;
}
export type MentionSuggestionItem =
| { entityType: "header"; label: string }
| {
id: string;
label: string;
entityType: "user";
entityId: string;
avatarUrl: string;
}
| {
id: string;
label: string;
entityType: "page";
entityId: string;
slugId: string;
icon: string;
};

View File

@ -13,6 +13,7 @@ import {
} from "@mantine/core";
import classes from "./slash-menu.module.css";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
const CommandList = ({
items,
@ -25,6 +26,7 @@ const CommandList = ({
editor: any;
range: any;
}) => {
const { t } = useTranslation();
const [selectedIndex, setSelectedIndex] = useState(0);
const viewportRef = useRef<HTMLDivElement>(null);
@ -104,18 +106,17 @@ const CommandList = ({
<ActionIcon
variant="default"
component="div"
aria-label={item.title}
>
<item.icon size={18} />
</ActionIcon>
<div style={{ flex: 1 }}>
<Text size="sm" fw={500}>
{item.title}
{t(item.title)}
</Text>
<Text c="dimmed" size="xs">
{item.description}
{t(item.description)}
</Text>
</div>
</Group>

View File

@ -34,6 +34,7 @@ import {
FigmaIcon,
FramerIcon,
GoogleDriveIcon,
GoogleSheetsIcon,
LoomIcon,
MiroIcon,
TypeformIcon,
@ -442,6 +443,15 @@ const CommandGroups: SlashMenuGroupedItemsType = {
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'gdrive' }).run();
},
},
{
title: "Google Sheets",
description: "Embed Google Sheets content",
searchTerms: ["google sheets", "gsheets"],
icon: GoogleSheetsIcon,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).setEmbed({ provider: 'gsheets' }).run();
},
},
],
};
@ -456,7 +466,7 @@ export const getSuggestionItems = ({
const fuzzyMatch = (query: string, target: string) => {
let queryIndex = 0;
target = target.toLowerCase();
for (let char of target) {
for (const char of target) {
if (query[queryIndex] === char) queryIndex++;
if (queryIndex === query.length) return true;
}

View File

@ -13,9 +13,11 @@ import {
IconRowRemove,
IconSquareToggle,
} from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
export const TableCellMenu = React.memo(
({ editor, appendTo }: EditorMenuProps): JSX.Element => {
const { t } = useTranslation();
const shouldShow = useCallback(
({ view, state, from }: ShouldShowProps) => {
if (!state) {
@ -58,45 +60,45 @@ export const TableCellMenu = React.memo(
shouldShow={shouldShow}
>
<ActionIcon.Group>
<Tooltip position="top" label="Merge cells">
<Tooltip position="top" label={t("Merge cells")}>
<ActionIcon
onClick={mergeCells}
variant="default"
size="lg"
aria-label="Merge cells"
aria-label={t("Merge cells")}
>
<IconBoxMargin size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label="Split cell">
<Tooltip position="top" label={t("Split cell")}>
<ActionIcon
onClick={splitCell}
variant="default"
size="lg"
aria-label="Split cell"
aria-label={t("Split cell")}
>
<IconSquareToggle size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label="Delete column">
<Tooltip position="top" label={t("Delete column")}>
<ActionIcon
onClick={deleteColumn}
variant="default"
size="lg"
aria-label="Delete column"
aria-label={t("Delete column")}
>
<IconColumnRemove size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label="Delete row">
<Tooltip position="top" label={t("Delete row")}>
<ActionIcon
onClick={deleteRow}
variant="default"
size="lg"
aria-label="Delete row"
aria-label={t("Delete row")}
>
<IconRowRemove size={18} />
</ActionIcon>

View File

@ -21,9 +21,11 @@ import {
IconTrashX,
} from "@tabler/icons-react";
import { isCellSelection } from "@docmost/editor-ext";
import { useTranslation } from "react-i18next";
export const TableMenu = React.memo(
({ editor }: EditorMenuProps): JSX.Element => {
const { t } = useTranslation();
const shouldShow = useCallback(
({ state }: ShouldShowProps) => {
if (!state) {
@ -111,79 +113,80 @@ export const TableMenu = React.memo(
shouldShow={shouldShow}
>
<ActionIcon.Group>
<Tooltip position="top" label="Add left column">
<Tooltip position="top" label={t("Add left column")}
>
<ActionIcon
onClick={addColumnLeft}
variant="default"
size="lg"
aria-label="Add left column"
aria-label={t("Add left column")}
>
<IconColumnInsertLeft size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label="Add right column">
<Tooltip position="top" label={t("Add right column")}>
<ActionIcon
onClick={addColumnRight}
variant="default"
size="lg"
aria-label="Add right column"
aria-label={t("Add right column")}
>
<IconColumnInsertRight size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label="Delete column">
<Tooltip position="top" label={t("Delete column")}>
<ActionIcon
onClick={deleteColumn}
variant="default"
size="lg"
aria-label="Delete column"
aria-label={t("Delete column")}
>
<IconColumnRemove size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label="Add row above">
<Tooltip position="top" label={t("Add row above")}>
<ActionIcon
onClick={addRowAbove}
variant="default"
size="lg"
aria-label="Add row above"
aria-label={t("Add row above")}
>
<IconRowInsertTop size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label="Add row below">
<Tooltip position="top" label={t("Add row below")}>
<ActionIcon
onClick={addRowBelow}
variant="default"
size="lg"
aria-label="Add row below"
aria-label={t("Add row below")}
>
<IconRowInsertBottom size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label="Delete row">
<Tooltip position="top" label={t("Delete row")}>
<ActionIcon
onClick={deleteRow}
variant="default"
size="lg"
aria-label="Delete row"
aria-label={t("Delete row")}
>
<IconRowRemove size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label="Delete table">
<Tooltip position="top" label={t("Delete table")}>
<ActionIcon
onClick={deleteTable}
variant="default"
size="lg"
color="red"
aria-label="Delete table"
aria-label={t("Delete table")}
>
<IconTrashX size={18} />
</ActionIcon>

View File

@ -1,8 +1,9 @@
import { handleVideoUpload } 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 { getFileUploadSizeLimit } from "@/lib/config.ts";
import { formatBytes } from "@/lib";
import i18n from "@/i18n.ts";
export const uploadVideoAction = handleVideoUpload({
onUpload: async (file: File, pageId: string): Promise<any> => {
@ -24,11 +25,12 @@ export const uploadVideoAction = handleVideoUpload({
if (file.size > getFileUploadSizeLimit()) {
notifications.show({
color: "red",
message: `File exceeds the ${formatBytes(getFileUploadSizeLimit())} attachment limit`,
message: i18n.t("File exceeds the {{limit}} attachment limit", {
limit: formatBytes(getFileUploadSizeLimit()),
}),
});
return false;
}
return true;
},
});

View File

@ -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 VideoMenu({ editor }: EditorMenuProps) {
const { t } = useTranslation();
const shouldShow = useCallback(
({ state }: ShouldShowProps) => {
if (!state) {
@ -96,11 +98,11 @@ export function VideoMenu({ editor }: EditorMenuProps) {
shouldShow={shouldShow}
>
<ActionIcon.Group className="actionIconGroup">
<Tooltip position="top" label="Align video left">
<Tooltip position="top" label={t("Align left")}>
<ActionIcon
onClick={alignVideoLeft}
size="lg"
aria-label="Align video left"
aria-label={t("Align left")}
variant={
editor.isActive("video", { align: "left" }) ? "light" : "default"
}
@ -109,11 +111,11 @@ export function VideoMenu({ editor }: EditorMenuProps) {
</ActionIcon>
</Tooltip>
<Tooltip position="top" label="Align video center">
<Tooltip position="top" label={t("Align center")}>
<ActionIcon
onClick={alignVideoCenter}
size="lg"
aria-label="Align video center"
aria-label={t("Align center")}
variant={
editor.isActive("video", { align: "center" })
? "light"
@ -124,11 +126,11 @@ export function VideoMenu({ editor }: EditorMenuProps) {
</ActionIcon>
</Tooltip>
<Tooltip position="top" label="Align video right">
<Tooltip position="top" label={t("Align right")}>
<ActionIcon
onClick={alignVideoRight}
size="lg"
aria-label="Align video right"
aria-label={t("Align right")}
variant={
editor.isActive("video", { align: "right" }) ? "light" : "default"
}

View File

@ -35,7 +35,8 @@ import {
CustomCodeBlock,
Drawio,
Excalidraw,
Embed
Embed,
Mention,
} from "@docmost/editor-ext";
import {
randomElement,
@ -64,6 +65,11 @@ import clojure from "highlight.js/lib/languages/clojure";
import fortran from "highlight.js/lib/languages/fortran";
import haskell from "highlight.js/lib/languages/haskell";
import scala from "highlight.js/lib/languages/scala";
import mentionRenderItems from "@/features/editor/components/mention/mention-suggestion.ts";
import { ReactNodeViewRenderer } from "@tiptap/react";
import MentionView from "@/features/editor/components/mention/mention-view.tsx";
import i18n from "@/i18n.ts";
import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboard.ts";
const lowlight = createLowlight(common);
lowlight.register("mermaid", plaintext);
@ -94,13 +100,13 @@ export const mainExtensions = [
Placeholder.configure({
placeholder: ({ node }) => {
if (node.type.name === "heading") {
return `Heading ${node.attrs.level}`;
return i18n.t("Heading {{level}}", { level: node.attrs.level });
}
if (node.type.name === "detailsSummary") {
return "Toggle title";
return i18n.t("Toggle title");
}
if (node.type.name === "paragraph") {
return 'Write anything. Enter "/" for commands';
return i18n.t('Write anything. Enter "/" for commands');
}
},
includeChildren: true,
@ -131,7 +137,23 @@ export const mainExtensions = [
class: "comment-mark",
},
}),
Mention.configure({
suggestion: {
allowSpaces: true,
items: () => {
return [];
},
// @ts-ignore
render: mentionRenderItems,
},
HTMLAttributes: {
class: "mention",
},
}).extend({
addNodeView() {
return ReactNodeViewRenderer(MentionView);
},
}),
Table.configure({
resizable: true,
lastColumnResizable: false,
@ -140,7 +162,6 @@ export const mainExtensions = [
TableRow,
TableCell,
TableHeader,
MathInline.configure({
view: MathInlineView,
}),
@ -184,7 +205,10 @@ export const mainExtensions = [
}),
Embed.configure({
view: EmbedView,
})
}),
MarkdownClipboard.configure({
transformPastedText: true,
}),
] as any;
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];

View File

@ -0,0 +1,53 @@
// adapted from: https://github.com/aguingand/tiptap-markdown/blob/main/src/extensions/tiptap/clipboard.js - MIT
import { Extension } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { DOMParser } from "@tiptap/pm/model";
import { find } from "linkifyjs";
import { markdownToHtml } from "@docmost/editor-ext";
export const MarkdownClipboard = Extension.create({
name: "markdownClipboard",
priority: 50,
addOptions() {
return {
transformPastedText: false,
};
},
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey("markdownClipboard"),
props: {
clipboardTextParser: (text, context, plainText) => {
const link = find(text, {
defaultProtocol: "http",
}).find((item) => item.isLink && item.value === text);
if (plainText || !this.options.transformPastedText || link) {
// don't parse plaintext link to allow link paste handler to work
// pasting with shift key prevents formatting
return null;
}
const parsed = markdownToHtml(text);
return DOMParser.fromSchema(this.editor.schema).parseSlice(
elementFromString(parsed),
{
preserveWhitespace: true,
context,
},
);
},
},
}),
];
},
});
function elementFromString(value) {
// add a wrapper to preserve leading and trailing whitespace
const wrappedValue = `<body>${value}</body>`;
return new window.DOMParser().parseFromString(wrappedValue, "text/html").body;
}

View File

@ -13,6 +13,7 @@ export interface FullEditorProps {
pageId: string;
slugId: string;
title: string;
content: string;
spaceSlug: string;
editable: boolean;
}
@ -21,6 +22,7 @@ export function FullEditor({
pageId,
title,
slugId,
content,
spaceSlug,
editable,
}: FullEditorProps) {
@ -30,8 +32,7 @@ export function FullEditor({
return (
<Container
fluid={fullPageWidth}
{...(fullPageWidth && { mx: 80 })}
size={850}
size={!fullPageWidth && 850}
className={classes.editor}
>
<MemoizedTitleEditor
@ -41,7 +42,7 @@ export function FullEditor({
spaceSlug={spaceSlug}
editable={editable}
/>
<MemoizedPageEditor pageId={pageId} editable={editable} />
<MemoizedPageEditor pageId={pageId} editable={editable} content={content} />
</Container>
);
}

View File

@ -8,24 +8,25 @@ import React, {
} from "react";
import { IndexeddbPersistence } from "y-indexeddb";
import * as Y from "yjs";
import { HocuspocusProvider } from "@hocuspocus/provider";
import { EditorContent, useEditor } from "@tiptap/react";
import { HocuspocusProvider, WebSocketStatus } from "@hocuspocus/provider";
import { EditorContent, EditorProvider, useEditor } from "@tiptap/react";
import {
collabExtensions,
mainExtensions,
} from "@/features/editor/extensions/extensions";
import { useAtom } from "jotai";
import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom";
import useCollaborationUrl from "@/features/editor/hooks/use-collaboration-url";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms";
import {
pageEditorAtom,
yjsConnectionStatusAtom,
} from "@/features/editor/atoms/editor-atoms";
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
import {
activeCommentIdAtom,
showCommentPopupAtom,
} from "@/features/comment/atoms/comment-atom";
import CommentDialog from "@/features/comment/components/comment-dialog";
import EditorSkeleton from "@/features/editor/components/editor-skeleton";
import { EditorBubbleMenu } from "@/features/editor/components/bubble-menu/bubble-menu";
import TableCellMenu from "@/features/editor/components/table/table-cell-menu.tsx";
import TableMenu from "@/features/editor/components/table/table-menu.tsx";
@ -34,19 +35,24 @@ import CalloutMenu from "@/features/editor/components/callout/callout-menu.tsx";
import VideoMenu from "@/features/editor/components/video/video-menu.tsx";
import {
handleFileDrop,
handleFilePaste,
} from "@/features/editor/components/common/file-upload-handler.tsx";
handlePaste,
} from "@/features/editor/components/common/editor-paste-handler.tsx";
import LinkMenu from "@/features/editor/components/link/link-menu.tsx";
import ExcalidrawMenu from "./components/excalidraw/excalidraw-menu";
import DrawioMenu from "./components/drawio/drawio-menu";
import { useCollabToken } from "@/features/auth/queries/auth-query.tsx";
interface PageEditorProps {
pageId: string;
editable: boolean;
content: any;
}
export default function PageEditor({ pageId, editable }: PageEditorProps) {
const [token] = useAtom(authTokensAtom);
export default function PageEditor({
pageId,
editable,
content,
}: PageEditorProps) {
const collaborationURL = useCollaborationUrl();
const [currentUser] = useAtom(currentUserAtom);
const [, setEditor] = useAtom(pageEditorAtom);
@ -56,8 +62,12 @@ export default function PageEditor({ pageId, editable }: PageEditorProps) {
const ydoc = useMemo(() => new Y.Doc(), [pageId]);
const [isLocalSynced, setLocalSynced] = useState(false);
const [isRemoteSynced, setRemoteSynced] = useState(false);
const documentName = `page.${pageId}`;
const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom(
yjsConnectionStatusAtom,
);
const menuContainerRef = useRef(null);
const documentName = `page.${pageId}`;
const { data } = useCollabToken();
const localProvider = useMemo(() => {
const provider = new IndexeddbPersistence(documentName, ydoc);
@ -67,23 +77,32 @@ export default function PageEditor({ pageId, editable }: PageEditorProps) {
});
return provider;
}, [pageId, ydoc]);
}, [pageId, ydoc, data?.token]);
const remoteProvider = useMemo(() => {
const provider = new HocuspocusProvider({
name: documentName,
url: collaborationURL,
document: ydoc,
token: token?.accessToken,
token: data?.token,
connect: false,
onStatus: (status) => {
if (status.status === "connected") {
setYjsConnectionStatus(status.status);
}
},
});
provider.on("synced", () => {
setRemoteSynced(true);
});
provider.on("disconnect", () => {
setYjsConnectionStatus(WebSocketStatus.Disconnected);
});
return provider;
}, [ydoc, pageId, token?.accessToken]);
}, [ydoc, pageId, data?.token]);
useLayoutEffect(() => {
remoteProvider.connect();
@ -105,7 +124,10 @@ export default function PageEditor({ pageId, editable }: PageEditorProps) {
{
extensions,
editable,
immediatelyRender: true,
editorProps: {
scrollThreshold: 80,
scrollMargin: 80,
handleDOMEvents: {
keydown: (_view, event) => {
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
@ -116,7 +138,8 @@ export default function PageEditor({ pageId, editable }: PageEditorProps) {
}
},
},
handlePaste: (view, event) => handleFilePaste(view, event, pageId),
handlePaste: (view, event, slice) =>
handlePaste(view, event, pageId, currentUser?.user.id),
handleDrop: (view, event, _slice, moved) =>
handleFileDrop(view, event, moved, pageId),
},
@ -157,35 +180,51 @@ export default function PageEditor({ pageId, editable }: PageEditorProps) {
setAsideState({ tab: "", isAsideOpen: false });
}, [pageId]);
const isSynced = isLocalSynced || isRemoteSynced;
useEffect(() => {
if (editable) {
if (yjsConnectionStatus === WebSocketStatus.Connected) {
editor.setEditable(true);
} else {
// disable edits if connection fails
editor.setEditable(false);
}
}
}, [yjsConnectionStatus]);
const isSynced = isLocalSynced && isRemoteSynced;
return isSynced ? (
<div>
{isSynced && (
<div ref={menuContainerRef}>
<EditorContent editor={editor} />
<div ref={menuContainerRef}>
<EditorContent editor={editor} />
{editor && editor.isEditable && (
<div>
<EditorBubbleMenu editor={editor} />
<TableMenu editor={editor} />
<TableCellMenu editor={editor} appendTo={menuContainerRef} />
<ImageMenu editor={editor} />
<VideoMenu editor={editor} />
<CalloutMenu editor={editor} />
<ExcalidrawMenu editor={editor} />
<DrawioMenu editor={editor} />
<LinkMenu editor={editor} appendTo={menuContainerRef} />
</div>
)}
{editor && editor.isEditable && (
<div>
<EditorBubbleMenu editor={editor} />
<TableMenu editor={editor} />
<TableCellMenu editor={editor} appendTo={menuContainerRef} />
<ImageMenu editor={editor} />
<VideoMenu editor={editor} />
<CalloutMenu editor={editor} />
<ExcalidrawMenu editor={editor} />
<DrawioMenu editor={editor} />
<LinkMenu editor={editor} appendTo={menuContainerRef} />
</div>
)}
{showCommentPopup && (
<CommentDialog editor={editor} pageId={pageId} />
)}
</div>
)}
{showCommentPopup && <CommentDialog editor={editor} pageId={pageId} />}
</div>
<div
onClick={() => editor.commands.focus("end")}
style={{ paddingBottom: "20vh" }}
></div>
</div>
) : (
<EditorSkeleton />
<EditorProvider
editable={false}
extensions={mainExtensions}
content={content}
></EditorProvider>
);
}

View File

@ -10,6 +10,7 @@
margin: 4px;
font-family: "JetBrainsMono", var(--mantine-font-family-monospace);
border-radius: var(--mantine-radius-default);
tab-size: 4;
@mixin light {
background-color: var(--mantine-color-gray-0);
@ -25,7 +26,7 @@
color: inherit;
padding: 0;
background: none;
font-size: inherit;
font-size: var(--mantine-font-size-sm);
}
/* Code styling */
@ -103,12 +104,12 @@
@mixin where-light {
background-color: var(--code-bg, var(--mantine-color-gray-1));
color: var(--mantine-color-black);
color: var(--mantine-color-pink-7);
}
@mixin where-dark {
background-color: var(--mantine-color-dark-8);
color: var(--mantine-color-gray-4);
color: var(--mantine-color-pink-7);
}
}
}

View File

@ -56,8 +56,14 @@
}
a {
color: light-dark(#207af1, #587da9);
/*font-weight: bold;*/
color: light-dark(var(--mantine-color-dark-4), var(--mantine-color-dark-1));
@mixin light {
border-bottom: 0.05em solid var(--mantine-color-dark-0);
}
@mixin dark {
border-bottom: 0.05em solid var(--mantine-color-dark-2);
}
/*font-weight: 500; */
text-decoration: none;
cursor: pointer;
}

View File

@ -9,3 +9,5 @@
@import "./media.css";
@import "./code.css";
@import "./print.css";
@import "./mention.css";

View File

@ -2,6 +2,10 @@
img {
max-width: 100%;
height: auto;
@media print {
break-inside: avoid;
}
}
.node-image, .node-video, .node-excalidraw, .node-drawio {

View File

@ -0,0 +1,5 @@
.node-mention {
&.ProseMirror-selectednode {
outline: none;
}
}

View File

@ -10,9 +10,7 @@ import {
pageEditorAtom,
titleEditorAtom,
} from "@/features/editor/atoms/editor-atoms";
import {
useUpdatePageMutation,
} from "@/features/page/queries/page-query";
import { useUpdatePageMutation } from "@/features/page/queries/page-query";
import { useDebouncedValue } from "@mantine/hooks";
import { useAtom } from "jotai";
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom";
@ -21,6 +19,7 @@ import { useQueryEmit } from "@/features/websocket/use-query-emit.ts";
import { History } from "@tiptap/extension-history";
import { buildPageUrl } from "@/features/page/page.utils.ts";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
export interface TitleEditorProps {
pageId: string;
@ -37,9 +36,14 @@ export function TitleEditor({
spaceSlug,
editable,
}: TitleEditorProps) {
const { t } = useTranslation();
const [debouncedTitleState, setDebouncedTitleState] = useState(null);
const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 500);
const updatePageMutation = useUpdatePageMutation();
const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 700);
const {
data: updatedPageData,
mutate: updatePageMutation,
status,
} = useUpdatePageMutation();
const pageEditor = useAtomValue(pageEditorAtom);
const [, setTitleEditor] = useAtom(titleEditorAtom);
const [treeData, setTreeData] = useAtom(treeDataAtom);
@ -47,7 +51,6 @@ export function TitleEditor({
const navigate = useNavigate();
const [activePageId, setActivePageId] = useState(pageId);
const titleEditor = useEditor({
extensions: [
Document.extend({
@ -58,7 +61,7 @@ export function TitleEditor({
}),
Text,
Placeholder.configure({
placeholder: "Untitled",
placeholder: t("Untitled"),
showOnlyWhenEditable: false,
}),
History.configure({
@ -78,6 +81,8 @@ export function TitleEditor({
},
editable: editable,
content: title,
immediatelyRender: true,
shouldRerenderOnTransaction: false,
});
useEffect(() => {
@ -87,24 +92,29 @@ export function TitleEditor({
useEffect(() => {
if (debouncedTitle !== null && activePageId === pageId) {
updatePageMutation.mutate({
updatePageMutation({
pageId: pageId,
title: debouncedTitle,
});
}
}, [debouncedTitle]);
useEffect(() => {
if (status === "success" && updatedPageData) {
const newTreeData = updateTreeNodeName(treeData, pageId, debouncedTitle);
setTreeData(newTreeData);
setTimeout(() => {
emit({
operation: "updateOne",
spaceId: updatedPageData.spaceId,
entity: ["pages"],
id: pageId,
payload: { title: debouncedTitle, slugId: slugId },
});
}, 50);
const newTreeData = updateTreeNodeName(treeData, pageId, debouncedTitle);
setTreeData(newTreeData);
}
}, [debouncedTitle]);
}, [updatedPageData, status]);
useEffect(() => {
if (titleEditor && title !== titleEditor.getText()) {

View File

@ -4,8 +4,10 @@ import React, { useState } from "react";
import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx";
import { useParams } from "react-router-dom";
import { useAddGroupMemberMutation } from "@/features/group/queries/group-query.ts";
import { useTranslation } from "react-i18next";
export default function AddGroupMemberModal() {
const { t } = useTranslation();
const { groupId } = useParams();
const [opened, { open, close }] = useDisclosure(false);
const [userIds, setUserIds] = useState<string[]>([]);
@ -27,19 +29,19 @@ export default function AddGroupMemberModal() {
return (
<>
<Button onClick={open}>Add group members</Button>
<Button onClick={open}>{t("Add group members")}</Button>
<Modal opened={opened} onClose={close} title="Add group members">
<Modal opened={opened} onClose={close} title={t("Add group members")}>
<Divider size="xs" mb="xs" />
<MultiUserSelect
label={"Add group members"}
label={t("Add group members")}
onChange={handleMultiSelectChange}
/>
<Group justify="flex-end" mt="md">
<Button onClick={handleSubmit} type="submit">
Add
{t("Add")}
</Button>
</Group>
</Modal>

View File

@ -5,6 +5,7 @@ import { useForm, zodResolver } from "@mantine/form";
import * as z from "zod";
import { useNavigate } from "react-router-dom";
import { MultiUserSelect } from "@/features/group/components/multi-user-select.tsx";
import { useTranslation } from "react-i18next";
const formSchema = z.object({
name: z.string().trim().min(2).max(50),
@ -14,6 +15,7 @@ const formSchema = z.object({
type FormValues = z.infer<typeof formSchema>;
export function CreateGroupForm() {
const { t } = useTranslation();
const createGroupMutation = useCreateGroupMutation();
const [userIds, setUserIds] = useState<string[]>([]);
const navigate = useNavigate();
@ -52,16 +54,16 @@ export function CreateGroupForm() {
<TextInput
withAsterisk
id="name"
label="Group name"
placeholder="e.g Developers"
label={t("Group name")}
placeholder={t("e.g Developers")}
variant="filled"
{...form.getInputProps("name")}
/>
<Textarea
id="description"
label="Group description"
placeholder="e.g Group for developers"
label={t("Group description")}
placeholder={t("e.g Group for developers")}
variant="filled"
autosize
minRows={2}
@ -70,13 +72,13 @@ export function CreateGroupForm() {
/>
<MultiUserSelect
label={"Add group members"}
label={t("Add group members")}
onChange={handleMultiSelectChange}
/>
</Stack>
<Group justify="flex-end" mt="md">
<Button type="submit">Create</Button>
<Button type="submit">{t("Create")}</Button>
</Group>
</form>
</Box>

View File

@ -1,15 +1,17 @@
import { Button, Divider, Modal } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { CreateGroupForm } from "@/features/group/components/create-group-form.tsx";
import { useTranslation } from "react-i18next";
export default function CreateGroupModal() {
const { t } = useTranslation();
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Button onClick={open}>Create group</Button>
<Button onClick={open}>{t("Create group")}</Button>
<Modal opened={opened} onClose={close} title="Create group">
<Modal opened={opened} onClose={close} title={t("Create group")}>
<Divider size="xs" mb="xs" />
<CreateGroupForm />
</Modal>

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