mirror of
https://github.com/docmost/docmost.git
synced 2025-11-10 10:32:06 +10:00
Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b41538b60 | |||
| 496f5d7384 | |||
| 32c7a16d06 | |||
| 64ecef09bc | |||
| 3e5cb92621 | |||
| fd5ad2f576 | |||
| 74a5360561 | |||
| 7580e8d1fe | |||
| f92d63261d | |||
| 4d51986250 | |||
| e209aaa272 | |||
| 0ef6b1978a | |||
| ae842f94d0 | |||
| 7121771f92 | |||
| 040d6625df | |||
| 33ddd92198 | |||
| 54e8d60840 | |||
| db986038c2 | |||
| de0b5f0046 | |||
| 638b811857 | |||
| d775a61c95 | |||
| 0f74f03264 | |||
| f8b93ce93f | |||
| 85d18b8cc8 | |||
| 4d9fe6f804 | |||
| 85159a2c95 | |||
| 990612793f | |||
| f2235fd2a2 | |||
| 2044cbb21c | |||
| 3d52b82cd4 | |||
| 89a2dd602b | |||
| 3cb954db69 | |||
| 71cfe3cd8e | |||
| f7efb6c2c9 | |||
| 59b514fa26 | |||
| 0c1f9304f4 | |||
| e876214eeb | |||
| 5fece5fc68 | |||
| f3dbf7cc5d | |||
| f7ac6bb4bb | |||
| 1f5ffe7f9d | |||
| 95715421c6 | |||
| f5bc99b449 | |||
| 287b833838 | |||
| 0cbbcb8eb1 | |||
| 670ee64179 | |||
| 290b7d9d94 | |||
| 2503bfd3a2 | |||
| f48d6dd60b | |||
| 1302b1b602 | |||
| 89a3f4cfc2 | |||
| e48b1c0dae | |||
| 4a2a5a7a4d | |||
| 532001fd82 | |||
| e6bf4cdd6c | |||
| a9a4a26db5 | |||
| ede5633415 | |||
| a25cf84671 | |||
| a37d558bac | |||
| ddb0f9225f | |||
| c717847ca8 | |||
| fe83557767 | |||
| 9fa432dba9 | |||
| c6aaefecbd | |||
| 311d81bc71 | |||
| f178e6654f | |||
| ca186f3c0e | |||
| a16d5d1bf4 | |||
| d97baf5824 | |||
| 8349d8271c | |||
| 2e6d16dbc3 | |||
| 4107793e73 | |||
| a1b6ac7f3e | |||
| dd0319a14d | |||
| 8194c7d42d | |||
| d01ced078b | |||
| da9c971050 | |||
| 4e7af507c6 | |||
| f7426a0b45 | |||
| b85b34d6b1 | |||
| e064e58f79 | |||
| 4f1a97ceb9 | |||
| d07338861b |
@ -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=
|
||||
@ -1,22 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:@tanstack/eslint-plugin-query/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
},
|
||||
}
|
||||
36
apps/client/eslint.config.mjs
Normal file
36
apps/client/eslint.config.mjs
Normal file
@ -0,0 +1,36 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import tseslint from "typescript-eslint";
|
||||
import pluginQuery from "@tanstack/eslint-plugin-query";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist"] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
"react-hooks": reactHooks,
|
||||
"react-refresh": reactRefresh,
|
||||
"@tanstack/query": pluginQuery,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"@typescript-eslint/no-unused-expressions": "off",
|
||||
"no-useless-escape": "off",
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -1,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"
|
||||
}
|
||||
}
|
||||
|
||||
342
apps/client/public/locales/de-DE/translation.json
Normal file
342
apps/client/public/locales/de-DE/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "Konto",
|
||||
"Active": "Aktiv",
|
||||
"Add": "Hinzufügen",
|
||||
"Add group members": "Gruppenmitglieder hinzufügen",
|
||||
"Add groups": "Gruppen hinzufügen",
|
||||
"Add members": "Mitglieder hinzufügen",
|
||||
"Add to groups": "Zu Gruppen hinzufügen",
|
||||
"Add space members": "Bereichsmitglieder hinzufügen",
|
||||
"Admin": "Administrator",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Sind Sie sicher, dass Sie diese Gruppe löschen möchten? Mitglieder verlieren den Zugang zu den Ressourcen, auf die diese Gruppe zugreifen kann.",
|
||||
"Are you sure you want to delete this page?": "Sind Sie sicher, dass Sie diese Seite löschen möchten?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Sind Sie sicher, dass Sie diesen Benutzer aus der Gruppe entfernen möchten? Der Benutzer verliert den Zugang zu den Ressourcen, auf die diese Gruppe zugreifen kann.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Sind Sie sicher, dass Sie diesen Benutzer aus dem Bereich entfernen möchten? Der Benutzer verliert den gesamten Zugang zu diesem Bereich.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Sind Sie sicher, dass Sie diese Version wiederherstellen möchten? Alle nicht versionierten Änderungen gehen verloren.",
|
||||
"Can become members of groups and spaces in workspace": "Kann Mitglied von Gruppen und Bereichen im Arbeitsbereich werden",
|
||||
"Can create and edit pages in space.": "Kann Seiten im Bereich erstellen und bearbeiten.",
|
||||
"Can edit": "Kann bearbeiten",
|
||||
"Can manage workspace": "Kann Arbeitsbereich verwalten",
|
||||
"Can manage workspace but cannot delete it": "Kann Arbeitsbereich verwalten, aber nicht löschen",
|
||||
"Can view": "Kann anzeigen",
|
||||
"Can view pages in space but not edit.": "Kann Seiten im Bereich anzeigen, aber nicht bearbeiten.",
|
||||
"Cancel": "Abbrechen",
|
||||
"Change email": "E-Mail ändern",
|
||||
"Change password": "Passwort ändern",
|
||||
"Change photo": "Foto ändern",
|
||||
"Choose a role": "Wählen Sie eine Rolle",
|
||||
"Choose your preferred color scheme.": "Wählen Sie Ihr bevorzugtes Farbschema.",
|
||||
"Choose your preferred interface language.": "Wählen Sie Ihre bevorzugte Benutzersprache.",
|
||||
"Choose your preferred page width.": "Wählen Sie Ihre bevorzugte Seitenbreite.",
|
||||
"Confirm": "Bestätigen",
|
||||
"Copy link": "Link kopieren",
|
||||
"Create": "Erstellen",
|
||||
"Create group": "Gruppe erstellen",
|
||||
"Create page": "Seite erstellen",
|
||||
"Create space": "Bereich erstellen",
|
||||
"Create workspace": "Arbeitsbereich erstellen",
|
||||
"Current password": "Aktuelles Passwort",
|
||||
"Dark": "Dunkel",
|
||||
"Date": "Datum",
|
||||
"Delete": "Löschen",
|
||||
"Delete group": "Gruppe löschen",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sind Sie sicher, dass Sie diese Seite löschen möchten? Dadurch werden ihre Unterseiten und die Seitengeschichte gelöscht. Diese Aktion ist unwiderruflich.",
|
||||
"Description": "Beschreibung",
|
||||
"Details": "Einzelheiten",
|
||||
"e.g ACME": "z.B. ACME",
|
||||
"e.g ACME Inc": "z.B. ACME Inc.",
|
||||
"e.g Developers": "z.B. Entwickler",
|
||||
"e.g Group for developers": "z.B. Gruppe für Entwickler",
|
||||
"e.g product": "z.B. Produkt",
|
||||
"e.g Product Team": "z.B. Produktteam",
|
||||
"e.g Sales": "z.B. Vertrieb",
|
||||
"e.g Space for product team": "z.B. Bereich für das Produktteam",
|
||||
"e.g Space for sales team to collaborate": "z.B. Bereich für das Vertriebsteam zur Zusammenarbeit",
|
||||
"Edit": "Bearbeiten",
|
||||
"Edit group": "Gruppe bearbeiten",
|
||||
"Email": "E-Mail",
|
||||
"Enter a strong password": "Geben Sie ein starkes Passwort ein",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Geben Sie gültige E-Mail-Adressen ein, getrennt durch Kommas oder Leerzeichen [max: 50]",
|
||||
"enter valid emails addresses": "gültige E-Mail-Adressen eingeben",
|
||||
"Enter your current password": "Geben Sie Ihr aktuelles Passwort ein",
|
||||
"enter your full name": "Geben Sie Ihren vollständigen Namen ein",
|
||||
"Enter your new password": "Geben Sie Ihr neues Passwort ein",
|
||||
"Enter your new preferred email": "Geben Sie Ihre neue bevorzugte E-Mail ein",
|
||||
"Enter your password": "Geben Sie Ihr Passwort ein",
|
||||
"Error fetching page data.": "Fehler beim Abrufen der Seitendaten.",
|
||||
"Error loading page history.": "Fehler beim Laden der Seitengeschichte.",
|
||||
"Export": "Exportieren",
|
||||
"Failed to create page": "Erstellung der Seite fehlgeschlagen",
|
||||
"Failed to delete page": "Löschen der Seite fehlgeschlagen",
|
||||
"Failed to fetch recent pages": "Fehler beim Abrufen der letzten Seiten",
|
||||
"Failed to import pages": "Import der Seiten fehlgeschlagen",
|
||||
"Failed to load page. An error occurred.": "Seite konnte nicht geladen werden. Es ist ein Fehler aufgetreten.",
|
||||
"Failed to update data": "Aktualisierung der Daten fehlgeschlagen",
|
||||
"Full access": "Voller Zugriff",
|
||||
"Full page width": "Volle Seitenbreite",
|
||||
"Full width": "Volle Breite",
|
||||
"General": "Allgemein",
|
||||
"Group": "Gruppe",
|
||||
"Group description": "Gruppenbeschreibung",
|
||||
"Group name": "Gruppenname",
|
||||
"Groups": "Gruppen",
|
||||
"Has full access to space settings and pages.": "Hat vollen Zugriff auf die Bereichseinstellungen und Seiten.",
|
||||
"Home": "Startseite",
|
||||
"Import pages": "Seiten importieren",
|
||||
"Import pages & space settings": "Seiten und Bereichseinstellungen importieren",
|
||||
"Importing pages": "Seiten werden importiert",
|
||||
"invalid invitation link": "ungültiger Einladungslink",
|
||||
"Invitation signup": "Einladung zur Anmeldung",
|
||||
"Invite by email": "Einladen per E-Mail",
|
||||
"Invite members": "Mitglieder einladen",
|
||||
"Invite new members": "Neue Mitglieder einladen",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Eingeladene Mitglieder, die ihre Einladung noch nicht angenommen haben, werden hier angezeigt.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Eingeladene Mitglieder erhalten Zugriff auf die Bereiche, auf die die Gruppen zugreifen können",
|
||||
"Join the workspace": "Dem Arbeitsbereich beitreten",
|
||||
"Language": "Sprache",
|
||||
"Light": "Hell",
|
||||
"Link copied": "Link kopiert",
|
||||
"Login": "Anmelden",
|
||||
"Logout": "Abmelden",
|
||||
"Manage Group": "Gruppe verwalten",
|
||||
"Manage members": "Mitglieder verwalten",
|
||||
"member": "Mitglied",
|
||||
"Member": "Mitglied",
|
||||
"members": "Mitglieder",
|
||||
"Members": "Mitglieder",
|
||||
"My preferences": "Meine Vorlieben",
|
||||
"My Profile": "Mein Profil",
|
||||
"My profile": "Mein Profil",
|
||||
"Name": "Name",
|
||||
"New email": "Neue E-Mail",
|
||||
"New page": "Neue Seite",
|
||||
"New password": "Neues Passwort",
|
||||
"No group found": "Keine Gruppe gefunden",
|
||||
"No page history saved yet.": "Es wurde noch keine Seitengeschichte gespeichert.",
|
||||
"No pages yet": "Noch keine Seiten",
|
||||
"No results found...": "Keine Ergebnisse gefunden...",
|
||||
"No user found": "Kein Benutzer gefunden",
|
||||
"Overview": "Überblick",
|
||||
"Owner": "Besitzer",
|
||||
"page": "Seite",
|
||||
"Page deleted successfully": "Seite erfolgreich gelöscht",
|
||||
"Page history": "Seitengeschichte",
|
||||
"Page import is in progress. Please do not close this tab.": "Der Seitenimport läuft. Bitte schließen Sie diesen Tab nicht.",
|
||||
"Pages": "Seiten",
|
||||
"pages": "Seiten",
|
||||
"Password": "Passwort",
|
||||
"Password changed successfully": "Passwort erfolgreich geändert",
|
||||
"Pending": "Ausstehend",
|
||||
"Please confirm your action": "Bitte bestätigen Sie Ihre Aktion",
|
||||
"Preferences": "Vorlieben",
|
||||
"Print PDF": "PDF drucken",
|
||||
"Profile": "Profil",
|
||||
"Recently updated": "Kürzlich aktualisiert",
|
||||
"Remove": "Entfernen",
|
||||
"Remove group member": "Gruppenmitglied entfernen",
|
||||
"Remove space member": "Bereichsmitglied entfernen",
|
||||
"Restore": "Wiederherstellen",
|
||||
"Role": "Rolle",
|
||||
"Save": "Speichern",
|
||||
"Search": "Suche",
|
||||
"Search for groups": "Suche nach Gruppen",
|
||||
"Search for users": "Suche nach Benutzern",
|
||||
"Search for users and groups": "Suche nach Benutzern und Gruppen",
|
||||
"Search...": "Suche...",
|
||||
"Select language": "Sprache auswählen",
|
||||
"Select role": "Rolle auswählen",
|
||||
"Select role to assign to all invited members": "Rolle für alle eingeladenen Mitglieder auswählen",
|
||||
"Select theme": "Design auswählen",
|
||||
"Send invitation": "Einladung senden",
|
||||
"Settings": "Einstellungen",
|
||||
"Setup workspace": "Arbeitsbereich einrichten",
|
||||
"Sign In": "Anmelden",
|
||||
"Sign Up": "Registrieren",
|
||||
"Slug": "Slug",
|
||||
"Space": "Bereich",
|
||||
"Space description": "Bereichsbeschreibung",
|
||||
"Space menu": "Bereichsmenü",
|
||||
"Space name": "Bereichsname",
|
||||
"Space settings": "Bereichseinstellungen",
|
||||
"Space slug": "Slug des Bereichs",
|
||||
"Spaces": "Bereiche",
|
||||
"Spaces you belong to": "Bereiche, denen Sie angehören",
|
||||
"No space found": "Keine Bereiche gefunden",
|
||||
"Search for spaces": "Nach Bereichen suchen",
|
||||
"Start typing to search...": "Anfangen zu tippen, um zu suchen...",
|
||||
"Status": "Status",
|
||||
"Successfully imported": "Erfolgreich importiert",
|
||||
"Successfully restored": "Erfolgreich wiederhergestellt",
|
||||
"System settings": "Systemeinstellungen",
|
||||
"Theme": "Design",
|
||||
"To change your email, you have to enter your password and new email.": "Um Ihre E-Mail-Adresse zu ändern, müssen Sie Ihr Passwort und Ihre neue E-Mail-Adresse eingeben.",
|
||||
"Toggle full page width": "Volle Seitenbreite umschalten",
|
||||
"Unable to import pages. Please try again.": "Seiten konnten nicht importiert werden. Bitte versuchen Sie es erneut.",
|
||||
"untitled": "ohne Titel",
|
||||
"Untitled": "Ohne Titel",
|
||||
"Updated successfully": "Erfolgreich aktualisiert",
|
||||
"User": "Benutzer",
|
||||
"Workspace": "Arbeitsbereich",
|
||||
"Workspace Name": "Arbeitsbereichsname",
|
||||
"Workspace settings": "Arbeitsbereich-Einstellungen",
|
||||
"You can change your password here.": "Hier können Sie Ihr Passwort ändern.",
|
||||
"Your Email": "Ihre E-Mail",
|
||||
"Your import is complete.": "Ihr Import ist abgeschlossen.",
|
||||
"Your name": "Ihr Name",
|
||||
"Your Name": "Ihr Name",
|
||||
"Your password": "Ihr Passwort",
|
||||
"Your password must be a minimum of 8 characters.": "Ihr Passwort muss mindestens 8 Zeichen lang sein.",
|
||||
"Sidebar toggle": "Seitenleiste umschalten",
|
||||
"Comments": "Kommentare",
|
||||
"404 page not found": "404 Seite nicht gefunden",
|
||||
"Sorry, we can't find the page you are looking for.": "Entschuldigung, wir können die gesuchte Seite nicht finden.",
|
||||
"Take me back to homepage": "Zurück zur Startseite",
|
||||
"Forgot password": "Passwort vergessen",
|
||||
"Forgot your password?": "Passwort vergessen?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail gesendet. Bitte überprüfen Sie Ihren Posteingang.",
|
||||
"Send reset link": "Zurücksetzungslink senden",
|
||||
"Password reset": "Passwort zurücksetzen",
|
||||
"Your new password": "Ihr neues Passwort",
|
||||
"Set password": "Passwort festlegen",
|
||||
"Write a comment": "Einen Kommentar schreiben",
|
||||
"Reply...": "Antworten...",
|
||||
"Error loading comments.": "Fehler beim Laden der Kommentare.",
|
||||
"No comments yet.": "Noch keine Kommentare.",
|
||||
"Edit comment": "Kommentar bearbeiten",
|
||||
"Delete comment": "Kommentar löschen",
|
||||
"Are you sure you want to delete this comment?": "Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?",
|
||||
"Comment created successfully": "Kommentar erfolgreich erstellt",
|
||||
"Error creating comment": "Fehler beim Erstellen des Kommentars",
|
||||
"Comment updated successfully": "Kommentar erfolgreich aktualisiert",
|
||||
"Failed to update comment": "Aktualisierung des Kommentars fehlgeschlagen",
|
||||
"Comment deleted successfully": "Kommentar erfolgreich gelöscht",
|
||||
"Failed to delete comment": "Löschen des Kommentars fehlgeschlagen",
|
||||
"Comment resolved successfully": "Kommentar erfolgreich gelöst",
|
||||
"Failed to resolve comment": "Lösen des Kommentars fehlgeschlagen",
|
||||
"Revoke invitation": "Einladung widerrufen",
|
||||
"Revoke": "Widerrufen",
|
||||
"Don't": "Nicht",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Sind Sie sicher, dass Sie diese Einladung widerrufen möchten? Der Benutzer kann dem Arbeitsbereich nicht beitreten.",
|
||||
"Resend invitation": "Einladung erneut senden",
|
||||
"Anyone with this link can join this workspace.": "Jeder mit diesem Link kann dem Arbeitsbereich beitreten.",
|
||||
"Invite link": "Einladungslink",
|
||||
"Copy": "Kopieren",
|
||||
"Copied": "Kopiert",
|
||||
"Select a user": "Benutzer auswählen",
|
||||
"Select a group": "Gruppe auswählen",
|
||||
"Export all pages and attachments in this space.": "Alle Seiten und Anhänge in diesem Bereich exportieren.",
|
||||
"Delete space": "Bereich löschen",
|
||||
"Are you sure you want to delete this space?": "Sind Sie sicher, dass Sie diesen Bereich löschen möchten?",
|
||||
"Delete this space with all its pages and data.": "Diesen Bereich mit allen Seiten und Daten löschen.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Alle Seiten, Kommentare, Anhänge und Berechtigungen in diesem Bereich werden unwiderruflich gelöscht.",
|
||||
"Confirm space name": "Bestätigen Sie den Namen des Arbeitsbereichs",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Geben Sie den Namen des Bereichs <b>{{spaceName}}</b> ein, um Ihre Aktion zu bestätigen.",
|
||||
"Format": "Format",
|
||||
"Include subpages": "Unterseiten einbeziehen",
|
||||
"Include attachments": "Anhänge einbeziehen",
|
||||
"Select export format": "Exportformat auswählen",
|
||||
"Export failed:": "Export fehlgeschlagen:",
|
||||
"export error": "Exportfehler",
|
||||
"Export page": "Seite exportieren",
|
||||
"Export space": "Bereich exportieren",
|
||||
"Export {{type}}": "Exportiere {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Datei überschreitet das Anhängelimit von {{limit}}",
|
||||
"Align left": "Links ausrichten",
|
||||
"Align right": "Rechts ausrichten",
|
||||
"Align center": "Zentrieren",
|
||||
"Merge cells": "Zellen zusammenführen",
|
||||
"Split cell": "Zelle teilen",
|
||||
"Delete column": "Spalte löschen",
|
||||
"Delete row": "Zeile löschen",
|
||||
"Add left column": "Linke Spalte hinzufügen",
|
||||
"Add right column": "Rechte Spalte hinzufügen",
|
||||
"Add row above": "Zeile oben hinzufügen",
|
||||
"Add row below": "Zeile unten hinzufügen",
|
||||
"Delete table": "Tabelle löschen",
|
||||
"Info": "Info",
|
||||
"Success": "Erfolg",
|
||||
"Warning": "Warnung",
|
||||
"Danger": "Gefahr",
|
||||
"Mermaid diagram error:": "Fehler im Mermaid-Diagramm:",
|
||||
"Invalid Mermaid diagram": "Ungültiges Mermaid-Diagramm",
|
||||
"Double-click to edit Draw.io diagram": "Zum Bearbeiten des Draw.io-Diagramms doppelklicken",
|
||||
"Exit": "Beenden",
|
||||
"Save & Exit": "Speichern & Beenden",
|
||||
"Double-click to edit Excalidraw diagram": "Zum Bearbeiten des Excalidraw-Diagramms doppelklicken",
|
||||
"Paste link": "Link einfügen",
|
||||
"Edit link": "Link bearbeiten",
|
||||
"Remove link": "Link entfernen",
|
||||
"Add link": "Link hinzufügen",
|
||||
"Please enter a valid url": "Bitte geben Sie eine gültige URL ein",
|
||||
"Empty equation": "Leere Gleichung",
|
||||
"Invalid equation": "Ungültige Gleichung",
|
||||
"Color": "Farbe",
|
||||
"Text color": "Textfarbe",
|
||||
"Default": "Standard",
|
||||
"Blue": "Blau",
|
||||
"Green": "Grün",
|
||||
"Purple": "Lila",
|
||||
"Red": "Rot",
|
||||
"Yellow": "Gelb",
|
||||
"Orange": "Orange",
|
||||
"Pink": "Rosa",
|
||||
"Gray": "Grau",
|
||||
"Embed link": "Link einbetten",
|
||||
"Invalid {{provider}} embed link": "Ungültiger {{provider}}-Einbettungslink",
|
||||
"Embed {{provider}}": "{{provider}} einbetten",
|
||||
"Enter {{provider}} link to embed": "Geben Sie den Einbettungslink für {{provider}} ein",
|
||||
"Bold": "Fett",
|
||||
"Italic": "Kursiv",
|
||||
"Underline": "Unterstreichen",
|
||||
"Strike": "Durchstreichen",
|
||||
"Code": "Code",
|
||||
"Comment": "Kommentar",
|
||||
"Text": "Text",
|
||||
"Heading 1": "Überschrift 1",
|
||||
"Heading 2": "Überschrift 2",
|
||||
"Heading 3": "Überschrift 3",
|
||||
"To-do List": "To-do-Liste",
|
||||
"Bullet List": "Aufzählungsliste",
|
||||
"Numbered List": "Nummerierte Liste",
|
||||
"Blockquote": "Blockzitat",
|
||||
"Just start typing with plain text.": "Tippen Sie einfach mit normalem Text los.",
|
||||
"Track tasks with a to-do list.": "Verfolgen Sie Aufgaben mit einer To-do-Liste.",
|
||||
"Big section heading.": "Große Abschnittsüberschrift.",
|
||||
"Medium section heading.": "Mittlere Abschnittsüberschrift.",
|
||||
"Small section heading.": "Kleine Abschnittsüberschrift.",
|
||||
"Create a simple bullet list.": "Erstellen Sie eine einfache Aufzählungsliste.",
|
||||
"Create a list with numbering.": "Erstellen Sie eine nummerierte Liste.",
|
||||
"Create block quote.": "Erstellen Sie ein Blockzitat.",
|
||||
"Insert code snippet.": "Code-Snippet einfügen.",
|
||||
"Insert horizontal rule divider": "Horizontale Trennlinie einfügen",
|
||||
"Upload any image from your device.": "Laden Sie ein beliebiges Bild von Ihrem Gerät hoch.",
|
||||
"Upload any video from your device.": "Laden Sie ein beliebiges Video von Ihrem Gerät hoch.",
|
||||
"Upload any file from your device.": "Laden Sie eine beliebige Datei von Ihrem Gerät hoch.",
|
||||
"Table": "Tabelle",
|
||||
"Insert a table.": "Tabelle einfügen.",
|
||||
"Insert collapsible block.": "Einklappbaren Block einfügen.",
|
||||
"Video": "Video",
|
||||
"Divider": "Trennlinie",
|
||||
"Quote": "Zitat",
|
||||
"Image": "Bild",
|
||||
"File attachment": "Dateianhang",
|
||||
"Toggle block": "Block umschalten",
|
||||
"Callout": "Hinweisbox",
|
||||
"Insert callout notice.": "Hinweisbox einfügen.",
|
||||
"Math inline": "Mathe inline",
|
||||
"Insert inline math equation.": "Mathe-Gleichung inline einfügen.",
|
||||
"Math block": "Matheblock",
|
||||
"Insert math equation": "Mathe-Gleichung einfügen",
|
||||
"Mermaid diagram": "Mermaid-Diagramm",
|
||||
"Insert mermaid diagram": "Mermaid-Diagramm einfügen",
|
||||
"Insert and design Drawio diagrams": "Drawio-Diagramme einfügen und gestalten",
|
||||
"Insert current date": "Aktuelles Datum einfügen",
|
||||
"Draw and sketch excalidraw diagrams": "Excalidraw-Diagramme zeichnen und skizzieren",
|
||||
"Multiple": "Mehrere",
|
||||
"Heading {{level}}": "Überschrift {{level}}",
|
||||
"Toggle title": "Titel umschalten",
|
||||
"Write anything. Enter \"/\" for commands": "Schreiben Sie irgendetwas. Geben Sie \"/\" für Befehle ein",
|
||||
"Names do not match": "Namen stimmen nicht überein",
|
||||
"Today, {{time}}": "Heute, {{time}}",
|
||||
"Yesterday, {{time}}": "Gestern, {{time}}"
|
||||
}
|
||||
343
apps/client/public/locales/en-US/translation.json
Normal file
343
apps/client/public/locales/en-US/translation.json
Normal file
@ -0,0 +1,343 @@
|
||||
{
|
||||
"Account": "Account",
|
||||
"Active": "Active",
|
||||
"Add": "Add",
|
||||
"Add group members": "Add group members",
|
||||
"Add groups": "Add groups",
|
||||
"Add members": "Add members",
|
||||
"Add to groups": "Add to groups",
|
||||
"Add space members": "Add space members",
|
||||
"Admin": "Admin",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Are you sure you want to delete this group? Members will lose access to resources this group has access to.",
|
||||
"Are you sure you want to delete this page?": "Are you sure you want to delete this page?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Are you sure you want to remove this user from the space? The user will lose all access to this space.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Are you sure you want to restore this version? Any changes not versioned will be lost.",
|
||||
"Can become members of groups and spaces in workspace": "Can become members of groups and spaces in workspace",
|
||||
"Can create and edit pages in space.": "Can create and edit pages in space.",
|
||||
"Can edit": "Can edit",
|
||||
"Can manage workspace": "Can manage workspace",
|
||||
"Can manage workspace but cannot delete it": "Can manage workspace but cannot delete it",
|
||||
"Can view": "Can view",
|
||||
"Can view pages in space but not edit.": "Can view pages in space but not edit.",
|
||||
"Cancel": "Cancel",
|
||||
"Change email": "Change email",
|
||||
"Change password": "Change password",
|
||||
"Change photo": "Change photo",
|
||||
"Choose a role": "Choose a role",
|
||||
"Choose your preferred color scheme.": "Choose your preferred color scheme.",
|
||||
"Choose your preferred interface language.": "Choose your preferred interface language.",
|
||||
"Choose your preferred page width.": "Choose your preferred page width.",
|
||||
"Confirm": "Confirm",
|
||||
"Copy link": "Copy link",
|
||||
"Create": "Create",
|
||||
"Create group": "Create group",
|
||||
"Create page": "Create page",
|
||||
"Create space": "Create space",
|
||||
"Create workspace": "Create workspace",
|
||||
"Current password": "Current password",
|
||||
"Dark": "Dark",
|
||||
"Date": "Date",
|
||||
"Delete": "Delete",
|
||||
"Delete group": "Delete group",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.",
|
||||
"Description": "Description",
|
||||
"Details": "Details",
|
||||
"e.g ACME": "e.g ACME",
|
||||
"e.g ACME Inc": "e.g ACME Inc",
|
||||
"e.g Developers": "e.g Developers",
|
||||
"e.g Group for developers": "e.g Group for developers",
|
||||
"e.g product": "e.g product",
|
||||
"e.g Product Team": "e.g Product Team",
|
||||
"e.g Sales": "e.g Sales",
|
||||
"e.g Space for product team": "e.g Space for product team",
|
||||
"e.g Space for sales team to collaborate": "e.g Space for sales team to collaborate",
|
||||
"Edit": "Edit",
|
||||
"Edit group": "Edit group",
|
||||
"Email": "Email",
|
||||
"Enter a strong password": "Enter a strong password",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Enter valid email addresses separated by comma or space [max: 50]",
|
||||
"enter valid emails addresses": "enter valid emails addresses",
|
||||
"Enter your current password": "Enter your current password",
|
||||
"enter your full name": "enter your full name",
|
||||
"Enter your new password": "Enter your new password",
|
||||
"Enter your new preferred email": "Enter your new preferred email",
|
||||
"Enter your password": "Enter your password",
|
||||
"Error fetching page data.": "Error fetching page data.",
|
||||
"Error loading page history.": "Error loading page history.",
|
||||
"Export": "Export",
|
||||
"Failed to create page": "Failed to create page",
|
||||
"Failed to delete page": "Failed to delete page",
|
||||
"Failed to fetch recent pages": "Failed to fetch recent pages",
|
||||
"Failed to import pages": "Failed to import pages",
|
||||
"Failed to load page. An error occurred.": "Failed to load page. An error occurred.",
|
||||
"Failed to update data": "Failed to update data",
|
||||
"Full access": "Full access",
|
||||
"Full page width": "Full page width",
|
||||
"Full width": "Full width",
|
||||
"General": "General",
|
||||
"Group": "Group",
|
||||
"Group description": "Group description",
|
||||
"Group name": "Group name",
|
||||
"Groups": "Groups",
|
||||
"Has full access to space settings and pages.": "Has full access to space settings and pages.",
|
||||
"Home": "Home",
|
||||
"Import pages": "Import pages",
|
||||
"Import pages & space settings": "Import pages & space settings",
|
||||
"Importing pages": "Importing pages",
|
||||
"invalid invitation link": "invalid invitation link",
|
||||
"Invitation signup": "Invitation signup",
|
||||
"Invite by email": "Invite by email",
|
||||
"Invite members": "Invite members",
|
||||
"Invite new members": "Invite new members",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Invited members who are yet to accept their invitation will appear here.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Invited members will be granted access to spaces the groups can access",
|
||||
"Join the workspace": "Join the workspace",
|
||||
"Language": "Language",
|
||||
"Light": "Light",
|
||||
"Link copied": "Link copied",
|
||||
"Login": "Login",
|
||||
"Logout": "Logout",
|
||||
"Manage Group": "Manage Group",
|
||||
"Manage members": "Manage members",
|
||||
"member": "member",
|
||||
"Member": "Member",
|
||||
"members": "members",
|
||||
"Members": "Members",
|
||||
"My preferences": "My preferences",
|
||||
"My Profile": "My Profile",
|
||||
"My profile": "My profile",
|
||||
"Name": "Name",
|
||||
"New email": "New email",
|
||||
"New page": "New page",
|
||||
"New password": "New password",
|
||||
"No group found": "No group found",
|
||||
"No page history saved yet.": "No page history saved yet.",
|
||||
"No pages yet": "No pages yet",
|
||||
"No results found...": "No results found...",
|
||||
"No user found": "No user found",
|
||||
"Overview": "Overview",
|
||||
"Owner": "Owner",
|
||||
"page": "page",
|
||||
"Page deleted successfully": "Page deleted successfully",
|
||||
"Page history": "Page history",
|
||||
"Page import is in progress. Please do not close this tab.": "Page import is in progress. Please do not close this tab.",
|
||||
"Pages": "Pages",
|
||||
"pages": "pages",
|
||||
"Password": "Password",
|
||||
"Password changed successfully": "Password changed successfully",
|
||||
"Pending": "Pending",
|
||||
"Please confirm your action": "Please confirm your action",
|
||||
"Preferences": "Preferences",
|
||||
"Print PDF": "Print PDF",
|
||||
"Profile": "Profile",
|
||||
"Recently updated": "Recently updated",
|
||||
"Remove": "Remove",
|
||||
"Remove group member": "Remove group member",
|
||||
"Remove space member": "Remove space member",
|
||||
"Restore": "Restore",
|
||||
"Role": "Role",
|
||||
"Save": "Save",
|
||||
"Search": "Search",
|
||||
"Search for groups": "Search for groups",
|
||||
"Search for users": "Search for users",
|
||||
"Search for users and groups": "Search for users and groups",
|
||||
"Search...": "Search...",
|
||||
"Select language": "Select language",
|
||||
"Select role": "Select role",
|
||||
"Select role to assign to all invited members": "Select role to assign to all invited members",
|
||||
"Select theme": "Select theme",
|
||||
"Send invitation": "Send invitation",
|
||||
"Settings": "Settings",
|
||||
"Setup workspace": "Setup workspace",
|
||||
"Sign In": "Sign In",
|
||||
"Sign Up": "Sign Up",
|
||||
"Slug": "Slug",
|
||||
"Space": "Space",
|
||||
"Space description": "Space description",
|
||||
"Space menu": "Space menu",
|
||||
"Space name": "Space name",
|
||||
"Space settings": "Space settings",
|
||||
"Space slug": "Space slug",
|
||||
"Spaces": "Spaces",
|
||||
"Spaces you belong to": "Spaces you belong to",
|
||||
"No space found": "No space found",
|
||||
"Search for spaces": "Search for spaces",
|
||||
"Start typing to search...": "Start typing to search...",
|
||||
"Status": "Status",
|
||||
"Successfully imported": "Successfully imported",
|
||||
"Successfully restored": "Successfully restored",
|
||||
"System settings": "System settings",
|
||||
"Theme": "Theme",
|
||||
"To change your email, you have to enter your password and new email.": "To change your email, you have to enter your password and new email.",
|
||||
"Toggle full page width": "Toggle full page width",
|
||||
"Unable to import pages. Please try again.": "Unable to import pages. Please try again.",
|
||||
"untitled": "untitled",
|
||||
"Untitled": "Untitled",
|
||||
"Updated successfully": "Updated successfully",
|
||||
"User": "User",
|
||||
"Workspace": "Workspace",
|
||||
"Workspace Name": "Workspace Name",
|
||||
"Workspace settings": "Workspace settings",
|
||||
"You can change your password here.": "You can change your password here.",
|
||||
"Your Email": "Your Email",
|
||||
"Your import is complete.": "Your import is complete.",
|
||||
"Your name": "Your name",
|
||||
"Your Name": "Your Name",
|
||||
"Your password": "Your password",
|
||||
"Your password must be a minimum of 8 characters.": "Your password must be a minimum of 8 characters.",
|
||||
"Sidebar toggle": "Sidebar toggle",
|
||||
"Comments": "Comments",
|
||||
"404 page not found": "404 page not found",
|
||||
"Sorry, we can't find the page you are looking for.": "Sorry, we can't find the page you are looking for.",
|
||||
"Take me back to homepage": "Take me back to homepage",
|
||||
"Forgot password": "Forgot password",
|
||||
"Forgot your password?": "Forgot your password?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "A password reset link has been sent to your email. Please check your inbox.",
|
||||
"Send reset link": "Send reset link",
|
||||
"Password reset": "Password reset",
|
||||
"Your new password": "Your new password",
|
||||
"Set password": "Set password",
|
||||
"Write a comment": "Write a comment",
|
||||
"Reply...": "Reply...",
|
||||
"Error loading comments.": "Error loading comments.",
|
||||
"No comments yet.": "No comments yet.",
|
||||
"Edit comment": "Edit comment",
|
||||
"Delete comment": "Delete comment",
|
||||
"Are you sure you want to delete this comment?": "Are you sure you want to delete this comment?",
|
||||
"Comment created successfully": "Comment created successfully",
|
||||
"Error creating comment": "Error creating comment",
|
||||
"Comment updated successfully": "Comment updated successfully",
|
||||
"Failed to update comment": "Failed to update comment",
|
||||
"Comment deleted successfully": "Comment deleted successfully",
|
||||
"Failed to delete comment": "Failed to delete comment",
|
||||
"Comment resolved successfully": "Comment resolved successfully",
|
||||
"Failed to resolve comment": "Failed to resolve comment",
|
||||
"Revoke invitation": "Revoke invitation",
|
||||
"Revoke": "Revoke",
|
||||
"Don't": "Don't",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Are you sure you want to revoke this invitation? The user will not be able to join the workspace.",
|
||||
"Resend invitation": "Resend invitation",
|
||||
"Anyone with this link can join this workspace.": "Anyone with this link can join this workspace.",
|
||||
"Invite link": "Invite link",
|
||||
"Copy": "Copy",
|
||||
"Copied": "Copied",
|
||||
"Select a user": "Select a user",
|
||||
"Select a group": "Select a group",
|
||||
"Export all pages and attachments in this space.": "Export all pages and attachments in this space.",
|
||||
"Delete space": "Delete space",
|
||||
"Are you sure you want to delete this space?": "Are you sure you want to delete this space?",
|
||||
"Delete this space with all its pages and data.": "Delete this space with all its pages and data.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "All pages, comments, attachments and permissions in this space will be deleted irreversibly.",
|
||||
"Confirm space name": "Confirm space name",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Type the space name <b>{{spaceName}}</b> to confirm your action.",
|
||||
"Format": "Format",
|
||||
"Include subpages": "Include subpages",
|
||||
"Include attachments": "Include attachments",
|
||||
"Select export format": "Select export format",
|
||||
"Export failed:": "Export failed:",
|
||||
"export error": "export error",
|
||||
"Export page": "Export page",
|
||||
"Export space": "Export space",
|
||||
"Export {{type}}": "Export {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "File exceeds the {{limit}} attachment limit",
|
||||
"Align left": "Align left",
|
||||
"Align right": "Align right",
|
||||
"Align center": "Align center",
|
||||
"Justify": "Justify",
|
||||
"Merge cells": "Merge cells",
|
||||
"Split cell": "Split cell",
|
||||
"Delete column": "Delete column",
|
||||
"Delete row": "Delete row",
|
||||
"Add left column": "Add left column",
|
||||
"Add right column": "Add right column",
|
||||
"Add row above": "Add row above",
|
||||
"Add row below": "Add row below",
|
||||
"Delete table": "Delete table",
|
||||
"Info": "Info",
|
||||
"Success": "Success",
|
||||
"Warning": "Warning",
|
||||
"Danger": "Danger",
|
||||
"Mermaid diagram error:": "Mermaid diagram error:",
|
||||
"Invalid Mermaid diagram": "Invalid Mermaid diagram",
|
||||
"Double-click to edit Draw.io diagram": "Double-click to edit Draw.io diagram",
|
||||
"Exit": "Exit",
|
||||
"Save & Exit": "Save & Exit",
|
||||
"Double-click to edit Excalidraw diagram": "Double-click to edit Excalidraw diagram",
|
||||
"Paste link": "Paste link",
|
||||
"Edit link": "Edit link",
|
||||
"Remove link": "Remove link",
|
||||
"Add link": "Add link",
|
||||
"Please enter a valid url": "Please enter a valid url",
|
||||
"Empty equation": "Empty equation",
|
||||
"Invalid equation": "Invalid equation",
|
||||
"Color": "Color",
|
||||
"Text color": "Text color",
|
||||
"Default": "Default",
|
||||
"Blue": "Blue",
|
||||
"Green": "Green",
|
||||
"Purple": "Purple",
|
||||
"Red": "Red",
|
||||
"Yellow": "Yellow",
|
||||
"Orange": "Orange",
|
||||
"Pink": "Pink",
|
||||
"Gray": "Gray",
|
||||
"Embed link": "Embed link",
|
||||
"Invalid {{provider}} embed link": "Invalid {{provider}} embed link",
|
||||
"Embed {{provider}}": "Embed {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Enter {{provider}} link to embed",
|
||||
"Bold": "Bold",
|
||||
"Italic": "Italic",
|
||||
"Underline": "Underline",
|
||||
"Strike": "Strike",
|
||||
"Code": "Code",
|
||||
"Comment": "Comment",
|
||||
"Text": "Text",
|
||||
"Heading 1": "Heading 1",
|
||||
"Heading 2": "Heading 2",
|
||||
"Heading 3": "Heading 3",
|
||||
"To-do List": "To-do List",
|
||||
"Bullet List": "Bullet List",
|
||||
"Numbered List": "Numbered List",
|
||||
"Blockquote": "Blockquote",
|
||||
"Just start typing with plain text.": "Just start typing with plain text.",
|
||||
"Track tasks with a to-do list.": "Track tasks with a to-do list.",
|
||||
"Big section heading.": "Big section heading.",
|
||||
"Medium section heading.": "Medium section heading.",
|
||||
"Small section heading.": "Small section heading.",
|
||||
"Create a simple bullet list.": "Create a simple bullet list.",
|
||||
"Create a list with numbering.": "Create a list with numbering.",
|
||||
"Create block quote.": "Create block quote.",
|
||||
"Insert code snippet.": "Insert code snippet.",
|
||||
"Insert horizontal rule divider": "Insert horizontal rule divider",
|
||||
"Upload any image from your device.": "Upload any image from your device.",
|
||||
"Upload any video from your device.": "Upload any video from your device.",
|
||||
"Upload any file from your device.": "Upload any file from your device.",
|
||||
"Table": "Table",
|
||||
"Insert a table.": "Insert a table.",
|
||||
"Insert collapsible block.": "Insert collapsible block.",
|
||||
"Video": "Video",
|
||||
"Divider": "Divider",
|
||||
"Quote": "Quote",
|
||||
"Image": "Image",
|
||||
"File attachment": "File attachment",
|
||||
"Toggle block": "Toggle block",
|
||||
"Callout": "Callout",
|
||||
"Insert callout notice.": "Insert callout notice.",
|
||||
"Math inline": "Math inline",
|
||||
"Insert inline math equation.": "Insert inline math equation.",
|
||||
"Math block": "Math block",
|
||||
"Insert math equation": "Insert math equation",
|
||||
"Mermaid diagram": "Mermaid diagram",
|
||||
"Insert mermaid diagram": "Insert mermaid diagram",
|
||||
"Insert and design Drawio diagrams": "Insert and design Drawio diagrams",
|
||||
"Insert current date": "Insert current date",
|
||||
"Draw and sketch excalidraw diagrams": "Draw and sketch excalidraw diagrams",
|
||||
"Multiple": "Multiple",
|
||||
"Heading {{level}}": "Heading {{level}}",
|
||||
"Toggle title": "Toggle title",
|
||||
"Write anything. Enter \"/\" for commands": "Write anything. Enter \"/\" for commands",
|
||||
"Names do not match": "Names do not match",
|
||||
"Today, {{time}}": "Today, {{time}}",
|
||||
"Yesterday, {{time}}": "Yesterday, {{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/es-ES/translation.json
Normal file
342
apps/client/public/locales/es-ES/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "Cuenta",
|
||||
"Active": "Activo",
|
||||
"Add": "Agregar",
|
||||
"Add group members": "Agregar miembros del grupo",
|
||||
"Add groups": "Agregar grupos",
|
||||
"Add members": "Agregar miembros",
|
||||
"Add to groups": "Agregar a grupos",
|
||||
"Add space members": "Agregar miembros al espacio",
|
||||
"Admin": "Administrador",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "¿Estás seguro de que deseas eliminar este grupo? Los miembros perderán acceso a los recursos a los que este grupo tiene acceso.",
|
||||
"Are you sure you want to delete this page?": "¿Está seguro de que desea eliminar esta página?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "¿Está seguro de que desea eliminar a este usuario del grupo? El usuario perderá acceso a los recursos a los que tiene acceso este grupo.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "¿Está seguro de que desea eliminar a este usuario del espacio? El usuario perderá todo acceso a este espacio.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "¿Está seguro de que desea restaurar esta versión? Cualquier cambio no versionado se perderá.",
|
||||
"Can become members of groups and spaces in workspace": "Pueden convertirse en miembros de grupos y espacios en el espacio de trabajo",
|
||||
"Can create and edit pages in space.": "Puede crear y editar páginas en el espacio.",
|
||||
"Can edit": "Puede editar",
|
||||
"Can manage workspace": "Puede gestionar el espacio de trabajo",
|
||||
"Can manage workspace but cannot delete it": "Puede gestionar el espacio de trabajo pero no puede eliminarlo",
|
||||
"Can view": "Puede ver",
|
||||
"Can view pages in space but not edit.": "Puede ver páginas en el espacio pero no editarlas.",
|
||||
"Cancel": "Cancelar",
|
||||
"Change email": "Cambiar correo electrónico",
|
||||
"Change password": "Cambiar contraseña",
|
||||
"Change photo": "Cambiar foto",
|
||||
"Choose a role": "Seleccione un rol",
|
||||
"Choose your preferred color scheme.": "Elige tu esquema de color preferido.",
|
||||
"Choose your preferred interface language.": "Elige tu idioma de interfaz preferido.",
|
||||
"Choose your preferred page width.": "Elige el ancho de página que prefieras.",
|
||||
"Confirm": "Confirmar",
|
||||
"Copy link": "Copiar enlace",
|
||||
"Create": "Crear",
|
||||
"Create group": "Crear grupo",
|
||||
"Create page": "Crear página",
|
||||
"Create space": "Crear espacio",
|
||||
"Create workspace": "Crear espacio de trabajo",
|
||||
"Current password": "Contraseña actual",
|
||||
"Dark": "Oscuro",
|
||||
"Date": "Fecha",
|
||||
"Delete": "Eliminar",
|
||||
"Delete group": "Eliminar grupo",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "¿Está seguro de que desea eliminar esta página? Esto eliminará sus dependientes y el historial de la página. Esta acción es irreversible.",
|
||||
"Description": "Descripción",
|
||||
"Details": "Detalles",
|
||||
"e.g ACME": "ej: ACME",
|
||||
"e.g ACME Inc": "ej: ACME Inc",
|
||||
"e.g Developers": "ej: Desarrolladores",
|
||||
"e.g Group for developers": "ej: Grupo para desarrolladores",
|
||||
"e.g product": "ej: producto",
|
||||
"e.g Product Team": "ej: Equipo de Producto",
|
||||
"e.g Sales": "ej: Ventas",
|
||||
"e.g Space for product team": "ej: Espacio para el equipo de producto",
|
||||
"e.g Space for sales team to collaborate": "ej: Espacio para que el equipo de ventas colabore",
|
||||
"Edit": "Editar",
|
||||
"Edit group": "Editar grupo",
|
||||
"Email": "Correo electrónico",
|
||||
"Enter a strong password": "Introduce una contraseña fuerte",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Ingrese direcciones de correo electrónico válidas separadas por coma o espacio [max: 50]",
|
||||
"enter valid emails addresses": "introduce direcciones de correo electrónico válidas",
|
||||
"Enter your current password": "Introduce tu contraseña actual",
|
||||
"enter your full name": "introduzca su nombre completo",
|
||||
"Enter your new password": "Ingrese su nueva contraseña",
|
||||
"Enter your new preferred email": "Introduce tu nuevo correo electrónico preferido",
|
||||
"Enter your password": "Introduce tu contraseña",
|
||||
"Error fetching page data.": "Error al obtener los datos de la página.",
|
||||
"Error loading page history.": "Error al cargar el historial de la página.",
|
||||
"Export": "Exportar",
|
||||
"Failed to create page": "No se pudo crear la página",
|
||||
"Failed to delete page": "No se pudo eliminar la página",
|
||||
"Failed to fetch recent pages": "Error al obtener las páginas recientes",
|
||||
"Failed to import pages": "No se pudieron importar las páginas",
|
||||
"Failed to load page. An error occurred.": "Error al cargar la página. Se produjo un error.",
|
||||
"Failed to update data": "No se pudo actualizar los datos",
|
||||
"Full access": "Acceso completo",
|
||||
"Full page width": "Ancho de página completa",
|
||||
"Full width": "Ancho completo",
|
||||
"General": "General",
|
||||
"Group": "Grupo",
|
||||
"Group description": "Descripción del grupo",
|
||||
"Group name": "Nombre del grupo",
|
||||
"Groups": "Grupos",
|
||||
"Has full access to space settings and pages.": "Tiene acceso completo a la configuración y páginas del espacio.",
|
||||
"Home": "Inicio",
|
||||
"Import pages": "Importar páginas",
|
||||
"Import pages & space settings": "Importar páginas y configuraciones del espacio",
|
||||
"Importing pages": "Importando páginas",
|
||||
"invalid invitation link": "enlace de invitación no válido",
|
||||
"Invitation signup": "Registro por invitación",
|
||||
"Invite by email": "Invitar por correo electrónico",
|
||||
"Invite members": "Invitar a miembros",
|
||||
"Invite new members": "Invitar a nuevos miembros",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Los miembros invitados que aún no han aceptado su invitación aparecerán aquí.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Los miembros invitados recibirán acceso a los espacios a los que los grupos pueden acceder",
|
||||
"Join the workspace": "Unirse al espacio de trabajo",
|
||||
"Language": "Idioma",
|
||||
"Light": "Ligero",
|
||||
"Link copied": "Enlace copiado",
|
||||
"Login": "Iniciar sesión",
|
||||
"Logout": "Cerrar sesión",
|
||||
"Manage Group": "Gestionar Grupo",
|
||||
"Manage members": "Gestionar miembros",
|
||||
"member": "miembro",
|
||||
"Member": "Miembro",
|
||||
"members": "miembros",
|
||||
"Members": "Miembros",
|
||||
"My preferences": "Mis preferencias",
|
||||
"My Profile": "Mi Perfil",
|
||||
"My profile": "Mi perfil",
|
||||
"Name": "Nombre",
|
||||
"New email": "Nuevo correo electrónico",
|
||||
"New page": "Nueva página",
|
||||
"New password": "Nueva contraseña",
|
||||
"No group found": "No se encontró grupo",
|
||||
"No page history saved yet.": "No hay historial de la página guardado aún.",
|
||||
"No pages yet": "No hay páginas todavía",
|
||||
"No results found...": "No se encontraron resultados...",
|
||||
"No user found": "No se encontró usuario",
|
||||
"Overview": "Visión general",
|
||||
"Owner": "Propietario",
|
||||
"page": "página",
|
||||
"Page deleted successfully": "Página eliminada con éxito",
|
||||
"Page history": "Historial de la página",
|
||||
"Page import is in progress. Please do not close this tab.": "La importación de la página está en curso. Por favor, no cierre esta pestaña.",
|
||||
"Pages": "Páginas",
|
||||
"pages": "páginas",
|
||||
"Password": "Contraseña",
|
||||
"Password changed successfully": "Contraseña cambiada con éxito",
|
||||
"Pending": "Pendiente",
|
||||
"Please confirm your action": "Por favor, confirme su acción",
|
||||
"Preferences": "Preferencias",
|
||||
"Print PDF": "Imprimir PDF",
|
||||
"Profile": "Perfil",
|
||||
"Recently updated": "Recientemente actualizado",
|
||||
"Remove": "Eliminar",
|
||||
"Remove group member": "Eliminar miembro del grupo",
|
||||
"Remove space member": "Eliminar miembro del espacio",
|
||||
"Restore": "Restaurar",
|
||||
"Role": "Rol",
|
||||
"Save": "Guardar",
|
||||
"Search": "Buscar",
|
||||
"Search for groups": "Buscar grupos",
|
||||
"Search for users": "Buscar usuarios",
|
||||
"Search for users and groups": "Buscar usuarios y grupos",
|
||||
"Search...": "Buscar...",
|
||||
"Select language": "Seleccionar idioma",
|
||||
"Select role": "Seleccionar rol",
|
||||
"Select role to assign to all invited members": "Seleccionar rol para asignar a todos los miembros invitados",
|
||||
"Select theme": "Seleccionar tema",
|
||||
"Send invitation": "Enviar invitación",
|
||||
"Settings": "Ajustes",
|
||||
"Setup workspace": "Configurar espacio de trabajo",
|
||||
"Sign In": "Iniciar sesión",
|
||||
"Sign Up": "Registrarse",
|
||||
"Slug": "Identificador",
|
||||
"Space": "Espacio",
|
||||
"Space description": "Descripción del espacio",
|
||||
"Space menu": "Menú de espacio",
|
||||
"Space name": "Nombre del espacio",
|
||||
"Space settings": "Configuración del espacio",
|
||||
"Space slug": "Identificador del espacio",
|
||||
"Spaces": "Espacios",
|
||||
"Spaces you belong to": "Espacios a los que perteneces",
|
||||
"No space found": "No se encontró espacio",
|
||||
"Search for spaces": "Buscar espacios",
|
||||
"Start typing to search...": "Empieza a escribir para buscar...",
|
||||
"Status": "Estado",
|
||||
"Successfully imported": "Importado con éxito",
|
||||
"Successfully restored": "Restaurado con éxito",
|
||||
"System settings": "Configuración del sistema",
|
||||
"Theme": "Tema",
|
||||
"To change your email, you have to enter your password and new email.": "Para cambiar tu correo electrónico, debes ingresar tu contraseña y nuevo correo electrónico.",
|
||||
"Toggle full page width": "Alternar el ancho de página completa",
|
||||
"Unable to import pages. Please try again.": "No se pueden importar las páginas. Por favor, inténtelo de nuevo.",
|
||||
"untitled": "sin título",
|
||||
"Untitled": "Sin título",
|
||||
"Updated successfully": "Actualizado con éxito",
|
||||
"User": "Usuario",
|
||||
"Workspace": "Espacio de trabajo",
|
||||
"Workspace Name": "Nombre del espacio de trabajo",
|
||||
"Workspace settings": "Configuración del espacio de trabajo",
|
||||
"You can change your password here.": "Puede cambiar su contraseña aquí.",
|
||||
"Your Email": "Su correo electrónico",
|
||||
"Your import is complete.": "Su importación está completa.",
|
||||
"Your name": "Tu nombre",
|
||||
"Your Name": "Tu Nombre",
|
||||
"Your password": "Tu contraseña",
|
||||
"Your password must be a minimum of 8 characters.": "Su contraseña debe tener un mínimo de 8 caracteres.",
|
||||
"Sidebar toggle": "Alternar barra lateral",
|
||||
"Comments": "Comentarios",
|
||||
"404 page not found": "404 página no encontrada",
|
||||
"Sorry, we can't find the page you are looking for.": "Lo sentimos, no podemos encontrar la página que buscas.",
|
||||
"Take me back to homepage": "Llévame de vuelta a la página de inicio",
|
||||
"Forgot password": "Olvidó la contraseña",
|
||||
"Forgot your password?": "¿Olvidó su contraseña?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Se ha enviado un enlace para restablecer la contraseña a tu correo electrónico. Por favor, revisa tu bandeja de entrada.",
|
||||
"Send reset link": "Enviar enlace de restablecimiento",
|
||||
"Password reset": "Restablecimiento de contraseña",
|
||||
"Your new password": "Tu nueva contraseña",
|
||||
"Set password": "Establecer contraseña",
|
||||
"Write a comment": "Escribir un comentario",
|
||||
"Reply...": "Responder...",
|
||||
"Error loading comments.": "Error al cargar comentarios.",
|
||||
"No comments yet.": "No hay comentarios todavía.",
|
||||
"Edit comment": "Editar comentario",
|
||||
"Delete comment": "Eliminar comentario",
|
||||
"Are you sure you want to delete this comment?": "¿Está seguro de que desea eliminar este comentario?",
|
||||
"Comment created successfully": "Comentario creado con éxito",
|
||||
"Error creating comment": "Error al crear comentario",
|
||||
"Comment updated successfully": "Comentario actualizado con éxito",
|
||||
"Failed to update comment": "No se pudo actualizar el comentario",
|
||||
"Comment deleted successfully": "Comentario eliminado con éxito",
|
||||
"Failed to delete comment": "No se pudo eliminar el comentario",
|
||||
"Comment resolved successfully": "Comentario resuelto con éxito",
|
||||
"Failed to resolve comment": "No se pudo resolver el comentario",
|
||||
"Revoke invitation": "Revocar invitación",
|
||||
"Revoke": "Revocar",
|
||||
"Don't": "No",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "¿Está seguro de que desea revocar esta invitación? El usuario no podrá unirse al espacio de trabajo.",
|
||||
"Resend invitation": "Reenviar invitación",
|
||||
"Anyone with this link can join this workspace.": "Cualquiera con este enlace puede unirse a este espacio de trabajo.",
|
||||
"Invite link": "Enlace de invitación",
|
||||
"Copy": "Copiar",
|
||||
"Copied": "Copiado",
|
||||
"Select a user": "Seleccionar un usuario",
|
||||
"Select a group": "Seleccionar un grupo",
|
||||
"Export all pages and attachments in this space.": "Exportar todas las páginas y archivos adjuntos en este espacio.",
|
||||
"Delete space": "Eliminar espacio",
|
||||
"Are you sure you want to delete this space?": "¿Está seguro de que desea eliminar este espacio?",
|
||||
"Delete this space with all its pages and data.": "Eliminar este espacio con todas sus páginas y datos.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Todas las páginas, comentarios, archivos adjuntos y permisos en este espacio se eliminarán de forma irreversible.",
|
||||
"Confirm space name": "Confirmar nombre del espacio",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Escribe el nombre del espacio <b>{{spaceName}}</b> para confirmar tu acción.",
|
||||
"Format": "Formato",
|
||||
"Include subpages": "Incluir subpáginas",
|
||||
"Include attachments": "Incluir adjuntos",
|
||||
"Select export format": "Seleccionar formato de exportación",
|
||||
"Export failed:": "Exportación fallida:",
|
||||
"export error": "error de exportación",
|
||||
"Export page": "Exportar página",
|
||||
"Export space": "Exportar espacio",
|
||||
"Export {{type}}": "Exportar {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "El archivo supera el límite de {{limit}} adjuntos",
|
||||
"Align left": "Alinear a la izquierda",
|
||||
"Align right": "Alinear a la derecha",
|
||||
"Align center": "Alinear al centro",
|
||||
"Merge cells": "Combinar celdas",
|
||||
"Split cell": "Dividir celda",
|
||||
"Delete column": "Eliminar columna",
|
||||
"Delete row": "Eliminar fila",
|
||||
"Add left column": "Agregar columna izquierda",
|
||||
"Add right column": "Agregar columna derecha",
|
||||
"Add row above": "Agregar fila arriba",
|
||||
"Add row below": "Agregar fila debajo",
|
||||
"Delete table": "Eliminar tabla",
|
||||
"Info": "Información",
|
||||
"Success": "Satisfactorio",
|
||||
"Warning": "Advertencia",
|
||||
"Danger": "Peligro",
|
||||
"Mermaid diagram error:": "Error en diagrama de Mermaid:",
|
||||
"Invalid Mermaid diagram": "Diagrama de Mermaid no válido",
|
||||
"Double-click to edit Draw.io diagram": "Doble clic para editar el diagrama de Draw.io",
|
||||
"Exit": "Salir",
|
||||
"Save & Exit": "Guardar y Salir",
|
||||
"Double-click to edit Excalidraw diagram": "Doble clic para editar el diagrama de Excalidraw",
|
||||
"Paste link": "Pegar enlace",
|
||||
"Edit link": "Editar enlace",
|
||||
"Remove link": "Eliminar enlace",
|
||||
"Add link": "Agregar enlace",
|
||||
"Please enter a valid url": "Por favor, ingrese una URL válida",
|
||||
"Empty equation": "Ecuación vacía",
|
||||
"Invalid equation": "Ecuación no válida",
|
||||
"Color": "Color",
|
||||
"Text color": "Color del texto",
|
||||
"Default": "Predeterminado",
|
||||
"Blue": "Azul",
|
||||
"Green": "Verde",
|
||||
"Purple": "Morado",
|
||||
"Red": "Rojo",
|
||||
"Yellow": "Amarillo",
|
||||
"Orange": "Naranja",
|
||||
"Pink": "Rosa",
|
||||
"Gray": "Gris",
|
||||
"Embed link": "Enlace adjunto",
|
||||
"Invalid {{provider}} embed link": "Enlace incrustado {{provider}} no válido",
|
||||
"Embed {{provider}}": "Incrustar {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Introduzca el enlace de {{provider}} para incrustar",
|
||||
"Bold": "Negrita",
|
||||
"Italic": "Cursiva",
|
||||
"Underline": "Subrayar",
|
||||
"Strike": "Tachar",
|
||||
"Code": "Código",
|
||||
"Comment": "Comentario",
|
||||
"Text": "Texto",
|
||||
"Heading 1": "Encabezado 1",
|
||||
"Heading 2": "Encabezado 2",
|
||||
"Heading 3": "Encabezado 3",
|
||||
"To-do List": "Lista de cosas por hacer",
|
||||
"Bullet List": "Lista con viñetas",
|
||||
"Numbered List": "Lista numerada",
|
||||
"Blockquote": "Cita en bloque",
|
||||
"Just start typing with plain text.": "Simplemente comienza a escribir con texto sin formato.",
|
||||
"Track tasks with a to-do list.": "Administra tareas con una lista de tareas pendientes.",
|
||||
"Big section heading.": "Gran encabezado de sección.",
|
||||
"Medium section heading.": "Encabezado de sección mediano.",
|
||||
"Small section heading.": "Pequeño encabezado de sección.",
|
||||
"Create a simple bullet list.": "Crear una lista con viñetas simple.",
|
||||
"Create a list with numbering.": "Crear una lista con numeración.",
|
||||
"Create block quote.": "Crear una cita en bloque.",
|
||||
"Insert code snippet.": "Insertar fragmento de código.",
|
||||
"Insert horizontal rule divider": "Insertar regla horizontal",
|
||||
"Upload any image from your device.": "Sube cualquier imagen desde tu dispositivo.",
|
||||
"Upload any video from your device.": "Sube cualquier video desde tu dispositivo.",
|
||||
"Upload any file from your device.": "Sube cualquier archivo desde tu dispositivo.",
|
||||
"Table": "Tabla",
|
||||
"Insert a table.": "Insertar una tabla.",
|
||||
"Insert collapsible block.": "Insertar bloque desplegable.",
|
||||
"Video": "Vídeo",
|
||||
"Divider": "Divisor",
|
||||
"Quote": "Cita",
|
||||
"Image": "Imagen",
|
||||
"File attachment": "Adjunto de archivo",
|
||||
"Toggle block": "Alternar bloque",
|
||||
"Callout": "Aviso",
|
||||
"Insert callout notice.": "Insertar aviso de llamada.",
|
||||
"Math inline": "Matemáticas en línea",
|
||||
"Insert inline math equation.": "Insertar ecuación matemática en línea.",
|
||||
"Math block": "Bloque de matemáticas",
|
||||
"Insert math equation": "Insertar ecuación matemática",
|
||||
"Mermaid diagram": "Diagrama de Mermaid",
|
||||
"Insert mermaid diagram": "Insertar diagrama de Mermaid",
|
||||
"Insert and design Drawio diagrams": "Insertar y diseñar diagramas Drawio",
|
||||
"Insert current date": "Insertar fecha actual",
|
||||
"Draw and sketch excalidraw diagrams": "Dibujar y esbozar diagramas de Excalidraw",
|
||||
"Multiple": "Múltiple",
|
||||
"Heading {{level}}": "Encabezado {{level}}",
|
||||
"Toggle title": "Alternar título",
|
||||
"Write anything. Enter \"/\" for commands": "Escribe cualquier cosa. Ingresa \"/\" para comandos",
|
||||
"Names do not match": "Los nombres no coinciden",
|
||||
"Today, {{time}}": "Hoy, {{time}}",
|
||||
"Yesterday, {{time}}": "Ayer, {{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/fr-FR/translation.json
Normal file
342
apps/client/public/locales/fr-FR/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "Compte",
|
||||
"Active": "Actif",
|
||||
"Add": "Ajouter",
|
||||
"Add group members": "Ajouter des membres au groupe",
|
||||
"Add groups": "Ajouter des groupes",
|
||||
"Add members": "Ajouter des membres",
|
||||
"Add to groups": "Ajouter aux groupes",
|
||||
"Add space members": "Ajouter des membres à l'espace",
|
||||
"Admin": "Admin",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Êtes-vous sûr de vouloir supprimer ce groupe ? Les membres perdront l'accès aux ressources auxquelles ce groupe a accès.",
|
||||
"Are you sure you want to delete this page?": "Êtes-vous sûr de vouloir supprimer cette page ?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Êtes-vous sûr de vouloir retirer cet utilisateur du groupe ? L'utilisateur perdra l'accès aux ressources auxquelles ce groupe a accès.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Êtes-vous sûr de vouloir retirer cet utilisateur de l'espace ? L'utilisateur perdra tout accès à cet espace.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Êtes-vous sûr de vouloir restaurer cette version ? Toutes les modifications non versionnées seront perdues.",
|
||||
"Can become members of groups and spaces in workspace": "Peut devenir membre de groupes et d'espaces dans l'espace de travail",
|
||||
"Can create and edit pages in space.": "Peut créer et modifier des pages dans l'espace.",
|
||||
"Can edit": "Peut modifier",
|
||||
"Can manage workspace": "Peut gérer l'espace de travail",
|
||||
"Can manage workspace but cannot delete it": "Peut gérer l'espace de travail mais ne peut pas le supprimer",
|
||||
"Can view": "Peut voir",
|
||||
"Can view pages in space but not edit.": "Peut voir les pages dans l'espace mais ne peut pas les modifier.",
|
||||
"Cancel": "Annuler",
|
||||
"Change email": "Changer l'email",
|
||||
"Change password": "Changer le mot de passe",
|
||||
"Change photo": "Changer la photo",
|
||||
"Choose a role": "Choisir un rôle",
|
||||
"Choose your preferred color scheme.": "Choisissez votre palette de couleurs préférée.",
|
||||
"Choose your preferred interface language.": "Choisissez votre langue d'interface préférée.",
|
||||
"Choose your preferred page width.": "Choisissez votre largeur de page préférée.",
|
||||
"Confirm": "Confirmer",
|
||||
"Copy link": "Copier le lien",
|
||||
"Create": "Créer",
|
||||
"Create group": "Créer groupe",
|
||||
"Create page": "Créer page",
|
||||
"Create space": "Créer espace",
|
||||
"Create workspace": "Créer espace de travail",
|
||||
"Current password": "Mot de passe actuel",
|
||||
"Dark": "Sombre",
|
||||
"Date": "Date",
|
||||
"Delete": "Supprimer",
|
||||
"Delete group": "Supprimer groupe",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Êtes-vous sûr de vouloir supprimer cette page ? Cela supprimera ses enfants et l'historique de la page. Cette action est irréversible.",
|
||||
"Description": "Description",
|
||||
"Details": "Détails",
|
||||
"e.g ACME": "par ex. ACME",
|
||||
"e.g ACME Inc": "par ex. ACME Inc",
|
||||
"e.g Developers": "par ex. Développeurs",
|
||||
"e.g Group for developers": "par ex. Groupe pour développeurs",
|
||||
"e.g product": "par ex. produit",
|
||||
"e.g Product Team": "par ex. Équipe Produit",
|
||||
"e.g Sales": "par ex. Ventes",
|
||||
"e.g Space for product team": "par ex. Espace pour l'équipe produit",
|
||||
"e.g Space for sales team to collaborate": "par ex. Espace pour l'équipe de vente pour collaborer",
|
||||
"Edit": "Modifier",
|
||||
"Edit group": "Modifier groupe",
|
||||
"Email": "Email",
|
||||
"Enter a strong password": "Entrez un mot de passe fort",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Entrez des adresses email valides séparées par une virgule ou un espace [max : 50]",
|
||||
"enter valid emails addresses": "entrez des adresses email valides",
|
||||
"Enter your current password": "Entrez votre mot de passe actuel",
|
||||
"enter your full name": "entrez votre nom complet",
|
||||
"Enter your new password": "Entrez votre nouveau mot de passe",
|
||||
"Enter your new preferred email": "Entrez votre nouvel email préféré",
|
||||
"Enter your password": "Entrez votre mot de passe",
|
||||
"Error fetching page data.": "Erreur lors de la récupération des données de la page.",
|
||||
"Error loading page history.": "Erreur lors du chargement de l'historique de la page.",
|
||||
"Export": "Exporter",
|
||||
"Failed to create page": "Échec de la création de la page",
|
||||
"Failed to delete page": "Échec de la suppression de la page",
|
||||
"Failed to fetch recent pages": "Échec de la récupération des pages récentes",
|
||||
"Failed to import pages": "Échec de l'importation des pages",
|
||||
"Failed to load page. An error occurred.": "Échec du chargement de la page. Une erreur s'est produite.",
|
||||
"Failed to update data": "Échec de la mise à jour des données",
|
||||
"Full access": "Accès complet",
|
||||
"Full page width": "Largeur de page complète",
|
||||
"Full width": "Largeur complète",
|
||||
"General": "Général",
|
||||
"Group": "Groupe",
|
||||
"Group description": "Description du groupe",
|
||||
"Group name": "Nom du groupe",
|
||||
"Groups": "Groupes",
|
||||
"Has full access to space settings and pages.": "A un accès complet aux paramètres de l'espace et aux pages.",
|
||||
"Home": "Accueil",
|
||||
"Import pages": "Importer des pages",
|
||||
"Import pages & space settings": "Importer des pages et paramètres de l'espace",
|
||||
"Importing pages": "Importation des pages",
|
||||
"invalid invitation link": "lien d'invitation invalide",
|
||||
"Invitation signup": "Inscription par invitation",
|
||||
"Invite by email": "Inviter par email",
|
||||
"Invite members": "Inviter des membres",
|
||||
"Invite new members": "Inviter de nouveaux membres",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Les membres invités qui n'ont pas encore accepté leur invitation apparaîtront ici.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Les membres invités auront accès aux espaces auxquels les groupes peuvent accéder",
|
||||
"Join the workspace": "Rejoindre l'espace de travail",
|
||||
"Language": "Langue",
|
||||
"Light": "Clair",
|
||||
"Link copied": "Lien copié",
|
||||
"Login": "Connexion",
|
||||
"Logout": "Déconnexion",
|
||||
"Manage Group": "Gérer le groupe",
|
||||
"Manage members": "Gérer les membres",
|
||||
"member": "membre",
|
||||
"Member": "Membre",
|
||||
"members": "membres",
|
||||
"Members": "Membres",
|
||||
"My preferences": "Mes préférences",
|
||||
"My Profile": "Mon Profil",
|
||||
"My profile": "Mon profil",
|
||||
"Name": "Nom",
|
||||
"New email": "Nouvel email",
|
||||
"New page": "Nouvelle page",
|
||||
"New password": "Nouveau mot de passe",
|
||||
"No group found": "Aucun groupe trouvé",
|
||||
"No page history saved yet.": "Aucun historique de la page enregistré pour l'instant.",
|
||||
"No pages yet": "Aucune page pour l'instant",
|
||||
"No results found...": "Aucun résultat trouvé...",
|
||||
"No user found": "Aucun utilisateur trouvé",
|
||||
"Overview": "Vue d'ensemble",
|
||||
"Owner": "Propriétaire",
|
||||
"page": "page",
|
||||
"Page deleted successfully": "Page supprimée avec succès",
|
||||
"Page history": "Historique de la page",
|
||||
"Page import is in progress. Please do not close this tab.": "L'importation de la page est en cours. Veuillez ne pas fermer cet onglet.",
|
||||
"Pages": "Pages",
|
||||
"pages": "pages",
|
||||
"Password": "Mot de passe",
|
||||
"Password changed successfully": "Mot de passe changé avec succès",
|
||||
"Pending": "En attente",
|
||||
"Please confirm your action": "Veuillez confirmer votre action",
|
||||
"Preferences": "Préférences",
|
||||
"Print PDF": "Imprimer PDF",
|
||||
"Profile": "Profil",
|
||||
"Recently updated": "Récemment mis à jour",
|
||||
"Remove": "Retirer",
|
||||
"Remove group member": "Retirer un membre du groupe",
|
||||
"Remove space member": "Retirer un membre de l'espace",
|
||||
"Restore": "Restaurer",
|
||||
"Role": "Rôle",
|
||||
"Save": "Enregistrer",
|
||||
"Search": "Rechercher",
|
||||
"Search for groups": "Rechercher des groupes",
|
||||
"Search for users": "Rechercher des utilisateurs",
|
||||
"Search for users and groups": "Rechercher des utilisateurs et des groupes",
|
||||
"Search...": "Rechercher...",
|
||||
"Select language": "Sélectionner la langue",
|
||||
"Select role": "Sélectionner un rôle",
|
||||
"Select role to assign to all invited members": "Sélectionner le rôle à attribuer à tous les membres invités",
|
||||
"Select theme": "Sélectionner le thème",
|
||||
"Send invitation": "Envoyer l'invitation",
|
||||
"Settings": "Paramètres",
|
||||
"Setup workspace": "Configurer l'espace de travail",
|
||||
"Sign In": "Se connecter",
|
||||
"Sign Up": "S'inscrire",
|
||||
"Slug": "Slug",
|
||||
"Space": "Espace",
|
||||
"Space description": "Description de l'espace",
|
||||
"Space menu": "Menu de l'espace",
|
||||
"Space name": "Nom de l'espace",
|
||||
"Space settings": "Paramètres de l'espace",
|
||||
"Space slug": "Slug de l'espace",
|
||||
"Spaces": "Espaces",
|
||||
"Spaces you belong to": "Espaces auxquels vous appartenez",
|
||||
"No space found": "Aucun espace trouvé",
|
||||
"Search for spaces": "Rechercher des espaces",
|
||||
"Start typing to search...": "Commencez à taper pour rechercher...",
|
||||
"Status": "Statut",
|
||||
"Successfully imported": "Importé avec succès",
|
||||
"Successfully restored": "Restauré avec succès",
|
||||
"System settings": "Paramètres système",
|
||||
"Theme": "Thème",
|
||||
"To change your email, you have to enter your password and new email.": "Pour changer votre email, vous devez entrer votre mot de passe et votre nouvel email.",
|
||||
"Toggle full page width": "Basculer sur la largeur complète de la page",
|
||||
"Unable to import pages. Please try again.": "Impossible d'importer les pages. Veuillez réessayer.",
|
||||
"untitled": "sans titre",
|
||||
"Untitled": "Sans titre",
|
||||
"Updated successfully": "Mis à jour avec succès",
|
||||
"User": "Utilisateur",
|
||||
"Workspace": "Espace de travail",
|
||||
"Workspace Name": "Nom de l'espace de travail",
|
||||
"Workspace settings": "Paramètres de l'espace de travail",
|
||||
"You can change your password here.": "Vous pouvez changer votre mot de passe ici.",
|
||||
"Your Email": "Votre Email",
|
||||
"Your import is complete.": "Votre importation est terminée.",
|
||||
"Your name": "Votre nom",
|
||||
"Your Name": "Votre Nom",
|
||||
"Your password": "Votre mot de passe",
|
||||
"Your password must be a minimum of 8 characters.": "Votre mot de passe doit contenir au moins 8 caractères.",
|
||||
"Sidebar toggle": "Bascule de la barre latérale",
|
||||
"Comments": "Commentaires",
|
||||
"404 page not found": "404 page non trouvée",
|
||||
"Sorry, we can't find the page you are looking for.": "Désolé, nous ne pouvons pas trouver la page que vous cherchez.",
|
||||
"Take me back to homepage": "Ramenez-moi à la page d'accueil",
|
||||
"Forgot password": "Mot de passe oublié",
|
||||
"Forgot your password?": "Mot de passe oublié?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Un lien de réinitialisation de mot de passe a été envoyé à votre e-mail. Veuillez vérifier votre boîte de réception.",
|
||||
"Send reset link": "Envoyer le lien de réinitialisation",
|
||||
"Password reset": "Réinitialisation du mot de passe",
|
||||
"Your new password": "Votre nouveau mot de passe",
|
||||
"Set password": "Définir le mot de passe",
|
||||
"Write a comment": "Écrire un commentaire",
|
||||
"Reply...": "Répondre...",
|
||||
"Error loading comments.": "Erreur lors du chargement des commentaires.",
|
||||
"No comments yet.": "Pas de commentaires pour l'instant.",
|
||||
"Edit comment": "Modifier le commentaire",
|
||||
"Delete comment": "Supprimer le commentaire",
|
||||
"Are you sure you want to delete this comment?": "Êtes-vous sûr de vouloir supprimer ce commentaire ?",
|
||||
"Comment created successfully": "Commentaire créé avec succès",
|
||||
"Error creating comment": "Erreur lors de la création du commentaire",
|
||||
"Comment updated successfully": "Commentaire mis à jour avec succès",
|
||||
"Failed to update comment": "Échec de la mise à jour du commentaire",
|
||||
"Comment deleted successfully": "Commentaire supprimé avec succès",
|
||||
"Failed to delete comment": "Échec de la suppression du commentaire",
|
||||
"Comment resolved successfully": "Commentaire résolu avec succès",
|
||||
"Failed to resolve comment": "Échec de la résolution du commentaire",
|
||||
"Revoke invitation": "Révoquer l'invitation",
|
||||
"Revoke": "Révoquer",
|
||||
"Don't": "Ne pas",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Êtes-vous sûr de vouloir révoquer cette invitation ? L'utilisateur ne pourra pas rejoindre l'espace de travail.",
|
||||
"Resend invitation": "Renvoyer l'invitation",
|
||||
"Anyone with this link can join this workspace.": "Toute personne ayant ce lien peut rejoindre cet espace de travail.",
|
||||
"Invite link": "Lien d'invitation",
|
||||
"Copy": "Copier",
|
||||
"Copied": "Copié",
|
||||
"Select a user": "Sélectionner un utilisateur",
|
||||
"Select a group": "Sélectionner un groupe",
|
||||
"Export all pages and attachments in this space.": "Exporter toutes les pages et pièces jointes dans cet espace.",
|
||||
"Delete space": "Supprimer l'espace",
|
||||
"Are you sure you want to delete this space?": "Êtes-vous sûr de vouloir supprimer cet espace ?",
|
||||
"Delete this space with all its pages and data.": "Supprimer cet espace avec toutes ses pages et données.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Toutes les pages, commentaires, pièces jointes et autorisations dans cet espace seront supprimés irréversiblement.",
|
||||
"Confirm space name": "Confirmer le nom de l'espace",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Tapez le nom de l'espace <b>{{spaceName}}</b> pour confirmer votre action.",
|
||||
"Format": "Format",
|
||||
"Include subpages": "Inclure les sous-pages",
|
||||
"Include attachments": "Inclure les pièces jointes",
|
||||
"Select export format": "Sélectionner le format d'exportation",
|
||||
"Export failed:": "Échec de l'exportation :",
|
||||
"export error": "exporter l'erreur",
|
||||
"Export page": "Exporter la page",
|
||||
"Export space": "Exporter l'espace",
|
||||
"Export {{type}}": "Exporter {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Le fichier dépasse la limite de {{limit}} pièces jointes",
|
||||
"Align left": "Aligner à gauche",
|
||||
"Align right": "Aligner à droite",
|
||||
"Align center": "Aligner au centre",
|
||||
"Merge cells": "Fusionner les cellules",
|
||||
"Split cell": "Diviser la cellule",
|
||||
"Delete column": "Supprimer la colonne",
|
||||
"Delete row": "Supprimer la ligne",
|
||||
"Add left column": "Ajouter colonne à gauche",
|
||||
"Add right column": "Ajouter colonne à droite",
|
||||
"Add row above": "Ajouter une ligne au-dessus",
|
||||
"Add row below": "Ajouter une ligne en dessous",
|
||||
"Delete table": "Supprimer le tableau",
|
||||
"Info": "Info",
|
||||
"Success": "Succès",
|
||||
"Warning": "Avertissement",
|
||||
"Danger": "Danger",
|
||||
"Mermaid diagram error:": "Erreur de diagramme Mermaid :",
|
||||
"Invalid Mermaid diagram": "Diagramme Mermaid invalide",
|
||||
"Double-click to edit Draw.io diagram": "Double-cliquez pour modifier le diagramme Draw.io",
|
||||
"Exit": "Quitter",
|
||||
"Save & Exit": "Enregistrer & Quitter",
|
||||
"Double-click to edit Excalidraw diagram": "Double-cliquez pour modifier le diagramme Excalidraw",
|
||||
"Paste link": "Coller le lien",
|
||||
"Edit link": "Modifier le lien",
|
||||
"Remove link": "Supprimer le lien",
|
||||
"Add link": "Ajouter un lien",
|
||||
"Please enter a valid url": "Veuillez entrer une URL valide",
|
||||
"Empty equation": "Équation vide",
|
||||
"Invalid equation": "Équation invalide",
|
||||
"Color": "Couleur",
|
||||
"Text color": "Couleur du texte",
|
||||
"Default": "Par défaut",
|
||||
"Blue": "Bleu",
|
||||
"Green": "Vert",
|
||||
"Purple": "Violet",
|
||||
"Red": "Rouge",
|
||||
"Yellow": "Jaune",
|
||||
"Orange": "Orange",
|
||||
"Pink": "Rose",
|
||||
"Gray": "Gris",
|
||||
"Embed link": "Intégrer un lien",
|
||||
"Invalid {{provider}} embed link": "Lien d'intégration {{provider}} non valide",
|
||||
"Embed {{provider}}": "Intégrer {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Entrez le lien {{provider}} à intégrer",
|
||||
"Bold": "Gras",
|
||||
"Italic": "Italique",
|
||||
"Underline": "Souligner",
|
||||
"Strike": "Barrer",
|
||||
"Code": "Code",
|
||||
"Comment": "Commentaire",
|
||||
"Text": "Texte",
|
||||
"Heading 1": "Titre 1",
|
||||
"Heading 2": "Titre 2",
|
||||
"Heading 3": "Titre 3",
|
||||
"To-do List": "Liste de tâches",
|
||||
"Bullet List": "Liste à puces",
|
||||
"Numbered List": "Liste numérotée",
|
||||
"Blockquote": "Bloc de citation",
|
||||
"Just start typing with plain text.": "Commencez simplement à taper avec du texte brut.",
|
||||
"Track tasks with a to-do list.": "Suivez les tâches avec une liste de tâches.",
|
||||
"Big section heading.": "Grand titre de section.",
|
||||
"Medium section heading.": "Titre de section moyen.",
|
||||
"Small section heading.": "Petit titre de section.",
|
||||
"Create a simple bullet list.": "Créez une simple liste à puces.",
|
||||
"Create a list with numbering.": "Créez une liste numérotée.",
|
||||
"Create block quote.": "Créez un bloc de citation.",
|
||||
"Insert code snippet.": "Insérez un extrait de code.",
|
||||
"Insert horizontal rule divider": "Insérer un séparateur de règle horizontale",
|
||||
"Upload any image from your device.": "Téléchargez n'importe quelle image depuis votre appareil.",
|
||||
"Upload any video from your device.": "Téléchargez n'importe quelle vidéo depuis votre appareil.",
|
||||
"Upload any file from your device.": "Téléchargez n'importe quel fichier depuis votre appareil.",
|
||||
"Table": "Tableau",
|
||||
"Insert a table.": "Insérez un tableau.",
|
||||
"Insert collapsible block.": "Insérer un bloc repliable.",
|
||||
"Video": "Vidéo",
|
||||
"Divider": "Diviseur",
|
||||
"Quote": "Citation",
|
||||
"Image": "Image",
|
||||
"File attachment": "Pièce jointe",
|
||||
"Toggle block": "Basculer le bloc",
|
||||
"Callout": "Appel",
|
||||
"Insert callout notice.": "Insérer un avis d'appel.",
|
||||
"Math inline": "Mathématiques en ligne",
|
||||
"Insert inline math equation.": "Insérez une équation mathématique en ligne.",
|
||||
"Math block": "Bloc mathématiques",
|
||||
"Insert math equation": "Insérer une équation mathématique",
|
||||
"Mermaid diagram": "Diagramme Mermaid",
|
||||
"Insert mermaid diagram": "Insérer un diagramme Mermaid",
|
||||
"Insert and design Drawio diagrams": "Insérer et concevoir des diagrammes Drawio",
|
||||
"Insert current date": "Insérer la date actuelle",
|
||||
"Draw and sketch excalidraw diagrams": "Dessiner et esquisser des diagrammes Excalidraw",
|
||||
"Multiple": "Multiple",
|
||||
"Heading {{level}}": "Titre {{level}}",
|
||||
"Toggle title": "Basculer le titre",
|
||||
"Write anything. Enter \"/\" for commands": "Écrivez n'importe quoi. Entrez \"/\" pour les commandes",
|
||||
"Names do not match": "Les noms ne correspondent pas",
|
||||
"Today, {{time}}": "Aujourd'hui, {{time}}",
|
||||
"Yesterday, {{time}}": "Hier, {{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/it-IT/translation.json
Normal file
342
apps/client/public/locales/it-IT/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "Account",
|
||||
"Active": "Attivo",
|
||||
"Add": "Aggiungi",
|
||||
"Add group members": "Aggiungi membri al gruppo",
|
||||
"Add groups": "Aggiungi gruppi",
|
||||
"Add members": "Aggiungi membri",
|
||||
"Add to groups": "Aggiungi ai gruppi",
|
||||
"Add space members": "Aggiungi membri allo spazio",
|
||||
"Admin": "Amministratore",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Sei sicuro di voler eliminare questo gruppo? I membri perderanno l'accesso alle risorse accessibili da questo gruppo.",
|
||||
"Are you sure you want to delete this page?": "Sei sicuro di voler eliminare questa pagina?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Sei sicuro di voler rimuovere questo utente dal gruppo? L'utente perderà l'accesso alle risorse accessibili da questo gruppo.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Sei sicuro di voler rimuovere questo utente dallo spazio? L'utente perderà tutti gli accessi a questo spazio.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Sei sicuro di voler ripristinare questa versione? Qualsiasi modifica non salvata come versione andrà persa.",
|
||||
"Can become members of groups and spaces in workspace": "Può diventare membro di gruppi e spazi nell'area di lavoro",
|
||||
"Can create and edit pages in space.": "Può creare e modificare le pagine nello spazio.",
|
||||
"Can edit": "Può modificare",
|
||||
"Can manage workspace": "Può gestire lo spazio di lavoro",
|
||||
"Can manage workspace but cannot delete it": "Può gestire lo spazio di lavoro ma non può eliminarlo",
|
||||
"Can view": "Può visualizzare",
|
||||
"Can view pages in space but not edit.": "Può visualizzare le pagine nello spazio ma non modificarle.",
|
||||
"Cancel": "Annulla",
|
||||
"Change email": "Cambia email",
|
||||
"Change password": "Cambia password",
|
||||
"Change photo": "Cambia foto",
|
||||
"Choose a role": "Scegli un ruolo",
|
||||
"Choose your preferred color scheme.": "Scegli il tuo schema di colori preferito.",
|
||||
"Choose your preferred interface language.": "Scegli la tua lingua preferita per l'interfaccia.",
|
||||
"Choose your preferred page width.": "Scegli la larghezza della pagina che preferisci.",
|
||||
"Confirm": "Conferma",
|
||||
"Copy link": "Copia link",
|
||||
"Create": "Crea",
|
||||
"Create group": "Crea gruppo",
|
||||
"Create page": "Crea pagina",
|
||||
"Create space": "Crea spazio",
|
||||
"Create workspace": "Crea spazio di lavoro",
|
||||
"Current password": "Password attuale",
|
||||
"Dark": "Scuro",
|
||||
"Date": "Data",
|
||||
"Delete": "Elimina",
|
||||
"Delete group": "Elimina gruppo",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sei sicuro di voler eliminare questa pagina? Verranno cancellate anche le sue sottopagine e la cronologia. Questa azione è irreversibile.",
|
||||
"Description": "Descrizione",
|
||||
"Details": "Dettagli",
|
||||
"e.g ACME": "ad es. ACME",
|
||||
"e.g ACME Inc": "es. ACME Inc",
|
||||
"e.g Developers": "es. Sviluppatori",
|
||||
"e.g Group for developers": "es. Gruppo per gli sviluppatori",
|
||||
"e.g product": "ad esempio prodotto",
|
||||
"e.g Product Team": "es. Team di Prodotto",
|
||||
"e.g Sales": "ad es. Vendite",
|
||||
"e.g Space for product team": "ad es. Spazio per il team di prodotto",
|
||||
"e.g Space for sales team to collaborate": "ad es. Spazio per il team di vendita per collaborare",
|
||||
"Edit": "Modifica",
|
||||
"Edit group": "Modifica gruppo",
|
||||
"Email": "Email",
|
||||
"Enter a strong password": "Inserisci una password sicura",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Inserisci indirizzi email validi separati da virgola o spazio [max: 50]",
|
||||
"enter valid emails addresses": "inserisci indirizzi email validi",
|
||||
"Enter your current password": "Inserisci la tua password attuale",
|
||||
"enter your full name": "inserisci il tuo nome completo",
|
||||
"Enter your new password": "Inserisci la tua nuova password",
|
||||
"Enter your new preferred email": "Inserisci la tua nuova email preferita",
|
||||
"Enter your password": "Inserisci la tua password",
|
||||
"Error fetching page data.": "Si è verificato un errore durante il recupero dei dati della pagina.",
|
||||
"Error loading page history.": "Si è verificato un errore durante il caricamento della cronologia della pagina.",
|
||||
"Export": "Esporta",
|
||||
"Failed to create page": "Impossibile creare pagina",
|
||||
"Failed to delete page": "Impossibile eliminare la pagina",
|
||||
"Failed to fetch recent pages": "Impossibile recuperare le pagine recenti",
|
||||
"Failed to import pages": "Impossibile importare le pagine",
|
||||
"Failed to load page. An error occurred.": "Il caricamento della pagina è fallito. Si è verificato un errore.",
|
||||
"Failed to update data": "Impossibile aggiornare i dati",
|
||||
"Full access": "Accesso completo",
|
||||
"Full page width": "Larghezza pagina intera",
|
||||
"Full width": "Larghezza intera",
|
||||
"General": "Generale",
|
||||
"Group": "Gruppo",
|
||||
"Group description": "Descrizione del gruppo",
|
||||
"Group name": "Nome del gruppo",
|
||||
"Groups": "Gruppi",
|
||||
"Has full access to space settings and pages.": "Ha pieno accesso alle impostazioni e alle pagine dello spazio.",
|
||||
"Home": "Casa",
|
||||
"Import pages": "Importa pagine",
|
||||
"Import pages & space settings": "Importa pagine e impostazioni dello spazio",
|
||||
"Importing pages": "Importazione pagine",
|
||||
"invalid invitation link": "link di invito non valido",
|
||||
"Invitation signup": "Iscrizione invito",
|
||||
"Invite by email": "Invita via email",
|
||||
"Invite members": "Invita membri",
|
||||
"Invite new members": "Invita nuovi membri",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "I membri invitati che non hanno ancora accettato il loro invito appariranno qui.",
|
||||
"Invited members will be granted access to spaces the groups can access": "I membri invitati avranno accesso agli spazi a cui i gruppi possono accedere",
|
||||
"Join the workspace": "Unisciti allo spazio di lavoro",
|
||||
"Language": "Lingua",
|
||||
"Light": "Chiaro",
|
||||
"Link copied": "Link copiato",
|
||||
"Login": "Login",
|
||||
"Logout": "Esci",
|
||||
"Manage Group": "Gestisci Gruppo",
|
||||
"Manage members": "Gestisci membri",
|
||||
"member": "membro",
|
||||
"Member": "Membro",
|
||||
"members": "membri",
|
||||
"Members": "Membri",
|
||||
"My preferences": "Le mie preferenze",
|
||||
"My Profile": "Il mio profilo",
|
||||
"My profile": "Il mio profilo",
|
||||
"Name": "Nome",
|
||||
"New email": "Nuova email",
|
||||
"New page": "Nuova pagina",
|
||||
"New password": "Nuova password",
|
||||
"No group found": "Nessun gruppo trovato",
|
||||
"No page history saved yet.": "Nessuna cronologia della pagina salvata.",
|
||||
"No pages yet": "Nessuna pagina ancora",
|
||||
"No results found...": "Nessun risultato trovato...",
|
||||
"No user found": "Nessun utente trovato",
|
||||
"Overview": "Panoramica",
|
||||
"Owner": "Proprietario",
|
||||
"page": "pagina",
|
||||
"Page deleted successfully": "Pagina eliminata con successo",
|
||||
"Page history": "Cronologia della pagina",
|
||||
"Page import is in progress. Please do not close this tab.": "L'importazione della pagina è in corso. Si prega di non chiudere questa scheda.",
|
||||
"Pages": "Pagine",
|
||||
"pages": "pagine",
|
||||
"Password": "Password",
|
||||
"Password changed successfully": "Password cambiata con successo",
|
||||
"Pending": "In sospeso",
|
||||
"Please confirm your action": "Si prega di confermare la propria azione",
|
||||
"Preferences": "Preferenze",
|
||||
"Print PDF": "Stampa PDF",
|
||||
"Profile": "Profilo",
|
||||
"Recently updated": "Aggiornato di recente",
|
||||
"Remove": "Rimuovi",
|
||||
"Remove group member": "Rimuovi membro dal gruppo",
|
||||
"Remove space member": "Rimuovi membro dallo spazio",
|
||||
"Restore": "Ripristina",
|
||||
"Role": "Ruolo",
|
||||
"Save": "Salva",
|
||||
"Search": "Cerca",
|
||||
"Search for groups": "Cerca gruppi",
|
||||
"Search for users": "Cerca un utente",
|
||||
"Search for users and groups": "Cerca utenti e gruppi",
|
||||
"Search...": "Cerca...",
|
||||
"Select language": "Seleziona lingua",
|
||||
"Select role": "Seleziona ruolo",
|
||||
"Select role to assign to all invited members": "Seleziona il ruolo da assegnare a tutti i membri invitati",
|
||||
"Select theme": "Seleziona tema",
|
||||
"Send invitation": "Invia invito",
|
||||
"Settings": "Impostazioni",
|
||||
"Setup workspace": "Imposta spazio di lavoro",
|
||||
"Sign In": "Accedi",
|
||||
"Sign Up": "Registrati",
|
||||
"Slug": "Identificatore",
|
||||
"Space": "Spazio",
|
||||
"Space description": "Descrizione dello spazio",
|
||||
"Space menu": "Menu spazio",
|
||||
"Space name": "Nome dello spazio",
|
||||
"Space settings": "Impostazioni dello spazio",
|
||||
"Space slug": "Lumaca spaziale",
|
||||
"Spaces": "Spazi",
|
||||
"Spaces you belong to": "Spazi a cui appartieni",
|
||||
"No space found": "Nessuno spazio trovato",
|
||||
"Search for spaces": "Cerca spazi",
|
||||
"Start typing to search...": "Inizia a digitare per cercare...",
|
||||
"Status": "Stato",
|
||||
"Successfully imported": "Importazione riuscita",
|
||||
"Successfully restored": "Ripristinato con successo",
|
||||
"System settings": "Impostazioni di sistema",
|
||||
"Theme": "Tema",
|
||||
"To change your email, you have to enter your password and new email.": "Per cambiare la tua email, devi inserire la tua password e la nuova email.",
|
||||
"Toggle full page width": "Attiva/disattiva larghezza pagina intera",
|
||||
"Unable to import pages. Please try again.": "Impossibile importare le pagine. Riprova.",
|
||||
"untitled": "senza titolo",
|
||||
"Untitled": "Senza titolo",
|
||||
"Updated successfully": "Aggiornato con successo",
|
||||
"User": "Utente",
|
||||
"Workspace": "Spazio di lavoro",
|
||||
"Workspace Name": "Nome dello spazio di lavoro",
|
||||
"Workspace settings": "Impostazioni dello spazio di lavoro",
|
||||
"You can change your password here.": "Puoi cambiare la tua password qui.",
|
||||
"Your Email": "La tua email",
|
||||
"Your import is complete.": "Il tuo importazione è completata.",
|
||||
"Your name": "Il tuo nome",
|
||||
"Your Name": "Il Tuo Nome",
|
||||
"Your password": "La tua password",
|
||||
"Your password must be a minimum of 8 characters.": "La tua password deve contenere almeno 8 caratteri.",
|
||||
"Sidebar toggle": "Attiva/disattiva barra laterale",
|
||||
"Comments": "Commenti",
|
||||
"404 page not found": "404 pagina non trovata",
|
||||
"Sorry, we can't find the page you are looking for.": "Siamo spiacenti, non riusciamo a trovare la pagina che stai cercando.",
|
||||
"Take me back to homepage": "Portami alla homepage",
|
||||
"Forgot password": "Hai dimenticato la password",
|
||||
"Forgot your password?": "Hai dimenticato la password?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Un link per il reset della password è stato inviato al tuo indirizzo email. Per favore, controlla la tua casella di posta.",
|
||||
"Send reset link": "Invia link di ripristino",
|
||||
"Password reset": "Reimposta password",
|
||||
"Your new password": "La tua nuova password",
|
||||
"Set password": "Imposta password",
|
||||
"Write a comment": "Scrivi un commento",
|
||||
"Reply...": "Rispondi...",
|
||||
"Error loading comments.": "Si è verificato un errore durante il caricamento dei commenti.",
|
||||
"No comments yet.": "Nessun commento ancora.",
|
||||
"Edit comment": "Modifica commento",
|
||||
"Delete comment": "Elimina commento",
|
||||
"Are you sure you want to delete this comment?": "Sei sicuro di voler eliminare questo commento?",
|
||||
"Comment created successfully": "Commento creato con successo",
|
||||
"Error creating comment": "Si è verificato un errore durante la creazione del commento",
|
||||
"Comment updated successfully": "Commento aggiornato con successo",
|
||||
"Failed to update comment": "Impossibile aggiornare il commento",
|
||||
"Comment deleted successfully": "Commento eliminato con successo",
|
||||
"Failed to delete comment": "Impossibile eliminare il commento",
|
||||
"Comment resolved successfully": "Commento risolto con successo",
|
||||
"Failed to resolve comment": "Impossibile risolvere il commento",
|
||||
"Revoke invitation": "Revoca invito",
|
||||
"Revoke": "Revoca",
|
||||
"Don't": "Non",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Sei sicuro di voler revocare questo invito? L'utente non potrà unirsi allo spazio di lavoro.",
|
||||
"Resend invitation": "Rispedisci invito",
|
||||
"Anyone with this link can join this workspace.": "Chiunque con questo link può unirsi a questo workspace.",
|
||||
"Invite link": "Link d'invito",
|
||||
"Copy": "Copia",
|
||||
"Copied": "Copiato",
|
||||
"Select a user": "Seleziona un utente",
|
||||
"Select a group": "Seleziona un gruppo",
|
||||
"Export all pages and attachments in this space.": "Esporta tutte le pagine e gli allegati in questo spazio.",
|
||||
"Delete space": "Elimina spazio",
|
||||
"Are you sure you want to delete this space?": "Sei sicuro di voler eliminare questo spazio?",
|
||||
"Delete this space with all its pages and data.": "Elimina questo spazio con tutte le sue pagine e i suoi dati.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Tutte le pagine, i commenti, gli allegati e i permessi in questo spazio verranno eliminati in modo irreversibile.",
|
||||
"Confirm space name": "Conferma nome spazio",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Digita il nome dello spazio <b>{{spaceName}}</b> per confermare la tua azione.",
|
||||
"Format": "Formato",
|
||||
"Include subpages": "Includi sottopagine",
|
||||
"Include attachments": "Includi allegati",
|
||||
"Select export format": "Seleziona formato di esportazione",
|
||||
"Export failed:": "Esportazione fallita:",
|
||||
"export error": "errore di esportazione",
|
||||
"Export page": "Esporta pagina",
|
||||
"Export space": "Esporta spazio",
|
||||
"Export {{type}}": "Esporta {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Il file supera il limite di allegati di {{limit}}",
|
||||
"Align left": "Allinea a sinistra",
|
||||
"Align right": "Allinea a destra",
|
||||
"Align center": "Allinea al centro",
|
||||
"Merge cells": "Unisci celle",
|
||||
"Split cell": "Dividi cella",
|
||||
"Delete column": "Elimina colonna",
|
||||
"Delete row": "Elimina riga",
|
||||
"Add left column": "Aggiungi colonna a sinistra",
|
||||
"Add right column": "Aggiungi colonna a destra",
|
||||
"Add row above": "Aggiungi riga sopra",
|
||||
"Add row below": "Aggiungi riga sotto",
|
||||
"Delete table": "Elimina tabella",
|
||||
"Info": "Informazioni",
|
||||
"Success": "Successo",
|
||||
"Warning": "Avviso",
|
||||
"Danger": "Pericolo",
|
||||
"Mermaid diagram error:": "Errore nel diagramma di Mermaid:",
|
||||
"Invalid Mermaid diagram": "Diagramma di Mermaid non valido",
|
||||
"Double-click to edit Draw.io diagram": "Doppio clic per modificare il diagramma Draw.io",
|
||||
"Exit": "Esci",
|
||||
"Save & Exit": "Salva ed esci",
|
||||
"Double-click to edit Excalidraw diagram": "Doppio clic per modificare il diagramma Excalidraw",
|
||||
"Paste link": "Incolla link",
|
||||
"Edit link": "Modifica link",
|
||||
"Remove link": "Rimuovi link",
|
||||
"Add link": "Aggiungi link",
|
||||
"Please enter a valid url": "Per favore inserisci un URL valido",
|
||||
"Empty equation": "Equazione vuota",
|
||||
"Invalid equation": "Equazione non valida",
|
||||
"Color": "Colore",
|
||||
"Text color": "Colore del testo",
|
||||
"Default": "Predefinito",
|
||||
"Blue": "Blu",
|
||||
"Green": "Verde",
|
||||
"Purple": "Viola",
|
||||
"Red": "Rosso",
|
||||
"Yellow": "Giallo",
|
||||
"Orange": "Arancione",
|
||||
"Pink": "Rosa",
|
||||
"Gray": "Grigio",
|
||||
"Embed link": "Incorpora collegamento",
|
||||
"Invalid {{provider}} embed link": "Link di incorporamento {{provider}} non valido",
|
||||
"Embed {{provider}}": "Incorpora {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Inserisci il link {{provider}} per incorporare",
|
||||
"Bold": "Grassetto",
|
||||
"Italic": "Corsivo",
|
||||
"Underline": "Sottolineato",
|
||||
"Strike": "Barrato",
|
||||
"Code": "Codice",
|
||||
"Comment": "Commento",
|
||||
"Text": "Testo",
|
||||
"Heading 1": "Intestazione 1",
|
||||
"Heading 2": "Intestazione 2",
|
||||
"Heading 3": "Intestazione 3",
|
||||
"To-do List": "Lista delle cose da fare",
|
||||
"Bullet List": "Elenco Puntato",
|
||||
"Numbered List": "Elenco Numerato",
|
||||
"Blockquote": "Blocco di citazione",
|
||||
"Just start typing with plain text.": "Inizia a digitare con testo semplice.",
|
||||
"Track tasks with a to-do list.": "Tieni traccia delle attività con una lista di cose da fare.",
|
||||
"Big section heading.": "Intestazione di una grande sezione.",
|
||||
"Medium section heading.": "Intestazione di sezione media.",
|
||||
"Small section heading.": "Piccolo titolo di sezione.",
|
||||
"Create a simple bullet list.": "Crea un semplice elenco puntato.",
|
||||
"Create a list with numbering.": "Crea un elenco numerato.",
|
||||
"Create block quote.": "Crea blocco citazione.",
|
||||
"Insert code snippet.": "Inserisci frammento di codice.",
|
||||
"Insert horizontal rule divider": "Inserisci divisore di regola orizzontale",
|
||||
"Upload any image from your device.": "Carica un'immagine dal tuo dispositivo.",
|
||||
"Upload any video from your device.": "Carica qualsiasi video dal tuo dispositivo.",
|
||||
"Upload any file from your device.": "Carica qualsiasi file dal tuo dispositivo.",
|
||||
"Table": "Tabella",
|
||||
"Insert a table.": "Inserisci una tabella.",
|
||||
"Insert collapsible block.": "Inserisci blocco comprimibile.",
|
||||
"Video": "Video",
|
||||
"Divider": "Divisore",
|
||||
"Quote": "Preventivo",
|
||||
"Image": "Immagine",
|
||||
"File attachment": "Allegato file",
|
||||
"Toggle block": "Attiva blocco",
|
||||
"Callout": "Avviso",
|
||||
"Insert callout notice.": "Inserisci avviso di richiamo.",
|
||||
"Math inline": "Matematica in linea",
|
||||
"Insert inline math equation.": "Inserisci equazione matematica in linea.",
|
||||
"Math block": "Blocco matematico",
|
||||
"Insert math equation": "Inserisci equazione matematica",
|
||||
"Mermaid diagram": "Diagramma di Mermaid",
|
||||
"Insert mermaid diagram": "Inserisci un diagramma di Mermaid",
|
||||
"Insert and design Drawio diagrams": "Inserisci e progetta diagrammi Drawio",
|
||||
"Insert current date": "Inserisci la data corrente",
|
||||
"Draw and sketch excalidraw diagrams": "Disegna e schizza diagrammi excalidraw",
|
||||
"Multiple": "Multiplo",
|
||||
"Heading {{level}}": "Intestazione {{level}}",
|
||||
"Toggle title": "Attiva/disattiva titolo",
|
||||
"Write anything. Enter \"/\" for commands": "Scrivi qualcosa. Digita \"/\" per i comandi",
|
||||
"Names do not match": "I nomi non corrispondono",
|
||||
"Today, {{time}}": "Oggi, {{time}}",
|
||||
"Yesterday, {{time}}": "Ieri, {{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/ja-JP/translation.json
Normal file
342
apps/client/public/locales/ja-JP/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "アカウント",
|
||||
"Active": "アクティブ",
|
||||
"Add": "追加",
|
||||
"Add group members": "グループメンバーを追加",
|
||||
"Add groups": "グループを追加",
|
||||
"Add members": "メンバーを追加",
|
||||
"Add to groups": "グループに追加",
|
||||
"Add space members": "スペースメンバーを追加",
|
||||
"Admin": "管理者",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "このグループを削除してもよろしいですか? メンバーはこのグループがアクセス権を持つリソースにアクセスできなくなります。",
|
||||
"Are you sure you want to delete this page?": "このページを削除してもよろしいですか?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "このユーザをグループから削除してもよろしいですか? ユーザはこのグループがアクセス権を持つリソースにアクセスできなくなります。",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "このユーザをスペースから削除してもよろしいですか? ユーザはこのスペースへのアクセス権をすべて失います。",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "このバージョンを復元してもよろしいですか? バージョン管理されていない変更は失われます。",
|
||||
"Can become members of groups and spaces in workspace": "ワークスペース内のグループやスペースのメンバーになることができます",
|
||||
"Can create and edit pages in space.": "スペース内のページを作成および編集できます。",
|
||||
"Can edit": "編集可能",
|
||||
"Can manage workspace": "ワークスペースを管理できます",
|
||||
"Can manage workspace but cannot delete it": "ワークスペースを管理できますが、削除はできません",
|
||||
"Can view": "閲覧可能",
|
||||
"Can view pages in space but not edit.": "スペース内のページを閲覧できますが、編集はできません。",
|
||||
"Cancel": "キャンセル",
|
||||
"Change email": "メールアドレスの変更",
|
||||
"Change password": "パスワードの変更",
|
||||
"Change photo": "画像の変更",
|
||||
"Choose a role": "ロールを選んでください",
|
||||
"Choose your preferred color scheme.": "お好みのカラースキームを選択してください。",
|
||||
"Choose your preferred interface language.": "お好みのインターフェース言語を選択してください。",
|
||||
"Choose your preferred page width.": "左右の余白を縮小する場合はオンにしてください。",
|
||||
"Confirm": "確認",
|
||||
"Copy link": "リンクをコピー",
|
||||
"Create": "新規作成",
|
||||
"Create group": "グループを作成",
|
||||
"Create page": "新規ページ",
|
||||
"Create space": "新規スペース",
|
||||
"Create workspace": "ワークスペースを作成",
|
||||
"Current password": "現在のパスワード",
|
||||
"Dark": "ダーク",
|
||||
"Date": "日付",
|
||||
"Delete": "削除",
|
||||
"Delete group": "グループを削除",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "このページを削除してもよろしいですか?この操作により、子ページおよびページ履歴が削除されます。この操作は元に戻せません。",
|
||||
"Description": "説明",
|
||||
"Details": "詳細",
|
||||
"e.g ACME": "例: 山田太郎",
|
||||
"e.g ACME Inc": "例: 株式会社サンプル",
|
||||
"e.g Developers": "例: エンジニア",
|
||||
"e.g Group for developers": "例: エンジニアグループ",
|
||||
"e.g product": "例: product",
|
||||
"e.g Product Team": "例: 製品チーム",
|
||||
"e.g Sales": "例: 営業",
|
||||
"e.g Space for product team": "例: 製品チームのスペース",
|
||||
"e.g Space for sales team to collaborate": "例: 営業チーム連携用スペース",
|
||||
"Edit": "編集",
|
||||
"Edit group": "グループを編集",
|
||||
"Email": "メールアドレス",
|
||||
"Enter a strong password": "強力なパスワードを入力してください",
|
||||
"Enter valid email addresses separated by comma or space max_50": "有効なメールアドレスをカンマまたはスペースで区切って入力してください(最大 50 個)",
|
||||
"enter valid emails addresses": "有効なメールアドレスを入力してください",
|
||||
"Enter your current password": "現在のパスワードを入力してください",
|
||||
"enter your full name": "氏名を入力してください",
|
||||
"Enter your new password": "新しいパスワードを入力してください",
|
||||
"Enter your new preferred email": "新しいメールアドレスを入力してください",
|
||||
"Enter your password": "パスワードを入力してください",
|
||||
"Error fetching page data.": "ページデータ取得中にエラーが発生しました。",
|
||||
"Error loading page history.": "ページ履歴の読み込み中にエラーが発生しました。",
|
||||
"Export": "エクスポート",
|
||||
"Failed to create page": "ページの作成に失敗しました",
|
||||
"Failed to delete page": "ページの削除に失敗しました",
|
||||
"Failed to fetch recent pages": "最近のページを取得できませんでした",
|
||||
"Failed to import pages": "ページのインポートに失敗しました",
|
||||
"Failed to load page. An error occurred.": "ページの読み込みに失敗しました。エラーが発生しました。",
|
||||
"Failed to update data": "データの更新に失敗しました",
|
||||
"Full access": "フルアクセス",
|
||||
"Full page width": "フルページ幅で表示",
|
||||
"Full width": "左右の余白を縮小",
|
||||
"General": "一般",
|
||||
"Group": "グループ",
|
||||
"Group description": "グループ説明",
|
||||
"Group name": "グループ名",
|
||||
"Groups": "グループ",
|
||||
"Has full access to space settings and pages.": "スペース設定とページにフルアクセスできます。",
|
||||
"Home": "ホーム",
|
||||
"Import pages": "ページをインポート",
|
||||
"Import pages & space settings": "ページとスペース設定をインポート",
|
||||
"Importing pages": "ページをインポートしています",
|
||||
"invalid invitation link": "招待リンクが間違っています",
|
||||
"Invitation signup": "招待登録",
|
||||
"Invite by email": "メールアドレスで招待する",
|
||||
"Invite members": "メンバーを招待する",
|
||||
"Invite new members": "新しいメンバーを招待する",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "招待をまだ承諾していないメンバーはここに表示されます。",
|
||||
"Invited members will be granted access to spaces the groups can access": "招待されたメンバーは、グループがアクセスできるスペースにアクセス権が付与されます",
|
||||
"Join the workspace": "ワークスペースに参加",
|
||||
"Language": "言語",
|
||||
"Light": "ライト",
|
||||
"Link copied": "リンクをコピーしました",
|
||||
"Login": "ログイン",
|
||||
"Logout": "ログアウト",
|
||||
"Manage Group": "グループを管理",
|
||||
"Manage members": "メンバーを管理",
|
||||
"member": "メンバー",
|
||||
"Member": "メンバー",
|
||||
"members": "メンバー",
|
||||
"Members": "メンバー",
|
||||
"My preferences": "個人設定",
|
||||
"My Profile": "プロフィール",
|
||||
"My profile": "プロフィール",
|
||||
"Name": "名前",
|
||||
"New email": "新しいメールアドレス",
|
||||
"New page": "新規ページ",
|
||||
"New password": "新しいパスワード",
|
||||
"No group found": "グループが見つかりません",
|
||||
"No page history saved yet.": "まだページの履歴が保存されていません。",
|
||||
"No pages yet": "ページがありません",
|
||||
"No results found...": "結果が見つかりませんでした...",
|
||||
"No user found": "ユーザがいません",
|
||||
"Overview": "概要",
|
||||
"Owner": "所有者",
|
||||
"page": "ページ",
|
||||
"Page deleted successfully": "ページが正常に削除されました",
|
||||
"Page history": "ページの履歴",
|
||||
"Page import is in progress. Please do not close this tab.": "ページのインポートが進行中です。このタブを閉じないでください。",
|
||||
"Pages": "ページ",
|
||||
"pages": "ページ",
|
||||
"Password": "パスワード",
|
||||
"Password changed successfully": "パスワードが正常に変更されました",
|
||||
"Pending": "保留中",
|
||||
"Please confirm your action": "アクションを確認してください",
|
||||
"Preferences": "設定",
|
||||
"Print PDF": "PDFを印刷",
|
||||
"Profile": "プロフィール",
|
||||
"Recently updated": "最近の更新",
|
||||
"Remove": "削除",
|
||||
"Remove group member": "グループメンバーを削除",
|
||||
"Remove space member": "スペースメンバーを削除",
|
||||
"Restore": "復元",
|
||||
"Role": "役割",
|
||||
"Save": "保存",
|
||||
"Search": "検索",
|
||||
"Search for groups": "グループを検索",
|
||||
"Search for users": "ユーザーを検索",
|
||||
"Search for users and groups": "ユーザーとグループを検索",
|
||||
"Search...": "検索する語句を入力",
|
||||
"Select language": "言語を選択",
|
||||
"Select role": "ロールを選択",
|
||||
"Select role to assign to all invited members": "招待されたすべてのメンバーに割り当てるロールを選択してください",
|
||||
"Select theme": "テーマを選択",
|
||||
"Send invitation": "招待を送る",
|
||||
"Settings": "設定",
|
||||
"Setup workspace": "ワークスペースを設定する",
|
||||
"Sign In": "サインイン",
|
||||
"Sign Up": "アカウント登録",
|
||||
"Slug": "Slug (URL用文字列)",
|
||||
"Space": "スペース",
|
||||
"Space description": "スペース説明",
|
||||
"Space menu": "スペースメニュー",
|
||||
"Space name": "スペース名",
|
||||
"Space settings": "スペース設定",
|
||||
"Space slug": "スペースのSlug (URL用文字列)",
|
||||
"Spaces": "スペース",
|
||||
"Spaces you belong to": "所属しているスペース",
|
||||
"No space found": "スペースが見つかりません",
|
||||
"Search for spaces": "スペースを検索",
|
||||
"Start typing to search...": "検索を開始するには入力してください...",
|
||||
"Status": "ステータス",
|
||||
"Successfully imported": "インポートに成功しました",
|
||||
"Successfully restored": "正常に復元されました",
|
||||
"System settings": "システム設定",
|
||||
"Theme": "テーマ",
|
||||
"To change your email, you have to enter your password and new email.": "メールアドレスを変更するには、パスワードと新しいメールアドレスを入力する必要があります。",
|
||||
"Toggle full page width": "ページ幅を切り替える",
|
||||
"Unable to import pages. Please try again.": "ページをインポートできません。もう一度お試しください。",
|
||||
"untitled": "無題",
|
||||
"Untitled": "無題",
|
||||
"Updated successfully": "正常に更新されました",
|
||||
"User": "ユーザー",
|
||||
"Workspace": "ワークスペース",
|
||||
"Workspace Name": "ワークスペース名",
|
||||
"Workspace settings": "ワークスペース設定",
|
||||
"You can change your password here.": "パスワードを変更できます。",
|
||||
"Your Email": "メールアドレス",
|
||||
"Your import is complete.": "インポートが完了しました。",
|
||||
"Your name": "名前",
|
||||
"Your Name": "名前",
|
||||
"Your password": "パスワード",
|
||||
"Your password must be a minimum of 8 characters.": "パスワードは最低 8 文字必要です。",
|
||||
"Sidebar toggle": "サイドバー切り替え",
|
||||
"Comments": "コメント",
|
||||
"404 page not found": "404 ページが見つかりません",
|
||||
"Sorry, we can't find the page you are looking for.": "お探しのページが見つかりません。",
|
||||
"Take me back to homepage": "ホームに戻る",
|
||||
"Forgot password": "パスワードを忘れた",
|
||||
"Forgot your password?": "パスワードを忘れましたか?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "パスワードリセットリンクがあなたのメールアドレスに送信されました。受信箱を確認してください。",
|
||||
"Send reset link": "リセットリンクを送る",
|
||||
"Password reset": "パスワードリセット",
|
||||
"Your new password": "新しいパスワード",
|
||||
"Set password": "パスワードを設定",
|
||||
"Write a comment": "コメントを書く",
|
||||
"Reply...": "返信...",
|
||||
"Error loading comments.": "コメントの読み込み中にエラーが発生しました。",
|
||||
"No comments yet.": "コメントがありません。",
|
||||
"Edit comment": "コメントを編集する",
|
||||
"Delete comment": "コメントを削除する",
|
||||
"Are you sure you want to delete this comment?": "このコメントを削除してもよろしいですか?",
|
||||
"Comment created successfully": "コメントが作成されました",
|
||||
"Error creating comment": "コメントの作成中にエラーが発生しました",
|
||||
"Comment updated successfully": "コメントが更新されました",
|
||||
"Failed to update comment": "コメントの更新に失敗しました",
|
||||
"Comment deleted successfully": "コメントが削除されました",
|
||||
"Failed to delete comment": "コメントの削除に失敗しました",
|
||||
"Comment resolved successfully": "コメントが解決されました",
|
||||
"Failed to resolve comment": "コメントの解決に失敗しました",
|
||||
"Revoke invitation": "招待を取り消す",
|
||||
"Revoke": "取り消す",
|
||||
"Don't": "取り消さない",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "この招待を取り消してもよろしいですか? ユーザはワークスペースに参加できなくなります。",
|
||||
"Resend invitation": "招待を再度送る",
|
||||
"Anyone with this link can join this workspace.": "このリンクを持っている人は誰でもこのワークスペースに参加できます。",
|
||||
"Invite link": "招待リンク",
|
||||
"Copy": "コピー",
|
||||
"Copied": "コピーしました",
|
||||
"Select a user": "ユーザを選択",
|
||||
"Select a group": "グループを選択",
|
||||
"Export all pages and attachments in this space.": "このスペースのすべてのページと添付ファイルをエクスポートします。",
|
||||
"Delete space": "スペースを削除",
|
||||
"Are you sure you want to delete this space?": "このスペースを削除してもよろしいですか?",
|
||||
"Delete this space with all its pages and data.": "このスペースおよびスペース内のすべてのページとデータを削除します。",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "このスペース内のすべてのページ、コメント、添付ファイル、および権限は完全に削除されます。",
|
||||
"Confirm space name": "スペース名を確認する",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "アクションを確認するためにスペース名 <b>{{spaceName}}</b> を入力してください。",
|
||||
"Format": "フォーマット",
|
||||
"Include subpages": "サブページを含める",
|
||||
"Include attachments": "添付ファイルを含める",
|
||||
"Select export format": "エクスポート形式を選択",
|
||||
"Export failed:": "エクスポートに失敗しました:",
|
||||
"export error": "エクスポートエラー",
|
||||
"Export page": "エクスポートページ",
|
||||
"Export space": "エクスポートスペース",
|
||||
"Export {{type}}": "{{type}}をエクスポート",
|
||||
"File exceeds the {{limit}} attachment limit": "ファイルが{{limit}}の添付制限を超えています",
|
||||
"Align left": "左揃え",
|
||||
"Align right": "右揃え",
|
||||
"Align center": "中央揃え",
|
||||
"Merge cells": "セルを結合",
|
||||
"Split cell": "セルを分割",
|
||||
"Delete column": "列を削除",
|
||||
"Delete row": "行を削除",
|
||||
"Add left column": "左側に列を追加",
|
||||
"Add right column": "右側の列を追加",
|
||||
"Add row above": "上に行を追加",
|
||||
"Add row below": "下に行を追加",
|
||||
"Delete table": "テーブルを削除",
|
||||
"Info": "情報",
|
||||
"Success": "成功",
|
||||
"Warning": "警告",
|
||||
"Danger": "危険",
|
||||
"Mermaid diagram error:": "Mermaid コードエラー",
|
||||
"Invalid Mermaid diagram": "無効な Mermaid コードです",
|
||||
"Double-click to edit Draw.io diagram": "ダブルクリックしてDraw.ioの図を編集",
|
||||
"Exit": "終了",
|
||||
"Save & Exit": "保存して終了",
|
||||
"Double-click to edit Excalidraw diagram": "ダブルクリックしてExcalidraw図を編集",
|
||||
"Paste link": "リンクを貼り付け",
|
||||
"Edit link": "リンクを編集",
|
||||
"Remove link": "リンクを削除",
|
||||
"Add link": "リンクを追加",
|
||||
"Please enter a valid url": "有効なURLを入力してください",
|
||||
"Empty equation": "空の数式です",
|
||||
"Invalid equation": "不正な数式です",
|
||||
"Color": "カラー",
|
||||
"Text color": "テキストカラー",
|
||||
"Default": "デフォルト",
|
||||
"Blue": "青色",
|
||||
"Green": "緑色",
|
||||
"Purple": "紫色",
|
||||
"Red": "赤色",
|
||||
"Yellow": "黄色",
|
||||
"Orange": "オレンジ色",
|
||||
"Pink": "ピンク色",
|
||||
"Gray": "灰色",
|
||||
"Embed link": "リンクを埋め込む",
|
||||
"Invalid {{provider}} embed link": "埋め込まれた {{provider}} のリンクは無効です",
|
||||
"Embed {{provider}}": "埋め込まれた {{provider}}",
|
||||
"Enter {{provider}} link to embed": "埋め込みたい {{provider}} のリンクを入力してください",
|
||||
"Bold": "太字",
|
||||
"Italic": "斜線",
|
||||
"Underline": "下線",
|
||||
"Strike": "打ち消し線",
|
||||
"Code": "コードブロック",
|
||||
"Comment": "コメント",
|
||||
"Text": "テキスト",
|
||||
"Heading 1": "見出し 1",
|
||||
"Heading 2": "見出し 2",
|
||||
"Heading 3": "見出し 3",
|
||||
"To-do List": "To-doリスト",
|
||||
"Bullet List": "箇条書きリスト",
|
||||
"Numbered List": "番号付きリスト",
|
||||
"Blockquote": "引用",
|
||||
"Just start typing with plain text.": "すぐに文章を書き始められます。",
|
||||
"Track tasks with a to-do list.": "Todoリストでタスクを追跡します。",
|
||||
"Big section heading.": "大きいフォントのセクション見出しです。",
|
||||
"Medium section heading.": "中くらいのフォントのセクション見出しです。",
|
||||
"Small section heading.": "小さいフォントのセクション見出しです。",
|
||||
"Create a simple bullet list.": "シンプルな箇条書きのリストを作成します。",
|
||||
"Create a list with numbering.": "番号付きのリストを作成します。",
|
||||
"Create block quote.": "引用文を作成します。",
|
||||
"Insert code snippet.": "コードスニペットを入力します。",
|
||||
"Insert horizontal rule divider": "水平線を挿入します。",
|
||||
"Upload any image from your device.": "画像をアップロードします。",
|
||||
"Upload any video from your device.": "動画をアップロードします。",
|
||||
"Upload any file from your device.": "ファイルをアップロードします。",
|
||||
"Table": "テーブル",
|
||||
"Insert a table.": "表を挿入します。",
|
||||
"Insert collapsible block.": "折りたたみ可能なブロックを挿入します。",
|
||||
"Video": "動画",
|
||||
"Divider": "区切り線",
|
||||
"Quote": "引用",
|
||||
"Image": "画像",
|
||||
"File attachment": "ファイル添付",
|
||||
"Toggle block": "ブロックを切り替える",
|
||||
"Callout": "コールアウト",
|
||||
"Insert callout notice.": "コールアウトブロックを挿入します。",
|
||||
"Math inline": "インライン数式",
|
||||
"Insert inline math equation.": "インライン数式を挿入します。",
|
||||
"Math block": "数式ブロック",
|
||||
"Insert math equation": "数式を挿入します",
|
||||
"Mermaid diagram": "Mermaidコード",
|
||||
"Insert mermaid diagram": "Mermaidコードを記述して図を挿入します",
|
||||
"Insert and design Drawio diagrams": "Drawioの図を挿入してデザインします",
|
||||
"Insert current date": "今日の日付を挿入します",
|
||||
"Draw and sketch excalidraw diagrams": "Excalidrawの図を埋め込みます",
|
||||
"Multiple": "複数",
|
||||
"Heading {{level}}": "見出し {{level}}",
|
||||
"Toggle title": "タイトルの表示/非表示を切り替える",
|
||||
"Write anything. Enter \"/\" for commands": "文字を入力するか、「/」でコマンドを呼び出します",
|
||||
"Names do not match": "名前が一致しません",
|
||||
"Today, {{time}}": "今日、{{time}}",
|
||||
"Yesterday, {{time}}": "昨日、{{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/ko-KR/translation.json
Normal file
342
apps/client/public/locales/ko-KR/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "계정",
|
||||
"Active": "활성",
|
||||
"Add": "추가",
|
||||
"Add group members": "팀에 사용자 추가",
|
||||
"Add groups": "팀 생성",
|
||||
"Add members": "사용자 추가",
|
||||
"Add to groups": "팀에 추가",
|
||||
"Add space members": "Space에 사용자 추가",
|
||||
"Admin": "관리자",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "이 팀을 삭제하시겠습니까? 해당 팀에 속한 사용자들은 이 팀이 가진 모든 권한을 잃게 됩니다.",
|
||||
"Are you sure you want to delete this page?": "이 페이지를 삭제하시겠습니까?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "이 사용자를 팀에서 제거하시겠습니까? 사용자는 이 팀이 가진 모든 권한을 잃게 됩니다.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "이 사용자를 Space에서 제거하시겠습니까? 사용자는 이 Space에 대한 모든 접근 권한을 잃게 됩니다.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "이 버전으로 복원하시겠습니까? 저장되지 않은 모든 변경사항이 손실됩니다.",
|
||||
"Can become members of groups and spaces in workspace": "Workspace 내 팀 및 Space의 사용자가 될 수 있습니다.",
|
||||
"Can create and edit pages in space.": "Space에 페이지를 생성하고 편집할 수 있습니다.",
|
||||
"Can edit": "편집할 수 있음",
|
||||
"Can manage workspace": "Workspace를 관리할 수 있음",
|
||||
"Can manage workspace but cannot delete it": "Workspace를 관리할 수 있지만, 삭제는 불가능.",
|
||||
"Can view": "볼 수 있음",
|
||||
"Can view pages in space but not edit.": "Space의 페이지를 볼 수 있지만, 편집은 불가능.",
|
||||
"Cancel": "취소",
|
||||
"Change email": "이메일 변경",
|
||||
"Change password": "비밀번호 변경",
|
||||
"Change photo": "사진 변경",
|
||||
"Choose a role": "역할 선택",
|
||||
"Choose your preferred color scheme.": "선호하는 배경 색을 선택하세요.",
|
||||
"Choose your preferred interface language.": "선호하는 인터페이스 언어를 선택하세요.",
|
||||
"Choose your preferred page width.": "선호하는 페이지 너비를 선택하세요.",
|
||||
"Confirm": "확인",
|
||||
"Copy link": "링크 복사",
|
||||
"Create": "생성",
|
||||
"Create group": "팀 생성",
|
||||
"Create page": "페이지 생성",
|
||||
"Create space": "Space 생성",
|
||||
"Create workspace": "Workspace 생성",
|
||||
"Current password": "현재 비밀번호",
|
||||
"Dark": "어두운",
|
||||
"Date": "날짜",
|
||||
"Delete": "삭제",
|
||||
"Delete group": "팀 삭제",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "이 페이지를 삭제하시겠습니까? 하위 페이지와 페이지 기록이 모두 삭제됩니다. 이 작업은 되돌릴 수 없습니다.",
|
||||
"Description": "설명",
|
||||
"Details": "세부사항",
|
||||
"e.g ACME": "예: ACME",
|
||||
"e.g ACME Inc": "예: ACME Inc",
|
||||
"e.g Developers": "예: 개발자",
|
||||
"e.g Group for developers": "예: 개발자를 위한 팀",
|
||||
"e.g product": "예: 제품",
|
||||
"e.g Product Team": "예: 제품 팀",
|
||||
"e.g Sales": "예: 영업",
|
||||
"e.g Space for product team": "예: 제품 팀을 위한 Space",
|
||||
"e.g Space for sales team to collaborate": "예: 영업 팀의 Space",
|
||||
"Edit": "편집",
|
||||
"Edit group": "팀 편집",
|
||||
"Email": "이메일",
|
||||
"Enter a strong password": "강력한 비밀번호를 입력하세요",
|
||||
"Enter valid email addresses separated by comma or space max_50": "유효한 이메일 주소를 쉼표나 공백으로 구분하여 입력하세요 [최대: 50]",
|
||||
"enter valid emails addresses": "유효한 이메일 주소를 입력하세요",
|
||||
"Enter your current password": "현재 비밀번호를 입력하세요",
|
||||
"enter your full name": "전체 이름을 입력하세요",
|
||||
"Enter your new password": "새 비밀번호를 입력하세요",
|
||||
"Enter your new preferred email": "새로운 이메일을 입력하세요",
|
||||
"Enter your password": "비밀번호를 입력하세요",
|
||||
"Error fetching page data.": "페이지 데이터 불러오기 오류.",
|
||||
"Error loading page history.": "페이지 기록 불러오기 오류.",
|
||||
"Export": "내보내기",
|
||||
"Failed to create page": "페이지 생성 실패",
|
||||
"Failed to delete page": "페이지 삭제 실패",
|
||||
"Failed to fetch recent pages": "최근 페이지 불러오기 실패",
|
||||
"Failed to import pages": "페이지 가져오기 실패",
|
||||
"Failed to load page. An error occurred.": "페이지 불러오기 실패. 오류가 발생했습니다.",
|
||||
"Failed to update data": "데이터 갱신 실패",
|
||||
"Full access": "전체 권한",
|
||||
"Full page width": "전체 페이지 너비",
|
||||
"Full width": "전체 너비",
|
||||
"General": "일반",
|
||||
"Group": "팀",
|
||||
"Group description": "팀 설명",
|
||||
"Group name": "팀 이름",
|
||||
"Groups": "팀",
|
||||
"Has full access to space settings and pages.": "Space 설정과 페이지에 대한 전체 접근 권한이 있습니다.",
|
||||
"Home": "홈",
|
||||
"Import pages": "페이지 가져오기",
|
||||
"Import pages & space settings": "페이지 및 Space 설정 가져오기",
|
||||
"Importing pages": "페이지 가져오기 중",
|
||||
"invalid invitation link": "유효하지 않은 초대 링크",
|
||||
"Invitation signup": "초대 가입",
|
||||
"Invite by email": "이메일로 초대",
|
||||
"Invite members": "사용자 초대",
|
||||
"Invite new members": "새 사용자 초대",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "초대를 아직 수락하지 않은 초대된 사용자가 여기에 표시됩니다.",
|
||||
"Invited members will be granted access to spaces the groups can access": "초대된 사용자는 팀이 접근할 수 있는 Space에 대한 접근 권한을 받게 됩니다",
|
||||
"Join the workspace": "Workspace 참여",
|
||||
"Language": "언어",
|
||||
"Light": "밝은",
|
||||
"Link copied": "링크 복사됨",
|
||||
"Login": "로그인",
|
||||
"Logout": "로그아웃",
|
||||
"Manage Group": "팀 관리",
|
||||
"Manage members": "사용자 관리",
|
||||
"member": "사용자",
|
||||
"Member": "사용자",
|
||||
"members": "사용자들",
|
||||
"Members": "사용자들",
|
||||
"My preferences": "내 설정",
|
||||
"My Profile": "내 프로필",
|
||||
"My profile": "내 프로필",
|
||||
"Name": "이름",
|
||||
"New email": "새 이메일",
|
||||
"New page": "새 페이지",
|
||||
"New password": "새 비밀번호",
|
||||
"No group found": "팀을 찾을 수 없음",
|
||||
"No page history saved yet.": "아직 저장된 페이지 기록이 없습니다.",
|
||||
"No pages yet": "아직 페이지가 없습니다",
|
||||
"No results found...": "결과를 찾을 수 없습니다...",
|
||||
"No user found": "사용자를 찾을 수 없음",
|
||||
"Overview": "개요",
|
||||
"Owner": "소유자",
|
||||
"page": "페이지",
|
||||
"Page deleted successfully": "페이지 삭제 완료",
|
||||
"Page history": "페이지 기록",
|
||||
"Page import is in progress. Please do not close this tab.": "페이지 가져오기가 진행 중입니다. 이 탭을 닫지 마세요.",
|
||||
"Pages": "페이지",
|
||||
"pages": "페이지",
|
||||
"Password": "비밀번호",
|
||||
"Password changed successfully": "비밀번호 변경 완료",
|
||||
"Pending": "대기 중",
|
||||
"Please confirm your action": "작업을 확인해 주세요",
|
||||
"Preferences": "설정",
|
||||
"Print PDF": "PDF로 인쇄",
|
||||
"Profile": "프로필",
|
||||
"Recently updated": "최근 업데이트",
|
||||
"Remove": "제거",
|
||||
"Remove group member": "팀에서 사용자 제거",
|
||||
"Remove space member": "Space에서 사용자 제거",
|
||||
"Restore": "복원",
|
||||
"Role": "역할",
|
||||
"Save": "저장",
|
||||
"Search": "검색",
|
||||
"Search for groups": "팀 검색",
|
||||
"Search for users": "사용자 검색",
|
||||
"Search for users and groups": "사용자 및 팀 검색",
|
||||
"Search...": "검색...",
|
||||
"Select language": "언어 선택",
|
||||
"Select role": "역할 선택",
|
||||
"Select role to assign to all invited members": "초대된 모든 사용자에게 할당할 역할 선택",
|
||||
"Select theme": "배경 선택",
|
||||
"Send invitation": "초대 보내기",
|
||||
"Settings": "설정",
|
||||
"Setup workspace": "Workspace 설정",
|
||||
"Sign In": "로그인",
|
||||
"Sign Up": "회원 가입",
|
||||
"Slug": "고유 경로",
|
||||
"Space": "Space",
|
||||
"Space description": "Space 설명",
|
||||
"Space menu": "Space 메뉴",
|
||||
"Space name": "Space 이름",
|
||||
"Space settings": "Space 설정",
|
||||
"Space slug": "Space의 고유 경로",
|
||||
"Spaces": "Space",
|
||||
"Spaces you belong to": "소속된 Space",
|
||||
"No space found": "Space을 찾을 수 없음",
|
||||
"Search for spaces": "Space 검색",
|
||||
"Start typing to search...": "검색하려면 입력을 시작하세요...",
|
||||
"Status": "상태",
|
||||
"Successfully imported": "가져오기 완료",
|
||||
"Successfully restored": "복원 완료",
|
||||
"System settings": "시스템 설정",
|
||||
"Theme": "배경",
|
||||
"To change your email, you have to enter your password and new email.": "이메일을 변경하려면 현재 비밀번호와 새 이메일을 입력해야 합니다.",
|
||||
"Toggle full page width": "전체 페이지 너비 전환",
|
||||
"Unable to import pages. Please try again.": "페이지를 가져올 수 없습니다. 다시 시도해주세요.",
|
||||
"untitled": "제목 없음",
|
||||
"Untitled": "제목 없음",
|
||||
"Updated successfully": "업데이트 완료",
|
||||
"User": "사용자",
|
||||
"Workspace": "Workspace",
|
||||
"Workspace Name": "Workspce 이름",
|
||||
"Workspace settings": "Workspace 설정",
|
||||
"You can change your password here.": "여기서 비밀번호를 변경할 수 있습니다.",
|
||||
"Your Email": "이메일",
|
||||
"Your import is complete.": "가져오기가 완료되었습니다.",
|
||||
"Your name": "이름",
|
||||
"Your Name": "이름",
|
||||
"Your password": "비밀번호",
|
||||
"Your password must be a minimum of 8 characters.": "비밀번호는 최소 8자 이상이어야 합니다.",
|
||||
"Sidebar toggle": "사이드바 전환",
|
||||
"Comments": "댓글",
|
||||
"404 page not found": "404 페이지를 찾을 수 없음",
|
||||
"Sorry, we can't find the page you are looking for.": "죄송합니다. 페이지를 찾을 수 없습니다.",
|
||||
"Take me back to homepage": "홈페이지로 돌아가기",
|
||||
"Forgot password": "비밀번호 찾기",
|
||||
"Forgot your password?": "비밀번호를 잊으셨나요?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "비밀번호 재설정 링크가 이메일로 전송되었습니다. 받은 편지함을 확인해주세요.",
|
||||
"Send reset link": "재설정 링크 보내기",
|
||||
"Password reset": "비밀번호 재설정",
|
||||
"Your new password": "새 비밀번호",
|
||||
"Set password": "비밀번호 설정",
|
||||
"Write a comment": "댓글 작성",
|
||||
"Reply...": "답글...",
|
||||
"Error loading comments.": "댓글 불러오기 오류.",
|
||||
"No comments yet.": "아직 댓글이 없습니다.",
|
||||
"Edit comment": "댓글 수정",
|
||||
"Delete comment": "댓글 삭제",
|
||||
"Are you sure you want to delete this comment?": "이 댓글을 삭제하시겠습니까?",
|
||||
"Comment created successfully": "댓글 생성 완료",
|
||||
"Error creating comment": "댓글 생성 오류",
|
||||
"Comment updated successfully": "댓글 업데이트 완료",
|
||||
"Failed to update comment": "댓글 업데이트 실패",
|
||||
"Comment deleted successfully": "댓글 삭제 완료",
|
||||
"Failed to delete comment": "댓글 삭제 실패",
|
||||
"Comment resolved successfully": "댓글 처리 완료",
|
||||
"Failed to resolve comment": "댓글 처리 실패",
|
||||
"Revoke invitation": "초대 취소",
|
||||
"Revoke": "취소",
|
||||
"Don't": "하지 않음",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "이 초대를 취소하시겠습니까? 사용자가 Workspace에 참여할 수 없게 됩니다.",
|
||||
"Resend invitation": "초대 재전송",
|
||||
"Anyone with this link can join this workspace.": "이 링크를 가진 모든 사람이 Workspace에 참여할 수 있습니다.",
|
||||
"Invite link": "초대 링크",
|
||||
"Copy": "복사",
|
||||
"Copied": "복사됨",
|
||||
"Select a user": "사용자 선택",
|
||||
"Select a group": "팀 선택",
|
||||
"Export all pages and attachments in this space.": "이 Space의 모든 페이지와 첨부파일을 내보냅니다.",
|
||||
"Delete space": "Space 삭제",
|
||||
"Are you sure you want to delete this space?": "이 Space을 삭제하시겠습니까?",
|
||||
"Delete this space with all its pages and data.": "이 Space의 모든 페이지와 데이터를 삭제합니다.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "이 Space의 모든 페이지, 댓글, 첨부파일 및 권한이 영구적으로 삭제됩니다.",
|
||||
"Confirm space name": "Space 이름 확인",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "작업을 진행하려면 Space 이름 <b>{{spaceName}}</b>을 입력하세요.",
|
||||
"Format": "형식",
|
||||
"Include subpages": "하위 페이지 포함",
|
||||
"Include attachments": "첨부파일 포함",
|
||||
"Select export format": "내보내기 형식 선택",
|
||||
"Export failed:": "내보내기 실패:",
|
||||
"export error": "내보내기 오류",
|
||||
"Export page": "페이지 내보내기",
|
||||
"Export space": "Space 내보내기",
|
||||
"Export {{type}}": "{{type}} 내보내기",
|
||||
"File exceeds the {{limit}} attachment limit": "첨부 파일 크기 제한 {{limit}}을 초과했습니다",
|
||||
"Align left": "왼쪽 정렬",
|
||||
"Align right": "오른쪽 정렬",
|
||||
"Align center": "가운데 정렬",
|
||||
"Merge cells": "셀 병합",
|
||||
"Split cell": "셀 분할",
|
||||
"Delete column": "열 삭제",
|
||||
"Delete row": "행 삭제",
|
||||
"Add left column": "왼쪽 열 추가",
|
||||
"Add right column": "오른쪽 열 추가",
|
||||
"Add row above": "위에 행 추가",
|
||||
"Add row below": "아래에 행 추가",
|
||||
"Delete table": "테이블 삭제",
|
||||
"Info": "정보",
|
||||
"Success": "완료",
|
||||
"Warning": "경고",
|
||||
"Danger": "위험",
|
||||
"Mermaid diagram error:": "Mermaid diagram 오류:",
|
||||
"Invalid Mermaid diagram": "잘못된 Mermaid diagram",
|
||||
"Double-click to edit Draw.io diagram": "Draw.io diagram을 편집하려면 더블 클릭하세요",
|
||||
"Exit": "나가기",
|
||||
"Save & Exit": "저장 후 나가기",
|
||||
"Double-click to edit Excalidraw diagram": "Excalidraw diagram을 편집하려면 더블 클릭하세요",
|
||||
"Paste link": "링크 붙여넣기",
|
||||
"Edit link": "링크 편집",
|
||||
"Remove link": "링크 제거",
|
||||
"Add link": "링크 추가",
|
||||
"Please enter a valid url": "유효한 URL을 입력하세요",
|
||||
"Empty equation": "빈 수식",
|
||||
"Invalid equation": "잘못된 수식",
|
||||
"Color": "색상",
|
||||
"Text color": "텍스트 색상",
|
||||
"Default": "기본값",
|
||||
"Blue": "파란색",
|
||||
"Green": "초록색",
|
||||
"Purple": "보라색",
|
||||
"Red": "빨간색",
|
||||
"Yellow": "노란색",
|
||||
"Orange": "주황색",
|
||||
"Pink": "분홍색",
|
||||
"Gray": "회색",
|
||||
"Embed link": "임베드 링크",
|
||||
"Invalid {{provider}} embed link": "잘못된 {{provider}} 임베드 링크",
|
||||
"Embed {{provider}}": "{{provider}} 임베드",
|
||||
"Enter {{provider}} link to embed": "임베드를 할 {{provider}} 링크 입력",
|
||||
"Bold": "굵게",
|
||||
"Italic": "기울임",
|
||||
"Underline": "밑줄",
|
||||
"Strike": "취소선",
|
||||
"Code": "코드",
|
||||
"Comment": "댓글",
|
||||
"Text": "텍스트",
|
||||
"Heading 1": "제목 1",
|
||||
"Heading 2": "제목 2",
|
||||
"Heading 3": "제목 3",
|
||||
"To-do List": "할 일 목록",
|
||||
"Bullet List": "글머리 기호 목록",
|
||||
"Numbered List": "번호 매기기 목록",
|
||||
"Blockquote": "인용구",
|
||||
"Just start typing with plain text.": "일반 텍스트로 입력을 시작하세요.",
|
||||
"Track tasks with a to-do list.": "할 일 목록으로 작업을 추적하세요.",
|
||||
"Big section heading.": "대제목.",
|
||||
"Medium section heading.": "중제목.",
|
||||
"Small section heading.": "소제목.",
|
||||
"Create a simple bullet list.": "글머리 기호 만들기.",
|
||||
"Create a list with numbering.": "숫자 목록 만들기.",
|
||||
"Create block quote.": "인용구 만들기.",
|
||||
"Insert code snippet.": "코드 블록 삽입.",
|
||||
"Insert horizontal rule divider": "가로 구분선 삽입",
|
||||
"Upload any image from your device.": "기기에서 이미지를 업로드하세요.",
|
||||
"Upload any video from your device.": "기기에서 비디오를 업로드하세요.",
|
||||
"Upload any file from your device.": "기기에서 파일을 업로드하세요.",
|
||||
"Table": "테이블",
|
||||
"Insert a table.": "테이블 삽입.",
|
||||
"Insert collapsible block.": "접을 수 있는 블록 삽입.",
|
||||
"Video": "비디오",
|
||||
"Divider": "구분선",
|
||||
"Quote": "인용",
|
||||
"Image": "이미지",
|
||||
"File attachment": "파일 첨부",
|
||||
"Toggle block": "블록 토글",
|
||||
"Callout": "경고 상자",
|
||||
"Insert callout notice.": "돋보이는 글을 작성하기.",
|
||||
"Math inline": "수식",
|
||||
"Insert inline math equation.": "수식 삽입.",
|
||||
"Math block": "수식 블록",
|
||||
"Insert math equation": "수식 삽입",
|
||||
"Mermaid diagram": "Mermaid diagram",
|
||||
"Insert mermaid diagram": "Mermaid diagram 삽입",
|
||||
"Insert and design Drawio diagrams": "Drawio diagram 삽입 및 디자인",
|
||||
"Insert current date": "현재 날짜 삽입",
|
||||
"Draw and sketch excalidraw diagrams": "Excalidraw diagram 그리기 및 스케치",
|
||||
"Multiple": "복제",
|
||||
"Heading {{level}}": "제목 {{level}}",
|
||||
"Toggle title": "제목 토글",
|
||||
"Write anything. Enter \"/\" for commands": "아무거나 입력하세요. 명령어를 사용하려면 \"/\"를 입력하세요",
|
||||
"Names do not match": "이름이 일치하지 않습니다",
|
||||
"Today, {{time}}": "오늘, {{time}}",
|
||||
"Yesterday, {{time}}": "어제, {{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/pt-BR/translation.json
Normal file
342
apps/client/public/locales/pt-BR/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "Conta",
|
||||
"Active": "Ativo",
|
||||
"Add": "Adicionar",
|
||||
"Add group members": "Adicionar membros ao grupo",
|
||||
"Add groups": "Adicionar grupos",
|
||||
"Add members": "Adicionar membros",
|
||||
"Add to groups": "Adicionar aos grupos",
|
||||
"Add space members": "Adicionar membros do espaço",
|
||||
"Admin": "Administrador",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Tem certeza de que deseja excluir este grupo? Os membros perderão acesso aos recursos que este grupo possui.",
|
||||
"Are you sure you want to delete this page?": "Tem certeza de que deseja excluir esta página?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Tem certeza de que deseja remover este usuário do grupo? O usuário perderá acesso aos recursos que este grupo possui.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Tem certeza de que deseja remover este usuário do espaço? O usuário perderá todo acesso a este espaço.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Tem certeza de que deseja restaurar esta versão? Quaisquer alterações não versionadas serão perdidas.",
|
||||
"Can become members of groups and spaces in workspace": "Pode se tornar membro de grupos e espaços no workspace",
|
||||
"Can create and edit pages in space.": "Pode criar e editar páginas no espaço.",
|
||||
"Can edit": "Pode editar",
|
||||
"Can manage workspace": "Pode gerenciar o workspace",
|
||||
"Can manage workspace but cannot delete it": "Pode gerenciar o workspace, mas não pode excluí-lo",
|
||||
"Can view": "Pode visualizar",
|
||||
"Can view pages in space but not edit.": "Pode visualizar páginas no espaço, mas não editar.",
|
||||
"Cancel": "Cancelar",
|
||||
"Change email": "Alterar email",
|
||||
"Change password": "Alterar senha",
|
||||
"Change photo": "Alterar foto",
|
||||
"Choose a role": "Escolha um papel",
|
||||
"Choose your preferred color scheme.": "Escolha seu esquema de cores preferido.",
|
||||
"Choose your preferred interface language.": "Escolha o idioma da interface.",
|
||||
"Choose your preferred page width.": "Escolha a largura preferida da página.",
|
||||
"Confirm": "Confirmar",
|
||||
"Copy link": "Copiar link",
|
||||
"Create": "Criar",
|
||||
"Create group": "Criar grupo",
|
||||
"Create page": "Criar página",
|
||||
"Create space": "Criar espaço",
|
||||
"Create workspace": "Criar workspace",
|
||||
"Current password": "Senha atual",
|
||||
"Dark": "Escuro",
|
||||
"Date": "Data",
|
||||
"Delete": "Excluir",
|
||||
"Delete group": "Excluir grupo",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Você tem certeza que quer deletar essa página? Isso irá deletar todas as páginas filhas e to o histórico. Esta ação é irreversível.",
|
||||
"Description": "Descrição",
|
||||
"Details": "Detalhes",
|
||||
"e.g ACME": "ex.: ACME",
|
||||
"e.g ACME Inc": "ex.: ACME Inc",
|
||||
"e.g Developers": "ex.: Desenvolvedores",
|
||||
"e.g Group for developers": "ex.: Grupo para desenvolvedores",
|
||||
"e.g product": "ex.: produto",
|
||||
"e.g Product Team": "ex.: Equipe de Produto",
|
||||
"e.g Sales": "ex.: Vendas",
|
||||
"e.g Space for product team": "ex.: Espaço para a equipe de produto",
|
||||
"e.g Space for sales team to collaborate": "ex.: Espaço para a equipe de vendas colaborar",
|
||||
"Edit": "Editar",
|
||||
"Edit group": "Editar grupo",
|
||||
"Email": "Email",
|
||||
"Enter a strong password": "Insira uma senha forte",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Insira endereços de email válidos separados por vírgula ou espaço [máx: 50]",
|
||||
"enter valid emails addresses": "insira endereços de email válidos",
|
||||
"Enter your current password": "Insira sua senha atual",
|
||||
"enter your full name": "insira seu nome completo",
|
||||
"Enter your new password": "Insira sua nova senha",
|
||||
"Enter your new preferred email": "Insira seu novo email preferido",
|
||||
"Enter your password": "Insira sua senha",
|
||||
"Error fetching page data.": "Erro ao buscar dados da página.",
|
||||
"Error loading page history.": "Erro ao carregar o histórico da página.",
|
||||
"Export": "Exportar",
|
||||
"Failed to create page": "Falha ao criar página",
|
||||
"Failed to delete page": "Falha ao excluir página",
|
||||
"Failed to fetch recent pages": "Falha ao buscar páginas recentes",
|
||||
"Failed to import pages": "Falha ao importar páginas",
|
||||
"Failed to load page. An error occurred.": "Falha ao carregar página. Ocorreu um erro.",
|
||||
"Failed to update data": "Falha ao atualizar dados",
|
||||
"Full access": "Acesso total",
|
||||
"Full page width": "Usar largura total da página",
|
||||
"Full width": "Largura total",
|
||||
"General": "Geral",
|
||||
"Group": "Grupo",
|
||||
"Group description": "Descrição do grupo",
|
||||
"Group name": "Nome do grupo",
|
||||
"Groups": "Grupos",
|
||||
"Has full access to space settings and pages.": "Tem acesso total às configurações do espaço e às páginas.",
|
||||
"Home": "Início",
|
||||
"Import pages": "Importar páginas",
|
||||
"Import pages & space settings": "Importar páginas e configurações de espaço",
|
||||
"Importing pages": "Importando páginas",
|
||||
"invalid invitation link": "link de convite inválido",
|
||||
"Invitation signup": "Cadastro por convite",
|
||||
"Invite by email": "Convidar por email",
|
||||
"Invite members": "Convidar membros",
|
||||
"Invite new members": "Convidar novos membros",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Membros convidados que ainda não aceitaram o convite aparecerão aqui.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Os membros convidados terão acesso aos espaços que os grupos podem acessar",
|
||||
"Join the workspace": "Entrar no workspace",
|
||||
"Language": "Idioma",
|
||||
"Light": "Claro",
|
||||
"Link copied": "Link copiado",
|
||||
"Login": "Entrar",
|
||||
"Logout": "Sair",
|
||||
"Manage Group": "Gerenciar Grupo",
|
||||
"Manage members": "Gerenciar membros",
|
||||
"member": "membro",
|
||||
"Member": "Membro",
|
||||
"members": "membros",
|
||||
"Members": "Membros",
|
||||
"My preferences": "Minhas preferências",
|
||||
"My Profile": "Meu Perfil",
|
||||
"My profile": "Meu perfil",
|
||||
"Name": "Nome",
|
||||
"New email": "Novo email",
|
||||
"New page": "Nova página",
|
||||
"New password": "Nova senha",
|
||||
"No group found": "Nenhum grupo encontrado",
|
||||
"No page history saved yet.": "Nenhum histórico de página salvo ainda.",
|
||||
"No pages yet": "Nenhuma página ainda",
|
||||
"No results found...": "Nenhum resultado encontrado...",
|
||||
"No user found": "Nenhum usuário encontrado",
|
||||
"Overview": "Visão geral",
|
||||
"Owner": "Proprietário",
|
||||
"page": "página",
|
||||
"Page deleted successfully": "Página excluída com sucesso",
|
||||
"Page history": "Histórico da página",
|
||||
"Page import is in progress. Please do not close this tab.": "A importação da página está em andamento. Por favor, não feche esta aba.",
|
||||
"Pages": "Páginas",
|
||||
"pages": "páginas",
|
||||
"Password": "Senha",
|
||||
"Password changed successfully": "Senha alterada com sucesso",
|
||||
"Pending": "Pendente",
|
||||
"Please confirm your action": "Por favor, confirme sua ação",
|
||||
"Preferences": "Preferências",
|
||||
"Print PDF": "Imprimir PDF",
|
||||
"Profile": "Perfil",
|
||||
"Recently updated": "Atualizado recentemente",
|
||||
"Remove": "Remover",
|
||||
"Remove group member": "Remover membro do grupo",
|
||||
"Remove space member": "Remover membro do espaço",
|
||||
"Restore": "Restaurar",
|
||||
"Role": "Função",
|
||||
"Save": "Salvar",
|
||||
"Search": "Buscar",
|
||||
"Search for groups": "Buscar grupos",
|
||||
"Search for users": "Buscar usuários",
|
||||
"Search for users and groups": "Buscar usuários e grupos",
|
||||
"Search...": "Buscar...",
|
||||
"Select language": "Selecionar idioma",
|
||||
"Select role": "Selecionar função",
|
||||
"Select role to assign to all invited members": "Selecione a função para atribuir a todos os membros convidados",
|
||||
"Select theme": "Selecionar tema",
|
||||
"Send invitation": "Enviar convite",
|
||||
"Settings": "Configurações",
|
||||
"Setup workspace": "Configurar workspace",
|
||||
"Sign In": "Entrar",
|
||||
"Sign Up": "Registrar-se",
|
||||
"Slug": "Slug",
|
||||
"Space": "Espaço",
|
||||
"Space description": "Descrição do espaço",
|
||||
"Space menu": "Menu do espaço",
|
||||
"Space name": "Nome do espaço",
|
||||
"Space settings": "Configurações do espaço",
|
||||
"Space slug": "Slug do espaço",
|
||||
"Spaces": "Espaços",
|
||||
"Spaces you belong to": "Espaços aos quais você pertence",
|
||||
"No space found": "Nenhum espaço encontrado",
|
||||
"Search for spaces": "Pesquisar espaços",
|
||||
"Start typing to search...": "Comece a digitar para buscar...",
|
||||
"Status": "Estado",
|
||||
"Successfully imported": "Importado com sucesso",
|
||||
"Successfully restored": "Restaurado com sucesso",
|
||||
"System settings": "Configurações do sistema",
|
||||
"Theme": "Tema",
|
||||
"To change your email, you have to enter your password and new email.": "Para alterar seu email, você precisa inserir sua senha e o novo email.",
|
||||
"Toggle full page width": "Alternar para largura total da página",
|
||||
"Unable to import pages. Please try again.": "Não foi possível importar as páginas. Por favor, tente novamente.",
|
||||
"untitled": "sem título",
|
||||
"Untitled": "Sem título",
|
||||
"Updated successfully": "Atualizado com sucesso",
|
||||
"User": "Usuário",
|
||||
"Workspace": "Espaço de Trabalho",
|
||||
"Workspace Name": "Nome do Workspace",
|
||||
"Workspace settings": "Configurações do workspace",
|
||||
"You can change your password here.": "Você pode alterar sua senha aqui.",
|
||||
"Your Email": "Seu email",
|
||||
"Your import is complete.": "Sua importação está concluída.",
|
||||
"Your name": "Seu nome",
|
||||
"Your Name": "Seu Nome",
|
||||
"Your password": "Sua senha",
|
||||
"Your password must be a minimum of 8 characters.": "Sua senha deve ter no mínimo 8 caracteres.",
|
||||
"Sidebar toggle": "Interruptor do painel lateral",
|
||||
"Comments": "Comentários",
|
||||
"404 page not found": "Erro 404: Página não encontrada",
|
||||
"Sorry, we can't find the page you are looking for.": "Desculpe, não conseguimos encontrar a página que você está procurando.",
|
||||
"Take me back to homepage": "Leve-me de volta para a página inicial",
|
||||
"Forgot password": "Esqueci a senha",
|
||||
"Forgot your password?": "Esqueceu sua senha?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Um link de redefinição de senha foi enviado para o seu email. Por favor, verifique sua caixa de entrada.",
|
||||
"Send reset link": "Enviar link de recuperação",
|
||||
"Password reset": "Resetar a senha",
|
||||
"Your new password": "Sua nova senha",
|
||||
"Set password": "Definir a senha",
|
||||
"Write a comment": "Escreva um comentário",
|
||||
"Reply...": "Responder...",
|
||||
"Error loading comments.": "Erro ao carregar comentários.",
|
||||
"No comments yet.": "Ainda sem comentários.",
|
||||
"Edit comment": "Editar comentário",
|
||||
"Delete comment": "Excluir comentário",
|
||||
"Are you sure you want to delete this comment?": "Você tem certeza de que deseja excluir este comentário?",
|
||||
"Comment created successfully": "Comentário criado com sucesso",
|
||||
"Error creating comment": "Erro ao criar comentário",
|
||||
"Comment updated successfully": "Comentário atualizado com sucesso",
|
||||
"Failed to update comment": "Falha ao atualizar comentário",
|
||||
"Comment deleted successfully": "Comentário excluído com sucesso",
|
||||
"Failed to delete comment": "Falha ao excluir comentário",
|
||||
"Comment resolved successfully": "Comentário resolvido com sucesso",
|
||||
"Failed to resolve comment": "Falha ao resolver comentário",
|
||||
"Revoke invitation": "Cancelar o convite",
|
||||
"Revoke": "Anular",
|
||||
"Don't": "Não",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Tem certeza de que deseja revogar este convite? O usuário não poderá participar do espaço de trabalho.",
|
||||
"Resend invitation": "Reenviar convite",
|
||||
"Anyone with this link can join this workspace.": "Qualquer um com este link pode participar deste espaço de trabalho.",
|
||||
"Invite link": "Link do convite",
|
||||
"Copy": "Copiar",
|
||||
"Copied": "Copiado",
|
||||
"Select a user": "Selecione um usuário",
|
||||
"Select a group": "Selecione um grupo",
|
||||
"Export all pages and attachments in this space.": "Exportar todas as páginas e anexos deste espaço.",
|
||||
"Delete space": "Excluir Espaço",
|
||||
"Are you sure you want to delete this space?": "Tem certeza de que deseja excluir este espaço?",
|
||||
"Delete this space with all its pages and data.": "Excluir este espaço com todas as suas páginas e dados.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Todas as páginas, comentários, anexos e permissões neste espaço serão excluídos de forma irreversível.",
|
||||
"Confirm space name": "Confirme o nome do espaço",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Digite o nome do espaço <b>{{spaceName}}</b> para confirmar sua ação.",
|
||||
"Format": "Formato",
|
||||
"Include subpages": "Incluir subpáginas",
|
||||
"Include attachments": "Incluir anexos",
|
||||
"Select export format": "Selecionado o formato para exportação",
|
||||
"Export failed:": "Falha ao exportar:",
|
||||
"export error": "erro de exportação",
|
||||
"Export page": "Exportar página",
|
||||
"Export space": "Exportar espaço",
|
||||
"Export {{type}}": "Exportar para {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "O arquivo excede o limite de anexos {{limit}}",
|
||||
"Align left": "Alinhar à esquerda",
|
||||
"Align right": "Alinhar à direita",
|
||||
"Align center": "Alinhar ao centro",
|
||||
"Merge cells": "Mesclar células",
|
||||
"Split cell": "Dividir célula",
|
||||
"Delete column": "Excluir coluna",
|
||||
"Delete row": "Excluir linha",
|
||||
"Add left column": "Adicionar coluna à esquerda",
|
||||
"Add right column": "Adicionar coluna à direita",
|
||||
"Add row above": "Adicionar linha acima",
|
||||
"Add row below": "Adicionar linha abaixo",
|
||||
"Delete table": "Excluir tabela",
|
||||
"Info": "Informação",
|
||||
"Success": "Sucesso",
|
||||
"Warning": "Aviso",
|
||||
"Danger": "Perigo",
|
||||
"Mermaid diagram error:": "Erro no diagrama Mermaid:",
|
||||
"Invalid Mermaid diagram": "Diagrama Mermaid inválido",
|
||||
"Double-click to edit Draw.io diagram": "Clique duas vezes para editar o diagrama Draw.io",
|
||||
"Exit": "Sair",
|
||||
"Save & Exit": "Salvar e Sair",
|
||||
"Double-click to edit Excalidraw diagram": "Clique duas vezes para editar o diagrama Excalidraw",
|
||||
"Paste link": "Colar link",
|
||||
"Edit link": "Editar link",
|
||||
"Remove link": "Remover link",
|
||||
"Add link": "Adicionar link",
|
||||
"Please enter a valid url": "Por favor, insira uma URL válida",
|
||||
"Empty equation": "Equação vazia",
|
||||
"Invalid equation": "Equação inválida",
|
||||
"Color": "Cor",
|
||||
"Text color": "Cor do texto",
|
||||
"Default": "Padrão",
|
||||
"Blue": "Azul",
|
||||
"Green": "Verde",
|
||||
"Purple": "Violeta",
|
||||
"Red": "Vermelho",
|
||||
"Yellow": "Amarelo",
|
||||
"Orange": "Laranja",
|
||||
"Pink": "Rosa",
|
||||
"Gray": "Cinza",
|
||||
"Embed link": "Link embutido",
|
||||
"Invalid {{provider}} embed link": "Link de incorporação {{provider}} inválido",
|
||||
"Embed {{provider}}": "Incorporar {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Digite o link do {{provider}} para incorporar",
|
||||
"Bold": "Negrito",
|
||||
"Italic": "Itálico",
|
||||
"Underline": "Sublinhado",
|
||||
"Strike": "Tracejado",
|
||||
"Code": "Código",
|
||||
"Comment": "Comentário",
|
||||
"Text": "Texto",
|
||||
"Heading 1": "Título 1",
|
||||
"Heading 2": "Título 2",
|
||||
"Heading 3": "Título 3",
|
||||
"To-do List": "Lista de Tarefas",
|
||||
"Bullet List": "Lista de Pontos",
|
||||
"Numbered List": "Lista Numerada",
|
||||
"Blockquote": "Bloco de Citação",
|
||||
"Just start typing with plain text.": "Basta começar a digita.",
|
||||
"Track tasks with a to-do list.": "Acompanhe tarefas com uma lista de tarefas.",
|
||||
"Big section heading.": "Título de seção grande.",
|
||||
"Medium section heading.": "Título de seção média.",
|
||||
"Small section heading.": "Título de seção pequena.",
|
||||
"Create a simple bullet list.": "Crie uma lista simples com marcadores.",
|
||||
"Create a list with numbering.": "Crie uma lista com numeração.",
|
||||
"Create block quote.": "Crie uma citação em bloco.",
|
||||
"Insert code snippet.": "Insira um trecho de código.",
|
||||
"Insert horizontal rule divider": "Insira um divisor horizontal",
|
||||
"Upload any image from your device.": "Envie qualquer imagem do seu dispositivo.",
|
||||
"Upload any video from your device.": "Envie qualquer vídeo do seu dispositivo.",
|
||||
"Upload any file from your device.": "Envie qualquer arquivo do seu dispositivo.",
|
||||
"Table": "Tabela",
|
||||
"Insert a table.": "Insira uma tabela.",
|
||||
"Insert collapsible block.": "Insira um bloco colapsável.",
|
||||
"Video": "Vídeo",
|
||||
"Divider": "Divisor",
|
||||
"Quote": "Citação",
|
||||
"Image": "Imagem",
|
||||
"File attachment": "Anexo de arquivo",
|
||||
"Toggle block": "Bloco colapsável",
|
||||
"Callout": "Aviso",
|
||||
"Insert callout notice.": "Insira um aviso.",
|
||||
"Math inline": "Matemática inline",
|
||||
"Insert inline math equation.": "Insira uma equação matemática inline.",
|
||||
"Math block": "Bloco de matemática",
|
||||
"Insert math equation": "Insira uma equação matemática",
|
||||
"Mermaid diagram": "Diagrama Mermaid",
|
||||
"Insert mermaid diagram": "Insira um diagrama Mermaid",
|
||||
"Insert and design Drawio diagrams": "Insira e projete diagramas Drawio",
|
||||
"Insert current date": "Insira a data atual",
|
||||
"Draw and sketch excalidraw diagrams": "Desenhe e esboce diagramas Excalidraw",
|
||||
"Multiple": "Múltiplo",
|
||||
"Heading {{level}}": "Título {{level}}",
|
||||
"Toggle title": "Alternar título",
|
||||
"Write anything. Enter \"/\" for commands": "Escreva qualquer coisa. Digite \"/\" para comandos",
|
||||
"Names do not match": "Os nomes não coincidem",
|
||||
"Today, {{time}}": "Hoje, {{time}}",
|
||||
"Yesterday, {{time}}": "Ontem, {{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/ru-RU/translation.json
Normal file
342
apps/client/public/locales/ru-RU/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "Аккаунт",
|
||||
"Active": "Активный",
|
||||
"Add": "Добавить",
|
||||
"Add group members": "Добавить участников группы",
|
||||
"Add groups": "Добавить группы",
|
||||
"Add members": "Добавить участников",
|
||||
"Add to groups": "Добавить в группы",
|
||||
"Add space members": "Добавить участников пространства",
|
||||
"Admin": "Администратор",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Вы уверены, что хотите удалить эту группу? Участники потеряют доступ к материалам, к которым у этой группы есть доступ.",
|
||||
"Are you sure you want to delete this page?": "Вы уверены, что хотите удалить эту страницу?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Вы уверены, что хотите удалить этого пользователя из группы? Пользователь потеряет доступ к материалам, к которым у этой группы есть доступ.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Вы уверены, что хотите удалить этого пользователя из пространства? Пользователь потеряет весь доступ к этому пространству.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Вы уверены, что хотите восстановить эту версию? Все не зафиксированные изменения будут потеряны.",
|
||||
"Can become members of groups and spaces in workspace": "Могут становиться участниками групп и пространств в рабочем пространстве",
|
||||
"Can create and edit pages in space.": "Может создавать и редактировать страницы в пространстве.",
|
||||
"Can edit": "Может изменять",
|
||||
"Can manage workspace": "Может управлять рабочим пространством",
|
||||
"Can manage workspace but cannot delete it": "Может управлять рабочим пространством, но не может его удалить",
|
||||
"Can view": "Может просматривать",
|
||||
"Can view pages in space but not edit.": "Может просматривать страницы в пространстве, но не может их редактировать.",
|
||||
"Cancel": "Отменить",
|
||||
"Change email": "Изменить электронную почту",
|
||||
"Change password": "Изменить пароль",
|
||||
"Change photo": "Изменить фото",
|
||||
"Choose a role": "Выберите роль",
|
||||
"Choose your preferred color scheme.": "Выберите предпочитаемую цветовую схему.",
|
||||
"Choose your preferred interface language.": "Выберите предпочитаемый язык интерфейса.",
|
||||
"Choose your preferred page width.": "Выберите предпочитаемую ширину страницы.",
|
||||
"Confirm": "Подтвердить",
|
||||
"Copy link": "Копировать ссылку",
|
||||
"Create": "Создать",
|
||||
"Create group": "Создать группу",
|
||||
"Create page": "Создать страницу",
|
||||
"Create space": "Создать пространство",
|
||||
"Create workspace": "Создать рабочее пространство",
|
||||
"Current password": "Текущий пароль",
|
||||
"Dark": "Темная",
|
||||
"Date": "Дата",
|
||||
"Delete": "Удалить",
|
||||
"Delete group": "Удалить группу",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Вы уверены, что хотите удалить эту страницу? Это удалит её дочерние страницы, а также историю страницы. Это действие необратимо.",
|
||||
"Description": "Описание",
|
||||
"Details": "Подробности",
|
||||
"e.g ACME": "например, ACME",
|
||||
"e.g ACME Inc": "например, ACME Inc",
|
||||
"e.g Developers": "например, Разработчики",
|
||||
"e.g Group for developers": "например, Группа для разработчиков",
|
||||
"e.g product": "например, продукт",
|
||||
"e.g Product Team": "например, Продуктовая команда",
|
||||
"e.g Sales": "например, Продажи",
|
||||
"e.g Space for product team": "например, Пространство для продуктовой команды",
|
||||
"e.g Space for sales team to collaborate": "например, Пространство для совместной работы команды продаж",
|
||||
"Edit": "Редактировать",
|
||||
"Edit group": "Редактировать группу",
|
||||
"Email": "Электронная почта",
|
||||
"Enter a strong password": "Введите надёжный пароль",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Введите действительные адреса электронной почты, разделенные запятой или пробелом [макс: 50]",
|
||||
"enter valid emails addresses": "введите действительные адреса электронной почты",
|
||||
"Enter your current password": "Введите ваш текущий пароль",
|
||||
"enter your full name": "введите ваше полное имя",
|
||||
"Enter your new password": "Введите ваш новый пароль",
|
||||
"Enter your new preferred email": "Введите ваш новый предпочитаемый адрес электронной почты",
|
||||
"Enter your password": "Введите ваш пароль",
|
||||
"Error fetching page data.": "Ошибка при загрузке данных страницы.",
|
||||
"Error loading page history.": "Ошибка при загрузке истории страницы.",
|
||||
"Export": "Экспорт",
|
||||
"Failed to create page": "Не удалось создать страницу",
|
||||
"Failed to delete page": "Не удалось удалить страницу",
|
||||
"Failed to fetch recent pages": "Не удалось получить недавние страницы",
|
||||
"Failed to import pages": "Не удалось импортировать страницы",
|
||||
"Failed to load page. An error occurred.": "Не удалось загрузить страницу. Произошла ошибка.",
|
||||
"Failed to update data": "Не удалось обновить данные",
|
||||
"Full access": "Полный доступ",
|
||||
"Full page width": "Ширина на всю страницу",
|
||||
"Full width": "Во всю ширину",
|
||||
"General": "Основные",
|
||||
"Group": "Группа",
|
||||
"Group description": "Описание группы",
|
||||
"Group name": "Название группы",
|
||||
"Groups": "Группы",
|
||||
"Has full access to space settings and pages.": "Имеет полный доступ к настройкам пространства и страницам.",
|
||||
"Home": "Главная",
|
||||
"Import pages": "Импортировать страницы",
|
||||
"Import pages & space settings": "Импорт страниц и настройки пространства",
|
||||
"Importing pages": "Импортирование страниц",
|
||||
"invalid invitation link": "ссылка на приглашение недействительна",
|
||||
"Invitation signup": "Регистрация по приглашению",
|
||||
"Invite by email": "Пригласить по электронной почте",
|
||||
"Invite members": "Пригласить участников",
|
||||
"Invite new members": "Пригласить новых участников",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Приглашённые участники, которые ещё не приняли приглашение, появятся здесь.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Приглашённые участники получат доступ к пространствам, доступ к которым есть у группы",
|
||||
"Join the workspace": "Присоединиться к рабочему пространству",
|
||||
"Language": "Язык",
|
||||
"Light": "Светлая",
|
||||
"Link copied": "Ссылка скопирована",
|
||||
"Login": "Войти",
|
||||
"Logout": "Выйти",
|
||||
"Manage Group": "Управление группой",
|
||||
"Manage members": "Управление участниками",
|
||||
"member": "участник",
|
||||
"Member": "Участник",
|
||||
"members": "участники",
|
||||
"Members": "Участники",
|
||||
"My preferences": "Мои настройки",
|
||||
"My Profile": "Мой Профиль",
|
||||
"My profile": "Мой профиль",
|
||||
"Name": "Имя",
|
||||
"New email": "Новый электронный адрес",
|
||||
"New page": "Новая страница",
|
||||
"New password": "Новый пароль",
|
||||
"No group found": "Группа не найдена",
|
||||
"No page history saved yet.": "История страниц ещё не сохранена.",
|
||||
"No pages yet": "Страниц пока нет",
|
||||
"No results found...": "Результаты не найдены...",
|
||||
"No user found": "Пользователь не найден",
|
||||
"Overview": "Обзор",
|
||||
"Owner": "Владелец",
|
||||
"page": "страница",
|
||||
"Page deleted successfully": "Страница успешно удалена",
|
||||
"Page history": "История страницы",
|
||||
"Page import is in progress. Please do not close this tab.": "Импорт страницы в процессе. Пожалуйста, не закрывайте эту вкладку.",
|
||||
"Pages": "Страницы",
|
||||
"pages": "страницы",
|
||||
"Password": "Пароль",
|
||||
"Password changed successfully": "Пароль успешно изменён",
|
||||
"Pending": "В ожидании",
|
||||
"Please confirm your action": "Пожалуйста, подтвердите ваше действие",
|
||||
"Preferences": "Внешний вид",
|
||||
"Print PDF": "Печать PDF",
|
||||
"Profile": "Профиль",
|
||||
"Recently updated": "Обновлено недавно",
|
||||
"Remove": "Удалить",
|
||||
"Remove group member": "Удалить участника группы",
|
||||
"Remove space member": "Удалить участника пространства",
|
||||
"Restore": "Восстановить",
|
||||
"Role": "Роль",
|
||||
"Save": "Сохранить",
|
||||
"Search": "Поиск",
|
||||
"Search for groups": "Поиск групп",
|
||||
"Search for users": "Поиск пользователей",
|
||||
"Search for users and groups": "Поиск пользователей и групп",
|
||||
"Search...": "Поиск...",
|
||||
"Select language": "Выберите язык",
|
||||
"Select role": "Выберите роль",
|
||||
"Select role to assign to all invited members": "Выберите роль для всех приглашённых участников",
|
||||
"Select theme": "Выберите тему",
|
||||
"Send invitation": "Отправить приглашение",
|
||||
"Settings": "Настройки",
|
||||
"Setup workspace": "Настроить рабочее пространство",
|
||||
"Sign In": "Вход",
|
||||
"Sign Up": "Регистрация",
|
||||
"Slug": "Slug",
|
||||
"Space": "Пространство",
|
||||
"Space description": "Описание пространства",
|
||||
"Space menu": "Меню пространства",
|
||||
"Space name": "Название пространства",
|
||||
"Space settings": "Настройки пространства",
|
||||
"Space slug": "Slug пространства",
|
||||
"Spaces": "Пространства",
|
||||
"Spaces you belong to": "Пространства, к которым вы принадлежите",
|
||||
"No space found": "Пространства не найдены",
|
||||
"Search for spaces": "Поиск пространств",
|
||||
"Start typing to search...": "Начните вводить для поиска...",
|
||||
"Status": "Статус",
|
||||
"Successfully imported": "Успешно импортировано",
|
||||
"Successfully restored": "Успешно восстановлено",
|
||||
"System settings": "Системные настройки",
|
||||
"Theme": "Тема",
|
||||
"To change your email, you have to enter your password and new email.": "Чтобы изменить электронную почту, вам нужно ввести пароль и новый адрес.",
|
||||
"Toggle full page width": "Переключить ширину на всю страницу",
|
||||
"Unable to import pages. Please try again.": "Не удалось импортировать страницы. Пожалуйста, попробуйте ещё раз.",
|
||||
"untitled": "без названия",
|
||||
"Untitled": "Без названия",
|
||||
"Updated successfully": "Обновлено успешно",
|
||||
"User": "Пользователь",
|
||||
"Workspace": "Рабочее пространство",
|
||||
"Workspace Name": "Имя рабочего пространства",
|
||||
"Workspace settings": "Настройки рабочего пространства",
|
||||
"You can change your password here.": "Вы можете изменить свой пароль здесь.",
|
||||
"Your Email": "Ваш адрес электронной почты",
|
||||
"Your import is complete.": "Ваш импорт завершен.",
|
||||
"Your name": "Ваше имя",
|
||||
"Your Name": "Ваше Имя",
|
||||
"Your password": "Ваш пароль",
|
||||
"Your password must be a minimum of 8 characters.": "Ваш пароль должен содержать минимум 8 символов.",
|
||||
"Sidebar toggle": "Переключить боковую панель",
|
||||
"Comments": "Комментарии",
|
||||
"404 page not found": "404 страница не найдена",
|
||||
"Sorry, we can't find the page you are looking for.": "К сожалению, мы не можем найти страницу, которую вы ищете.",
|
||||
"Take me back to homepage": "Вернуться на главную страницу",
|
||||
"Forgot password": "Забыли пароль",
|
||||
"Forgot your password?": "Забыли пароль?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Ссылка для сброса пароля была отправлена на ваш электронный адрес. Пожалуйста, проверьте входящие сообщения.",
|
||||
"Send reset link": "Отправить ссылку для сброса",
|
||||
"Password reset": "Сброс пароля",
|
||||
"Your new password": "Ваш новый пароль",
|
||||
"Set password": "Установить пароль",
|
||||
"Write a comment": "Написать комментарий",
|
||||
"Reply...": "Ответить...",
|
||||
"Error loading comments.": "Ошибка при загрузке комментариев.",
|
||||
"No comments yet.": "Комментариев пока нет.",
|
||||
"Edit comment": "Редактировать комментарий",
|
||||
"Delete comment": "Удалить комментарий",
|
||||
"Are you sure you want to delete this comment?": "Вы уверены, что хотите удалить этот комментарий?",
|
||||
"Comment created successfully": "Комментарий успешно создан",
|
||||
"Error creating comment": "Ошибка при создании комментария",
|
||||
"Comment updated successfully": "Комментарий успешно обновлён",
|
||||
"Failed to update comment": "Не удалось обновить комментарий",
|
||||
"Comment deleted successfully": "Комментарий успешно удалён",
|
||||
"Failed to delete comment": "Не удалось удалить комментарий",
|
||||
"Comment resolved successfully": "Комментарий успешно разрешён",
|
||||
"Failed to resolve comment": "Не удалось разрешить комментарий",
|
||||
"Revoke invitation": "Отозвать приглашение",
|
||||
"Revoke": "Отозвать",
|
||||
"Don't": "Нет",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Вы уверены, что хотите отозвать это приглашение? Пользователь не сможет присоединиться к рабочему пространству.",
|
||||
"Resend invitation": "Отправить приглашение повторно",
|
||||
"Anyone with this link can join this workspace.": "Любой, у кого есть эта ссылка, может присоединиться к этому рабочему пространству.",
|
||||
"Invite link": "Ссылка для приглашения",
|
||||
"Copy": "Копировать",
|
||||
"Copied": "Скопировано",
|
||||
"Select a user": "Выберите пользователя",
|
||||
"Select a group": "Выберите группу",
|
||||
"Export all pages and attachments in this space.": "Экспортировать все страницы и вложения в этом пространстве.",
|
||||
"Delete space": "Удалить пространство",
|
||||
"Are you sure you want to delete this space?": "Вы уверены, что хотите удалить это пространство?",
|
||||
"Delete this space with all its pages and data.": "Удалить это пространство со всеми его страницами и данными.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Все страницы, комментарии, вложения и разрешения в этом пространстве будут удалены безвозвратно.",
|
||||
"Confirm space name": "Подтвердите название пространства",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Введите название пространства <b>{{spaceName}}</b>, чтобы подтвердить ваше действие.",
|
||||
"Format": "Формат",
|
||||
"Include subpages": "Включить вложенные страницы",
|
||||
"Include attachments": "Включить вложения",
|
||||
"Select export format": "Выберите формат экспорта",
|
||||
"Export failed:": "Экспортирование не удалось:",
|
||||
"export error": "ошибка экспорта",
|
||||
"Export page": "Экспорт страницы",
|
||||
"Export space": "Экспорт пространства",
|
||||
"Export {{type}}": "Экспорт {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Файл превышает лимит вложений {{limit}}",
|
||||
"Align left": "По левому краю",
|
||||
"Align right": "По правому краю",
|
||||
"Align center": "По центру",
|
||||
"Merge cells": "Объединить ячейки",
|
||||
"Split cell": "Разделить ячейку",
|
||||
"Delete column": "Удалить столбец",
|
||||
"Delete row": "Удалить строку",
|
||||
"Add left column": "Добавить левый столбец",
|
||||
"Add right column": "Добавить правый столбец",
|
||||
"Add row above": "Добавить строку выше",
|
||||
"Add row below": "Добавить строку ниже",
|
||||
"Delete table": "Удалить таблицу",
|
||||
"Info": "Информация",
|
||||
"Success": "Успешно",
|
||||
"Warning": "Предупреждение",
|
||||
"Danger": "Важно",
|
||||
"Mermaid diagram error:": "Ошибка диаграммы Mermaid:",
|
||||
"Invalid Mermaid diagram": "Недопустимая диаграмма Mermaid",
|
||||
"Double-click to edit Draw.io diagram": "Кликните дважды для редактирования диаграммы Draw.io",
|
||||
"Exit": "Выйти",
|
||||
"Save & Exit": "Сохранить и выйти",
|
||||
"Double-click to edit Excalidraw diagram": "Кликните дважды для редактирования диаграммы Excalidraw",
|
||||
"Paste link": "Вставить ссылку",
|
||||
"Edit link": "Редактировать ссылку",
|
||||
"Remove link": "Удалить ссылку",
|
||||
"Add link": "Добавить ссылку",
|
||||
"Please enter a valid url": "Пожалуйста, введите корректный url",
|
||||
"Empty equation": "Пустое выражение",
|
||||
"Invalid equation": "Недопустимое уравнение",
|
||||
"Color": "Цвет",
|
||||
"Text color": "Цвет текста",
|
||||
"Default": "По умолчанию",
|
||||
"Blue": "Синий",
|
||||
"Green": "Зелёный",
|
||||
"Purple": "Фиолетовый",
|
||||
"Red": "Красный",
|
||||
"Yellow": "Жёлтый",
|
||||
"Orange": "Оранжевый",
|
||||
"Pink": "Розовый",
|
||||
"Gray": "Серый",
|
||||
"Embed link": "Встроенная ссылка",
|
||||
"Invalid {{provider}} embed link": "Неверная ссылка для встраивания {{provider}}",
|
||||
"Embed {{provider}}": "Встроить {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Введите ссылку для встраивания {{provider}}",
|
||||
"Bold": "Жирный",
|
||||
"Italic": "Курсив",
|
||||
"Underline": "Подчёркнутый",
|
||||
"Strike": "Перечёркнутый",
|
||||
"Code": "Код",
|
||||
"Comment": "Комментарий",
|
||||
"Text": "Текст",
|
||||
"Heading 1": "Заголовок 1",
|
||||
"Heading 2": "Заголовок 2",
|
||||
"Heading 3": "Заголовок 3",
|
||||
"To-do List": "Список дел",
|
||||
"Bullet List": "Маркированный список",
|
||||
"Numbered List": "Нумерованный список",
|
||||
"Blockquote": "Блок цитирования",
|
||||
"Just start typing with plain text.": "Просто начните печатать обычный текст.",
|
||||
"Track tasks with a to-do list.": "Отследить задачи с помощью списка дел.",
|
||||
"Big section heading.": "Большой заголовок раздела.",
|
||||
"Medium section heading.": "Средний заголовок раздела.",
|
||||
"Small section heading.": "Маленький заголовок раздела.",
|
||||
"Create a simple bullet list.": "Создать простой маркированный список.",
|
||||
"Create a list with numbering.": "Создать нумерованный список.",
|
||||
"Create block quote.": "Создать блок цитирования.",
|
||||
"Insert code snippet.": "Вставить фрагмент кода.",
|
||||
"Insert horizontal rule divider": "Вставить горизонтальный разделитель",
|
||||
"Upload any image from your device.": "Загрузить любое изображение с вашего устройства.",
|
||||
"Upload any video from your device.": "Загрузить любое видео с вашего устройства.",
|
||||
"Upload any file from your device.": "Загрузить любой файл с вашего устройства.",
|
||||
"Table": "Таблица",
|
||||
"Insert a table.": "Вставить таблицу.",
|
||||
"Insert collapsible block.": "Вставить сворачиваемый блок.",
|
||||
"Video": "Видео",
|
||||
"Divider": "Разделитель",
|
||||
"Quote": "Цитата",
|
||||
"Image": "Изображение",
|
||||
"File attachment": "Прикрепленный файл",
|
||||
"Toggle block": "Переключить блок",
|
||||
"Callout": "Выноска",
|
||||
"Insert callout notice.": "Вставить выноску с сообщением.",
|
||||
"Math inline": "Формула в строке",
|
||||
"Insert inline math equation.": "Вставить математическое выражение в строку.",
|
||||
"Math block": "Блок формул",
|
||||
"Insert math equation": "Вставить математическое выражение",
|
||||
"Mermaid diagram": "Диаграмма Mermaid",
|
||||
"Insert mermaid diagram": "Вставить диаграмму Mermaid",
|
||||
"Insert and design Drawio diagrams": "Вставьте и редактируйте диаграммы Draw.io",
|
||||
"Insert current date": "Вставить текущую дату",
|
||||
"Draw and sketch excalidraw diagrams": "Создайте и рисуйте диаграммы Excalidraw",
|
||||
"Multiple": "Несколько",
|
||||
"Heading {{level}}": "Заголовок {{level}}",
|
||||
"Toggle title": "Переключить заголовок",
|
||||
"Write anything. Enter \"/\" for commands": "Пишите что угодно. Введите \"/\" для выбора команд",
|
||||
"Names do not match": "Названия не совпадают",
|
||||
"Today, {{time}}": "Сегодня, {{time}}",
|
||||
"Yesterday, {{time}}": "Вчера, {{time}}"
|
||||
}
|
||||
342
apps/client/public/locales/zh-CN/translation.json
Normal file
342
apps/client/public/locales/zh-CN/translation.json
Normal file
@ -0,0 +1,342 @@
|
||||
{
|
||||
"Account": "账户",
|
||||
"Active": "活跃",
|
||||
"Add": "添加",
|
||||
"Add group members": "添加群组成员",
|
||||
"Add groups": "添加群组",
|
||||
"Add members": "添加成员",
|
||||
"Add to groups": "添加到群组",
|
||||
"Add space members": "添加空间成员",
|
||||
"Admin": "管理员",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "您确定要删除这个群组吗?成员将失去对该群组可访问资源的访问权限。",
|
||||
"Are you sure you want to delete this page?": "您确定要删除这个页面吗?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "您确定要从群组中移除这个用户吗?该用户将失去对该群组可访问资源的访问权限。",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "您确定要从空间中移除这个用户吗?该用户将失去对这个空间的所有访问权限。",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "您确定要恢复此版本吗?任何未版本化的更改将会丢失。",
|
||||
"Can become members of groups and spaces in workspace": "可以成为工作区中群组和空间的成员",
|
||||
"Can create and edit pages in space.": "能够在空间中创建和编辑页面",
|
||||
"Can edit": "可以编辑",
|
||||
"Can manage workspace": "可以管理工作区",
|
||||
"Can manage workspace but cannot delete it": "可以管理工作区但不能删除它",
|
||||
"Can view": "可以查看",
|
||||
"Can view pages in space but not edit.": "能够在空间中读取页面,但不允许编辑",
|
||||
"Cancel": "取消",
|
||||
"Change email": "更改电子邮箱",
|
||||
"Change password": "更改密码",
|
||||
"Change photo": "更改照片",
|
||||
"Choose a role": "选择一个角色",
|
||||
"Choose your preferred color scheme.": "选择您喜欢的配色方案。",
|
||||
"Choose your preferred interface language.": "选择您喜欢的界面语言。",
|
||||
"Choose your preferred page width.": "选择您喜欢的页面宽度。",
|
||||
"Confirm": "确认",
|
||||
"Copy link": "复制链接",
|
||||
"Create": "创建",
|
||||
"Create group": "创建群组",
|
||||
"Create page": "创建页面",
|
||||
"Create space": "创建空间",
|
||||
"Create workspace": "创建工作空间",
|
||||
"Current password": "当前密码",
|
||||
"Dark": "深色",
|
||||
"Date": "日期",
|
||||
"Delete": "删除",
|
||||
"Delete group": "删除群组",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "您确定要删除这个页面吗?这将删除其子页面和页面历史记录。此操作不可逆。",
|
||||
"Description": "描述",
|
||||
"Details": "详情",
|
||||
"e.g ACME": "例如:ACME",
|
||||
"e.g ACME Inc": "例如:ACME Inc",
|
||||
"e.g Developers": "例如:开发人员",
|
||||
"e.g Group for developers": "例如:开发人员群组",
|
||||
"e.g product": "例如:product",
|
||||
"e.g Product Team": "例如:产品团队",
|
||||
"e.g Sales": "例如:销售",
|
||||
"e.g Space for product team": "例如:产品团队的空间",
|
||||
"e.g Space for sales team to collaborate": "例如:销售团队协作的空间",
|
||||
"Edit": "编辑",
|
||||
"Edit group": "编辑群组",
|
||||
"Email": "电子邮箱",
|
||||
"Enter a strong password": "输入一个强密码",
|
||||
"Enter valid email addresses separated by comma or space max_50": "输入有效的电子邮箱地址,用逗号或空格分隔 [最多:50个]",
|
||||
"enter valid emails addresses": "输入有效的电子邮箱地址",
|
||||
"Enter your current password": "输入您的当前密码",
|
||||
"enter your full name": "输入您的全名",
|
||||
"Enter your new password": "输入您的新密码",
|
||||
"Enter your new preferred email": "输入您新的首选电子邮箱",
|
||||
"Enter your password": "输入您的密码",
|
||||
"Error fetching page data.": "获取页面数据时出错。",
|
||||
"Error loading page history.": "加载页面历史时出错。",
|
||||
"Export": "导出",
|
||||
"Failed to create page": "创建页面失败",
|
||||
"Failed to delete page": "删除页面失败",
|
||||
"Failed to fetch recent pages": "获取最近页面失败",
|
||||
"Failed to import pages": "导入页面失败",
|
||||
"Failed to load page. An error occurred.": "页面加载失败。发生了一个错误。",
|
||||
"Failed to update data": "数据更新失败",
|
||||
"Full access": "完全访问",
|
||||
"Full page width": "全页宽度",
|
||||
"Full width": "全宽",
|
||||
"General": "常规",
|
||||
"Group": "群组",
|
||||
"Group description": "群组描述",
|
||||
"Group name": "群组名称",
|
||||
"Groups": "群组",
|
||||
"Has full access to space settings and pages.": "能够更改全部空间设置和页面",
|
||||
"Home": "首页",
|
||||
"Import pages": "导入页面",
|
||||
"Import pages & space settings": "导入页面和空间设置",
|
||||
"Importing pages": "正在导入页面",
|
||||
"invalid invitation link": "无效的邀请链接",
|
||||
"Invitation signup": "邀请注册",
|
||||
"Invite by email": "通过电子邮箱邀请",
|
||||
"Invite members": "邀请成员",
|
||||
"Invite new members": "邀请新成员",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "尚未接受邀请的成员将显示在这里。",
|
||||
"Invited members will be granted access to spaces the groups can access": "被邀请的成员将被授予访问群组可以访问的空间的权限",
|
||||
"Join the workspace": "加入工作空间",
|
||||
"Language": "语言",
|
||||
"Light": "浅色",
|
||||
"Link copied": "链接已复制",
|
||||
"Login": "登录",
|
||||
"Logout": "退出登录",
|
||||
"Manage Group": "管理群组",
|
||||
"Manage members": "管理成员",
|
||||
"member": "成员",
|
||||
"Member": "成员",
|
||||
"members": "成员",
|
||||
"Members": "成员",
|
||||
"My preferences": "我的偏好设置",
|
||||
"My Profile": "我的个人资料",
|
||||
"My profile": "我的个人资料",
|
||||
"Name": "名称",
|
||||
"New email": "新电子邮箱",
|
||||
"New page": "新建页面",
|
||||
"New password": "新密码",
|
||||
"No group found": "未找到群组",
|
||||
"No page history saved yet.": "尚未保存页面历史。",
|
||||
"No pages yet": "暂无页面",
|
||||
"No results found...": "未找到结果...",
|
||||
"No user found": "未找到用户",
|
||||
"Overview": "概览",
|
||||
"Owner": "所有者",
|
||||
"page": "个页面",
|
||||
"Page deleted successfully": "页面已成功删除",
|
||||
"Page history": "页面历史",
|
||||
"Page import is in progress. Please do not close this tab.": "页面导入正在进行中。请不要关闭此标签页。",
|
||||
"Pages": "页面",
|
||||
"pages": "个页面",
|
||||
"Password": "密码",
|
||||
"Password changed successfully": "密码更改成功",
|
||||
"Pending": "待定",
|
||||
"Please confirm your action": "请确认您的操作",
|
||||
"Preferences": "偏好设置",
|
||||
"Print PDF": "打印 PDF",
|
||||
"Profile": "个人资料",
|
||||
"Recently updated": "最近更新",
|
||||
"Remove": "移除",
|
||||
"Remove group member": "移除群组成员",
|
||||
"Remove space member": "移除空间成员",
|
||||
"Restore": "恢复",
|
||||
"Role": "角色",
|
||||
"Save": "保存",
|
||||
"Search": "搜索",
|
||||
"Search for groups": "搜索群组",
|
||||
"Search for users": "搜索用户",
|
||||
"Search for users and groups": "搜索用户和群组",
|
||||
"Search...": "搜索...",
|
||||
"Select language": "选择语言",
|
||||
"Select role": "选择角色",
|
||||
"Select role to assign to all invited members": "选择要分配给所有被邀请成员的角色",
|
||||
"Select theme": "选择主题",
|
||||
"Send invitation": "发送邀请",
|
||||
"Settings": "设置",
|
||||
"Setup workspace": "设置工作空间",
|
||||
"Sign In": "登录",
|
||||
"Sign Up": "注册",
|
||||
"Slug": "短链接",
|
||||
"Space": "空间",
|
||||
"Space description": "空间描述",
|
||||
"Space menu": "空间菜单",
|
||||
"Space name": "空间名称",
|
||||
"Space settings": "空间设置",
|
||||
"Space slug": "空间短链接",
|
||||
"Spaces": "空间",
|
||||
"Spaces you belong to": "您所属的空间",
|
||||
"No space found": "未找到空间",
|
||||
"Search for spaces": "搜索空间",
|
||||
"Start typing to search...": "开始输入以搜索...",
|
||||
"Status": "状态",
|
||||
"Successfully imported": "成功导入",
|
||||
"Successfully restored": "恢复成功",
|
||||
"System settings": "系统设置",
|
||||
"Theme": "主题",
|
||||
"To change your email, you have to enter your password and new email.": "要更改您的电子邮箱,您需要输入密码和新的电子邮箱地址。",
|
||||
"Toggle full page width": "切换全页宽度",
|
||||
"Unable to import pages. Please try again.": "无法导入页面。请重试。",
|
||||
"untitled": "无标题",
|
||||
"Untitled": "无标题",
|
||||
"Updated successfully": "更新成功",
|
||||
"User": "用户",
|
||||
"Workspace": "工作区",
|
||||
"Workspace Name": "工作空间名称",
|
||||
"Workspace settings": "工作区设置",
|
||||
"You can change your password here.": "您可以在这里更改密码。",
|
||||
"Your Email": "您的电子邮箱",
|
||||
"Your import is complete.": "导入已完成。",
|
||||
"Your name": "您的姓名",
|
||||
"Your Name": "您的姓名",
|
||||
"Your password": "您的密码",
|
||||
"Your password must be a minimum of 8 characters.": "您的密码必须至少包含8个字符。",
|
||||
"Sidebar toggle": "切换侧边栏",
|
||||
"Comments": "评论",
|
||||
"404 page not found": "404 页面未找到",
|
||||
"Sorry, we can't find the page you are looking for.": "抱歉,我们无法找到你所需要的页面",
|
||||
"Take me back to homepage": "回到主页",
|
||||
"Forgot password": "忘记密码",
|
||||
"Forgot your password?": "忘记密码了吗?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "密码重置链接已经发送到您的邮箱,请检查收件箱",
|
||||
"Send reset link": "发送重置链接",
|
||||
"Password reset": "重置密码",
|
||||
"Your new password": "您的新密码",
|
||||
"Set password": "设置密码",
|
||||
"Write a comment": "编写评论",
|
||||
"Reply...": "回复...",
|
||||
"Error loading comments.": "加载评论时出错",
|
||||
"No comments yet.": "目前还没有评论",
|
||||
"Edit comment": "编辑评论",
|
||||
"Delete comment": "删除评论",
|
||||
"Are you sure you want to delete this comment?": "你确定要删除这条评论吗?",
|
||||
"Comment created successfully": "成功创建评论",
|
||||
"Error creating comment": "创建评论时出错",
|
||||
"Comment updated successfully": "评论更新成功",
|
||||
"Failed to update comment": "更新评论失败",
|
||||
"Comment deleted successfully": "成功删除评论",
|
||||
"Failed to delete comment": "删除评论失败",
|
||||
"Comment resolved successfully": "成功标记评论为解决",
|
||||
"Failed to resolve comment": "标记评论为解决失败",
|
||||
"Revoke invitation": "撤回邀请",
|
||||
"Revoke": "撤销",
|
||||
"Don't": "不要",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "你确定要撤回这个邀请吗?此用户将不能再加入此工作空间。",
|
||||
"Resend invitation": "重新发送邀请",
|
||||
"Anyone with this link can join this workspace.": "任何拥有此连接的人都可以加入此工作区",
|
||||
"Invite link": "邀请链接",
|
||||
"Copy": "复制",
|
||||
"Copied": "已复制",
|
||||
"Select a user": "选择一个用户",
|
||||
"Select a group": "选择一个组",
|
||||
"Export all pages and attachments in this space.": "导出当前空间的所有页面和附件",
|
||||
"Delete space": "删除空间",
|
||||
"Are you sure you want to delete this space?": "您确定要删除这个空间吗?",
|
||||
"Delete this space with all its pages and data.": "删除空间及其所有页面和数据",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "此空间中的所有页面、评论、附件和权限将被不可逆转地删除。",
|
||||
"Confirm space name": "确认空间名称",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "输入空间名称<b>{{spaceName}}</b>以确认您的操作。",
|
||||
"Format": "格式",
|
||||
"Include subpages": "包括子页面",
|
||||
"Include attachments": "包括附件",
|
||||
"Select export format": "选择导出格式",
|
||||
"Export failed:": "导出失败:",
|
||||
"export error": "导出出错",
|
||||
"Export page": "导出页面",
|
||||
"Export space": "导出空间",
|
||||
"Export {{type}}": "导出为 {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "文件超出了 {{limit}} 类型附件限制",
|
||||
"Align left": "靠左对齐",
|
||||
"Align right": "靠右对齐",
|
||||
"Align center": "居中对齐",
|
||||
"Merge cells": "合并单元格",
|
||||
"Split cell": "分割单元格",
|
||||
"Delete column": "删除整列",
|
||||
"Delete row": "删除整行",
|
||||
"Add left column": "在左侧添加列",
|
||||
"Add right column": "在右侧添加列",
|
||||
"Add row above": "在上方添加行",
|
||||
"Add row below": "在下方插入行",
|
||||
"Delete table": "删除表格",
|
||||
"Info": "信息",
|
||||
"Success": "成功",
|
||||
"Warning": "警告",
|
||||
"Danger": "危险",
|
||||
"Mermaid diagram error:": "Mermaid 图表错误:",
|
||||
"Invalid Mermaid diagram": "无效的 Mermaid 图表",
|
||||
"Double-click to edit Draw.io diagram": "双击以编辑 Draw.io 图表",
|
||||
"Exit": "退出",
|
||||
"Save & Exit": "保存并退出",
|
||||
"Double-click to edit Excalidraw diagram": "双击以编辑 Excalidraw 图表",
|
||||
"Paste link": "粘贴链接",
|
||||
"Edit link": "编辑链接",
|
||||
"Remove link": "移除链接",
|
||||
"Add link": "添加链接",
|
||||
"Please enter a valid url": "请输入一个合法的 URL",
|
||||
"Empty equation": "空白公式",
|
||||
"Invalid equation": "无效的公式",
|
||||
"Color": "颜色",
|
||||
"Text color": "文字颜色",
|
||||
"Default": "默认",
|
||||
"Blue": "蓝色",
|
||||
"Green": "绿色",
|
||||
"Purple": "紫色",
|
||||
"Red": "红色",
|
||||
"Yellow": "黄色",
|
||||
"Orange": "橙色",
|
||||
"Pink": "粉色",
|
||||
"Gray": "灰色",
|
||||
"Embed link": "嵌入链接",
|
||||
"Invalid {{provider}} embed link": "无效的 {{provider}} 嵌入链接",
|
||||
"Embed {{provider}}": "嵌入 {{provider}}",
|
||||
"Enter {{provider}} link to embed": "输入 {{provider}} 链接来嵌入",
|
||||
"Bold": "粗体",
|
||||
"Italic": "斜体",
|
||||
"Underline": "下划线",
|
||||
"Strike": "删除线",
|
||||
"Code": "代码",
|
||||
"Comment": "评论",
|
||||
"Text": "文字",
|
||||
"Heading 1": "1 级标题",
|
||||
"Heading 2": "2 级标题",
|
||||
"Heading 3": "3 级标题",
|
||||
"To-do List": "代办列表",
|
||||
"Bullet List": "无需列表",
|
||||
"Numbered List": "有序列表",
|
||||
"Blockquote": "引用块",
|
||||
"Just start typing with plain text.": "只需开始键入纯文本",
|
||||
"Track tasks with a to-do list.": "使用代办列表跟踪任务",
|
||||
"Big section heading.": "大标题",
|
||||
"Medium section heading.": "中标题",
|
||||
"Small section heading.": "小标题",
|
||||
"Create a simple bullet list.": "创建一个简单的无序列表",
|
||||
"Create a list with numbering.": "创建一个有序列表",
|
||||
"Create block quote.": "创建引用块",
|
||||
"Insert code snippet.": "插入代码片段",
|
||||
"Insert horizontal rule divider": "插入水平分割线",
|
||||
"Upload any image from your device.": "从设备上传任何图像",
|
||||
"Upload any video from your device.": "从设备上传任何视频",
|
||||
"Upload any file from your device.": "从设备上传任何文件",
|
||||
"Table": "表格",
|
||||
"Insert a table.": "插入一个表格",
|
||||
"Insert collapsible block.": "插入一个折叠块",
|
||||
"Video": "视频",
|
||||
"Divider": "分割线",
|
||||
"Quote": "引用",
|
||||
"Image": "图像",
|
||||
"File attachment": "文件附件",
|
||||
"Toggle block": "切换块",
|
||||
"Callout": "标注块",
|
||||
"Insert callout notice.": "插入标注提示块",
|
||||
"Math inline": "行内公式",
|
||||
"Insert inline math equation.": "插入行内公式",
|
||||
"Math block": "公式块",
|
||||
"Insert math equation": "插入数学公式",
|
||||
"Mermaid diagram": "Mermaid 图表",
|
||||
"Insert mermaid diagram": "插入 Mermaid 图表",
|
||||
"Insert and design Drawio diagrams": "插入并设计 Draw.io 图表",
|
||||
"Insert current date": "插入当前日期",
|
||||
"Draw and sketch excalidraw diagrams": "绘制 Excalidraw 图表",
|
||||
"Multiple": "多个",
|
||||
"Heading {{level}}": "{{level}} 级标题",
|
||||
"Toggle title": "切换标题",
|
||||
"Write anything. Enter \"/\" for commands": "开始编写内容,输入 \"/\" 以使用指令",
|
||||
"Names do not match": "名称不匹配",
|
||||
"Today, {{time}}": "今天,{{time}}",
|
||||
"Yesterday, {{time}}": "昨天,{{time}}"
|
||||
}
|
||||
@ -10,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>
|
||||
|
||||
153
apps/client/src/components/common/export-modal.tsx
Normal file
153
apps/client/src/components/common/export-modal.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
Group,
|
||||
Text,
|
||||
Select,
|
||||
Switch,
|
||||
Divider,
|
||||
} from "@mantine/core";
|
||||
import { exportPage } from "@/features/page/services/page-service.ts";
|
||||
import { useState } from "react";
|
||||
import { ExportFormat } from "@/features/page/types/page.types.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { exportSpace } from "@/features/space/services/space-service";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ExportModalProps {
|
||||
id: string;
|
||||
type: "space" | "page";
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function ExportModal({
|
||||
id,
|
||||
type,
|
||||
open,
|
||||
onClose,
|
||||
}: ExportModalProps) {
|
||||
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
|
||||
const [includeChildren, setIncludeChildren] = useState<boolean>(false);
|
||||
const [includeAttachments, setIncludeAttachments] = useState<boolean>(true);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
if (type === "page") {
|
||||
await exportPage({ pageId: id, format, includeChildren });
|
||||
}
|
||||
if (type === "space") {
|
||||
await exportSpace({ spaceId: id, format, includeAttachments });
|
||||
}
|
||||
setIncludeChildren(false);
|
||||
setIncludeAttachments(true);
|
||||
onClose();
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
message: "Export failed:" + err.response?.data.message,
|
||||
color: "red",
|
||||
});
|
||||
console.error("export error", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (format: ExportFormat) => {
|
||||
setFormat(format);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal.Root
|
||||
opened={open}
|
||||
onClose={onClose}
|
||||
size={500}
|
||||
padding="xl"
|
||||
yOffset="10vh"
|
||||
xOffset={0}
|
||||
mah={400}
|
||||
>
|
||||
<Modal.Overlay />
|
||||
<Modal.Content style={{ overflow: "hidden" }}>
|
||||
<Modal.Header py={0}>
|
||||
<Modal.Title fw={500}>Export {type}</Modal.Title>
|
||||
<Modal.CloseButton />
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<div>
|
||||
<Text size="md">{t("Format")}</Text>
|
||||
</div>
|
||||
<ExportFormatSelection format={format} onChange={handleChange} />
|
||||
</Group>
|
||||
|
||||
{type === "page" && (
|
||||
<>
|
||||
<Divider my="sm" />
|
||||
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<div>
|
||||
<Text size="md">{t("Include subpages")}</Text>
|
||||
</div>
|
||||
<Switch
|
||||
onChange={(event) =>
|
||||
setIncludeChildren(event.currentTarget.checked)
|
||||
}
|
||||
checked={includeChildren}
|
||||
/>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "space" && (
|
||||
<>
|
||||
<Divider my="sm" />
|
||||
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<div>
|
||||
<Text size="md">{t("Include attachments")}</Text>
|
||||
</div>
|
||||
<Switch
|
||||
onChange={(event) =>
|
||||
setIncludeAttachments(event.currentTarget.checked)
|
||||
}
|
||||
checked={includeAttachments}
|
||||
/>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Group justify="center" mt="md">
|
||||
<Button onClick={onClose} variant="default">
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleExport}>{t("Export")}</Button>
|
||||
</Group>
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
</Modal.Root>
|
||||
);
|
||||
}
|
||||
|
||||
interface ExportFormatSelection {
|
||||
format: ExportFormat;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Select
|
||||
data={[
|
||||
{ value: "markdown", label: "Markdown" },
|
||||
{ value: "html", label: "HTML" },
|
||||
]}
|
||||
defaultValue={format}
|
||||
onChange={onChange}
|
||||
styles={{ wrapper: { maxWidth: 120 } }}
|
||||
comboboxProps={{ width: "120" }}
|
||||
allowDeselect={false}
|
||||
withCheckIcon={false}
|
||||
aria-label={t("Select export format")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
19
apps/client/src/components/common/no-table-results.tsx
Normal file
19
apps/client/src/components/common/no-table-results.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Table, Text } from "@mantine/core";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface NoTableResultsProps {
|
||||
colSpan: number;
|
||||
}
|
||||
export default function NoTableResults({ colSpan }: NoTableResultsProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={colSpan}>
|
||||
<Text fw={500} c="dimmed" ta="center">
|
||||
{t("No results found...")}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
}
|
||||
44
apps/client/src/components/common/paginate.tsx
Normal file
44
apps/client/src/components/common/paginate.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { Button, Group } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface PagePaginationProps {
|
||||
currentPage: number;
|
||||
hasPrevPage: boolean;
|
||||
hasNextPage: boolean;
|
||||
onPageChange: (newPage: number) => void;
|
||||
}
|
||||
|
||||
export default function Paginate({
|
||||
currentPage,
|
||||
hasPrevPage,
|
||||
hasNextPage,
|
||||
onPageChange,
|
||||
}: PagePaginationProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!hasPrevPage && !hasNextPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Group mt="md">
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-sm"
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={!hasPrevPage}
|
||||
>
|
||||
{t("Prev")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-sm"
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={!hasNextPage}
|
||||
>
|
||||
{t("Next")}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
37
apps/client/src/components/common/search-input.tsx
Normal file
37
apps/client/src/components/common/search-input.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { TextInput, Group } from "@mantine/core";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface SearchInputProps {
|
||||
placeholder?: string;
|
||||
debounceDelay?: number;
|
||||
onSearch: (value: string) => void;
|
||||
}
|
||||
|
||||
export function SearchInput({
|
||||
placeholder,
|
||||
debounceDelay = 500,
|
||||
onSearch,
|
||||
}: SearchInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState("");
|
||||
const [debouncedValue] = useDebouncedValue(value, debounceDelay);
|
||||
|
||||
useEffect(() => {
|
||||
onSearch(debouncedValue);
|
||||
}, [debouncedValue, onSearch]);
|
||||
|
||||
return (
|
||||
<Group mb="sm">
|
||||
<TextInput
|
||||
size="sm"
|
||||
placeholder={placeholder || t("Search...")}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
23
apps/client/src/components/icons/google-sheets-icon.tsx
Normal file
23
apps/client/src/components/icons/google-sheets-icon.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { rem } from '@mantine/core';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function GoogleSheetsIcon({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 48 48"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path fill="#43a047" d="M37,45H11c-1.657,0-3-1.343-3-3V6c0-1.657,1.343-3,3-3h19l10,10v29C40,43.657,38.657,45,37,45z"/>
|
||||
<path fill="#c8e6c9" d="M40 13L30 13 30 3z"/>
|
||||
<path fill="#2e7d32" d="M30 13L40 23 40 13z"/>
|
||||
<path
|
||||
fill="#e8f5e9"
|
||||
d="M31,23H17h-2v2v2v2v2v2v2v2h18v-2v-2v-2v-2v-2v-2v-2H31z M17,25h4v2h-4V25z M17,29h4v2h-4V29z M17,33h4v2h-4V33z M31,35h-8v-2h8V35z M31,31h-8v-2h8V31z M31,27h-8v-2h8V27z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -14,3 +14,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.resizeHandle {
|
||||
width: 3px;
|
||||
cursor: col-resize;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
&:hover, &:active {
|
||||
width: 5px;
|
||||
background: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-5))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -3,9 +3,11 @@ import CommentList from "@/features/comment/components/comment-list.tsx";
|
||||
import { useAtom } from "jotai";
|
||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import React, { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Aside() {
|
||||
const [{ tab }] = useAtom(asideStateAtom);
|
||||
const { t } = useTranslation();
|
||||
|
||||
let title: string;
|
||||
let component: ReactNode;
|
||||
@ -25,7 +27,7 @@ export default function Aside() {
|
||||
{component && (
|
||||
<>
|
||||
<Text mb="md" fw={500}>
|
||||
{title}
|
||||
{t(title)}
|
||||
</Text>
|
||||
|
||||
<ScrollArea
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { AppShell, Container } from "@mantine/core";
|
||||
import React from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import SettingsSidebar from "@/components/settings/settings-sidebar.tsx";
|
||||
import { useAtom } from "jotai";
|
||||
import {
|
||||
asideStateAtom,
|
||||
desktopSidebarAtom,
|
||||
mobileSidebarAtom,
|
||||
mobileSidebarAtom, sidebarWidthAtom,
|
||||
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx";
|
||||
import { AppHeader } from "@/components/layouts/global/app-header.tsx";
|
||||
@ -21,6 +21,46 @@ export default function GlobalAppShell({
|
||||
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
||||
const [desktopOpened] = useAtom(desktopSidebarAtom);
|
||||
const [{ isAsideOpen }] = useAtom(asideStateAtom);
|
||||
const [sidebarWidth, setSidebarWidth] = useAtom(sidebarWidthAtom);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const sidebarRef = useRef(null);
|
||||
|
||||
const startResizing = React.useCallback((mouseDownEvent) => {
|
||||
mouseDownEvent.preventDefault();
|
||||
setIsResizing(true);
|
||||
}, []);
|
||||
|
||||
const stopResizing = React.useCallback(() => {
|
||||
setIsResizing(false);
|
||||
}, []);
|
||||
|
||||
const resize = React.useCallback(
|
||||
(mouseMoveEvent) => {
|
||||
if (isResizing) {
|
||||
const newWidth = mouseMoveEvent.clientX - sidebarRef.current.getBoundingClientRect().left;
|
||||
if (newWidth < 220) {
|
||||
setSidebarWidth(220);
|
||||
return;
|
||||
}
|
||||
if (newWidth > 600) {
|
||||
setSidebarWidth(600);
|
||||
return;
|
||||
}
|
||||
setSidebarWidth(newWidth);
|
||||
}
|
||||
},
|
||||
[isResizing]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
//https://codesandbox.io/p/sandbox/kz9de
|
||||
window.addEventListener("mousemove", resize);
|
||||
window.addEventListener("mouseup", stopResizing);
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", resize);
|
||||
window.removeEventListener("mouseup", stopResizing);
|
||||
};
|
||||
}, [resize, stopResizing]);
|
||||
|
||||
const location = useLocation();
|
||||
const isSettingsRoute = location.pathname.startsWith("/settings");
|
||||
@ -33,7 +73,7 @@ export default function GlobalAppShell({
|
||||
header={{ height: 45 }}
|
||||
navbar={
|
||||
!isHomeRoute && {
|
||||
width: 300,
|
||||
width: isSpaceRoute ? sidebarWidth : 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: {
|
||||
mobile: !mobileOpened,
|
||||
@ -54,7 +94,8 @@ export default function GlobalAppShell({
|
||||
<AppHeader />
|
||||
</AppShell.Header>
|
||||
{!isHomeRoute && (
|
||||
<AppShell.Navbar className={classes.navbar} withBorder={false}>
|
||||
<AppShell.Navbar className={classes.navbar} withBorder={false} ref={sidebarRef}>
|
||||
<div className={classes.resizeHandle} onMouseDown={startResizing} />
|
||||
{isSpaceRoute && <SpaceSidebar />}
|
||||
{isSettingsRoute && <SettingsSidebar />}
|
||||
</AppShell.Navbar>
|
||||
|
||||
@ -19,3 +19,5 @@ export const asideStateAtom = atom<AsideStateType>({
|
||||
tab: "",
|
||||
isAsideOpen: false,
|
||||
});
|
||||
|
||||
export const sidebarWidthAtom = atomWithWebStorage<number>('sidebarWidth', 300);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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} />}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -1,14 +1,32 @@
|
||||
import { Button, Group } from '@mantine/core';
|
||||
import { Button, Group } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CommentActionsProps = {
|
||||
onSave: () => void;
|
||||
isLoading?: boolean;
|
||||
onCancel?: () => void;
|
||||
isCommentEditor?: boolean;
|
||||
};
|
||||
|
||||
function CommentActions({ onSave, isLoading }: CommentActionsProps) {
|
||||
function CommentActions({
|
||||
onSave,
|
||||
isLoading,
|
||||
onCancel,
|
||||
isCommentEditor,
|
||||
}: CommentActionsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group justify="flex-end" pt={2} wrap="nowrap">
|
||||
<Button size="compact-sm" loading={isLoading} onClick={onSave}>Save</Button>
|
||||
<Group justify="flex-end" pt="sm" wrap="nowrap">
|
||||
{isCommentEditor && (
|
||||
<Button size="compact-sm" variant="default" onClick={onCancel}>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button size="compact-sm" loading={isLoading} onClick={onSave}>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import { useCreateCommentMutation } from "@/features/comment/queries/comment-que
|
||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface CommentDialogProps {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
@ -21,6 +22,7 @@ interface CommentDialogProps {
|
||||
}
|
||||
|
||||
function CommentDialog({ editor, pageId }: CommentDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [comment, setComment] = useState("");
|
||||
const [, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
||||
const [, setActiveCommentId] = useAtom(activeCommentIdAtom);
|
||||
@ -107,7 +109,7 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) {
|
||||
|
||||
<CommentEditor
|
||||
onUpdate={handleCommentEditorChange}
|
||||
placeholder="Write a comment"
|
||||
placeholder={t("Write a comment")}
|
||||
editable={true}
|
||||
autofocus={true}
|
||||
/>
|
||||
|
||||
@ -7,6 +7,7 @@ import classes from "./comment.module.css";
|
||||
import { useFocusWithin } from "@mantine/hooks";
|
||||
import clsx from "clsx";
|
||||
import { forwardRef, useEffect, useImperativeHandle } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface CommentEditorProps {
|
||||
defaultContent?: any;
|
||||
@ -27,6 +28,7 @@ const CommentEditor = forwardRef(
|
||||
}: CommentEditorProps,
|
||||
ref,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const { ref: focusRef, focused } = useFocusWithin();
|
||||
|
||||
const commentEditor = useEditor({
|
||||
@ -36,7 +38,7 @@ const CommentEditor = forwardRef(
|
||||
dropcursor: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: placeholder || "Reply...",
|
||||
placeholder: placeholder || t("Reply..."),
|
||||
}),
|
||||
Underline,
|
||||
Link,
|
||||
@ -46,6 +48,8 @@ const CommentEditor = forwardRef(
|
||||
},
|
||||
content: defaultContent,
|
||||
editable,
|
||||
immediatelyRender: true,
|
||||
shouldRerenderOnTransaction: false,
|
||||
autofocus: (autofocus && "end") || false,
|
||||
});
|
||||
|
||||
|
||||
@ -24,7 +24,6 @@ function CommentListItem({ comment }: CommentListItemProps) {
|
||||
const { hovered, ref } = useHover();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const editor = useAtomValue(pageEditorAtom);
|
||||
const [content, setContent] = useState<string>(comment.content);
|
||||
const updateCommentMutation = useUpdateCommentMutation();
|
||||
@ -59,6 +58,9 @@ function CommentListItem({ comment }: CommentListItemProps) {
|
||||
function handleEditToggle() {
|
||||
setIsEditing(true);
|
||||
}
|
||||
function cancelEdit() {
|
||||
setIsEditing(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box ref={ref} pb="xs">
|
||||
@ -116,6 +118,8 @@ function CommentListItem({ comment }: CommentListItemProps) {
|
||||
<CommentActions
|
||||
onSave={handleUpdateComment}
|
||||
isLoading={isLoading}
|
||||
onCancel={cancelEdit}
|
||||
isCommentEditor={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -6,7 +6,6 @@ import {
|
||||
useCommentsQuery,
|
||||
useCreateCommentMutation,
|
||||
} from "@/features/comment/queries/comment-query";
|
||||
|
||||
import CommentEditor from "@/features/comment/components/comment-editor";
|
||||
import CommentActions from "@/features/comment/components/comment-actions";
|
||||
import { useFocusWithin } from "@mantine/hooks";
|
||||
@ -14,8 +13,10 @@ import { IComment } from "@/features/comment/types/comment.types.ts";
|
||||
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
||||
import { IPagination } from "@/lib/types.ts";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function CommentList() {
|
||||
const { t } = useTranslation();
|
||||
const { pageSlug } = useParams();
|
||||
const { data: page } = usePageQuery({ pageId: extractPageSlugId(pageSlug) });
|
||||
const {
|
||||
@ -79,11 +80,11 @@ function CommentList() {
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <div>Error loading comments.</div>;
|
||||
return <div>{t("Error loading comments.")}</div>;
|
||||
}
|
||||
|
||||
if (!comments || comments.items.length === 0) {
|
||||
return <>No comments yet.</>;
|
||||
return <>{t("No comments yet.")}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { ActionIcon, Menu } from '@mantine/core';
|
||||
import { IconDots, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { ActionIcon, Menu } from "@mantine/core";
|
||||
import { IconDots, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type CommentMenuProps = {
|
||||
onEditComment: () => void;
|
||||
@ -8,34 +9,35 @@ type CommentMenuProps = {
|
||||
};
|
||||
|
||||
function CommentMenu({ onEditComment, onDeleteComment }: CommentMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
//@ts-ignore
|
||||
const openDeleteModal = () =>
|
||||
modals.openConfirmModal({
|
||||
title: 'Are you sure you want to delete this comment?',
|
||||
title: t("Are you sure you want to delete this comment?"),
|
||||
centered: true,
|
||||
labels: { confirm: 'Delete', cancel: 'Cancel' },
|
||||
confirmProps: { color: 'red' },
|
||||
labels: { confirm: t("Delete"), cancel: t("Cancel") },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: onDeleteComment,
|
||||
});
|
||||
|
||||
return (
|
||||
<Menu shadow="md" width={200}>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="default" style={{ border: 'none' }}>
|
||||
<ActionIcon variant="default" style={{ border: "none" }}>
|
||||
<IconDots size={20} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item onClick={onEditComment}
|
||||
leftSection={<IconEdit size={14} />}>
|
||||
Edit comment
|
||||
<Menu.Item onClick={onEditComment} leftSection={<IconEdit size={14} />}>
|
||||
{t("Edit comment")}
|
||||
</Menu.Item>
|
||||
<Menu.Item leftSection={<IconTrash size={14} />}
|
||||
onClick={openDeleteModal}
|
||||
<Menu.Item
|
||||
leftSection={<IconTrash size={14} />}
|
||||
onClick={openDeleteModal}
|
||||
>
|
||||
Delete comment
|
||||
{t("Delete comment")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@ -1,34 +1,44 @@
|
||||
import { ActionIcon } from '@mantine/core';
|
||||
import { IconCircleCheck } from '@tabler/icons-react';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { useResolveCommentMutation } from '@/features/comment/queries/comment-query';
|
||||
import { ActionIcon } from "@mantine/core";
|
||||
import { IconCircleCheck } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { useResolveCommentMutation } from "@/features/comment/queries/comment-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function ResolveComment({ commentId, pageId, resolvedAt }) {
|
||||
const { t } = useTranslation();
|
||||
const resolveCommentMutation = useResolveCommentMutation();
|
||||
|
||||
const isResolved = resolvedAt != null;
|
||||
const iconColor = isResolved ? 'green' : 'gray';
|
||||
const iconColor = isResolved ? "green" : "gray";
|
||||
|
||||
//@ts-ignore
|
||||
const openConfirmModal = () =>
|
||||
modals.openConfirmModal({
|
||||
title: 'Are you sure you want to resolve this comment thread?',
|
||||
title: t("Are you sure you want to resolve this comment thread?"),
|
||||
centered: true,
|
||||
labels: { confirm: 'Confirm', cancel: 'Cancel' },
|
||||
labels: { confirm: t("Confirm"), cancel: t("Cancel") },
|
||||
onConfirm: handleResolveToggle,
|
||||
});
|
||||
|
||||
const handleResolveToggle = async () => {
|
||||
try {
|
||||
await resolveCommentMutation.mutateAsync({ commentId, resolved: !isResolved });
|
||||
await resolveCommentMutation.mutateAsync({
|
||||
commentId,
|
||||
resolved: !isResolved,
|
||||
});
|
||||
//TODO: remove comment mark
|
||||
// Remove comment thread from state on resolve
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle resolved state:', error);
|
||||
console.error("Failed to toggle resolved state:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionIcon onClick={openConfirmModal} variant="default" style={{ border: 'none' }}>
|
||||
<ActionIcon
|
||||
onClick={openConfirmModal}
|
||||
variant="default"
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
<IconCircleCheck size={20} stroke={2} color={iconColor} />
|
||||
</ActionIcon>
|
||||
);
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
} from "@/features/comment/types/comment.types";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { IPagination } from "@/lib/types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const RQ_KEY = (pageId: string) => ["comments", pageId];
|
||||
|
||||
@ -25,7 +26,6 @@ export function useCommentsQuery(
|
||||
params: ICommentParams,
|
||||
): UseQueryResult<IPagination<IComment>, Error> {
|
||||
return useQuery({
|
||||
// eslint-disable-next-line @tanstack/query/exhaustive-deps
|
||||
queryKey: RQ_KEY(params.pageId),
|
||||
queryFn: () => getPageComments(params),
|
||||
enabled: !!params.pageId,
|
||||
@ -34,6 +34,7 @@ export function useCommentsQuery(
|
||||
|
||||
export function useCreateCommentMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation<IComment, Error, Partial<IComment>>({
|
||||
mutationFn: (data) => createComment(data),
|
||||
@ -46,28 +47,37 @@ export function useCreateCommentMutation() {
|
||||
//}
|
||||
|
||||
queryClient.refetchQueries({ queryKey: RQ_KEY(data.pageId) });
|
||||
notifications.show({ message: "Comment created successfully" });
|
||||
notifications.show({ message: t("Comment created successfully") });
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({ message: "Error creating comment", color: "red" });
|
||||
notifications.show({
|
||||
message: t("Error creating comment"),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateCommentMutation() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation<IComment, Error, Partial<IComment>>({
|
||||
mutationFn: (data) => updateComment(data),
|
||||
onSuccess: (data) => {
|
||||
notifications.show({ message: "Comment updated successfully" });
|
||||
notifications.show({ message: t("Comment updated successfully") });
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({ message: "Failed to update comment", color: "red" });
|
||||
notifications.show({
|
||||
message: t("Failed to update comment"),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteCommentMutation(pageId?: string) {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (commentId: string) => deleteComment(commentId),
|
||||
@ -87,16 +97,20 @@ export function useDeleteCommentMutation(pageId?: string) {
|
||||
});
|
||||
}
|
||||
|
||||
notifications.show({ message: "Comment deleted successfully" });
|
||||
notifications.show({ message: t("Comment deleted successfully") });
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({ message: "Failed to delete comment", color: "red" });
|
||||
notifications.show({
|
||||
message: t("Failed to delete comment"),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useResolveCommentMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (data: IResolveComment) => resolveComment(data),
|
||||
@ -115,11 +129,11 @@ export function useResolveCommentMutation() {
|
||||
queryClient.setQueryData(RQ_KEY(data.pageId), updatedComments);
|
||||
}*/
|
||||
|
||||
notifications.show({ message: "Comment resolved successfully" });
|
||||
notifications.show({ message: t("Comment resolved successfully") });
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
message: "Failed to resolve comment",
|
||||
message: t("Failed to resolve comment"),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { atom } from 'jotai';
|
||||
import { Editor } from '@tiptap/core';
|
||||
import { atom } from "jotai";
|
||||
import { Editor } from "@tiptap/core";
|
||||
|
||||
export const pageEditorAtom = atom<Editor | null>(null);
|
||||
|
||||
export const titleEditorAtom = atom<Editor | null>(null);
|
||||
|
||||
export const yjsConnectionStatusAtom = atom<string>("");
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -18,14 +18,16 @@ import classes from "./bubble-menu.module.css";
|
||||
import { ActionIcon, rem, Tooltip } from "@mantine/core";
|
||||
import { ColorSelector } from "./color-selector";
|
||||
import { NodeSelector } from "./node-selector";
|
||||
import { TextAlignmentSelector } from "./text-alignment-selector";
|
||||
import {
|
||||
draftCommentIdAtom,
|
||||
showCommentPopupAtom,
|
||||
} from "@/features/comment/atoms/comment-atom";
|
||||
import { useAtom } from "jotai";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { v7 as uuid7 } from "uuid";
|
||||
import { isCellSelection, isTextSelected } from "@docmost/editor-ext";
|
||||
import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface BubbleMenuItem {
|
||||
name: string;
|
||||
@ -39,6 +41,7 @@ type EditorBubbleMenuProps = Omit<BubbleMenuProps, "children" | "editor"> & {
|
||||
};
|
||||
|
||||
export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom);
|
||||
const [, setDraftCommentId] = useAtom(draftCommentIdAtom);
|
||||
const showCommentPopupRef = useRef(showCommentPopup);
|
||||
@ -49,31 +52,31 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
|
||||
const items: BubbleMenuItem[] = [
|
||||
{
|
||||
name: "bold",
|
||||
name: "Bold",
|
||||
isActive: () => props.editor.isActive("bold"),
|
||||
command: () => props.editor.chain().focus().toggleBold().run(),
|
||||
icon: IconBold,
|
||||
},
|
||||
{
|
||||
name: "italic",
|
||||
name: "Italic",
|
||||
isActive: () => props.editor.isActive("italic"),
|
||||
command: () => props.editor.chain().focus().toggleItalic().run(),
|
||||
icon: IconItalic,
|
||||
},
|
||||
{
|
||||
name: "underline",
|
||||
name: "Underline",
|
||||
isActive: () => props.editor.isActive("underline"),
|
||||
command: () => props.editor.chain().focus().toggleUnderline().run(),
|
||||
icon: IconUnderline,
|
||||
},
|
||||
{
|
||||
name: "strike",
|
||||
name: "Strike",
|
||||
isActive: () => props.editor.isActive("strike"),
|
||||
command: () => props.editor.chain().focus().toggleStrike().run(),
|
||||
icon: IconStrikethrough,
|
||||
},
|
||||
{
|
||||
name: "code",
|
||||
name: "Code",
|
||||
isActive: () => props.editor.isActive("code"),
|
||||
command: () => props.editor.chain().focus().toggleCode().run(),
|
||||
icon: IconCode,
|
||||
@ -81,10 +84,10 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
];
|
||||
|
||||
const commentItem: BubbleMenuItem = {
|
||||
name: "comment",
|
||||
name: "Comment",
|
||||
isActive: () => props.editor.isActive("comment"),
|
||||
command: () => {
|
||||
const commentId = uuidv4();
|
||||
const commentId = uuid7();
|
||||
|
||||
props.editor.chain().focus().setCommentDecoration().run();
|
||||
setDraftCommentId(commentId);
|
||||
@ -115,6 +118,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
moveTransition: "transform 0.15s ease-out",
|
||||
onHide: () => {
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsTextAlignmentOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
},
|
||||
@ -122,6 +126,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
};
|
||||
|
||||
const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false);
|
||||
const [isTextAlignmentSelectorOpen, setIsTextAlignmentOpen] = useState(false);
|
||||
const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false);
|
||||
const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false);
|
||||
|
||||
@ -133,18 +138,32 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
isOpen={isNodeSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
||||
setIsTextAlignmentOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextAlignmentSelector
|
||||
editor={props.editor}
|
||||
isOpen={isTextAlignmentSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsTextAlignmentOpen(!isTextAlignmentSelectorOpen);
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
<ActionIcon.Group>
|
||||
{items.map((item, index) => (
|
||||
<Tooltip key={index} label={item.name} withArrow>
|
||||
<Tooltip key={index} label={t(item.name)} withArrow>
|
||||
<ActionIcon
|
||||
key={index}
|
||||
variant="default"
|
||||
size="lg"
|
||||
radius="0"
|
||||
aria-label={item.name}
|
||||
aria-label={t(item.name)}
|
||||
className={clsx({ [classes.active]: item.isActive() })}
|
||||
style={{ border: "none" }}
|
||||
onClick={item.command}
|
||||
@ -160,6 +179,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
isOpen={isLinkSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsLinkSelectorOpen(!isLinkSelectorOpen);
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsTextAlignmentOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -168,6 +190,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
isOpen={isColorSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsColorSelectorOpen(!isColorSelectorOpen);
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsTextAlignmentOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -175,7 +200,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
variant="default"
|
||||
size="lg"
|
||||
radius="0"
|
||||
aria-label={commentItem.name}
|
||||
aria-label={t(commentItem.name)}
|
||||
style={{ border: "none" }}
|
||||
onClick={commentItem.command}
|
||||
>
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface BubbleColorMenuItem {
|
||||
name: string;
|
||||
@ -106,6 +107,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const activeColorItem = TEXT_COLORS.find(({ color }) =>
|
||||
editor.isActive("textStyle", { color }),
|
||||
);
|
||||
@ -117,7 +119,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
return (
|
||||
<Popover width={200} opened={isOpen} withArrow>
|
||||
<Popover.Target>
|
||||
<Tooltip label="Text color" withArrow>
|
||||
<Tooltip label={t("Text color")} withArrow>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
@ -136,8 +138,8 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
<Popover.Dropdown>
|
||||
{/* make mah responsive */}
|
||||
<ScrollArea.Autosize type="scroll" mah="400">
|
||||
<Text span c="dimmed" inherit>
|
||||
COLOR
|
||||
<Text span c="dimmed" tt="uppercase" inherit>
|
||||
{t("Color")}
|
||||
</Text>
|
||||
|
||||
<Button.Group orientation="vertical">
|
||||
@ -155,7 +157,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
}
|
||||
onClick={() => {
|
||||
editor.commands.unsetColor();
|
||||
name !== "Default" &&
|
||||
name !== t("Default") &&
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
@ -165,7 +167,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
}}
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
{name}
|
||||
{t(name)}
|
||||
</Button>
|
||||
))}
|
||||
</Button.Group>
|
||||
|
||||
@ -3,6 +3,7 @@ import { IconLink } from "@tabler/icons-react";
|
||||
import { ActionIcon, Popover, Tooltip } from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface LinkSelectorProps {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
@ -15,6 +16,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const onLink = useCallback(
|
||||
(url: string) => {
|
||||
setIsOpen(false);
|
||||
@ -32,7 +34,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
||||
withArrow
|
||||
>
|
||||
<Popover.Target>
|
||||
<Tooltip label="Add link" withArrow>
|
||||
<Tooltip label={t("Add link")} withArrow>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
} from "@tabler/icons-react";
|
||||
import { Popover, Button, ScrollArea } from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface NodeSelectorProps {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
@ -33,6 +34,8 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const items: BubbleMenuItem[] = [
|
||||
{
|
||||
name: "Text",
|
||||
@ -114,7 +117,7 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
rightSection={<IconChevronDown size={16} />}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{activeItem?.name}
|
||||
{t(activeItem?.name)}
|
||||
</Button>
|
||||
</Popover.Target>
|
||||
|
||||
@ -137,7 +140,7 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
}}
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
{item.name}
|
||||
{t(item.name)}
|
||||
</Button>
|
||||
))}
|
||||
</Button.Group>
|
||||
|
||||
@ -0,0 +1,107 @@
|
||||
import React, { Dispatch, FC, SetStateAction } from "react";
|
||||
import {
|
||||
IconAlignCenter,
|
||||
IconAlignJustified,
|
||||
IconAlignLeft,
|
||||
IconAlignRight,
|
||||
IconCheck,
|
||||
IconChevronDown,
|
||||
} from "@tabler/icons-react";
|
||||
import { Popover, Button, ScrollArea, rem } from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface TextAlignmentProps {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export interface BubbleMenuItem {
|
||||
name: string;
|
||||
icon: React.ElementType;
|
||||
command: () => void;
|
||||
isActive: () => boolean;
|
||||
}
|
||||
|
||||
export const TextAlignmentSelector: FC<TextAlignmentProps> = ({
|
||||
editor,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const items: BubbleMenuItem[] = [
|
||||
{
|
||||
name: "Align left",
|
||||
isActive: () => editor.isActive({ textAlign: "left" }),
|
||||
command: () => editor.chain().focus().setTextAlign("left").run(),
|
||||
icon: IconAlignLeft,
|
||||
},
|
||||
{
|
||||
name: "Align center",
|
||||
isActive: () => editor.isActive({ textAlign: "center" }),
|
||||
command: () => editor.chain().focus().setTextAlign("center").run(),
|
||||
icon: IconAlignCenter,
|
||||
},
|
||||
{
|
||||
name: "Align right",
|
||||
isActive: () => editor.isActive({ textAlign: "right" }),
|
||||
command: () => editor.chain().focus().setTextAlign("right").run(),
|
||||
icon: IconAlignRight,
|
||||
},
|
||||
{
|
||||
name: "Justify",
|
||||
isActive: () => editor.isActive({ textAlign: "justify" }),
|
||||
command: () => editor.chain().focus().setTextAlign("justify").run(),
|
||||
icon: IconAlignJustified,
|
||||
},
|
||||
];
|
||||
|
||||
const activeItem = items.filter((item) => item.isActive()).pop() ?? {
|
||||
name: "Multiple",
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover opened={isOpen} withArrow>
|
||||
<Popover.Target>
|
||||
<Button
|
||||
variant="default"
|
||||
style={{ border: "none", height: "34px" }}
|
||||
px="5"
|
||||
radius="0"
|
||||
rightSection={<IconChevronDown size={16} />}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<IconAlignLeft style={{ width: rem(16) }} stroke={2} />
|
||||
</Button>
|
||||
</Popover.Target>
|
||||
|
||||
<Popover.Dropdown>
|
||||
<ScrollArea.Autosize type="scroll" mah={400}>
|
||||
<Button.Group orientation="vertical">
|
||||
{items.map((item, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="default"
|
||||
leftSection={<item.icon size={16} />}
|
||||
rightSection={
|
||||
activeItem.name === item.name && <IconCheck size={16} />
|
||||
}
|
||||
justify="left"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
item.command();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
style={{ border: "none" }}
|
||||
>
|
||||
{t(item.name)}
|
||||
</Button>
|
||||
))}
|
||||
</Button.Group>
|
||||
</ScrollArea.Autosize>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@ -17,8 +17,10 @@ import {
|
||||
IconInfoCircleFilled,
|
||||
} from "@tabler/icons-react";
|
||||
import { CalloutType } from "@docmost/editor-ext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const shouldShow = useCallback(
|
||||
({ state }: ShouldShowProps) => {
|
||||
if (!state) {
|
||||
@ -71,11 +73,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
<ActionIcon.Group className="actionIconGroup">
|
||||
<Tooltip position="top" label="Info">
|
||||
<Tooltip position="top" label={t("Info")}>
|
||||
<ActionIcon
|
||||
onClick={() => setCalloutType("info")}
|
||||
size="lg"
|
||||
aria-label="Info"
|
||||
aria-label={t("Info")}
|
||||
variant={
|
||||
editor.isActive("callout", { type: "info" }) ? "light" : "default"
|
||||
}
|
||||
@ -84,11 +86,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Success">
|
||||
<Tooltip position="top" label={t("Success")}>
|
||||
<ActionIcon
|
||||
onClick={() => setCalloutType("success")}
|
||||
size="lg"
|
||||
aria-label="Success"
|
||||
aria-label={t("Success")}
|
||||
variant={
|
||||
editor.isActive("callout", { type: "success" })
|
||||
? "light"
|
||||
@ -99,11 +101,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Warning">
|
||||
<Tooltip position="top" label={t("Warning")}>
|
||||
<ActionIcon
|
||||
onClick={() => setCalloutType("warning")}
|
||||
size="lg"
|
||||
aria-label="Warning"
|
||||
aria-label={t("Warning")}
|
||||
variant={
|
||||
editor.isActive("callout", { type: "warning" })
|
||||
? "light"
|
||||
@ -114,11 +116,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip position="top" label="Danger">
|
||||
<Tooltip position="top" label={t("Danger")}>
|
||||
<ActionIcon
|
||||
onClick={() => setCalloutType("danger")}
|
||||
size="lg"
|
||||
aria-label="Danger"
|
||||
aria-label={t("Danger")}
|
||||
variant={
|
||||
editor.isActive("callout", { type: "danger" })
|
||||
? "light"
|
||||
|
||||
@ -1,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>
|
||||
|
||||
@ -16,3 +16,9 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.menuGroup {
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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);
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
import { EditorView } from "@tiptap/pm/view";
|
||||
import { getPageById } from "@/features/page/services/page-service.ts";
|
||||
import { IPage } from "@/features/page/types/page.types.ts";
|
||||
import { v7 } from "uuid";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
|
||||
export type LinkFn = (
|
||||
url: string,
|
||||
view: EditorView,
|
||||
pos: number,
|
||||
creatorId: string,
|
||||
) => void;
|
||||
|
||||
export interface InternalLinkOptions {
|
||||
validateFn: (url: string, view: EditorView) => boolean;
|
||||
onResolveLink: (linkedPageId: string, creatorId: string) => Promise<any>;
|
||||
}
|
||||
|
||||
export const handleInternalLink =
|
||||
({ validateFn, onResolveLink }: InternalLinkOptions): LinkFn =>
|
||||
async (url: string, view, pos, creatorId) => {
|
||||
const validated = validateFn(url, view);
|
||||
if (!validated) return;
|
||||
|
||||
const linkedPageId = extractPageSlugId(url);
|
||||
|
||||
await onResolveLink(linkedPageId, creatorId).then(
|
||||
(page: IPage) => {
|
||||
const { schema } = view.state;
|
||||
|
||||
const node = schema.nodes.mention.create({
|
||||
id: v7(),
|
||||
label: page.title || "Untitled",
|
||||
entityType: "page",
|
||||
entityId: page.id,
|
||||
slugId: page.slugId,
|
||||
creatorId: creatorId,
|
||||
});
|
||||
|
||||
if (!node) return;
|
||||
|
||||
const transaction = view.state.tr.replaceWith(pos, pos, node);
|
||||
view.dispatch(transaction);
|
||||
},
|
||||
() => {
|
||||
// on failure, insert as normal link
|
||||
const { schema } = view.state;
|
||||
|
||||
const transaction = view.state.tr.insertText(url, pos);
|
||||
transaction.addMark(
|
||||
pos,
|
||||
pos + url.length,
|
||||
schema.marks.link.create({ href: url }),
|
||||
);
|
||||
|
||||
view.dispatch(transaction);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const createMentionAction = handleInternalLink({
|
||||
onResolveLink: async (linkedPageId: string): Promise<any> => {
|
||||
// eslint-disable-next-line no-useless-catch
|
||||
try {
|
||||
return await getPageById({ pageId: linkedPageId });
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
validateFn: (url: string, view: EditorView) => {
|
||||
// validation is already done on the paste handler
|
||||
return true;
|
||||
},
|
||||
});
|
||||
@ -3,11 +3,13 @@ import { Button, Group, TextInput } from "@mantine/core";
|
||||
import { IconLink } from "@tabler/icons-react";
|
||||
import { useLinkEditorState } from "@/features/editor/components/link/use-link-editor-state.tsx";
|
||||
import { LinkEditorPanelProps } from "@/features/editor/components/link/types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const LinkEditorPanel = ({
|
||||
onSetLink,
|
||||
initialUrl,
|
||||
}: LinkEditorPanelProps) => {
|
||||
const { t } = useTranslation();
|
||||
const state = useLinkEditorState({
|
||||
onSetLink,
|
||||
initialUrl,
|
||||
@ -20,12 +22,12 @@ export const LinkEditorPanel = ({
|
||||
<TextInput
|
||||
leftSection={<IconLink size={16} />}
|
||||
variant="filled"
|
||||
placeholder="Paste link"
|
||||
placeholder={t("Paste link")}
|
||||
value={state.url}
|
||||
onChange={state.onChange}
|
||||
/>
|
||||
<Button p={"xs"} type="submit" disabled={!state.isValidUrl}>
|
||||
Save
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
|
||||
@ -7,6 +7,8 @@ import {
|
||||
Flex,
|
||||
} from "@mantine/core";
|
||||
import { IconLinkOff, IconPencil } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classes from "./link.module.css";
|
||||
|
||||
export type LinkPreviewPanelProps = {
|
||||
url: string;
|
||||
@ -19,6 +21,8 @@ export const LinkPreviewPanel = ({
|
||||
onEdit,
|
||||
url,
|
||||
}: LinkPreviewPanelProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card withBorder radius="md" padding="xs" bg="var(--mantine-color-body)">
|
||||
@ -28,12 +32,7 @@ export const LinkPreviewPanel = ({
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
inherit
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
className={classes.link}
|
||||
>
|
||||
{url}
|
||||
</Anchor>
|
||||
@ -42,13 +41,13 @@ export const LinkPreviewPanel = ({
|
||||
<Flex align="center">
|
||||
<Divider mx={4} orientation="vertical" />
|
||||
|
||||
<Tooltip label="Edit link">
|
||||
<Tooltip label={t("Edit link")}>
|
||||
<ActionIcon onClick={onEdit} variant="subtle" color="gray">
|
||||
<IconPencil size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Remove link">
|
||||
<Tooltip label={t("Remove link")}>
|
||||
<ActionIcon onClick={onClear} variant="subtle" color="red">
|
||||
<IconLinkOff size={16} />
|
||||
</ActionIcon>
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
.link {
|
||||
color: light-dark(var(--mantine-color-dark-4), var(--mantine-color-dark-1));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@ -8,8 +8,10 @@ import classes from "./math.module.css";
|
||||
import { v4 } from "uuid";
|
||||
import { IconTrashX } from "@tabler/icons-react";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function MathBlockView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node, updateAttributes, editor, getPos } = props;
|
||||
const mathResultContainer = useRef<HTMLDivElement>(null);
|
||||
const mathPreviewContainer = useRef<HTMLDivElement>(null);
|
||||
@ -94,9 +96,9 @@ export default function MathBlockView(props: NodeViewProps) {
|
||||
></div>
|
||||
{((isEditing && !preview?.trim().length) ||
|
||||
(!isEditing && !node.attrs.text.trim().length)) && (
|
||||
<div>Empty equation</div>
|
||||
<div>{t("Empty equation")}</div>
|
||||
)}
|
||||
{error && <div>Invalid equation</div>}
|
||||
{error && <div>{t("Invalid equation")}</div>}
|
||||
</NodeViewWrapper>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
|
||||
@ -6,8 +6,10 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { Popover, Textarea } from "@mantine/core";
|
||||
import classes from "./math.module.css";
|
||||
import { v4 } from "uuid";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function MathInlineView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { node, updateAttributes, editor, getPos } = props;
|
||||
const mathResultContainer = useRef<HTMLDivElement>(null);
|
||||
const mathPreviewContainer = useRef<HTMLDivElement>(null);
|
||||
@ -38,7 +40,7 @@ export default function MathInlineView(props: NodeViewProps) {
|
||||
renderMath(preview || "", mathPreviewContainer.current);
|
||||
} else if (preview !== null) {
|
||||
queueMicrotask(() => {
|
||||
updateAttributes({ text: preview });
|
||||
updateAttributes({ text: preview.trim() });
|
||||
});
|
||||
}
|
||||
}, [preview, isEditing]);
|
||||
@ -84,9 +86,9 @@ export default function MathInlineView(props: NodeViewProps) {
|
||||
></div>
|
||||
{((isEditing && !preview?.trim().length) ||
|
||||
(!isEditing && !node.attrs.text.trim().length)) && (
|
||||
<div>Empty equation</div>
|
||||
<div>{t("Empty equation")}</div>
|
||||
)}
|
||||
{error && <div>Invalid equation</div>}
|
||||
{error && <div>{t("Invalid equation")}</div>}
|
||||
</NodeViewWrapper>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown p={"xs"}>
|
||||
@ -97,7 +99,7 @@ export default function MathInlineView(props: NodeViewProps) {
|
||||
ref={textAreaRef}
|
||||
draggable={false}
|
||||
classNames={{ input: classes.textInput }}
|
||||
value={preview?.trim() ?? ""}
|
||||
value={preview ?? ""}
|
||||
placeholder={"E = mc^2"}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape" || (e.key === "Enter" && !e.shiftKey)) {
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
transition: background-color 0.2s;
|
||||
padding: 0 0.25rem;
|
||||
margin: 0 0.1rem;
|
||||
user-select: none;
|
||||
|
||||
&.empty {
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-gray-4));
|
||||
@ -30,6 +31,7 @@
|
||||
transition: background-color 0.2s;
|
||||
margin: 0 0.1rem;
|
||||
overflow-x: auto;
|
||||
user-select: none;
|
||||
|
||||
.textInput {
|
||||
width: 400px;
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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[];
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -9,3 +9,5 @@
|
||||
@import "./media.css";
|
||||
@import "./code.css";
|
||||
@import "./print.css";
|
||||
@import "./mention.css";
|
||||
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
|
||||
@media print {
|
||||
break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
.node-image, .node-video, .node-excalidraw, .node-drawio {
|
||||
|
||||
5
apps/client/src/features/editor/styles/mention.css
Normal file
5
apps/client/src/features/editor/styles/mention.css
Normal file
@ -0,0 +1,5 @@
|
||||
.node-mention {
|
||||
&.ProseMirror-selectednode {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
@ -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()) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
Reference in New Issue
Block a user