Compare commits

..
60 Commits
Author SHA1 Message Date
Philipinho 857b1143b7 sync 2026-06-21 02:35:24 +01:00
Philipinho cbd1045610 Merge branch 'main' into feat/bases 2026-06-21 02:34:49 +01:00
Philipinho 31c9b8239a Merge branch 'main' into feat/bases 2026-06-20 23:17:57 +01:00
Philipinho 3d924550f6 Merge branch 'main' into feat/bases 2026-06-20 18:38:05 +01:00
Philipinho f9b4a49e25 Merge branch 'main' into feat/bases 2026-06-20 02:35:39 +01:00
Philipinho c73a010478 sync 2026-06-20 02:26:35 +01:00
Philipinho 493221ef1f sync 2026-06-20 02:14:54 +01:00
Philipinho 1c72f469de fix(base): mint kanban choice ids as nanoid 2026-06-19 23:27:09 +01:00
Philipinho d59b8c7144 chore(base): seed choice ids with nanoid 2026-06-19 23:15:54 +01:00
Philipinho c8250c0262 feat(base): generate choice ids as nanoid on the client 2026-06-19 23:12:26 +01:00
Philipinho 9bc3933167 feat(base): generate status template choice ids as nanoid 2026-06-19 23:09:34 +01:00
Philipinho 1f9a2ade4a feat(base): scope property lookups per base and generate property ids in repo 2026-06-19 23:03:10 +01:00
Philipinho c8c9fd4306 feat(base): pass property id as text to cell extractors 2026-06-19 22:57:15 +01:00
Philipinho 53a2440139 feat(base): make property id varchar with per-base composite pk 2026-06-19 22:55:25 +01:00
Philipinho d1cd64d7ea feat(base): enforce strict property/choice id validation 2026-06-19 22:53:09 +01:00
Philipinho f68c59dfcb feat(base): add prefixed nanoid id schemas and generators 2026-06-19 22:36:09 +01:00
Philipinho e717371368 fix: read-only export 2026-06-19 18:23:04 +01:00
Philipinho 61c076f48d sync 2026-06-18 23:11:54 +01:00
Philipinho 8685c32249 fix grid cells on mobile 2026-06-18 23:06:43 +01:00
Philipinho 0b7bc70a7d fix(base): prevent popover inputs from losing focus on mobile in row detail modal 2026-06-17 14:37:41 +01:00
Philipinho 0e3549faf1 fix(base): place caret at end instead of selecting all when editing cells 2026-06-16 21:40:15 +01:00
Philipinho 2e80fc457c feat(base): insert row below via Shift+Enter on the primary cell 2026-06-16 13:22:54 +01:00
Philipinho c43ac7fc8c feat(editor): add base and kanban inserts to the toolbar 2026-06-16 12:37:08 +01:00
Philipinho ad00efe317 fix(base): enable grid cell editing on touch devices
Cells could only enter edit mode via double-click or a physical keyboard,
so touch devices had no way to edit a cell. Treat a touch/pen tap as the
edit gesture, distinguishing a tap from a scroll by movement and branching
per pointer type so mouse double-click stays unchanged. Also reveal the
row expand button on hover-less devices so the row detail view stays
reachable.
2026-06-16 12:14:21 +01:00
Philipinho 4deb13e73e fix(base): keep add-property popover within viewport on mobile
Opened from the row detail modal, the create-property popover anchors to the bottom Add property button and flips upward on small screens, clipping its top (name field, formula editor) off-screen with no way to scroll to it.

Bound the dropdown to the available height with the floating-ui size middleware and give it an internal scroll container. Disable react-remove-scroll isolation on the modal so the body-portaled popover can scroll on touch while the modal scroll lock stays active.
2026-06-16 12:04:56 +01:00
Philipinho 770c9b9d98 fix: activate New row button via keyboard (Enter/Space)
The New row control is a role=button div with no keydown handler, so Enter/Space never triggered it. It also lives inside the grid element, whose native keydown listener caught the Enter and ran cell navigation against the previously focused cell.

Add Enter/Space activation to the button, and make the grid keyboard handler ignore keydowns that originate from a focusable child rather than the grid element itself, so in-grid controls handle their own keys.
2026-06-16 10:49:53 +01:00
Philipinho dc63ed8dee fix(base): select grid cell on mousedown to avoid stale focus ring flash
The focus ring is gated on the grid having DOM focus (.bodyGrid:focus .cellFocused), but the focusedCell atom is never cleared when the grid blurs. Clicking outside hides the ring via the :focus gate while the atom still points at the old cell.

Selection was committed on click (mouseup), while the grid receives focus on mousedown. Clicking a new cell re-focused the grid before the atom updated, briefly painting the ring on the previously selected cell. Commit selection on mousedown so the atom updates in the same event that grants focus, before the browser paints.
2026-06-16 00:30:54 +01:00
Philipinho 8cce2ffca0 fix spacing 2026-06-16 00:02:56 +01:00
Philipinho 7203e12c78 fix focus 2026-06-15 23:33:10 +01:00
Philipinho 0962fd1575 fix refetch 2026-06-15 23:33:00 +01:00
Philipinho f16c027701 fix: skip duplicate row fetch when opening new kanban card 2026-06-15 22:26:59 +01:00
Philipinho 58cead4199 fix: preserve sidebar title/icon on partial page updates 2026-06-15 22:25:50 +01:00
Philipinho e9e6fba886 fix base cache 2026-06-15 22:07:50 +01:00
Philipinho 398bd499cc fix tsconfig 2026-06-15 19:43:01 +01:00
Philipinho b386f8286c license file 2026-06-15 19:12:18 +01:00
Philipinho a45dd8919d fix: change permissions object shape 2026-06-15 17:51:12 +01:00
Philipinho ab1b3723bc fix(base): refer to the feature as 'base' rather than 'database' 2026-06-15 09:46:08 +01:00
Philipinho 748401e1d4 fix(base): render view-tab rename as an inline pill so the tab band height stays put 2026-06-15 08:29:29 +01:00
Philipinho 6a6de54c92 fix(base): show cell focus ring only while the grid body itself is focused 2026-06-15 08:05:14 +01:00
Philipinho 07bfa3ba73 fix(base): close view popovers on Escape regardless of focus; drop redundant property switch tab stop 2026-06-15 05:23:19 +01:00
Philipinho 1b5c081e1e fix(base): tab from long-text editor moves to next cell instead of leaving the table 2026-06-15 04:54:23 +01:00
Philipinho 8fc538386d fix(base): sync header/body horizontal scroll on header focus; expand row via Space, drop expander from tab order 2026-06-15 04:08:52 +01:00
Philipinho cc951ae883 feat(base): keyboard-navigate the row-number column for selection 2026-06-15 03:59:03 +01:00
Philipinho 84faf187de fix(base): show cell focus ring only while the grid is focused 2026-06-15 03:41:16 +01:00
Philipinho 7af1695c85 fix(base): remove focus outline on grid container 2026-06-15 03:26:32 +01:00
Philipinho 308079af8b feat(base): make column headers keyboard-focusable as tab stops 2026-06-15 03:13:27 +01:00
Philipinho 16708cd7cc feat(base): seed editor value on type-to-edit for free-text cells 2026-06-15 02:39:04 +01:00
Philipinho 514c68e049 feat(base): row ARIA index and selected state 2026-06-15 02:34:00 +01:00
Philipinho dec87947cf feat(base): cell focus ring, click-to-focus, and gridcell ARIA 2026-06-15 02:31:32 +01:00
Philipinho bb7fddfc54 update offerings 2026-06-15 02:27:31 +01:00
Philipinho f5831a256a feat(base): keyboard navigation controller and grid wiring 2026-06-15 02:13:59 +01:00
Philipinho b8f56fee03 feat(base): add pure next-cell navigation helper 2026-06-15 02:08:33 +01:00
Philipinho b88f56cb90 feat(base): add cell focus-ring style 2026-06-15 02:07:08 +01:00
Philipinho 3bfe0c0b7d feat(base): add focused-cell atom and cell coordinate types 2026-06-15 02:02:38 +01:00
Philipinho 6187150224 fix number precision 2026-06-15 01:14:17 +01:00
Philipinho 4b5f42cc9b feat: translation 2026-06-14 21:45:31 +01:00
Philipinho f1af3e78a1 feat: base nodeview menu 2026-06-14 15:32:36 +01:00
Philipinho 7d98c7c069 fix: base trash list handling 2026-06-14 15:31:50 +01:00
Philipinho d4bcc43ec9 - default status
- type fix
- error helper
2026-06-14 11:28:39 +01:00
Philipinho 4e5bff6d55 feat(ee): bases
Table and kanban UI, formula engine package, and the base-embed editor extension
2026-06-14 01:29:06 +01:00
254 changed files with 4624 additions and 13200 deletions
+1
View File
@@ -0,0 +1 @@
shamefully-hoist = true
+4 -12
View File
@@ -1,7 +1,7 @@
FROM node:26-slim AS base FROM node:22-slim AS base
LABEL org.opencontainers.image.source="https://github.com/docmost/docmost" LABEL org.opencontainers.image.source="https://github.com/docmost/docmost"
RUN npm install -g pnpm@11.23.0 RUN npm install -g pnpm@10.4.0
FROM base AS builder FROM base AS builder
@@ -18,15 +18,6 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends curl bash \ && apt-get install -y --no-install-recommends curl bash \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# drop npm and corepack
RUN rm -rf /usr/local/lib/node_modules/npm \
&& rm -rf /usr/local/lib/node_modules/corepack \
&& rm -rf /usr/local/bin/npm \
&& rm -rf /usr/local/bin/npx \
&& rm -rf /usr/local/bin/corepack \
&& rm -rf /root/.npm \
&& rm -rf /root/.node-gyp
WORKDIR /app WORKDIR /app
# Copy apps # Copy apps
@@ -43,6 +34,7 @@ COPY --from=builder /app/packages/base-formula/package.json /app/packages/base-f
# Copy root package files # Copy root package files
COPY --from=builder /app/package.json /app/package.json COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/pnpm*.yaml /app/ COPY --from=builder /app/pnpm*.yaml /app/
COPY --from=builder /app/.npmrc /app/.npmrc
# Copy patches # Copy patches
COPY --from=builder /app/patches /app/patches COPY --from=builder /app/patches /app/patches
@@ -51,7 +43,7 @@ RUN chown -R node:node /app
USER node USER node
RUN pnpm install --frozen-lockfile --prod && rm -rf /home/node/.cache/pnpm RUN pnpm install --frozen-lockfile --prod
RUN mkdir -p /app/data/storage RUN mkdir -p /app/data/storage
+6 -7
View File
@@ -1,7 +1,7 @@
{ {
"name": "client", "name": "client",
"private": true, "private": true,
"version": "0.95.0", "version": "0.90.1",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
@@ -36,7 +36,7 @@
"@tanstack/react-table": "8.21.3", "@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.14.3", "@tanstack/react-virtual": "3.14.3",
"alfaaz": "1.1.0", "alfaaz": "1.1.0",
"axios": "1.18.1", "axios": "1.16.0",
"blueimp-load-image": "5.16.0", "blueimp-load-image": "5.16.0",
"clsx": "2.1.1", "clsx": "2.1.1",
"file-saver": "2.0.5", "file-saver": "2.0.5",
@@ -50,9 +50,9 @@
"katex": "0.16.40", "katex": "0.16.40",
"lowlight": "3.3.0", "lowlight": "3.3.0",
"mantine-form-zod-resolver": "1.3.0", "mantine-form-zod-resolver": "1.3.0",
"mermaid": "11.16.1", "mermaid": "11.15.0",
"mitt": "3.0.1", "mitt": "3.0.1",
"nanoid": "3.3.18", "nanoid": "3.3.8",
"posthog-js": "1.391.2", "posthog-js": "1.391.2",
"react": "19.2.7", "react": "19.2.7",
"react-clear-modal": "^2.0.18", "react-clear-modal": "^2.0.18",
@@ -61,10 +61,9 @@
"react-error-boundary": "6.1.1", "react-error-boundary": "6.1.1",
"react-helmet-async": "3.0.0", "react-helmet-async": "3.0.0",
"react-i18next": "16.5.8", "react-i18next": "16.5.8",
"react-router-dom": "7.18.2", "react-router-dom": "7.18.0",
"semver": "7.7.4", "semver": "7.7.4",
"socket.io-client": "4.8.3", "socket.io-client": "4.8.3",
"yet-another-react-lightbox": "^3.32.2",
"zod": "4.3.6" "zod": "4.3.6"
}, },
"devDependencies": { "devDependencies": {
@@ -87,7 +86,7 @@
"globals": "15.13.0", "globals": "15.13.0",
"jsdom": "25.0.0", "jsdom": "25.0.0",
"optics-ts": "2.4.1", "optics-ts": "2.4.1",
"postcss": "8.5.25", "postcss": "8.5.14",
"postcss-preset-mantine": "1.18.0", "postcss-preset-mantine": "1.18.0",
"postcss-simple-vars": "7.0.1", "postcss-simple-vars": "7.0.1",
"prettier": "3.8.1", "prettier": "3.8.1",
@@ -41,8 +41,6 @@
"Dark": "Dunkel", "Dark": "Dunkel",
"Date": "Datum", "Date": "Datum",
"Delete": "Löschen", "Delete": "Löschen",
"Remove from page": "Von Seite entfernen",
"Base options": "Basisoptionen",
"Delete group": "Gruppe 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? Dabei werden auch alle Unterseiten und der Seitenverlauf gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.", "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? Dabei werden auch alle Unterseiten und der Seitenverlauf gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.",
"Description": "Beschreibung", "Description": "Beschreibung",
@@ -78,24 +76,6 @@
"Failed to import pages": "Import der Seiten fehlgeschlagen", "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 load page. An error occurred.": "Seite konnte nicht geladen werden. Es ist ein Fehler aufgetreten.",
"Failed to update data": "Aktualisierung der Daten fehlgeschlagen", "Failed to update data": "Aktualisierung der Daten fehlgeschlagen",
"Failed to create base": "Basis konnte nicht erstellt werden",
"Failed to update base": "Basis konnte nicht aktualisiert werden",
"Failed to delete base": "Basis konnte nicht gelöscht werden",
"Failed to create property": "Eigenschaft konnte nicht erstellt werden",
"Failed to update property": "Eigenschaft konnte nicht aktualisiert werden",
"Failed to delete property": "Eigenschaft konnte nicht gelöscht werden",
"Failed to reorder property": "Eigenschaft konnte nicht neu angeordnet werden",
"Failed to create view": "Ansicht konnte nicht erstellt werden",
"Failed to update view": "Ansicht konnte nicht aktualisiert werden",
"Failed to delete view": "Ansicht konnte nicht gelöscht werden",
"Failed to create row": "Zeile konnte nicht erstellt werden",
"Failed to update row": "Zeile konnte nicht aktualisiert werden",
"Failed to delete row": "Zeile konnte nicht gelöscht werden",
"Failed to delete rows": "Zeilen konnten nicht gelöscht werden",
"Failed to reorder row": "Zeile konnte nicht neu angeordnet werden",
"Failed to move card": "Karte konnte nicht verschoben werden",
"Failed to add card": "Karte konnte nicht hinzugefügt werden",
"Failed to export CSV": "CSV konnte nicht exportiert werden",
"Favorite spaces": "Favorisierte Bereiche", "Favorite spaces": "Favorisierte Bereiche",
"Favorite spaces appear here": "Favorisierte Bereiche werden hier angezeigt", "Favorite spaces appear here": "Favorisierte Bereiche werden hier angezeigt",
"Favorites": "Favoriten", "Favorites": "Favoriten",
@@ -294,7 +274,6 @@
"Export space": "Bereich exportieren", "Export space": "Bereich exportieren",
"Export {{type}}": "Exportiere {{type}}", "Export {{type}}": "Exportiere {{type}}",
"File exceeds the {{limit}} attachment limit": "Datei überschreitet das Anhängelimit von {{limit}}", "File exceeds the {{limit}} attachment limit": "Datei überschreitet das Anhängelimit von {{limit}}",
"Media": "Medien",
"Align left": "Links ausrichten", "Align left": "Links ausrichten",
"Align right": "Rechts ausrichten", "Align right": "Rechts ausrichten",
"Align center": "Zentrieren", "Align center": "Zentrieren",
@@ -388,8 +367,6 @@
"Insert horizontal rule divider": "Horizontale Trennlinie einfügen", "Insert horizontal rule divider": "Horizontale Trennlinie einfügen",
"Page break": "Seitenumbruch", "Page break": "Seitenumbruch",
"Insert a page break for printing.": "Einen Seitenumbruch zum Drucken einfügen.", "Insert a page break for printing.": "Einen Seitenumbruch zum Drucken einfügen.",
"Footnote": "Fußnote",
"Insert a footnote reference.": "Einen Fußnotenverweis einfügen.",
"Upload any image from your device.": "Laden Sie ein beliebiges Bild von Ihrem Gerät hoch.", "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 video from your device.": "Laden Sie ein beliebiges Video von Ihrem Gerät hoch.",
"Upload any audio from your device.": "Laden Sie beliebige Audiodateien von Ihrem Gerät hoch.", "Upload any audio from your device.": "Laden Sie beliebige Audiodateien von Ihrem Gerät hoch.",
@@ -421,8 +398,6 @@
"Insert mermaid diagram": "Mermaid-Diagramm einfügen", "Insert mermaid diagram": "Mermaid-Diagramm einfügen",
"Insert and design Drawio diagrams": "Drawio-Diagramme einfügen und gestalten", "Insert and design Drawio diagrams": "Drawio-Diagramme einfügen und gestalten",
"Insert current date": "Aktuelles Datum einfügen", "Insert current date": "Aktuelles Datum einfügen",
"Time": "Uhrzeit",
"Insert current time": "Aktuelle Uhrzeit einfügen",
"Draw and sketch excalidraw diagrams": "Excalidraw-Diagramme zeichnen und skizzieren", "Draw and sketch excalidraw diagrams": "Excalidraw-Diagramme zeichnen und skizzieren",
"Multiple": "Mehrfach", "Multiple": "Mehrfach",
"Turn into": "In verwandeln", "Turn into": "In verwandeln",
@@ -449,7 +424,6 @@
"Names do not match": "Namen stimmen nicht überein", "Names do not match": "Namen stimmen nicht überein",
"Today, {{time}}": "Heute, {{time}}", "Today, {{time}}": "Heute, {{time}}",
"Yesterday, {{time}}": "Gestern, {{time}}", "Yesterday, {{time}}": "Gestern, {{time}}",
"now": "jetzt",
"Space created successfully": "Bereich erfolgreich erstellt", "Space created successfully": "Bereich erfolgreich erstellt",
"Space updated successfully": "Bereich erfolgreich aktualisiert", "Space updated successfully": "Bereich erfolgreich aktualisiert",
"Space deleted successfully": "Bereich erfolgreich gelöscht", "Space deleted successfully": "Bereich erfolgreich gelöscht",
@@ -622,8 +596,6 @@
"Deleted by": "Gelöscht von", "Deleted by": "Gelöscht von",
"Deleted at": "Gelöscht am", "Deleted at": "Gelöscht am",
"Preview": "Vorschau", "Preview": "Vorschau",
"Base preview unavailable": "Basisvorschau nicht verfügbar",
"Restore this base to view its contents.": "Stelle diese Basis wieder her, um ihren Inhalt anzuzeigen.",
"Subpages": "Unterseiten", "Subpages": "Unterseiten",
"Failed to load subpages": "Unterseiten konnten nicht geladen werden", "Failed to load subpages": "Unterseiten konnten nicht geladen werden",
"No subpages": "Keine Unterseiten", "No subpages": "Keine Unterseiten",
@@ -699,7 +671,7 @@
"Upgrade your plan": "Upgrade Ihres Plans", "Upgrade your plan": "Upgrade Ihres Plans",
"Available with a paid license": "Verfügbar mit einer kostenpflichtigen Lizenz", "Available with a paid license": "Verfügbar mit einer kostenpflichtigen Lizenz",
"Upgrade your license tier.": "Stufen Sie Ihre Lizenz hoch.", "Upgrade your license tier.": "Stufen Sie Ihre Lizenz hoch.",
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "KI ist in den kostenpflichtigen Docmost-Editionen verfügbar. Kontaktieren Sie sales@docmost.com.", "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "KI ist nur in der Docmost Enterprise-Edition verfügbar. Kontaktieren Sie sales@docmost.com.",
"AI & MCP": "KI & MCP", "AI & MCP": "KI & MCP",
"AI": "KI", "AI": "KI",
"MCP": "MCP", "MCP": "MCP",
@@ -707,11 +679,9 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Aktivieren Sie den MCP-Server, damit KI-Assistenten und -Tools mit den Inhalten Ihres Arbeitsbereichs interagieren können.", "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Aktivieren Sie den MCP-Server, damit KI-Assistenten und -Tools mit den Inhalten Ihres Arbeitsbereichs interagieren können.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP ist nur in der Docmost Enterprise-Edition verfügbar. Kontaktieren Sie sales@docmost.com.", "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP ist nur in der Docmost Enterprise-Edition verfügbar. Kontaktieren Sie sales@docmost.com.",
"MCP Server URL": "MCP-Server-URL", "MCP Server URL": "MCP-Server-URL",
"Connect AI assistants with your Docmost account via OAuth.": "Verbinde AI-Assistenten über OAuth mit deinem Docmost-Konto.", "Use your API key for authentication. You can manage API keys in your account settings.": "Verwenden Sie Ihren API-Schlüssel zur Authentifizierung. API-Schlüssel können in Ihren Kontoeinstellungen verwaltet werden.",
"Enforce OAuth": "Enforce OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI-Assistenten müssen sich über OAuth mit einem Docmost-Konto verbinden. API-Schlüssel können nicht mit dem MCP-Server verwendet werden.",
"Toggle enforce OAuth for MCP": "Toggle enforce OAuth for MCP",
"Supported tools": "Unterstützte Tools", "Supported tools": "Unterstützte Tools",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "In Ihrem Arbeitsbereich ist MCP aktiviert. Verwenden Sie Ihren API-Schlüssel, um KI-Assistenten anzubinden.",
"MCP server URL:": "MCP-Server-URL:", "MCP server URL:": "MCP-Server-URL:",
"Learn more": "Mehr erfahren", "Learn more": "Mehr erfahren",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Verwalten Sie API-Schlüssel für alle Nutzer im Arbeitsbereich. Siehe die <anchor>API-Dokumentation</anchor> für Details zur Verwendung.", "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Verwalten Sie API-Schlüssel für alle Nutzer im Arbeitsbereich. Siehe die <anchor>API-Dokumentation</anchor> für Details zur Verwendung.",
@@ -995,8 +965,6 @@
"Are you sure you want to delete this template?": "Sind Sie sicher, dass Sie diese Vorlage löschen möchten?", "Are you sure you want to delete this template?": "Sind Sie sicher, dass Sie diese Vorlage löschen möchten?",
"Template scope updated": "Vorlagenbereich aktualisiert", "Template scope updated": "Vorlagenbereich aktualisiert",
"Choose which space this template belongs to": "Wählen Sie den Bereich aus, zu dem diese Vorlage gehört", "Choose which space this template belongs to": "Wählen Sie den Bereich aus, zu dem diese Vorlage gehört",
"Only workspace admins can create global templates.": "Nur Workspace-Admins können globale Vorlagen erstellen.",
"You need edit access to this space to create templates.": "Sie benötigen Bearbeitungszugriff auf diesen Space, um Vorlagen zu erstellen.",
"Scope": "Bereich", "Scope": "Bereich",
"Select scope": "Bereich auswählen", "Select scope": "Bereich auswählen",
"Title": "Titel", "Title": "Titel",
@@ -1009,7 +977,7 @@
"Search pages and spaces...": "Seiten und Bereiche suchen...", "Search pages and spaces...": "Seiten und Bereiche suchen...",
"No results found": "Keine Ergebnisse gefunden", "No results found": "Keine Ergebnisse gefunden",
"You don't have permission to create pages here": "Sie haben hier keine Berechtigung, Seiten zu erstellen", "You don't have permission to create pages here": "Sie haben hier keine Berechtigung, Seiten zu erstellen",
"Chat menu for {{title}}": "Chat-Menü für {{title}}", "Chat menu": "Chatmenü",
"API key menu": "API-Schlüssel-Menü", "API key menu": "API-Schlüssel-Menü",
"Jump to comment selection": "Zur Kommentarauswahl springen", "Jump to comment selection": "Zur Kommentarauswahl springen",
"Slash commands": "Slash-Befehle", "Slash commands": "Slash-Befehle",
@@ -1095,7 +1063,7 @@
"Filter": "Filter", "Filter": "Filter",
"Page title": "Seitentitel", "Page title": "Seitentitel",
"Page content": "Seiteninhalt", "Page content": "Seiteninhalt",
"Member actions for {{name}}": "Mitgliederaktionen für {{name}}", "Member actions": "Mitgliederaktionen",
"Toggle password visibility": "Passwortsichtbarkeit umschalten", "Toggle password visibility": "Passwortsichtbarkeit umschalten",
"Send comment": "Kommentar senden", "Send comment": "Kommentar senden",
"Token actions": "Token-Aktionen", "Token actions": "Token-Aktionen",
@@ -1116,226 +1084,5 @@
"Added {{name}} to favorites": "{{name}} zu Favoriten hinzugefügt", "Added {{name}} to favorites": "{{name}} zu Favoriten hinzugefügt",
"Removed {{name}} from favorites": "{{name}} aus Favoriten entfernt", "Removed {{name}} from favorites": "{{name}} aus Favoriten entfernt",
"Page menu for {{name}}": "Seitenmenü für {{name}}", "Page menu for {{name}}": "Seitenmenü für {{name}}",
"Create subpage of {{name}}": "Unterseite von {{name}} erstellen", "Create subpage of {{name}}": "Unterseite von {{name}} erstellen"
"Allow personal spaces": "Persönliche Bereiche erlauben",
"Members can create their own personal space.": "Mitglieder können ihren eigenen persönlichen Bereich erstellen.",
"Toggle allow personal spaces": "Persönliche Bereiche erlauben umschalten",
"Create personal space": "Persönlichen Bereich erstellen",
"Personal space": "Persönlicher Bereich",
"{{name}}'s space": "Bereich von {{name}}",
"Apply": "Anwenden",
"Cells that aren't already a page reference will be cleared.": "Zellen, die noch keine Seitenreferenz sind, werden geleert.",
"Cells that aren't a valid URL will be cleared.": "Zellen, die keine gültige URL sind, werden geleert.",
"Cells that aren't a valid email address will be cleared.": "Zellen, die keine gültige E-Mail-Adresse sind, werden geleert.",
"Cells that can't be parsed as a date will be cleared.": "Zellen, die nicht als Datum geparst werden können, werden geleert.",
"Cells that can't be parsed as a number will be cleared.": "Zellen, die nicht als Zahl geparst werden können, werden geleert.",
"Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).": "Zellen werden umgewandelt (yes/true/1 werden als ausgewählt markiert; alles andere wird als nicht ausgewählt markiert oder geleert).",
"Cells will be reinterpreted under the new type.": "Zellen werden unter dem neuen Typ neu interpretiert.",
"Cells will be replaced with a comma-separated list of file names.": "Zellen werden durch eine kommagetrennte Liste von Dateinamen ersetzt.",
"Cells will be replaced with a comma-separated list of option names.": "Zellen werden durch eine kommagetrennte Liste von Optionsnamen ersetzt.",
"Cells will be replaced with the option name.": "Zellen werden durch den Optionsnamen ersetzt.",
"Cells will be replaced with the page title.": "Zellen werden durch den Seitentitel ersetzt.",
"Cells will be replaced with the person's name.": "Zellen werden durch den Namen der Person ersetzt.",
"Change type": "Typ ändern",
"Change type to {{label}}?": "Typ in {{label}} ändern?",
"Converting…": "Wird umgewandelt…",
"Existing values become single-item lists. No data is lost.": "Vorhandene Werte werden zu Einzellisten. Es gehen keine Daten verloren.",
"Only the first selected item per row will be kept; the rest will be discarded.": "Nur das erste ausgewählte Element pro Zeile wird beibehalten; der Rest wird verworfen.",
"Previous record": "Vorheriger Datensatz",
"Next record": "Nächster Datensatz",
"Record actions": "Datensatzaktionen",
"Delete record": "Datensatz löschen",
"Delete record?": "Datensatz löschen?",
"This action cannot be undone.": "Diese Aktion kann nicht rückgängig gemacht werden.",
"to navigate": "zum Navigieren",
"to close": "zum Schließen",
"Expand row {{number}}": "Zeile {{number}} erweitern",
"Saving…": "Wird gespeichert…",
"Read-only": "Schreibgeschützt",
"Loading…": "Wird geladen…",
"Updated {{when}}": "Aktualisiert {{when}}",
"Add property": "Eigenschaft hinzufügen",
"Create property": "Eigenschaft erstellen",
"Hide properties": "Eigenschaften ausblenden",
"Find a property type": "Einen Eigenschaftstyp finden",
"Properties": "Eigenschaften",
"A property with this name already exists": "Eine Eigenschaft mit diesem Namen existiert bereits",
"Add card": "Karte hinzufügen",
"Add filter": "Filter hinzufügen",
"Add option": "Option hinzufügen",
"Add sort": "Sortierung hinzufügen",
"Add view": "Ansicht hinzufügen",
"All data in this column will be lost.": "Alle Daten in dieser Spalte gehen verloren.",
"Allow multiple people": "Mehrere Personen zulassen",
"Alphabetize": "Alphabetisch sortieren",
"Are you sure you want to delete": "Sind Sie sicher, dass Sie löschen möchten",
"Ascending": "Aufsteigend",
"Base deleted": "Base gelöscht",
"Base name": "Base-Name",
"Base table": "Base-Tabelle",
"Card properties": "Karteneigenschaften",
"Cells longer than the Text limit will be truncated and the extra content permanently lost.": "Zellen, die das Textlimit überschreiten, werden abgeschnitten und der zusätzliche Inhalt geht dauerhaft verloren.",
"Checkbox": "Kontrollkästchen",
"Checked by default": "Standardmäßig aktiviert",
"Choose a property": "Wählen Sie eine Eigenschaft",
"Clear selection": "Auswahl aufheben",
"Column options": "Spaltenoptionen",
"Comma, period": "Komma, Punkt",
"Complete": "Abgeschlossen",
"Contains": "Enthält",
"Copy link to view": "Link zur Ansicht kopieren",
"Create a status property": "Eine Statuseigenschaft erstellen",
"Created at": "Erstellt am",
"Currency": "Währung",
"Decimal places": "Dezimalstellen",
"Default value": "Standardwert",
"Delete property": "Eigenschaft löschen",
"Delete view": "Ansicht löschen",
"Delete {{count}} rows?_one": "Delete 1 row?",
"Delete {{count}} rows?_other": "Delete {{count}} rows?",
"Descending": "Absteigend",
"Discard": "Verwerfen",
"Doesn't contain": "Enthält nicht",
"Done": "Fertig",
"Edit formula": "Formel bearbeiten",
"Edit property": "Eigenschaft bearbeiten",
"Export CSV": "CSV exportieren",
"Failed to load base": "Base konnte nicht geladen werden",
"False": "Falsch",
"File": "Datei",
"Filter and sort changes are visible only to you": "Änderungen an Filtern und Sortierungen sind nur für Sie sichtbar",
"Filter by": "Filtern nach",
"Formula": "Formel",
"Get started with": "Erste Schritte mit",
"Greater than": "Größer als",
"Group by": "Gruppieren nach",
"Group this board by a select or status property.": "Gruppieren Sie dieses Board nach einer Auswahl- oder Statuseigenschaft.",
"Hide all": "Alle ausblenden",
"Hide group": "Gruppe ausblenden",
"In Progress": "In Bearbeitung",
"In progress": "In Bearbeitung",
"Include time": "Uhrzeit einschließen",
"Insert a kanban board on this page": "Fügen Sie ein Kanban-Board auf dieser Seite ein",
"Insert an inline base on this page": "Fügen Sie eine Inline-Base auf dieser Seite ein",
"Is": "Ist",
"Is after": "Ist nach",
"Is any of": "Ist eines von",
"Is before": "Ist vor",
"Is empty": "Ist leer",
"Is none of": "Ist keines von",
"Is not": "Ist nicht",
"Is not empty": "Ist nicht leer",
"Is on or after": "Ist am oder nach",
"Is on or before": "Ist am oder vor",
"Is within": "Ist innerhalb von",
"Keep editing": "Weiter bearbeiten",
"Last edited at": "Zuletzt bearbeitet am",
"Last edited by": "Zuletzt bearbeitet von",
"Less than": "Kleiner als",
"Link copied to clipboard": "Link in die Zwischenablage kopiert",
"Local": "Lokal",
"Long text": "Langer Text",
"Moved card to {{column}}": "Karte nach {{column}} verschoben",
"Moved column to {{column}}": "Spalte nach {{column}} verschoben",
"Multi-select": "Mehrfachauswahl",
"New row": "Neue Zeile",
"Next month": "Nächster Monat",
"Next week": "Nächste Woche",
"Next year": "Nächstes Jahr",
"No filters applied": "Keine Filter angewendet",
"No match": "Keine Übereinstimmung",
"No options for this property type": "Keine Optionen für diesen Eigenschaftstyp",
"No sorts applied": "Keine Sortierungen angewendet",
"No value": "Kein Wert",
"None": "Keine",
"Not started": "Nicht begonnen",
"Number": "Zahl",
"One month ago": "Vor einem Monat",
"One month from now": "In einem Monat",
"One week ago": "Vor einer Woche",
"One week from now": "In einer Woche",
"Open as page": "Als Seite öffnen",
"Options": "Optionen",
"Past month": "Letzter Monat",
"Past week": "Letzte Woche",
"Past year": "Letztes Jahr",
"Percent": "Prozent",
"Period, comma": "Punkt, Komma",
"Person": "Person",
"Please enter a valid email": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"Progress": "Fortschritt",
"Property options": "Eigenschaftsoptionen",
"Relative": "Relativ",
"Rename property": "Eigenschaft umbenennen",
"Reset": "Zurücksetzen",
"Save for everyone": "Für alle speichern",
"Select": "Auswahl",
"Select a property": "Eigenschaft auswählen",
"Select all loaded rows": "Alle geladenen Zeilen auswählen",
"Show all": "Alle anzeigen",
"Sort": "Sortieren",
"Sort by": "Sortieren nach",
"Space, comma": "Leerzeichen, Komma",
"Space, period": "Leerzeichen, Punkt",
"This board has no grouping property yet.": "Dieses Board hat noch keine Gruppierungseigenschaft.",
"This month": "Diesen Monat",
"This year": "Dieses Jahr",
"Thousands and decimal separators": "Tausender- und Dezimaltrennzeichen",
"Time format": "Zeitformat",
"To Do": "Zu erledigen",
"Tomorrow": "Morgen",
"True": "Wahr",
"Unsaved changes": "Nicht gespeicherte Änderungen",
"Untitled base": "Unbenannte Base",
"URL": "URL",
"Value": "Wert",
"View updated for everyone": "Ansicht für alle aktualisiert",
"You have unsaved changes. Do you want to discard them?": "Sie haben nicht gespeicherte Änderungen. Möchten Sie sie verwerfen?",
"{{count}} rows deleted_one": "1 row deleted",
"{{count}} rows deleted_other": "{{count}} rows deleted",
"{{count}} selected_one": "1 selected",
"{{count}} selected_other": "{{count}} selected",
"Compare": "Vergleichen",
"Compare versions": "Versionen vergleichen",
"Select version from {{date}}": "Version vom {{date}} auswählen",
"Version actions for {{date}}": "Versionsaktionen für {{date}}",
"Comparing {{newer}} and {{older}}": "{{newer}} und {{older}} werden verglichen",
"Exit compare": "Vergleich beenden",
"Search attachments...": "Anhänge suchen...",
"Error loading attachments.": "Fehler beim Laden der Anhänge.",
"No attachments on this page yet.": "Auf dieser Seite gibt es noch keine Anhänge.",
"Uploaded by {{name}}": "Hochgeladen von {{name}}",
"Download {{name}}": "{{name}} herunterladen",
"Access revoked": "Zugriff widerrufen",
"Authorize application": "Anwendung autorisieren",
"{{name}} wants to access {{workspace}}": "{{name}} möchte auf {{workspace}} zugreifen",
"Not you? Switch account": "Nicht du? Konto wechseln",
"This application will be able to:": "Diese Anwendung kann Folgendes:",
"Write": "Schreiben",
"Invalid authorization request": "Ungültige Autorisierungsanfrage",
"Authorize": "Autorisieren",
"Application": "Anwendung",
"Permissions": "Berechtigungen",
"Authorized": "Autorisiert",
"Revoke access": "Zugriff widerrufen",
"Revoke access for {{name}}": "Zugriff für {{name}} widerrufen",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Möchtest du den Zugriff für {{name}} wirklich widerrufen? Die Anwendung kann dann nicht mehr auf dein Konto zugreifen.",
"Something went wrong. Please try again.": "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
"Remove {{name}}": "{{name}} entfernen",
"Make sure you trust this application before authorizing it.": "Stelle sicher, dass du dieser Anwendung vertraust, bevor du sie autorisierst.",
"You will be redirected to": "Du wirst weitergeleitet zu",
"View content without making changes.": "Inhalte ansehen, ohne Änderungen vorzunehmen.",
"Create and modify content.": "Inhalte erstellen und bearbeiten.",
"Applications and AI assistants you have authorized to access your account.": "Anwendungen und AI-Assistenten, denen du den Zugriff auf dein Konto autorisiert hast.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "MCP ist für deinen Workspace aktiviert. Verbinde AI-Assistenten über OAuth mit deinem Docmost-Konto.",
"Authorized apps": "Autorisierte Apps",
"No authorized apps yet.": "Noch keine autorisierten Apps.",
"Workspace knowledge only": "Nur Workspace-Wissen",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Beschränke den AI-Chat darauf, nur anhand deiner Workspace-Seiten und hochgeladenen Dateien zu antworten. Es wird kein externes Wissen verwendet.",
"Toggle workspace knowledge only": "Nur Workspace-Wissen umschalten",
"Read-only mode": "Schreibgeschützter Modus",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI-Chat kann Workspace-Inhalte durchsuchen und lesen, aber keine Seiten erstellen oder bearbeiten.",
"Toggle AI Chat read-only mode": "Schreibgeschützten Modus für AI-Chat umschalten",
"Title only": "Nur Titel",
"you": "du"
} }
+22 -208
View File
@@ -47,24 +47,24 @@
"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.", "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", "Description": "Description",
"Details": "Details", "Details": "Details",
"e.g ACME": "e.g. ACME", "e.g ACME": "e.g ACME",
"e.g ACME Inc": "e.g. ACME Inc", "e.g ACME Inc": "e.g ACME Inc",
"e.g Developers": "e.g. Developers", "e.g Developers": "e.g Developers",
"e.g Group for developers": "e.g. Group for developers", "e.g Group for developers": "e.g Group for developers",
"e.g product": "e.g. product", "e.g product": "e.g product",
"e.g Product Team": "e.g. Product Team", "e.g Product Team": "e.g Product Team",
"e.g Sales": "e.g. Sales", "e.g Sales": "e.g Sales",
"e.g Space for product team": "e.g. Space for product team", "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", "e.g Space for sales team to collaborate": "e.g Space for sales team to collaborate",
"Edit": "Edit", "Edit": "Edit",
"Read": "Read", "Read": "Read",
"Edit group": "Edit group", "Edit group": "Edit group",
"Email": "Email", "Email": "Email",
"Enter a strong password": "Enter a strong password", "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 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 email addresses", "enter valid emails addresses": "enter valid emails addresses",
"Enter your current password": "Enter your current password", "Enter your current password": "Enter your current password",
"enter your full name": "Enter your full name", "enter your full name": "enter your full name",
"Enter your new password": "Enter your new password", "Enter your new password": "Enter your new password",
"Enter your new preferred email": "Enter your new preferred email", "Enter your new preferred email": "Enter your new preferred email",
"Enter your password": "Enter your password", "Enter your password": "Enter your password",
@@ -112,7 +112,7 @@
"Import pages": "Import pages", "Import pages": "Import pages",
"Import pages & space settings": "Import pages & space settings", "Import pages & space settings": "Import pages & space settings",
"Importing pages": "Importing pages", "Importing pages": "Importing pages",
"invalid invitation link": "Invalid invitation link", "invalid invitation link": "invalid invitation link",
"Invitation signup": "Invitation signup", "Invitation signup": "Invitation signup",
"Invite by email": "Invite by email", "Invite by email": "Invite by email",
"Invite members": "Invite members", "Invite members": "Invite members",
@@ -223,7 +223,7 @@
"Your password must be a minimum of 8 characters.": "Your password must be a minimum of 8 characters.", "Your password must be a minimum of 8 characters.": "Your password must be a minimum of 8 characters.",
"Sidebar toggle": "Sidebar toggle", "Sidebar toggle": "Sidebar toggle",
"Comments": "Comments", "Comments": "Comments",
"404 page not found": "404! Page not found", "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.", "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", "Take me back to homepage": "Take me back to homepage",
"Forgot password": "Forgot password", "Forgot password": "Forgot password",
@@ -294,7 +294,6 @@
"Export space": "Export space", "Export space": "Export space",
"Export {{type}}": "Export {{type}}", "Export {{type}}": "Export {{type}}",
"File exceeds the {{limit}} attachment limit": "File exceeds the {{limit}} attachment limit", "File exceeds the {{limit}} attachment limit": "File exceeds the {{limit}} attachment limit",
"Media": "Media",
"Align left": "Align left", "Align left": "Align left",
"Align right": "Align right", "Align right": "Align right",
"Align center": "Align center", "Align center": "Align center",
@@ -388,8 +387,6 @@
"Insert horizontal rule divider": "Insert horizontal rule divider", "Insert horizontal rule divider": "Insert horizontal rule divider",
"Page break": "Page break", "Page break": "Page break",
"Insert a page break for printing.": "Insert a page break for printing.", "Insert a page break for printing.": "Insert a page break for printing.",
"Footnote": "Footnote",
"Insert a footnote reference.": "Insert a footnote reference.",
"Upload any image from your device.": "Upload any image from your device.", "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 video from your device.": "Upload any video from your device.",
"Upload any audio from your device.": "Upload any audio from your device.", "Upload any audio from your device.": "Upload any audio from your device.",
@@ -423,7 +420,7 @@
"Insert current date": "Insert current date", "Insert current date": "Insert current date",
"Time": "Time", "Time": "Time",
"Insert current time": "Insert current time", "Insert current time": "Insert current time",
"Draw and sketch excalidraw diagrams": "Draw and sketch Excalidraw diagrams", "Draw and sketch excalidraw diagrams": "Draw and sketch excalidraw diagrams",
"Multiple": "Multiple", "Multiple": "Multiple",
"Turn into": "Turn into", "Turn into": "Turn into",
"Text align": "Text align", "Text align": "Text align",
@@ -564,7 +561,7 @@
"Make sure to save these codes in a secure place. Your old backup codes are no longer valid.": "Make sure to save these codes in a secure place. Your old backup codes are no longer valid.", "Make sure to save these codes in a secure place. Your old backup codes are no longer valid.": "Make sure to save these codes in a secure place. Your old backup codes are no longer valid.",
"Your new backup codes": "Your new backup codes", "Your new backup codes": "Your new backup codes",
"I've saved my backup codes": "I've saved my backup codes", "I've saved my backup codes": "I've saved my backup codes",
"Failed to setup MFA": "Failed to set up MFA", "Failed to setup MFA": "Failed to setup MFA",
"Setup & Verify": "Setup & Verify", "Setup & Verify": "Setup & Verify",
"Add to authenticator": "Add to authenticator", "Add to authenticator": "Add to authenticator",
"1. Scan this QR code with your authenticator app": "1. Scan this QR code with your authenticator app", "1. Scan this QR code with your authenticator app": "1. Scan this QR code with your authenticator app",
@@ -638,7 +635,7 @@
"Enterprise": "Enterprise", "Enterprise": "Enterprise",
"Download attachment": "Download attachment", "Download attachment": "Download attachment",
"Allowed email domains": "Allowed email domains", "Allowed email domains": "Allowed email domains",
"Only users with email addresses from these domains can signup via SSO.": "Only users with email addresses from these domains can sign up via SSO.", "Only users with email addresses from these domains can signup via SSO.": "Only users with email addresses from these domains can signup via SSO.",
"Enter valid domain names separated by comma or space": "Enter valid domain names separated by comma or space", "Enter valid domain names separated by comma or space": "Enter valid domain names separated by comma or space",
"Enforce two-factor authentication": "Enforce two-factor authentication", "Enforce two-factor authentication": "Enforce two-factor authentication",
"Once enforced, all members must enable two-factor authentication to access the workspace.": "Once enforced, all members must enable two-factor authentication to access the workspace.", "Once enforced, all members must enable two-factor authentication to access the workspace.": "Once enforced, all members must enable two-factor authentication to access the workspace.",
@@ -699,7 +696,7 @@
"Upgrade your plan": "Upgrade your plan", "Upgrade your plan": "Upgrade your plan",
"Available with a paid license": "Available with a paid license", "Available with a paid license": "Available with a paid license",
"Upgrade your license tier.": "Upgrade your license tier.", "Upgrade your license tier.": "Upgrade your license tier.",
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "AI is available in the Docmost paid editions. Contact sales@docmost.com.", "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
"AI & MCP": "AI & MCP", "AI & MCP": "AI & MCP",
"AI": "AI", "AI": "AI",
"MCP": "MCP", "MCP": "MCP",
@@ -707,11 +704,9 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.", "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.", "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
"MCP Server URL": "MCP Server URL", "MCP Server URL": "MCP Server URL",
"Connect AI assistants with your Docmost account via OAuth.": "Connect AI assistants with your Docmost account via OAuth.", "Use your API key for authentication. You can manage API keys in your account settings.": "Use your API key for authentication. You can manage API keys in your account settings.",
"Enforce OAuth": "Enforce OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.",
"Toggle enforce OAuth for MCP": "Toggle enforce OAuth for MCP",
"Supported tools": "Supported tools", "Supported tools": "Supported tools",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Your workspace has MCP enabled. Use your API key to connect AI assistants.",
"MCP server URL:": "MCP server URL:", "MCP server URL:": "MCP server URL:",
"Learn more": "Learn more", "Learn more": "Learn more",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.", "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.",
@@ -829,7 +824,7 @@
"Choose how this page should stay accurate.": "Choose how this page should stay accurate.", "Choose how this page should stay accurate.": "Choose how this page should stay accurate.",
"Recurring verification": "Recurring verification", "Recurring verification": "Recurring verification",
"Verifiers re-confirm this page on a schedule.": "Verifiers re-confirm this page on a schedule.", "Verifiers re-confirm this page on a schedule.": "Verifiers re-confirm this page on a schedule.",
"Re-verify on a schedule (e.g every 30 days )": "Re-verify on a schedule (e.g. every 30 days)", "Re-verify on a schedule (e.g every 30 days )": "Re-verify on a schedule (e.g every 30 days )",
"Page stays editable at all times": "Page stays editable at all times", "Page stays editable at all times": "Page stays editable at all times",
"Best for runbooks, FAQs, living documentation": "Best for runbooks, FAQs, living documentation", "Best for runbooks, FAQs, living documentation": "Best for runbooks, FAQs, living documentation",
"Approval workflow": "Approval workflow", "Approval workflow": "Approval workflow",
@@ -905,7 +900,7 @@
"Publish": "Publish", "Publish": "Publish",
"Security": "Security", "Security": "Security",
"Enforce SSO": "Enforce SSO", "Enforce SSO": "Enforce SSO",
"Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to log in with email and password.", "Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to login with email and password.",
"AI-generated content may not be accurate.": "AI-generated content may not be accurate.", "AI-generated content may not be accurate.": "AI-generated content may not be accurate.",
"AI Chat": "AI Chat", "AI Chat": "AI Chat",
"Analyze for insights": "Analyze for insights", "Analyze for insights": "Analyze for insights",
@@ -995,8 +990,6 @@
"Are you sure you want to delete this template?": "Are you sure you want to delete this template?", "Are you sure you want to delete this template?": "Are you sure you want to delete this template?",
"Template scope updated": "Template scope updated", "Template scope updated": "Template scope updated",
"Choose which space this template belongs to": "Choose which space this template belongs to", "Choose which space this template belongs to": "Choose which space this template belongs to",
"Only workspace admins can create global templates.": "Only workspace admins can create global templates.",
"You need edit access to this space to create templates.": "You need edit access to this space to create templates.",
"Scope": "Scope", "Scope": "Scope",
"Select scope": "Select scope", "Select scope": "Select scope",
"Title": "Title", "Title": "Title",
@@ -1158,184 +1151,5 @@
"Create property": "Create property", "Create property": "Create property",
"Hide properties": "Hide properties", "Hide properties": "Hide properties",
"Find a property type": "Find a property type", "Find a property type": "Find a property type",
"Properties": "Properties", "Properties": "Properties"
"A property with this name already exists": "A property with this name already exists",
"Add card": "Add card",
"Add filter": "Add filter",
"Add option": "Add option",
"Add sort": "Add sort",
"Add view": "Add view",
"All data in this column will be lost.": "All data in this column will be lost.",
"Allow multiple people": "Allow multiple people",
"Alphabetize": "Alphabetize",
"Are you sure you want to delete": "Are you sure you want to delete",
"Ascending": "Ascending",
"Base deleted": "Base deleted",
"Base name": "Base name",
"Base table": "Base table",
"Card properties": "Card properties",
"Cells longer than the Text limit will be truncated and the extra content permanently lost.": "Cells longer than the Text limit will be truncated and the extra content permanently lost.",
"Checkbox": "Checkbox",
"Checked by default": "Checked by default",
"Choose a property": "Choose a property",
"Clear selection": "Clear selection",
"Column options": "Column options",
"Comma, period": "Comma, period",
"Complete": "Complete",
"Contains": "Contains",
"Copy link to view": "Copy link to view",
"Create a status property": "Create a status property",
"Created at": "Created at",
"Currency": "Currency",
"Decimal places": "Decimal places",
"Default value": "Default value",
"Delete property": "Delete property",
"Delete view": "Delete view",
"Delete {{count}} rows?_one": "Delete 1 row?",
"Delete {{count}} rows?_other": "Delete {{count}} rows?",
"Descending": "Descending",
"Discard": "Discard",
"Doesn't contain": "Doesn't contain",
"Done": "Done",
"Edit formula": "Edit formula",
"Edit property": "Edit property",
"Export CSV": "Export CSV",
"Failed to load base": "Failed to load base",
"False": "False",
"File": "File",
"Filter and sort changes are visible only to you": "Filter and sort changes are visible only to you",
"Filter by": "Filter by",
"Formula": "Formula",
"Get started with": "Get started with",
"Greater than": "Greater than",
"Group by": "Group by",
"Group this board by a select or status property.": "Group this board by a select or status property.",
"Hide all": "Hide all",
"Hide group": "Hide group",
"In Progress": "In Progress",
"In progress": "In progress",
"Include time": "Include time",
"Insert a kanban board on this page": "Insert a kanban board on this page",
"Insert an inline base on this page": "Insert an inline base on this page",
"Is": "Is",
"Is after": "Is after",
"Is any of": "Is any of",
"Is before": "Is before",
"Is empty": "Is empty",
"Is none of": "Is none of",
"Is not": "Is not",
"Is not empty": "Is not empty",
"Is on or after": "Is on or after",
"Is on or before": "Is on or before",
"Is within": "Is within",
"Keep editing": "Keep editing",
"Last edited at": "Last edited at",
"Last edited by": "Last edited by",
"Less than": "Less than",
"Link copied to clipboard": "Link copied to clipboard",
"Local": "Local",
"Long text": "Long text",
"Moved card to {{column}}": "Moved card to {{column}}",
"Moved column to {{column}}": "Moved column to {{column}}",
"Multi-select": "Multi-select",
"New row": "New row",
"Next month": "Next month",
"Next week": "Next week",
"Next year": "Next year",
"No filters applied": "No filters applied",
"No match": "No match",
"No options for this property type": "No options for this property type",
"No sorts applied": "No sorts applied",
"No value": "No value",
"None": "None",
"Not started": "Not started",
"Number": "Number",
"One month ago": "One month ago",
"One month from now": "In one month",
"One week ago": "One week ago",
"One week from now": "One week from now",
"Open as page": "Open as page",
"Options": "Options",
"Past month": "Past month",
"Past week": "Past week",
"Past year": "Past year",
"Percent": "Percent",
"Period, comma": "Period, comma",
"Person": "Person",
"Please enter a valid email": "Please enter a valid email",
"Progress": "Progress",
"Property options": "Property options",
"Relative": "Relative",
"Rename property": "Rename property",
"Reset": "Reset",
"Save for everyone": "Save for everyone",
"Select": "Select",
"Select a property": "Select a property",
"Select all loaded rows": "Select all loaded rows",
"Show all": "Show all",
"Sort": "Sort",
"Sort by": "Sort by",
"Space, comma": "Space, comma",
"Space, period": "Space, period",
"This board has no grouping property yet.": "This board has no grouping property yet.",
"This month": "This month",
"This year": "This year",
"Thousands and decimal separators": "Thousands and decimal separators",
"Time format": "Time format",
"To Do": "To Do",
"Tomorrow": "Tomorrow",
"True": "True",
"Unsaved changes": "Unsaved changes",
"Untitled base": "Untitled base",
"URL": "URL",
"Value": "Value",
"View updated for everyone": "View updated for everyone",
"You have unsaved changes. Do you want to discard them?": "You have unsaved changes. Do you want to discard them?",
"{{count}} rows deleted_one": "1 row deleted",
"{{count}} rows deleted_other": "{{count}} rows deleted",
"{{count}} selected_one": "1 selected",
"{{count}} selected_other": "{{count}} selected",
"Compare": "Compare",
"Compare versions": "Compare versions",
"Select version from {{date}}": "Select version from {{date}}",
"Version actions for {{date}}": "Version actions for {{date}}",
"Comparing {{newer}} and {{older}}": "Comparing {{newer}} and {{older}}",
"Exit compare": "Exit compare",
"Search attachments...": "Search attachments...",
"Error loading attachments.": "Error loading attachments.",
"No attachments on this page yet.": "No attachments on this page yet.",
"Uploaded by {{name}}": "Uploaded by {{name}}",
"Download {{name}}": "Download {{name}}",
"Access revoked": "Access revoked",
"Authorize application": "Authorize application",
"{{name}} wants to access {{workspace}}": "{{name}} wants to access {{workspace}}",
"Not you? Switch account": "Not you? Switch account",
"This application will be able to:": "This application will be able to:",
"Write": "Write",
"Invalid authorization request": "Invalid authorization request",
"Authorize": "Authorize",
"Application": "Application",
"Permissions": "Permissions",
"Authorized": "Authorized",
"Revoke access": "Revoke access",
"Revoke access for {{name}}": "Revoke access for {{name}}",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.",
"Something went wrong. Please try again.": "Something went wrong. Please try again.",
"Remove {{name}}": "Remove {{name}}",
"Make sure you trust this application before authorizing it.": "Make sure you trust this application before authorizing it.",
"You will be redirected to": "You will be redirected to",
"View content without making changes.": "View content without making changes.",
"Create and modify content.": "Create and modify content.",
"Applications and AI assistants you have authorized to access your account.": "Applications and AI assistants you have authorized to access your account.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.",
"Authorized apps": "Authorized apps",
"No authorized apps yet.": "No authorized apps yet.",
"Workspace knowledge only": "Workspace knowledge only",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.",
"Toggle workspace knowledge only": "Toggle workspace knowledge only",
"Read-only mode": "Read-only mode",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat can search and read workspace content, but cannot create or edit pages.",
"Toggle AI Chat read-only mode": "Toggle AI Chat read-only mode",
"Title only": "Title only",
"you": "you"
} }
@@ -41,8 +41,6 @@
"Dark": "Oscuro", "Dark": "Oscuro",
"Date": "Fecha", "Date": "Fecha",
"Delete": "Eliminar", "Delete": "Eliminar",
"Remove from page": "Quitar de la página",
"Base options": "Opciones de la base",
"Delete group": "Eliminar grupo", "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.", "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", "Description": "Descripción",
@@ -78,24 +76,6 @@
"Failed to import pages": "No se pudieron importar las páginas", "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 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", "Failed to update data": "No se pudo actualizar los datos",
"Failed to create base": "No se pudo crear la base",
"Failed to update base": "No se pudo actualizar la base",
"Failed to delete base": "No se pudo eliminar la base",
"Failed to create property": "No se pudo crear la propiedad",
"Failed to update property": "No se pudo actualizar la propiedad",
"Failed to delete property": "No se pudo eliminar la propiedad",
"Failed to reorder property": "No se pudo reordenar la propiedad",
"Failed to create view": "No se pudo crear la vista",
"Failed to update view": "No se pudo actualizar la vista",
"Failed to delete view": "No se pudo eliminar la vista",
"Failed to create row": "No se pudo crear la fila",
"Failed to update row": "No se pudo actualizar la fila",
"Failed to delete row": "No se pudo eliminar la fila",
"Failed to delete rows": "No se pudieron eliminar las filas",
"Failed to reorder row": "No se pudo reordenar la fila",
"Failed to move card": "No se pudo mover la tarjeta",
"Failed to add card": "No se pudo añadir la tarjeta",
"Failed to export CSV": "No se pudo exportar el CSV",
"Favorite spaces": "Espacios favoritos", "Favorite spaces": "Espacios favoritos",
"Favorite spaces appear here": "Los espacios favoritos aparecen aquí", "Favorite spaces appear here": "Los espacios favoritos aparecen aquí",
"Favorites": "Favoritos", "Favorites": "Favoritos",
@@ -294,7 +274,6 @@
"Export space": "Exportar espacio", "Export space": "Exportar espacio",
"Export {{type}}": "Exportar {{type}}", "Export {{type}}": "Exportar {{type}}",
"File exceeds the {{limit}} attachment limit": "El archivo supera el límite de {{limit}} adjuntos", "File exceeds the {{limit}} attachment limit": "El archivo supera el límite de {{limit}} adjuntos",
"Media": "Multimedia",
"Align left": "Alinear a la izquierda", "Align left": "Alinear a la izquierda",
"Align right": "Alinear a la derecha", "Align right": "Alinear a la derecha",
"Align center": "Alinear al centro", "Align center": "Alinear al centro",
@@ -388,8 +367,6 @@
"Insert horizontal rule divider": "Insertar regla horizontal", "Insert horizontal rule divider": "Insertar regla horizontal",
"Page break": "Salto de página", "Page break": "Salto de página",
"Insert a page break for printing.": "Inserta un salto de página para imprimir.", "Insert a page break for printing.": "Inserta un salto de página para imprimir.",
"Footnote": "Nota al pie",
"Insert a footnote reference.": "Insertar una referencia de nota al pie.",
"Upload any image from your device.": "Sube cualquier imagen desde tu dispositivo.", "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 video from your device.": "Sube cualquier video desde tu dispositivo.",
"Upload any audio from your device.": "Sube cualquier audio desde tu dispositivo.", "Upload any audio from your device.": "Sube cualquier audio desde tu dispositivo.",
@@ -421,8 +398,6 @@
"Insert mermaid diagram": "Insertar diagrama Mermaid", "Insert mermaid diagram": "Insertar diagrama Mermaid",
"Insert and design Drawio diagrams": "Insertar y diseñar diagramas de Drawio", "Insert and design Drawio diagrams": "Insertar y diseñar diagramas de Drawio",
"Insert current date": "Insertar fecha actual", "Insert current date": "Insertar fecha actual",
"Time": "Hora",
"Insert current time": "Insertar hora actual",
"Draw and sketch excalidraw diagrams": "Dibujar y crear bocetos de diagramas de Excalidraw", "Draw and sketch excalidraw diagrams": "Dibujar y crear bocetos de diagramas de Excalidraw",
"Multiple": "Múltiple", "Multiple": "Múltiple",
"Turn into": "Convertir en", "Turn into": "Convertir en",
@@ -449,7 +424,6 @@
"Names do not match": "Los nombres no coinciden", "Names do not match": "Los nombres no coinciden",
"Today, {{time}}": "Hoy, {{time}}", "Today, {{time}}": "Hoy, {{time}}",
"Yesterday, {{time}}": "Ayer, {{time}}", "Yesterday, {{time}}": "Ayer, {{time}}",
"now": "ahora",
"Space created successfully": "Espacio creado correctamente", "Space created successfully": "Espacio creado correctamente",
"Space updated successfully": "Espacio actualizado correctamente", "Space updated successfully": "Espacio actualizado correctamente",
"Space deleted successfully": "Espacio eliminado correctamente", "Space deleted successfully": "Espacio eliminado correctamente",
@@ -622,8 +596,6 @@
"Deleted by": "Eliminado por", "Deleted by": "Eliminado por",
"Deleted at": "Eliminado el", "Deleted at": "Eliminado el",
"Preview": "Vista previa", "Preview": "Vista previa",
"Base preview unavailable": "Vista previa de la base no disponible",
"Restore this base to view its contents.": "Restaura esta base para ver su contenido.",
"Subpages": "Subpáginas", "Subpages": "Subpáginas",
"Failed to load subpages": "No se pudieron cargar las subpáginas", "Failed to load subpages": "No se pudieron cargar las subpáginas",
"No subpages": "No hay subpáginas", "No subpages": "No hay subpáginas",
@@ -699,7 +671,7 @@
"Upgrade your plan": "Mejora tu plan", "Upgrade your plan": "Mejora tu plan",
"Available with a paid license": "Disponible con una licencia de pago", "Available with a paid license": "Disponible con una licencia de pago",
"Upgrade your license tier.": "Mejora el nivel de tu licencia.", "Upgrade your license tier.": "Mejora el nivel de tu licencia.",
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "La IA está disponible en las ediciones de pago de Docmost. Contacte con sales@docmost.com.", "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "La IA solo está disponible en la edición empresarial de Docmost. Contacte con sales@docmost.com.",
"AI & MCP": "IA y MCP", "AI & MCP": "IA y MCP",
"AI": "IA", "AI": "IA",
"MCP": "MCP", "MCP": "MCP",
@@ -707,11 +679,9 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Habilite el servidor MCP para permitir que asistentes de IA y herramientas interactúen con el contenido de su espacio de trabajo.", "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Habilite el servidor MCP para permitir que asistentes de IA y herramientas interactúen con el contenido de su espacio de trabajo.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP solo está disponible en la edición empresarial de Docmost. Contacte con sales@docmost.com.", "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP solo está disponible en la edición empresarial de Docmost. Contacte con sales@docmost.com.",
"MCP Server URL": "URL del servidor MCP", "MCP Server URL": "URL del servidor MCP",
"Connect AI assistants with your Docmost account via OAuth.": "Conecta asistentes de IA con tu cuenta de Docmost mediante OAuth.", "Use your API key for authentication. You can manage API keys in your account settings.": "Use su clave API para la autenticación. Puede gestionar las claves API en la configuración de su cuenta.",
"Enforce OAuth": "Enforce OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "Los asistentes de IA deben conectarse con una cuenta de Docmost mediante OAuth. No se pueden usar claves API con el servidor MCP.",
"Toggle enforce OAuth for MCP": "Toggle enforce OAuth for MCP",
"Supported tools": "Herramientas compatibles", "Supported tools": "Herramientas compatibles",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Su espacio de trabajo tiene MCP habilitado. Use su clave API para conectar asistentes de IA.",
"MCP server URL:": "URL del servidor MCP:", "MCP server URL:": "URL del servidor MCP:",
"Learn more": "Más información", "Learn more": "Más información",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gestiona las claves de API para todos los usuarios en el espacio de trabajo. Consulta la <anchor>documentación de la API</anchor> para detalles de uso.", "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gestiona las claves de API para todos los usuarios en el espacio de trabajo. Consulta la <anchor>documentación de la API</anchor> para detalles de uso.",
@@ -995,8 +965,6 @@
"Are you sure you want to delete this template?": "¿Seguro que quieres eliminar esta plantilla?", "Are you sure you want to delete this template?": "¿Seguro que quieres eliminar esta plantilla?",
"Template scope updated": "Alcance de la plantilla actualizado", "Template scope updated": "Alcance de la plantilla actualizado",
"Choose which space this template belongs to": "Elige a qué espacio pertenece esta plantilla", "Choose which space this template belongs to": "Elige a qué espacio pertenece esta plantilla",
"Only workspace admins can create global templates.": "Solo los administradores del espacio de trabajo pueden crear plantillas globales.",
"You need edit access to this space to create templates.": "Necesitas acceso de edición a este espacio para crear plantillas.",
"Scope": "Alcance", "Scope": "Alcance",
"Select scope": "Seleccionar alcance", "Select scope": "Seleccionar alcance",
"Title": "Título", "Title": "Título",
@@ -1009,7 +977,7 @@
"Search pages and spaces...": "Buscar páginas y espacios...", "Search pages and spaces...": "Buscar páginas y espacios...",
"No results found": "No se encontraron resultados", "No results found": "No se encontraron resultados",
"You don't have permission to create pages here": "No tienes permiso para crear páginas aquí", "You don't have permission to create pages here": "No tienes permiso para crear páginas aquí",
"Chat menu for {{title}}": "Menú del chat de {{title}}", "Chat menu": "Menú del chat",
"API key menu": "Menú de la clave API", "API key menu": "Menú de la clave API",
"Jump to comment selection": "Ir a la selección de comentarios", "Jump to comment selection": "Ir a la selección de comentarios",
"Slash commands": "Comandos de barra", "Slash commands": "Comandos de barra",
@@ -1095,7 +1063,7 @@
"Filter": "Filtrar", "Filter": "Filtrar",
"Page title": "Título de la página", "Page title": "Título de la página",
"Page content": "Contenido de la página", "Page content": "Contenido de la página",
"Member actions for {{name}}": "Acciones de miembro para {{name}}", "Member actions": "Acciones de miembro",
"Toggle password visibility": "Alternar visibilidad de la contraseña", "Toggle password visibility": "Alternar visibilidad de la contraseña",
"Send comment": "Enviar comentario", "Send comment": "Enviar comentario",
"Token actions": "Acciones de token", "Token actions": "Acciones de token",
@@ -1116,226 +1084,5 @@
"Added {{name}} to favorites": "Se agregó {{name}} a favoritos", "Added {{name}} to favorites": "Se agregó {{name}} a favoritos",
"Removed {{name}} from favorites": "Se quitó {{name}} de favoritos", "Removed {{name}} from favorites": "Se quitó {{name}} de favoritos",
"Page menu for {{name}}": "Menú de página para {{name}}", "Page menu for {{name}}": "Menú de página para {{name}}",
"Create subpage of {{name}}": "Crear subpágina de {{name}}", "Create subpage of {{name}}": "Crear subpágina de {{name}}"
"Allow personal spaces": "Permitir espacios personales",
"Members can create their own personal space.": "Los miembros pueden crear su propio espacio personal.",
"Toggle allow personal spaces": "Alternar permitir espacios personales",
"Create personal space": "Crear espacio personal",
"Personal space": "Espacio personal",
"{{name}}'s space": "Espacio de {{name}}",
"Apply": "Aplicar",
"Cells that aren't already a page reference will be cleared.": "Las celdas que aún no sean una referencia de página se borrarán.",
"Cells that aren't a valid URL will be cleared.": "Las celdas que no sean una URL válida se borrarán.",
"Cells that aren't a valid email address will be cleared.": "Las celdas que no sean una dirección de correo electrónico válida se borrarán.",
"Cells that can't be parsed as a date will be cleared.": "Las celdas que no puedan interpretarse como una fecha se borrarán.",
"Cells that can't be parsed as a number will be cleared.": "Las celdas que no puedan interpretarse como un número se borrarán.",
"Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).": "Las celdas se convertirán de forma forzada (yes/true/1 pasarán a estar marcadas; todo lo demás pasará a estar desmarcado o se borrará).",
"Cells will be reinterpreted under the new type.": "Las celdas se reinterpretarán según el nuevo tipo.",
"Cells will be replaced with a comma-separated list of file names.": "Las celdas se reemplazarán por una lista de nombres de archivo separados por comas.",
"Cells will be replaced with a comma-separated list of option names.": "Las celdas se reemplazarán por una lista de nombres de opciones separados por comas.",
"Cells will be replaced with the option name.": "Las celdas se reemplazarán por el nombre de la opción.",
"Cells will be replaced with the page title.": "Las celdas se reemplazarán por el título de la página.",
"Cells will be replaced with the person's name.": "Las celdas se reemplazarán por el nombre de la persona.",
"Change type": "Cambiar tipo",
"Change type to {{label}}?": "¿Cambiar el tipo a {{label}}?",
"Converting…": "Convirtiendo…",
"Existing values become single-item lists. No data is lost.": "Los valores existentes se convierten en listas de un solo elemento. No se pierde ningún dato.",
"Only the first selected item per row will be kept; the rest will be discarded.": "Solo se conservará el primer elemento seleccionado por fila; el resto se descartará.",
"Previous record": "Registro anterior",
"Next record": "Siguiente registro",
"Record actions": "Acciones del registro",
"Delete record": "Eliminar registro",
"Delete record?": "¿Eliminar registro?",
"This action cannot be undone.": "Esta acción no se puede deshacer.",
"to navigate": "para navegar",
"to close": "para cerrar",
"Expand row {{number}}": "Expandir fila {{number}}",
"Saving…": "Guardando…",
"Read-only": "Solo lectura",
"Loading…": "Cargando…",
"Updated {{when}}": "Actualizado {{when}}",
"Add property": "Añadir propiedad",
"Create property": "Crear propiedad",
"Hide properties": "Ocultar propiedades",
"Find a property type": "Buscar un tipo de propiedad",
"Properties": "Propiedades",
"A property with this name already exists": "Ya existe una propiedad con este nombre",
"Add card": "Agregar tarjeta",
"Add filter": "Agregar filtro",
"Add option": "Agregar opción",
"Add sort": "Agregar orden",
"Add view": "Agregar vista",
"All data in this column will be lost.": "Todos los datos de esta columna se perderán.",
"Allow multiple people": "Permitir varias personas",
"Alphabetize": "Ordenar alfabéticamente",
"Are you sure you want to delete": "¿Está seguro de que desea eliminar",
"Ascending": "Ascendente",
"Base deleted": "Base eliminada",
"Base name": "Nombre de la base",
"Base table": "Tabla base",
"Card properties": "Propiedades de la tarjeta",
"Cells longer than the Text limit will be truncated and the extra content permanently lost.": "Las celdas más largas que el límite de texto se truncarán y el contenido adicional se perderá permanentemente.",
"Checkbox": "Casilla de verificación",
"Checked by default": "Marcado por defecto",
"Choose a property": "Seleccione una propiedad",
"Clear selection": "Borrar selección",
"Column options": "Opciones de columna",
"Comma, period": "Coma, punto",
"Complete": "Completo",
"Contains": "Contiene",
"Copy link to view": "Copiar enlace a la vista",
"Create a status property": "Crear una propiedad de estado",
"Created at": "Creado el",
"Currency": "Moneda",
"Decimal places": "Decimales",
"Default value": "Valor predeterminado",
"Delete property": "Eliminar propiedad",
"Delete view": "Eliminar vista",
"Delete {{count}} rows?_one": "¿Eliminar 1 fila?",
"Delete {{count}} rows?_other": "¿Eliminar {{count}} filas?",
"Descending": "Descendente",
"Discard": "Descartar",
"Doesn't contain": "No contiene",
"Done": "Hecho",
"Edit formula": "Editar fórmula",
"Edit property": "Editar propiedad",
"Export CSV": "Exportar CSV",
"Failed to load base": "No se pudo cargar la base",
"False": "Falso",
"File": "Archivo",
"Filter and sort changes are visible only to you": "Los cambios de filtro y ordenación solo son visibles para usted",
"Filter by": "Filtrar por",
"Formula": "Fórmula",
"Get started with": "Comenzar con",
"Greater than": "Mayor que",
"Group by": "Agrupar por",
"Group this board by a select or status property.": "Agrupe este tablero por una propiedad de selección o estado.",
"Hide all": "Ocultar todo",
"Hide group": "Ocultar grupo",
"In Progress": "En curso",
"In progress": "En curso",
"Include time": "Incluir hora",
"Insert a kanban board on this page": "Insertar un tablero kanban en esta página",
"Insert an inline base on this page": "Insertar una base integrada en esta página",
"Is": "Es",
"Is after": "Es posterior a",
"Is any of": "Es cualquiera de",
"Is before": "Es anterior a",
"Is empty": "Está vacío",
"Is none of": "No es ninguno de",
"Is not": "No es",
"Is not empty": "No está vacío",
"Is on or after": "Es en o después de",
"Is on or before": "Es en o antes de",
"Is within": "Está dentro de",
"Keep editing": "Seguir editando",
"Last edited at": "Última edición el",
"Last edited by": "Última edición por",
"Less than": "Menor que",
"Link copied to clipboard": "Enlace copiado al portapapeles",
"Local": "Local",
"Long text": "Texto largo",
"Moved card to {{column}}": "Tarjeta movida a {{column}}",
"Moved column to {{column}}": "Columna movida a {{column}}",
"Multi-select": "Selección múltiple",
"New row": "Nueva fila",
"Next month": "Próximo mes",
"Next week": "Próxima semana",
"Next year": "Próximo año",
"No filters applied": "No hay filtros aplicados",
"No match": "Sin coincidencias",
"No options for this property type": "No hay opciones para este tipo de propiedad",
"No sorts applied": "No hay ordenaciones aplicadas",
"No value": "Sin valor",
"None": "Ninguno",
"Not started": "No iniciado",
"Number": "Número",
"One month ago": "Hace un mes",
"One month from now": "Dentro de un mes",
"One week ago": "Hace una semana",
"One week from now": "Dentro de una semana",
"Open as page": "Abrir como página",
"Options": "Opciones",
"Past month": "Último mes",
"Past week": "Última semana",
"Past year": "Último año",
"Percent": "Porcentaje",
"Period, comma": "Punto, coma",
"Person": "Persona",
"Please enter a valid email": "Por favor, introduzca un correo electrónico válido",
"Progress": "Progreso",
"Property options": "Opciones de propiedad",
"Relative": "Relativo",
"Rename property": "Renombrar propiedad",
"Reset": "Restablecer",
"Save for everyone": "Guardar para todos",
"Select": "Selección",
"Select a property": "Seleccionar una propiedad",
"Select all loaded rows": "Seleccionar todas las filas cargadas",
"Show all": "Mostrar todo",
"Sort": "Ordenar",
"Sort by": "Ordenar por",
"Space, comma": "Espacio, coma",
"Space, period": "Espacio, punto",
"This board has no grouping property yet.": "Este tablero aún no tiene una propiedad de agrupación.",
"This month": "Este mes",
"This year": "Este año",
"Thousands and decimal separators": "Separadores de miles y decimales",
"Time format": "Formato de hora",
"To Do": "Por hacer",
"Tomorrow": "Mañana",
"True": "Verdadero",
"Unsaved changes": "Cambios no guardados",
"Untitled base": "Base sin título",
"URL": "URL",
"Value": "Valor",
"View updated for everyone": "Vista actualizada para todos",
"You have unsaved changes. Do you want to discard them?": "Tiene cambios no guardados. ¿Quiere descartarlos?",
"{{count}} rows deleted_one": "1 fila eliminada",
"{{count}} rows deleted_other": "{{count}} filas eliminadas",
"{{count}} selected_one": "1 seleccionado",
"{{count}} selected_other": "{{count}} seleccionadas",
"Compare": "Comparar",
"Compare versions": "Comparar versiones",
"Select version from {{date}}": "Seleccionar la versión del {{date}}",
"Version actions for {{date}}": "Acciones de la versión del {{date}}",
"Comparing {{newer}} and {{older}}": "Comparando {{newer}} y {{older}}",
"Exit compare": "Salir de la comparación",
"Search attachments...": "Buscar archivos adjuntos...",
"Error loading attachments.": "Error al cargar los archivos adjuntos.",
"No attachments on this page yet.": "Todavía no hay archivos adjuntos en esta página.",
"Uploaded by {{name}}": "Subido por {{name}}",
"Download {{name}}": "Descargar {{name}}",
"Access revoked": "Acceso revocado",
"Authorize application": "Autorizar aplicación",
"{{name}} wants to access {{workspace}}": "{{name}} quiere acceder a {{workspace}}",
"Not you? Switch account": "¿No eres tú? Cambiar de cuenta",
"This application will be able to:": "Esta aplicación podrá:",
"Write": "Escribir",
"Invalid authorization request": "Solicitud de autorización no válida",
"Authorize": "Autorizar",
"Application": "Aplicación",
"Permissions": "Permisos",
"Authorized": "Autorizado",
"Revoke access": "Revocar acceso",
"Revoke access for {{name}}": "Revocar acceso para {{name}}",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "¿Seguro que quieres revocar el acceso de {{name}}? La aplicación ya no podrá acceder a tu cuenta.",
"Something went wrong. Please try again.": "Algo salió mal. Inténtalo de nuevo.",
"Remove {{name}}": "Eliminar {{name}}",
"Make sure you trust this application before authorizing it.": "Asegúrate de confiar en esta aplicación antes de autorizarla.",
"You will be redirected to": "Serás redirigido a",
"View content without making changes.": "Ver contenido sin realizar cambios.",
"Create and modify content.": "Crear y modificar contenido.",
"Applications and AI assistants you have authorized to access your account.": "Aplicaciones y asistentes de IA que has autorizado para acceder a tu cuenta.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "Tu espacio de trabajo tiene MCP habilitado. Conecta asistentes de IA con tu cuenta de Docmost mediante OAuth.",
"Authorized apps": "Aplicaciones autorizadas",
"No authorized apps yet.": "Todavía no hay aplicaciones autorizadas.",
"Workspace knowledge only": "Solo conocimiento del espacio de trabajo",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Restringe AI Chat para que responda solo con información de las páginas y archivos subidos de tu espacio de trabajo. No usará conocimiento externo.",
"Toggle workspace knowledge only": "Alternar solo conocimiento del espacio de trabajo",
"Read-only mode": "Modo de solo lectura",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat puede buscar y leer el contenido del espacio de trabajo, pero no puede crear ni editar páginas.",
"Toggle AI Chat read-only mode": "Alternar modo de solo lectura de AI Chat",
"Title only": "Solo título",
"you": "tú"
} }
@@ -41,8 +41,6 @@
"Dark": "Sombre", "Dark": "Sombre",
"Date": "Date", "Date": "Date",
"Delete": "Supprimer", "Delete": "Supprimer",
"Remove from page": "Supprimer de la page",
"Base options": "Options de la base",
"Delete group": "Supprimer groupe", "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.", "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", "Description": "Description",
@@ -78,24 +76,6 @@
"Failed to import pages": "Échec de l'importation des pages", "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 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", "Failed to update data": "Échec de la mise à jour des données",
"Failed to create base": "Échec de la création de la base",
"Failed to update base": "Échec de la mise à jour de la base",
"Failed to delete base": "Échec de la suppression de la base",
"Failed to create property": "Échec de la création de la propriété",
"Failed to update property": "Échec de la mise à jour de la propriété",
"Failed to delete property": "Échec de la suppression de la propriété",
"Failed to reorder property": "Échec de la réorganisation de la propriété",
"Failed to create view": "Échec de la création de la vue",
"Failed to update view": "Échec de la mise à jour de la vue",
"Failed to delete view": "Échec de la suppression de la vue",
"Failed to create row": "Échec de la création de la ligne",
"Failed to update row": "Échec de la mise à jour de la ligne",
"Failed to delete row": "Échec de la suppression de la ligne",
"Failed to delete rows": "Échec de la suppression des lignes",
"Failed to reorder row": "Échec de la réorganisation de la ligne",
"Failed to move card": "Échec du déplacement de la carte",
"Failed to add card": "Échec de l'ajout de la carte",
"Failed to export CSV": "Échec de l'exportation du CSV",
"Favorite spaces": "Espaces favoris", "Favorite spaces": "Espaces favoris",
"Favorite spaces appear here": "Les espaces favoris apparaissent ici", "Favorite spaces appear here": "Les espaces favoris apparaissent ici",
"Favorites": "Favoris", "Favorites": "Favoris",
@@ -294,7 +274,6 @@
"Export space": "Exporter l'espace", "Export space": "Exporter l'espace",
"Export {{type}}": "Exporter {{type}}", "Export {{type}}": "Exporter {{type}}",
"File exceeds the {{limit}} attachment limit": "Le fichier dépasse la limite de {{limit}} pièces jointes", "File exceeds the {{limit}} attachment limit": "Le fichier dépasse la limite de {{limit}} pièces jointes",
"Media": "Médias",
"Align left": "Aligner à gauche", "Align left": "Aligner à gauche",
"Align right": "Aligner à droite", "Align right": "Aligner à droite",
"Align center": "Aligner au centre", "Align center": "Aligner au centre",
@@ -388,8 +367,6 @@
"Insert horizontal rule divider": "Insérer un séparateur de règle horizontale", "Insert horizontal rule divider": "Insérer un séparateur de règle horizontale",
"Page break": "Saut de page", "Page break": "Saut de page",
"Insert a page break for printing.": "Insérer un saut de page pour limpression.", "Insert a page break for printing.": "Insérer un saut de page pour limpression.",
"Footnote": "Note de bas de page",
"Insert a footnote reference.": "Insérer une référence de note de bas de page.",
"Upload any image from your device.": "Téléchargez n'importe quelle image depuis votre appareil.", "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 video from your device.": "Téléchargez n'importe quelle vidéo depuis votre appareil.",
"Upload any audio from your device.": "Téléchargez n'importe quel fichier audio depuis votre appareil.", "Upload any audio from your device.": "Téléchargez n'importe quel fichier audio depuis votre appareil.",
@@ -421,8 +398,6 @@
"Insert mermaid diagram": "Insérer un diagramme Mermaid", "Insert mermaid diagram": "Insérer un diagramme Mermaid",
"Insert and design Drawio diagrams": "Insérer et concevoir des diagrammes Drawio", "Insert and design Drawio diagrams": "Insérer et concevoir des diagrammes Drawio",
"Insert current date": "Insérer la date actuelle", "Insert current date": "Insérer la date actuelle",
"Time": "Heure",
"Insert current time": "Insérer l'heure actuelle",
"Draw and sketch excalidraw diagrams": "Dessiner et esquisser des diagrammes Excalidraw", "Draw and sketch excalidraw diagrams": "Dessiner et esquisser des diagrammes Excalidraw",
"Multiple": "Multiple", "Multiple": "Multiple",
"Turn into": "Transformer en", "Turn into": "Transformer en",
@@ -449,7 +424,6 @@
"Names do not match": "Les noms ne correspondent pas", "Names do not match": "Les noms ne correspondent pas",
"Today, {{time}}": "Aujourdhui, {{time}}", "Today, {{time}}": "Aujourdhui, {{time}}",
"Yesterday, {{time}}": "Hier, {{time}}", "Yesterday, {{time}}": "Hier, {{time}}",
"now": "maintenant",
"Space created successfully": "Espace créé avec succès", "Space created successfully": "Espace créé avec succès",
"Space updated successfully": "Espace mis à jour avec succès", "Space updated successfully": "Espace mis à jour avec succès",
"Space deleted successfully": "Espace supprimé avec succès", "Space deleted successfully": "Espace supprimé avec succès",
@@ -622,8 +596,6 @@
"Deleted by": "Supprimé par", "Deleted by": "Supprimé par",
"Deleted at": "Supprimé le", "Deleted at": "Supprimé le",
"Preview": "Aperçu", "Preview": "Aperçu",
"Base preview unavailable": "Aperçu de la base indisponible",
"Restore this base to view its contents.": "Restaurez cette base pour afficher son contenu.",
"Subpages": "Sous-pages", "Subpages": "Sous-pages",
"Failed to load subpages": "Échec du chargement des sous-pages", "Failed to load subpages": "Échec du chargement des sous-pages",
"No subpages": "Aucune sous-page", "No subpages": "Aucune sous-page",
@@ -699,7 +671,7 @@
"Upgrade your plan": "Mettez à niveau votre forfait", "Upgrade your plan": "Mettez à niveau votre forfait",
"Available with a paid license": "Disponible avec une licence payante", "Available with a paid license": "Disponible avec une licence payante",
"Upgrade your license tier.": "Mettez à niveau votre niveau de licence.", "Upgrade your license tier.": "Mettez à niveau votre niveau de licence.",
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "L'IA est disponible dans les éditions payantes de Docmost. Contactez sales@docmost.com.", "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "L'IA n'est disponible que dans l'édition Entreprise de Docmost. Contactez sales@docmost.com.",
"AI & MCP": "IA & MCP", "AI & MCP": "IA & MCP",
"AI": "IA", "AI": "IA",
"MCP": "MCP", "MCP": "MCP",
@@ -707,11 +679,9 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Activez le serveur MCP pour permettre aux assistants et outils IA d'interagir avec le contenu de votre espace de travail.", "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Activez le serveur MCP pour permettre aux assistants et outils IA d'interagir avec le contenu de votre espace de travail.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP n'est disponible que dans l'édition Entreprise de Docmost. Contactez sales@docmost.com.", "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP n'est disponible que dans l'édition Entreprise de Docmost. Contactez sales@docmost.com.",
"MCP Server URL": "URL du serveur MCP", "MCP Server URL": "URL du serveur MCP",
"Connect AI assistants with your Docmost account via OAuth.": "Connectez des assistants IA à votre compte Docmost via OAuth.", "Use your API key for authentication. You can manage API keys in your account settings.": "Utilisez votre clé API pour l'authentification. Vous pouvez gérer les clés API dans les paramètres de votre compte.",
"Enforce OAuth": "Enforce OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "Les assistants IA doivent se connecter avec un compte Docmost via OAuth. Les clés API ne peuvent pas être utilisées avec le serveur MCP.",
"Toggle enforce OAuth for MCP": "Toggle enforce OAuth for MCP",
"Supported tools": "Outils pris en charge", "Supported tools": "Outils pris en charge",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Votre espace de travail a MCP activé. Utilisez votre clé API pour connecter des assistants IA.",
"MCP server URL:": "URL du serveur MCP :", "MCP server URL:": "URL du serveur MCP :",
"Learn more": "En savoir plus", "Learn more": "En savoir plus",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gérez les clés API pour tous les utilisateurs de l'espace de travail. Consultez la <anchor>documentation API</anchor> pour plus de détails sur l'utilisation.", "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gérez les clés API pour tous les utilisateurs de l'espace de travail. Consultez la <anchor>documentation API</anchor> pour plus de détails sur l'utilisation.",
@@ -995,8 +965,6 @@
"Are you sure you want to delete this template?": "Êtes-vous sûr de vouloir supprimer ce modèle ?", "Are you sure you want to delete this template?": "Êtes-vous sûr de vouloir supprimer ce modèle ?",
"Template scope updated": "Portée du modèle mise à jour", "Template scope updated": "Portée du modèle mise à jour",
"Choose which space this template belongs to": "Choisissez à quel espace appartient ce modèle", "Choose which space this template belongs to": "Choisissez à quel espace appartient ce modèle",
"Only workspace admins can create global templates.": "Seuls les administrateurs de lespace de travail peuvent créer des modèles globaux.",
"You need edit access to this space to create templates.": "Vous devez disposer dun accès en modification à cet espace pour créer des modèles.",
"Scope": "Portée", "Scope": "Portée",
"Select scope": "Sélectionner la portée", "Select scope": "Sélectionner la portée",
"Title": "Titre", "Title": "Titre",
@@ -1009,7 +977,7 @@
"Search pages and spaces...": "Rechercher des pages et des espaces...", "Search pages and spaces...": "Rechercher des pages et des espaces...",
"No results found": "Aucun résultat trouvé", "No results found": "Aucun résultat trouvé",
"You don't have permission to create pages here": "Vous navez pas lautorisation de créer des pages ici", "You don't have permission to create pages here": "Vous navez pas lautorisation de créer des pages ici",
"Chat menu for {{title}}": "Menu de discussion pour {{title}}", "Chat menu": "Menu du chat",
"API key menu": "Menu de la clé API", "API key menu": "Menu de la clé API",
"Jump to comment selection": "Aller à la sélection de commentaires", "Jump to comment selection": "Aller à la sélection de commentaires",
"Slash commands": "Commandes slash", "Slash commands": "Commandes slash",
@@ -1095,7 +1063,7 @@
"Filter": "Filtrer", "Filter": "Filtrer",
"Page title": "Titre de la page", "Page title": "Titre de la page",
"Page content": "Contenu de la page", "Page content": "Contenu de la page",
"Member actions for {{name}}": "Actions du membre pour {{name}}", "Member actions": "Actions des membres",
"Toggle password visibility": "Afficher/masquer le mot de passe", "Toggle password visibility": "Afficher/masquer le mot de passe",
"Send comment": "Envoyer le commentaire", "Send comment": "Envoyer le commentaire",
"Token actions": "Actions du jeton", "Token actions": "Actions du jeton",
@@ -1116,226 +1084,5 @@
"Added {{name}} to favorites": "{{name}} a été ajouté aux favoris", "Added {{name}} to favorites": "{{name}} a été ajouté aux favoris",
"Removed {{name}} from favorites": "{{name}} a été retiré des favoris", "Removed {{name}} from favorites": "{{name}} a été retiré des favoris",
"Page menu for {{name}}": "Menu de la page pour {{name}}", "Page menu for {{name}}": "Menu de la page pour {{name}}",
"Create subpage of {{name}}": "Créer une sous-page de {{name}}", "Create subpage of {{name}}": "Créer une sous-page de {{name}}"
"Allow personal spaces": "Autoriser les espaces personnels",
"Members can create their own personal space.": "Les membres peuvent créer leur propre espace personnel.",
"Toggle allow personal spaces": "Activer/désactiver l'autorisation des espaces personnels",
"Create personal space": "Créer un espace personnel",
"Personal space": "Espace personnel",
"{{name}}'s space": "Espace de {{name}}",
"Apply": "Appliquer",
"Cells that aren't already a page reference will be cleared.": "Les cellules qui ne sont pas déjà une référence de page seront effacées.",
"Cells that aren't a valid URL will be cleared.": "Les cellules qui ne sont pas une URL valide seront effacées.",
"Cells that aren't a valid email address will be cleared.": "Les cellules qui ne sont pas une adresse e-mail valide seront effacées.",
"Cells that can't be parsed as a date will be cleared.": "Les cellules qui ne peuvent pas être interprétées comme une date seront effacées.",
"Cells that can't be parsed as a number will be cleared.": "Les cellules qui ne peuvent pas être interprétées comme un nombre seront effacées.",
"Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).": "Les cellules seront converties (yes/true/1 deviendront cochées ; tout le reste deviendra décoché ou sera effacé).",
"Cells will be reinterpreted under the new type.": "Les cellules seront réinterprétées selon le nouveau type.",
"Cells will be replaced with a comma-separated list of file names.": "Les cellules seront remplacées par une liste de noms de fichiers séparés par des virgules.",
"Cells will be replaced with a comma-separated list of option names.": "Les cellules seront remplacées par une liste de noms d'options séparés par des virgules.",
"Cells will be replaced with the option name.": "Les cellules seront remplacées par le nom de l'option.",
"Cells will be replaced with the page title.": "Les cellules seront remplacées par le titre de la page.",
"Cells will be replaced with the person's name.": "Les cellules seront remplacées par le nom de la personne.",
"Change type": "Changer le type",
"Change type to {{label}}?": "Changer le type en {{label}} ?",
"Converting…": "Conversion…",
"Existing values become single-item lists. No data is lost.": "Les valeurs existantes deviennent des listes à un seul élément. Aucune donnée n'est perdue.",
"Only the first selected item per row will be kept; the rest will be discarded.": "Seul le premier élément sélectionné par ligne sera conservé ; les autres seront ignorés.",
"Previous record": "Enregistrement précédent",
"Next record": "Enregistrement suivant",
"Record actions": "Actions de l'enregistrement",
"Delete record": "Supprimer l'enregistrement",
"Delete record?": "Supprimer l'enregistrement ?",
"This action cannot be undone.": "Cette action ne peut pas être annulée.",
"to navigate": "pour naviguer",
"to close": "pour fermer",
"Expand row {{number}}": "Développer la ligne {{number}}",
"Saving…": "Enregistrement…",
"Read-only": "Lecture seule",
"Loading…": "Chargement…",
"Updated {{when}}": "Mis à jour {{when}}",
"Add property": "Ajouter une propriété",
"Create property": "Créer une propriété",
"Hide properties": "Masquer les propriétés",
"Find a property type": "Rechercher un type de propriété",
"Properties": "Propriétés",
"A property with this name already exists": "Une propriété portant ce nom existe déjà",
"Add card": "Ajouter une carte",
"Add filter": "Ajouter un filtre",
"Add option": "Ajouter une option",
"Add sort": "Ajouter un tri",
"Add view": "Ajouter une vue",
"All data in this column will be lost.": "Toutes les données de cette colonne seront perdues.",
"Allow multiple people": "Autoriser plusieurs personnes",
"Alphabetize": "Trier par ordre alphabétique",
"Are you sure you want to delete": "Êtes-vous sûr de vouloir supprimer",
"Ascending": "Croissant",
"Base deleted": "Base supprimée",
"Base name": "Nom de la base",
"Base table": "Table de base",
"Card properties": "Propriétés de la carte",
"Cells longer than the Text limit will be truncated and the extra content permanently lost.": "Les cellules dépassant la limite de texte seront tronquées et le contenu supplémentaire sera définitivement perdu.",
"Checkbox": "Case à cocher",
"Checked by default": "Coché par défaut",
"Choose a property": "Choisir une propriété",
"Clear selection": "Effacer la sélection",
"Column options": "Options de la colonne",
"Comma, period": "Virgule, point",
"Complete": "Terminé",
"Contains": "Contient",
"Copy link to view": "Copier le lien vers la vue",
"Create a status property": "Créer une propriété de statut",
"Created at": "Créé le",
"Currency": "Devise",
"Decimal places": "Décimales",
"Default value": "Valeur par défaut",
"Delete property": "Supprimer la propriété",
"Delete view": "Supprimer la vue",
"Delete {{count}} rows?_one": "Supprimer 1 ligne ?",
"Delete {{count}} rows?_other": "Supprimer {{count}} lignes ?",
"Descending": "Décroissant",
"Discard": "Ignorer",
"Doesn't contain": "Ne contient pas",
"Done": "Terminé",
"Edit formula": "Modifier la formule",
"Edit property": "Modifier la propriété",
"Export CSV": "Exporter en CSV",
"Failed to load base": "Échec du chargement de la base",
"False": "Faux",
"File": "Fichier",
"Filter and sort changes are visible only to you": "Les modifications de filtre et de tri ne sont visibles que par vous",
"Filter by": "Filtrer par",
"Formula": "Formule",
"Get started with": "Commencer avec",
"Greater than": "Supérieur à",
"Group by": "Grouper par",
"Group this board by a select or status property.": "Groupez ce tableau par une propriété de sélection ou de statut.",
"Hide all": "Tout masquer",
"Hide group": "Masquer le groupe",
"In Progress": "En cours",
"In progress": "En cours",
"Include time": "Inclure l'heure",
"Insert a kanban board on this page": "Insérer un tableau kanban sur cette page",
"Insert an inline base on this page": "Insérer une base intégrée sur cette page",
"Is": "Est",
"Is after": "Est après",
"Is any of": "Est l'un de",
"Is before": "Est avant",
"Is empty": "Est vide",
"Is none of": "N'est aucun de",
"Is not": "N'est pas",
"Is not empty": "N'est pas vide",
"Is on or after": "Est le ou après le",
"Is on or before": "Est le ou avant le",
"Is within": "Est dans la plage",
"Keep editing": "Continuer la modification",
"Last edited at": "Dernière modification le",
"Last edited by": "Dernière modification par",
"Less than": "Inférieur à",
"Link copied to clipboard": "Lien copié dans le presse-papiers",
"Local": "Local",
"Long text": "Texte long",
"Moved card to {{column}}": "Carte déplacée vers {{column}}",
"Moved column to {{column}}": "Colonne déplacée vers {{column}}",
"Multi-select": "Sélection multiple",
"New row": "Nouvelle ligne",
"Next month": "Le mois prochain",
"Next week": "La semaine prochaine",
"Next year": "L'année prochaine",
"No filters applied": "Aucun filtre appliqué",
"No match": "Aucune correspondance",
"No options for this property type": "Aucune option pour ce type de propriété",
"No sorts applied": "Aucun tri appliqué",
"No value": "Aucune valeur",
"None": "Aucun",
"Not started": "Non commencé",
"Number": "Nombre",
"One month ago": "Il y a un mois",
"One month from now": "Dans un mois",
"One week ago": "Il y a une semaine",
"One week from now": "Dans une semaine",
"Open as page": "Ouvrir comme page",
"Options": "Options",
"Past month": "Le mois dernier",
"Past week": "La semaine dernière",
"Past year": "L'année dernière",
"Percent": "Pourcentage",
"Period, comma": "Point, virgule",
"Person": "Personne",
"Please enter a valid email": "Veuillez entrer une adresse e-mail valide",
"Progress": "Progression",
"Property options": "Options de la propriété",
"Relative": "Relatif",
"Rename property": "Renommer la propriété",
"Reset": "Réinitialiser",
"Save for everyone": "Enregistrer pour tout le monde",
"Select": "Sélection",
"Select a property": "Sélectionner une propriété",
"Select all loaded rows": "Sélectionner toutes les lignes chargées",
"Show all": "Tout afficher",
"Sort": "Trier",
"Sort by": "Trier par",
"Space, comma": "Espace, virgule",
"Space, period": "Espace, point",
"This board has no grouping property yet.": "Ce tableau n'a pas encore de propriété de groupement.",
"This month": "Ce mois-ci",
"This year": "Cette année",
"Thousands and decimal separators": "Séparateurs des milliers et des décimales",
"Time format": "Format de l'heure",
"To Do": "À faire",
"Tomorrow": "Demain",
"True": "Vrai",
"Unsaved changes": "Modifications non enregistrées",
"Untitled base": "Base sans titre",
"URL": "URL",
"Value": "Valeur",
"View updated for everyone": "Vue mise à jour pour tout le monde",
"You have unsaved changes. Do you want to discard them?": "Vous avez des modifications non enregistrées. Voulez-vous les ignorer ?",
"{{count}} rows deleted_one": "1 ligne supprimée",
"{{count}} rows deleted_other": "{{count}} lignes supprimées",
"{{count}} selected_one": "1 sélectionné(e)",
"{{count}} selected_other": "{{count}} sélectionné(e)s",
"Compare": "Comparer",
"Compare versions": "Comparer les versions",
"Select version from {{date}}": "Sélectionner la version du {{date}}",
"Version actions for {{date}}": "Actions de version pour le {{date}}",
"Comparing {{newer}} and {{older}}": "Comparaison de {{newer}} et {{older}}",
"Exit compare": "Quitter la comparaison",
"Search attachments...": "Rechercher des pièces jointes...",
"Error loading attachments.": "Erreur lors du chargement des pièces jointes.",
"No attachments on this page yet.": "Aucune pièce jointe sur cette page pour le moment.",
"Uploaded by {{name}}": "Téléversé par {{name}}",
"Download {{name}}": "Télécharger {{name}}",
"Access revoked": "Accès révoqué",
"Authorize application": "Autoriser lapplication",
"{{name}} wants to access {{workspace}}": "{{name}} souhaite accéder à {{workspace}}",
"Not you? Switch account": "Ce nest pas vous ? Changer de compte",
"This application will be able to:": "Cette application pourra :",
"Write": "Écriture",
"Invalid authorization request": "Demande dautorisation invalide",
"Authorize": "Autoriser",
"Application": "Application",
"Permissions": "Autorisations",
"Authorized": "Autorisé",
"Revoke access": "Révoquer laccès",
"Revoke access for {{name}}": "Révoquer laccès pour {{name}}",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Êtes-vous sûr de vouloir révoquer laccès pour {{name}} ? Lapplication ne pourra plus accéder à votre compte.",
"Something went wrong. Please try again.": "Une erreur sest produite. Veuillez réessayer.",
"Remove {{name}}": "Supprimer {{name}}",
"Make sure you trust this application before authorizing it.": "Assurez-vous de faire confiance à cette application avant de lautoriser.",
"You will be redirected to": "Vous serez redirigé vers",
"View content without making changes.": "Afficher le contenu sans apporter de modifications.",
"Create and modify content.": "Créer et modifier du contenu.",
"Applications and AI assistants you have authorized to access your account.": "Applications et assistants IA que vous avez autorisés à accéder à votre compte.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "MCP est activé dans votre espace de travail. Connectez des assistants IA à votre compte Docmost via OAuth.",
"Authorized apps": "Applications autorisées",
"No authorized apps yet.": "Aucune application autorisée pour le moment.",
"Workspace knowledge only": "Connaissances de lespace de travail uniquement",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Limiter AI Chat aux réponses provenant uniquement des pages de votre espace de travail et des fichiers téléversés. Il nutilisera pas de connaissances externes.",
"Toggle workspace knowledge only": "Activer/désactiver les connaissances de lespace de travail uniquement",
"Read-only mode": "Mode lecture seule",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat peut rechercher et lire le contenu de lespace de travail, mais ne peut pas créer ni modifier des pages.",
"Toggle AI Chat read-only mode": "Activer/désactiver le mode lecture seule dAI Chat",
"Title only": "Titre uniquement",
"you": "vous"
} }
@@ -41,8 +41,6 @@
"Dark": "Scuro", "Dark": "Scuro",
"Date": "Data", "Date": "Data",
"Delete": "Elimina", "Delete": "Elimina",
"Remove from page": "Rimuovi dalla pagina",
"Base options": "Opzioni della base",
"Delete group": "Elimina gruppo", "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.", "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", "Description": "Descrizione",
@@ -78,24 +76,6 @@
"Failed to import pages": "Impossibile importare le pagine", "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 load page. An error occurred.": "Il caricamento della pagina è fallito. Si è verificato un errore.",
"Failed to update data": "Impossibile aggiornare i dati", "Failed to update data": "Impossibile aggiornare i dati",
"Failed to create base": "Impossibile creare la base",
"Failed to update base": "Impossibile aggiornare la base",
"Failed to delete base": "Impossibile eliminare la base",
"Failed to create property": "Impossibile creare la proprietà",
"Failed to update property": "Impossibile aggiornare la proprietà",
"Failed to delete property": "Impossibile eliminare la proprietà",
"Failed to reorder property": "Impossibile riordinare la proprietà",
"Failed to create view": "Impossibile creare la vista",
"Failed to update view": "Impossibile aggiornare la vista",
"Failed to delete view": "Impossibile eliminare la vista",
"Failed to create row": "Impossibile creare la riga",
"Failed to update row": "Impossibile aggiornare la riga",
"Failed to delete row": "Impossibile eliminare la riga",
"Failed to delete rows": "Impossibile eliminare le righe",
"Failed to reorder row": "Impossibile riordinare la riga",
"Failed to move card": "Impossibile spostare la scheda",
"Failed to add card": "Impossibile aggiungere la scheda",
"Failed to export CSV": "Impossibile esportare il CSV",
"Favorite spaces": "Spazi preferiti", "Favorite spaces": "Spazi preferiti",
"Favorite spaces appear here": "Gli spazi preferiti appariranno qui", "Favorite spaces appear here": "Gli spazi preferiti appariranno qui",
"Favorites": "Preferiti", "Favorites": "Preferiti",
@@ -205,7 +185,7 @@
"Templates": "Modelli", "Templates": "Modelli",
"Theme": "Tema", "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.", "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 intera della pagina", "Toggle full page width": "Attiva/disattiva larghezza completa della pagina",
"Unable to import pages. Please try again.": "Impossibile importare le pagine. Riprova.", "Unable to import pages. Please try again.": "Impossibile importare le pagine. Riprova.",
"untitled": "senza titolo", "untitled": "senza titolo",
"Untitled": "Senza titolo", "Untitled": "Senza titolo",
@@ -294,7 +274,6 @@
"Export space": "Esporta spazio", "Export space": "Esporta spazio",
"Export {{type}}": "Esporta {{type}}", "Export {{type}}": "Esporta {{type}}",
"File exceeds the {{limit}} attachment limit": "Il file supera il limite per gli allegati di {{limit}}", "File exceeds the {{limit}} attachment limit": "Il file supera il limite per gli allegati di {{limit}}",
"Media": "Contenuti multimediali",
"Align left": "Allinea a sinistra", "Align left": "Allinea a sinistra",
"Align right": "Allinea a destra", "Align right": "Allinea a destra",
"Align center": "Allinea al centro", "Align center": "Allinea al centro",
@@ -388,8 +367,6 @@
"Insert horizontal rule divider": "Inserisci divisore di regola orizzontale", "Insert horizontal rule divider": "Inserisci divisore di regola orizzontale",
"Page break": "Interruzione di pagina", "Page break": "Interruzione di pagina",
"Insert a page break for printing.": "Inserisci un'interruzione di pagina per la stampa.", "Insert a page break for printing.": "Inserisci un'interruzione di pagina per la stampa.",
"Footnote": "Nota a piè di pagina",
"Insert a footnote reference.": "Inserisci un riferimento a una nota a piè di pagina.",
"Upload any image from your device.": "Carica un'immagine dal tuo dispositivo.", "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 video from your device.": "Carica qualsiasi video dal tuo dispositivo.",
"Upload any audio from your device.": "Carica qualsiasi audio dal tuo dispositivo.", "Upload any audio from your device.": "Carica qualsiasi audio dal tuo dispositivo.",
@@ -421,8 +398,6 @@
"Insert mermaid diagram": "Inserisci diagramma Mermaid", "Insert mermaid diagram": "Inserisci diagramma Mermaid",
"Insert and design Drawio diagrams": "Inserisci e progetta diagrammi Drawio", "Insert and design Drawio diagrams": "Inserisci e progetta diagrammi Drawio",
"Insert current date": "Inserisci data corrente", "Insert current date": "Inserisci data corrente",
"Time": "Ora",
"Insert current time": "Inserisci ora corrente",
"Draw and sketch excalidraw diagrams": "Disegna e abbozza diagrammi Excalidraw", "Draw and sketch excalidraw diagrams": "Disegna e abbozza diagrammi Excalidraw",
"Multiple": "Multiplo", "Multiple": "Multiplo",
"Turn into": "Trasforma in", "Turn into": "Trasforma in",
@@ -449,7 +424,6 @@
"Names do not match": "I nomi non corrispondono", "Names do not match": "I nomi non corrispondono",
"Today, {{time}}": "Oggi, {{time}}", "Today, {{time}}": "Oggi, {{time}}",
"Yesterday, {{time}}": "Ieri, {{time}}", "Yesterday, {{time}}": "Ieri, {{time}}",
"now": "ora",
"Space created successfully": "Spazio creato con successo", "Space created successfully": "Spazio creato con successo",
"Space updated successfully": "Spazio aggiornato con successo", "Space updated successfully": "Spazio aggiornato con successo",
"Space deleted successfully": "Spazio eliminato con successo", "Space deleted successfully": "Spazio eliminato con successo",
@@ -622,8 +596,6 @@
"Deleted by": "Eliminato da", "Deleted by": "Eliminato da",
"Deleted at": "Eliminato il", "Deleted at": "Eliminato il",
"Preview": "Anteprima", "Preview": "Anteprima",
"Base preview unavailable": "Anteprima della base non disponibile",
"Restore this base to view its contents.": "Ripristina questa base per visualizzarne i contenuti.",
"Subpages": "Sottopagine", "Subpages": "Sottopagine",
"Failed to load subpages": "Caricamento delle sottopagine non riuscito", "Failed to load subpages": "Caricamento delle sottopagine non riuscito",
"No subpages": "Nessuna sottopagina", "No subpages": "Nessuna sottopagina",
@@ -699,7 +671,7 @@
"Upgrade your plan": "Aggiorna il tuo piano", "Upgrade your plan": "Aggiorna il tuo piano",
"Available with a paid license": "Disponibile con una licenza a pagamento", "Available with a paid license": "Disponibile con una licenza a pagamento",
"Upgrade your license tier.": "Aggiorna il livello della tua licenza.", "Upgrade your license tier.": "Aggiorna il livello della tua licenza.",
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "L'IA è disponibile nelle edizioni a pagamento di Docmost. Contatta sales@docmost.com.", "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "L'IA è disponibile solo nell'edizione Enterprise di Docmost. Contatta sales@docmost.com.",
"AI & MCP": "IA e MCP", "AI & MCP": "IA e MCP",
"AI": "IA", "AI": "IA",
"MCP": "MCP", "MCP": "MCP",
@@ -707,11 +679,9 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Abilita il server MCP per consentire ad assistenti e strumenti IA di interagire con i contenuti del tuo spazio di lavoro.", "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Abilita il server MCP per consentire ad assistenti e strumenti IA di interagire con i contenuti del tuo spazio di lavoro.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP è disponibile solo nell'edizione Enterprise di Docmost. Contatta sales@docmost.com.", "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP è disponibile solo nell'edizione Enterprise di Docmost. Contatta sales@docmost.com.",
"MCP Server URL": "URL del server MCP", "MCP Server URL": "URL del server MCP",
"Connect AI assistants with your Docmost account via OAuth.": "Connetti gli assistenti AI al tuo account Docmost tramite OAuth.", "Use your API key for authentication. You can manage API keys in your account settings.": "Usa la tua chiave API per l'autenticazione. Puoi gestire le chiavi API nelle impostazioni del tuo account.",
"Enforce OAuth": "Enforce OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "Gli assistenti AI devono connettersi con un account Docmost tramite OAuth. Le chiavi API non possono essere utilizzate con il server MCP.",
"Toggle enforce OAuth for MCP": "Toggle enforce OAuth for MCP",
"Supported tools": "Strumenti supportati", "Supported tools": "Strumenti supportati",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Il tuo spazio di lavoro ha MCP abilitato. Usa la tua chiave API per collegare gli assistenti IA.",
"MCP server URL:": "URL del server MCP:", "MCP server URL:": "URL del server MCP:",
"Learn more": "Scopri di più", "Learn more": "Scopri di più",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gestisci le API key per tutti gli utenti nello spazio di lavoro. Consulta la <anchor>documentazione API</anchor> per i dettagli sull'utilizzo.", "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gestisci le API key per tutti gli utenti nello spazio di lavoro. Consulta la <anchor>documentazione API</anchor> per i dettagli sull'utilizzo.",
@@ -995,8 +965,6 @@
"Are you sure you want to delete this template?": "Sei sicuro di voler eliminare questo modello?", "Are you sure you want to delete this template?": "Sei sicuro di voler eliminare questo modello?",
"Template scope updated": "Ambito del modello aggiornato", "Template scope updated": "Ambito del modello aggiornato",
"Choose which space this template belongs to": "Scegli a quale spazio appartiene questo modello", "Choose which space this template belongs to": "Scegli a quale spazio appartiene questo modello",
"Only workspace admins can create global templates.": "Solo gli amministratori dell'area di lavoro possono creare modelli globali.",
"You need edit access to this space to create templates.": "Hai bisogno dell'accesso in modifica a questo spazio per creare modelli.",
"Scope": "Ambito", "Scope": "Ambito",
"Select scope": "Seleziona ambito", "Select scope": "Seleziona ambito",
"Title": "Titolo", "Title": "Titolo",
@@ -1009,7 +977,7 @@
"Search pages and spaces...": "Cerca pagine e spazi...", "Search pages and spaces...": "Cerca pagine e spazi...",
"No results found": "Nessun risultato trovato", "No results found": "Nessun risultato trovato",
"You don't have permission to create pages here": "Non hai l'autorizzazione per creare pagine qui", "You don't have permission to create pages here": "Non hai l'autorizzazione per creare pagine qui",
"Chat menu for {{title}}": "Menu chat per {{title}}", "Chat menu": "Menu della chat",
"API key menu": "Menu della chiave API", "API key menu": "Menu della chiave API",
"Jump to comment selection": "Vai alla selezione dei commenti", "Jump to comment selection": "Vai alla selezione dei commenti",
"Slash commands": "Comandi slash", "Slash commands": "Comandi slash",
@@ -1095,7 +1063,7 @@
"Filter": "Filtro", "Filter": "Filtro",
"Page title": "Titolo pagina", "Page title": "Titolo pagina",
"Page content": "Contenuto della pagina", "Page content": "Contenuto della pagina",
"Member actions for {{name}}": "Azioni membro per {{name}}", "Member actions": "Azioni membro",
"Toggle password visibility": "Attiva/disattiva visibilità password", "Toggle password visibility": "Attiva/disattiva visibilità password",
"Send comment": "Invia commento", "Send comment": "Invia commento",
"Token actions": "Azioni token", "Token actions": "Azioni token",
@@ -1116,226 +1084,5 @@
"Added {{name}} to favorites": "{{name}} aggiunto ai preferiti", "Added {{name}} to favorites": "{{name}} aggiunto ai preferiti",
"Removed {{name}} from favorites": "{{name}} rimosso dai preferiti", "Removed {{name}} from favorites": "{{name}} rimosso dai preferiti",
"Page menu for {{name}}": "Menu della pagina per {{name}}", "Page menu for {{name}}": "Menu della pagina per {{name}}",
"Create subpage of {{name}}": "Crea sottopagina di {{name}}", "Create subpage of {{name}}": "Crea sottopagina di {{name}}"
"Allow personal spaces": "Consenti spazi personali",
"Members can create their own personal space.": "I membri possono creare il proprio spazio personale.",
"Toggle allow personal spaces": "Attiva/disattiva spazi personali",
"Create personal space": "Crea spazio personale",
"Personal space": "Spazio personale",
"{{name}}'s space": "Spazio di {{name}}",
"Apply": "Applica",
"Cells that aren't already a page reference will be cleared.": "Le celle che non sono già un riferimento a una pagina verranno svuotate.",
"Cells that aren't a valid URL will be cleared.": "Le celle che non contengono un URL valido verranno svuotate.",
"Cells that aren't a valid email address will be cleared.": "Le celle che non contengono un indirizzo email valido verranno svuotate.",
"Cells that can't be parsed as a date will be cleared.": "Le celle che non possono essere interpretate come una data verranno svuotate.",
"Cells that can't be parsed as a number will be cleared.": "Le celle che non possono essere interpretate come un numero verranno svuotate.",
"Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).": "Le celle verranno convertite forzatamente (yes/true/1 diventeranno selezionate; tutto il resto diventerà non selezionato o verrà svuotato).",
"Cells will be reinterpreted under the new type.": "Le celle verranno reinterpretate con il nuovo tipo.",
"Cells will be replaced with a comma-separated list of file names.": "Le celle verranno sostituite con un elenco di nomi file separati da virgole.",
"Cells will be replaced with a comma-separated list of option names.": "Le celle verranno sostituite con un elenco di nomi di opzioni separati da virgole.",
"Cells will be replaced with the option name.": "Le celle verranno sostituite con il nome dell'opzione.",
"Cells will be replaced with the page title.": "Le celle verranno sostituite con il titolo della pagina.",
"Cells will be replaced with the person's name.": "Le celle verranno sostituite con il nome della persona.",
"Change type": "Cambia tipo",
"Change type to {{label}}?": "Cambiare tipo in {{label}}?",
"Converting…": "Conversione…",
"Existing values become single-item lists. No data is lost.": "I valori esistenti diventano elenchi con un solo elemento. Nessun dato andrà perso.",
"Only the first selected item per row will be kept; the rest will be discarded.": "Verrà mantenuto solo il primo elemento selezionato per riga; gli altri verranno scartati.",
"Previous record": "Record precedente",
"Next record": "Record successivo",
"Record actions": "Azioni record",
"Delete record": "Elimina record",
"Delete record?": "Eliminare il record?",
"This action cannot be undone.": "Questa azione non può essere annullata.",
"to navigate": "per navigare",
"to close": "per chiudere",
"Expand row {{number}}": "Espandi riga {{number}}",
"Saving…": "Salvataggio…",
"Read-only": "Sola lettura",
"Loading…": "Caricamento…",
"Updated {{when}}": "Aggiornato {{when}}",
"Add property": "Aggiungi proprietà",
"Create property": "Crea proprietà",
"Hide properties": "Nascondi proprietà",
"Find a property type": "Trova un tipo di proprietà",
"Properties": "Proprietà",
"A property with this name already exists": "Esiste già una proprietà con questo nome",
"Add card": "Aggiungi scheda",
"Add filter": "Aggiungi filtro",
"Add option": "Aggiungi opzione",
"Add sort": "Aggiungi ordinamento",
"Add view": "Aggiungi vista",
"All data in this column will be lost.": "Tutti i dati in questa colonna andranno persi.",
"Allow multiple people": "Consenti più persone",
"Alphabetize": "Ordina alfabeticamente",
"Are you sure you want to delete": "Sei sicuro di voler eliminare",
"Ascending": "Crescente",
"Base deleted": "Base eliminata",
"Base name": "Nome della base",
"Base table": "Tabella della base",
"Card properties": "Proprietà della scheda",
"Cells longer than the Text limit will be truncated and the extra content permanently lost.": "Le celle più lunghe del limite del testo verranno troncate e il contenuto aggiuntivo andrà perso in modo permanente.",
"Checkbox": "Casella di controllo",
"Checked by default": "Selezionata per impostazione predefinita",
"Choose a property": "Scegli una proprietà",
"Clear selection": "Cancella selezione",
"Column options": "Opzioni della colonna",
"Comma, period": "Virgola, punto",
"Complete": "Completato",
"Contains": "Contiene",
"Copy link to view": "Copia link alla vista",
"Create a status property": "Crea una proprietà di stato",
"Created at": "Creato il",
"Currency": "Valuta",
"Decimal places": "Posizioni decimali",
"Default value": "Valore predefinito",
"Delete property": "Elimina proprietà",
"Delete view": "Elimina vista",
"Delete {{count}} rows?_one": "Eliminare 1 riga?",
"Delete {{count}} rows?_other": "Eliminare {{count}} righe?",
"Descending": "Decrescente",
"Discard": "Ignora",
"Doesn't contain": "Non contiene",
"Done": "Fatto",
"Edit formula": "Modifica formula",
"Edit property": "Modifica proprietà",
"Export CSV": "Esporta CSV",
"Failed to load base": "Impossibile caricare la base",
"False": "Falso",
"File": "File",
"Filter and sort changes are visible only to you": "Le modifiche a filtri e ordinamenti sono visibili solo a te",
"Filter by": "Filtra per",
"Formula": "Formula",
"Get started with": "Inizia con",
"Greater than": "Maggiore di",
"Group by": "Raggruppa per",
"Group this board by a select or status property.": "Raggruppa questa bacheca per una proprietà di selezione o di stato.",
"Hide all": "Nascondi tutto",
"Hide group": "Nascondi gruppo",
"In Progress": "In corso",
"In progress": "In corso",
"Include time": "Includi ora",
"Insert a kanban board on this page": "Inserisci una bacheca kanban in questa pagina",
"Insert an inline base on this page": "Inserisci una base in linea in questa pagina",
"Is": "È",
"Is after": "È dopo",
"Is any of": "È uno tra",
"Is before": "È prima",
"Is empty": "È vuoto",
"Is none of": "Non è nessuno di",
"Is not": "Non è",
"Is not empty": "Non è vuoto",
"Is on or after": "È il o dopo il",
"Is on or before": "È il o prima del",
"Is within": "È entro",
"Keep editing": "Continua a modificare",
"Last edited at": "Ultima modifica il",
"Last edited by": "Ultima modifica di",
"Less than": "Minore di",
"Link copied to clipboard": "Link copiato negli appunti",
"Local": "Locale",
"Long text": "Testo lungo",
"Moved card to {{column}}": "Scheda spostata in {{column}}",
"Moved column to {{column}}": "Colonna spostata in {{column}}",
"Multi-select": "Selezione multipla",
"New row": "Nuova riga",
"Next month": "Prossimo mese",
"Next week": "Prossima settimana",
"Next year": "Prossimo anno",
"No filters applied": "Nessun filtro applicato",
"No match": "Nessuna corrispondenza",
"No options for this property type": "Nessuna opzione per questo tipo di proprietà",
"No sorts applied": "Nessun ordinamento applicato",
"No value": "Nessun valore",
"None": "Nessuno",
"Not started": "Non iniziato",
"Number": "Numero",
"One month ago": "Un mese fa",
"One month from now": "Tra un mese",
"One week ago": "Una settimana fa",
"One week from now": "Tra una settimana",
"Open as page": "Apri come pagina",
"Options": "Opzioni",
"Past month": "Mese scorso",
"Past week": "Settimana scorsa",
"Past year": "Anno scorso",
"Percent": "Percentuale",
"Period, comma": "Punto, virgola",
"Person": "Persona",
"Please enter a valid email": "Inserisci un indirizzo email valido",
"Progress": "Avanzamento",
"Property options": "Opzioni della proprietà",
"Relative": "Relativo",
"Rename property": "Rinomina proprietà",
"Reset": "Reimposta",
"Save for everyone": "Salva per tutti",
"Select": "Seleziona",
"Select a property": "Seleziona una proprietà",
"Select all loaded rows": "Seleziona tutte le righe caricate",
"Show all": "Mostra tutto",
"Sort": "Ordina",
"Sort by": "Ordina per",
"Space, comma": "Spazio, virgola",
"Space, period": "Spazio, punto",
"This board has no grouping property yet.": "Questa bacheca non ha ancora una proprietà di raggruppamento.",
"This month": "Questo mese",
"This year": "Quest'anno",
"Thousands and decimal separators": "Separatori delle migliaia e dei decimali",
"Time format": "Formato ora",
"To Do": "Da fare",
"Tomorrow": "Domani",
"True": "Vero",
"Unsaved changes": "Modifiche non salvate",
"Untitled base": "Base senza titolo",
"URL": "URL",
"Value": "Valore",
"View updated for everyone": "Vista aggiornata per tutti",
"You have unsaved changes. Do you want to discard them?": "Hai modifiche non salvate. Vuoi ignorarle?",
"{{count}} rows deleted_one": "1 riga eliminata",
"{{count}} rows deleted_other": "{{count}} righe eliminate",
"{{count}} selected_one": "1 selezionato",
"{{count}} selected_other": "{{count}} selezionati",
"Compare": "Confronta",
"Compare versions": "Confronta versioni",
"Select version from {{date}}": "Seleziona la versione del {{date}}",
"Version actions for {{date}}": "Azioni della versione del {{date}}",
"Comparing {{newer}} and {{older}}": "Confronto tra {{newer}} e {{older}}",
"Exit compare": "Esci dal confronto",
"Search attachments...": "Cerca allegati...",
"Error loading attachments.": "Errore durante il caricamento degli allegati.",
"No attachments on this page yet.": "Ancora nessun allegato in questa pagina.",
"Uploaded by {{name}}": "Caricato da {{name}}",
"Download {{name}}": "Scarica {{name}}",
"Access revoked": "Accesso revocato",
"Authorize application": "Autorizza applicazione",
"{{name}} wants to access {{workspace}}": "{{name}} vuole accedere a {{workspace}}",
"Not you? Switch account": "Non sei tu? Cambia account",
"This application will be able to:": "Questa applicazione potrà:",
"Write": "Scrivere",
"Invalid authorization request": "Richiesta di autorizzazione non valida",
"Authorize": "Autorizza",
"Application": "Applicazione",
"Permissions": "Autorizzazioni",
"Authorized": "Autorizzato",
"Revoke access": "Revoca accesso",
"Revoke access for {{name}}": "Revoca l'accesso per {{name}}",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Sei sicuro di voler revocare l'accesso per {{name}}? L'applicazione non potrà più accedere al tuo account.",
"Something went wrong. Please try again.": "Qualcosa è andato storto. Riprova.",
"Remove {{name}}": "Rimuovi {{name}}",
"Make sure you trust this application before authorizing it.": "Assicurati di fidarti di questa applicazione prima di autorizzarla.",
"You will be redirected to": "Verrai reindirizzato a",
"View content without making changes.": "Visualizza il contenuto senza apportare modifiche.",
"Create and modify content.": "Crea e modifica contenuti.",
"Applications and AI assistants you have authorized to access your account.": "Applicazioni e assistenti AI che hai autorizzato ad accedere al tuo account.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "Il tuo workspace ha MCP abilitato. Connetti gli assistenti AI al tuo account Docmost tramite OAuth.",
"Authorized apps": "App autorizzate",
"No authorized apps yet.": "Ancora nessuna app autorizzata.",
"Workspace knowledge only": "Solo conoscenze del workspace",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Limita AI Chat a rispondere solo in base alle pagine del tuo workspace e ai file caricati. Non userà conoscenze esterne.",
"Toggle workspace knowledge only": "Attiva/disattiva solo conoscenze del workspace",
"Read-only mode": "Modalità di sola lettura",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat può cercare e leggere i contenuti del workspace, ma non può creare o modificare pagine.",
"Toggle AI Chat read-only mode": "Attiva/disattiva la modalità di sola lettura di AI Chat",
"Title only": "Solo titolo",
"you": "tu"
} }
@@ -41,8 +41,6 @@
"Dark": "ダーク", "Dark": "ダーク",
"Date": "日付", "Date": "日付",
"Delete": "削除", "Delete": "削除",
"Remove from page": "ページから削除",
"Base options": "ベースのオプション",
"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": "説明",
@@ -78,24 +76,6 @@
"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": "データの更新に失敗しました",
"Failed to create base": "ベースの作成に失敗しました",
"Failed to update base": "ベースの更新に失敗しました",
"Failed to delete base": "ベースの削除に失敗しました",
"Failed to create property": "プロパティの作成に失敗しました",
"Failed to update property": "プロパティの更新に失敗しました",
"Failed to delete property": "プロパティの削除に失敗しました",
"Failed to reorder property": "プロパティの並べ替えに失敗しました",
"Failed to create view": "ビューの作成に失敗しました",
"Failed to update view": "ビューの更新に失敗しました",
"Failed to delete view": "ビューの削除に失敗しました",
"Failed to create row": "行の作成に失敗しました",
"Failed to update row": "行の更新に失敗しました",
"Failed to delete row": "行の削除に失敗しました",
"Failed to delete rows": "行の削除に失敗しました",
"Failed to reorder row": "行の並べ替えに失敗しました",
"Failed to move card": "カードの移動に失敗しました",
"Failed to add card": "カードの追加に失敗しました",
"Failed to export CSV": "CSV のエクスポートに失敗しました",
"Favorite spaces": "お気に入りのスペース", "Favorite spaces": "お気に入りのスペース",
"Favorite spaces appear here": "お気に入りのスペースがここに表示されます", "Favorite spaces appear here": "お気に入りのスペースがここに表示されます",
"Favorites": "お気に入り", "Favorites": "お気に入り",
@@ -294,7 +274,6 @@
"Export space": "エクスポートスペース", "Export space": "エクスポートスペース",
"Export {{type}}": "{{type}}をエクスポート", "Export {{type}}": "{{type}}をエクスポート",
"File exceeds the {{limit}} attachment limit": "ファイルが{{limit}}の添付制限を超えています", "File exceeds the {{limit}} attachment limit": "ファイルが{{limit}}の添付制限を超えています",
"Media": "メディア",
"Align left": "左揃え", "Align left": "左揃え",
"Align right": "右揃え", "Align right": "右揃え",
"Align center": "中央揃え", "Align center": "中央揃え",
@@ -388,8 +367,6 @@
"Insert horizontal rule divider": "区切り線を挿入します", "Insert horizontal rule divider": "区切り線を挿入します",
"Page break": "改ページ", "Page break": "改ページ",
"Insert a page break for printing.": "印刷用に改ページを挿入します。", "Insert a page break for printing.": "印刷用に改ページを挿入します。",
"Footnote": "脚注",
"Insert a footnote reference.": "脚注参照を挿入します。",
"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 audio from your device.": "デバイスから音声ファイルをアップロードします。", "Upload any audio from your device.": "デバイスから音声ファイルをアップロードします。",
@@ -421,8 +398,6 @@
"Insert mermaid diagram": "Mermaid ダイアグラムを挿入", "Insert mermaid diagram": "Mermaid ダイアグラムを挿入",
"Insert and design Drawio diagrams": "Drawio ダイアグラムを挿入して作成", "Insert and design Drawio diagrams": "Drawio ダイアグラムを挿入して作成",
"Insert current date": "現在の日付を挿入", "Insert current date": "現在の日付を挿入",
"Time": "時刻",
"Insert current time": "現在の時刻を挿入",
"Draw and sketch excalidraw diagrams": "Excalidraw ダイアグラムを描画・スケッチ", "Draw and sketch excalidraw diagrams": "Excalidraw ダイアグラムを描画・スケッチ",
"Multiple": "複数", "Multiple": "複数",
"Turn into": "変換する", "Turn into": "変換する",
@@ -449,7 +424,6 @@
"Names do not match": "名前が一致しません", "Names do not match": "名前が一致しません",
"Today, {{time}}": "今日 {{time}}", "Today, {{time}}": "今日 {{time}}",
"Yesterday, {{time}}": "昨日 {{time}}", "Yesterday, {{time}}": "昨日 {{time}}",
"now": "今すぐ",
"Space created successfully": "スペースが正常に作成されました", "Space created successfully": "スペースが正常に作成されました",
"Space updated successfully": "スペースが正常に更新されました", "Space updated successfully": "スペースが正常に更新されました",
"Space deleted successfully": "スペースが正常に削除されました", "Space deleted successfully": "スペースが正常に削除されました",
@@ -622,8 +596,6 @@
"Deleted by": "削除者", "Deleted by": "削除者",
"Deleted at": "削除日時", "Deleted at": "削除日時",
"Preview": "プレビュー", "Preview": "プレビュー",
"Base preview unavailable": "ベースのプレビューを利用できません",
"Restore this base to view its contents.": "内容を表示するには、このベースを復元してください。",
"Subpages": "サブページ", "Subpages": "サブページ",
"Failed to load subpages": "サブページの読み込みに失敗しました", "Failed to load subpages": "サブページの読み込みに失敗しました",
"No subpages": "サブページはありません", "No subpages": "サブページはありません",
@@ -699,7 +671,7 @@
"Upgrade your plan": "プランをアップグレードする", "Upgrade your plan": "プランをアップグレードする",
"Available with a paid license": "有料ライセンスで利用可能", "Available with a paid license": "有料ライセンスで利用可能",
"Upgrade your license tier.": "ライセンスタイアをアップグレードしてください。", "Upgrade your license tier.": "ライセンスタイアをアップグレードしてください。",
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "AI は Docmost の有料版で利用可能です。sales@docmost.com までお問い合わせください。", "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI は Docmost のエンタープライズ版でのみ利用可能です。sales@docmost.com までお問い合わせください。",
"AI & MCP": "AI と MCP", "AI & MCP": "AI と MCP",
"AI": "AI", "AI": "AI",
"MCP": "MCP", "MCP": "MCP",
@@ -707,11 +679,9 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "MCP サーバーを有効にして、AI アシスタントやツールがワークスペースのコンテンツとやり取りできるようにします。", "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "MCP サーバーを有効にして、AI アシスタントやツールがワークスペースのコンテンツとやり取りできるようにします。",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP は Docmost のエンタープライズ版でのみ利用可能です。sales@docmost.com までお問い合わせください。", "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP は Docmost のエンタープライズ版でのみ利用可能です。sales@docmost.com までお問い合わせください。",
"MCP Server URL": "MCP サーバーの URL", "MCP Server URL": "MCP サーバーの URL",
"Connect AI assistants with your Docmost account via OAuth.": "OAuth を使用して AI アシスタントを Docmost アカウントに接続します。", "Use your API key for authentication. You can manage API keys in your account settings.": "認証には API キーを使用してください。API キーはアカウント設定で管理できます。",
"Enforce OAuth": "Enforce OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI アシスタントは OAuth を使用して Docmost アカウントに接続する必要があります。MCP サーバーでは API キーは使用できません。",
"Toggle enforce OAuth for MCP": "Toggle enforce OAuth for MCP",
"Supported tools": "サポートされているツール", "Supported tools": "サポートされているツール",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "このワークスペースでは MCP が有効になっています。AI アシスタントを接続するには API キーを使用してください。",
"MCP server URL:": "MCP サーバーの URL:", "MCP server URL:": "MCP サーバーの URL:",
"Learn more": "詳細を見る", "Learn more": "詳細を見る",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "ワークスペース内のすべてのユーザーのAPIキーを管理します。利用方法の詳細は<anchor>APIドキュメント</anchor>をご覧ください。", "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "ワークスペース内のすべてのユーザーのAPIキーを管理します。利用方法の詳細は<anchor>APIドキュメント</anchor>をご覧ください。",
@@ -995,8 +965,6 @@
"Are you sure you want to delete this template?": "このテンプレートを削除してもよろしいですか?", "Are you sure you want to delete this template?": "このテンプレートを削除してもよろしいですか?",
"Template scope updated": "テンプレートのスコープを更新しました", "Template scope updated": "テンプレートのスコープを更新しました",
"Choose which space this template belongs to": "このテンプレートを所属させるスペースを選択", "Choose which space this template belongs to": "このテンプレートを所属させるスペースを選択",
"Only workspace admins can create global templates.": "グローバルテンプレートを作成できるのは、ワークスペース管理者のみです。",
"You need edit access to this space to create templates.": "テンプレートを作成するには、このスペースへの編集アクセス権が必要です。",
"Scope": "スコープ", "Scope": "スコープ",
"Select scope": "スコープを選択", "Select scope": "スコープを選択",
"Title": "タイトル", "Title": "タイトル",
@@ -1009,7 +977,7 @@
"Search pages and spaces...": "ページとスペースを検索…", "Search pages and spaces...": "ページとスペースを検索…",
"No results found": "結果が見つかりません", "No results found": "結果が見つかりません",
"You don't have permission to create pages here": "ここにページを作成する権限がありません", "You don't have permission to create pages here": "ここにページを作成する権限がありません",
"Chat menu for {{title}}": "{{title}} のチャットメニュー", "Chat menu": "チャットメニュー",
"API key menu": "API キーメニュー", "API key menu": "API キーメニュー",
"Jump to comment selection": "コメント選択に移動", "Jump to comment selection": "コメント選択に移動",
"Slash commands": "スラッシュコマンド", "Slash commands": "スラッシュコマンド",
@@ -1095,7 +1063,7 @@
"Filter": "フィルター", "Filter": "フィルター",
"Page title": "ページタイトル", "Page title": "ページタイトル",
"Page content": "ページ内容", "Page content": "ページ内容",
"Member actions for {{name}}": "{{name}} のメンバー操作", "Member actions": "メンバー操作",
"Toggle password visibility": "パスワードの表示を切り替え", "Toggle password visibility": "パスワードの表示を切り替え",
"Send comment": "コメントを送信", "Send comment": "コメントを送信",
"Token actions": "トークンの操作", "Token actions": "トークンの操作",
@@ -1116,226 +1084,5 @@
"Added {{name}} to favorites": "{{name}} をお気に入りに追加しました", "Added {{name}} to favorites": "{{name}} をお気に入りに追加しました",
"Removed {{name}} from favorites": "{{name}} をお気に入りから削除しました", "Removed {{name}} from favorites": "{{name}} をお気に入りから削除しました",
"Page menu for {{name}}": "{{name}} のページメニュー", "Page menu for {{name}}": "{{name}} のページメニュー",
"Create subpage of {{name}}": "{{name}} のサブページを作成", "Create subpage of {{name}}": "{{name}} のサブページを作成"
"Allow personal spaces": "パーソナルスペースを許可",
"Members can create their own personal space.": "メンバーは自分専用のパーソナルスペースを作成できます。",
"Toggle allow personal spaces": "パーソナルスペースの許可を切り替える",
"Create personal space": "パーソナルスペースを作成",
"Personal space": "パーソナルスペース",
"{{name}}'s space": "{{name}} のスペース",
"Apply": "適用",
"Cells that aren't already a page reference will be cleared.": "すでにページ参照になっているセル以外は消去されます。",
"Cells that aren't a valid URL will be cleared.": "有効な URL ではないセルは消去されます。",
"Cells that aren't a valid email address will be cleared.": "有効なメールアドレスではないセルは消去されます。",
"Cells that can't be parsed as a date will be cleared.": "日付として解析できないセルは消去されます。",
"Cells that can't be parsed as a number will be cleared.": "数値として解析できないセルは消去されます。",
"Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).": "セルは強制変換されます(yes/true/1 はチェック済みになり、それ以外は未チェックまたは空になります)。",
"Cells will be reinterpreted under the new type.": "セルは新しい型で再解釈されます。",
"Cells will be replaced with a comma-separated list of file names.": "セルは、ファイル名のカンマ区切りリストに置き換えられます。",
"Cells will be replaced with a comma-separated list of option names.": "セルは、オプション名のカンマ区切りリストに置き換えられます。",
"Cells will be replaced with the option name.": "セルはオプション名に置き換えられます。",
"Cells will be replaced with the page title.": "セルはページタイトルに置き換えられます。",
"Cells will be replaced with the person's name.": "セルはその人の名前に置き換えられます。",
"Change type": "型を変更",
"Change type to {{label}}?": "型を {{label}} に変更しますか?",
"Converting…": "変換中…",
"Existing values become single-item lists. No data is lost.": "既存の値は単一項目のリストになります。データは失われません。",
"Only the first selected item per row will be kept; the rest will be discarded.": "各行で最初に選択された項目のみ保持され、残りは破棄されます。",
"Previous record": "前のレコード",
"Next record": "次のレコード",
"Record actions": "レコード操作",
"Delete record": "レコードを削除",
"Delete record?": "レコードを削除しますか?",
"This action cannot be undone.": "この操作は元に戻せません。",
"to navigate": "移動するには",
"to close": "閉じるには",
"Expand row {{number}}": "行 {{number}} を展開",
"Saving…": "保存中…",
"Read-only": "読み取り専用",
"Loading…": "読み込み中…",
"Updated {{when}}": "{{when}} に更新",
"Add property": "プロパティを追加",
"Create property": "プロパティを作成",
"Hide properties": "プロパティを非表示",
"Find a property type": "プロパティの種類を検索",
"Properties": "プロパティ",
"A property with this name already exists": "この名前のプロパティはすでに存在します",
"Add card": "カードを追加",
"Add filter": "フィルターを追加",
"Add option": "オプションを追加",
"Add sort": "並べ替えを追加",
"Add view": "ビューを追加",
"All data in this column will be lost.": "この列内のすべてのデータは失われます。",
"Allow multiple people": "複数のユーザーを許可",
"Alphabetize": "アルファベット順",
"Are you sure you want to delete": "削除してもよろしいですか",
"Ascending": "昇順",
"Base deleted": "ベースを削除しました",
"Base name": "ベース名",
"Base table": "ベーステーブル",
"Card properties": "カードのプロパティ",
"Cells longer than the Text limit will be truncated and the extra content permanently lost.": "テキストの制限を超える長さのセルは切り詰められ、超過分の内容は完全に失われます。",
"Checkbox": "チェックボックス",
"Checked by default": "デフォルトでチェックを入れる",
"Choose a property": "プロパティを選択",
"Clear selection": "選択をクリア",
"Column options": "列のオプション",
"Comma, period": "カンマ、小数点",
"Complete": "完了",
"Contains": "含む",
"Copy link to view": "ビューへのリンクをコピー",
"Create a status property": "ステータスプロパティを作成",
"Created at": "作成日時",
"Currency": "通貨",
"Decimal places": "小数点以下の桁数",
"Default value": "デフォルト値",
"Delete property": "プロパティを削除",
"Delete view": "ビューを削除",
"Delete {{count}} rows?_one": "Delete 1 row?",
"Delete {{count}} rows?_other": "Delete {{count}} rows?",
"Descending": "降順",
"Discard": "破棄",
"Doesn't contain": "含まない",
"Done": "完了",
"Edit formula": "数式を編集",
"Edit property": "プロパティを編集",
"Export CSV": "CSVをエクスポート",
"Failed to load base": "ベースの読み込みに失敗しました",
"False": "偽",
"File": "ファイル",
"Filter and sort changes are visible only to you": "フィルターと並べ替えの変更は自分にのみ表示されます",
"Filter by": "条件",
"Formula": "数式",
"Get started with": "次から始める",
"Greater than": "より大きい",
"Group by": "グループ化",
"Group this board by a select or status property.": "このボードをセレクトまたはステータスのプロパティでグループ化します。",
"Hide all": "すべて非表示",
"Hide group": "グループを非表示",
"In Progress": "進行中",
"In progress": "進行中",
"Include time": "時刻を含める",
"Insert a kanban board on this page": "このページにカンバンボードを挿入",
"Insert an inline base on this page": "このページにインラインベースを挿入",
"Is": "次に等しい",
"Is after": "次より後",
"Is any of": "次のいずれか",
"Is before": "次より前",
"Is empty": "空である",
"Is none of": "次のいずれでもない",
"Is not": "次に等しくない",
"Is not empty": "空ではない",
"Is on or after": "次以降",
"Is on or before": "次以前",
"Is within": "次の期間内",
"Keep editing": "編集を続ける",
"Last edited at": "最終編集日時",
"Last edited by": "最終編集者",
"Less than": "より小さい",
"Link copied to clipboard": "リンクをクリップボードにコピーしました",
"Local": "ローカル",
"Long text": "長いテキスト",
"Moved card to {{column}}": "カードを{{column}}に移動しました",
"Moved column to {{column}}": "列を{{column}}に移動しました",
"Multi-select": "複数選択",
"New row": "新規行",
"Next month": "来月",
"Next week": "来週",
"Next year": "来年",
"No filters applied": "フィルターは適用されていません",
"No match": "一致なし",
"No options for this property type": "このプロパティタイプにはオプションがありません",
"No sorts applied": "並べ替えは適用されていません",
"No value": "値なし",
"None": "なし",
"Not started": "未着手",
"Number": "数値",
"One month ago": "1か月前",
"One month from now": "1か月後",
"One week ago": "1週間前",
"One week from now": "1週間後",
"Open as page": "ページとして開く",
"Options": "オプション",
"Past month": "先月",
"Past week": "先週",
"Past year": "昨年",
"Percent": "パーセント",
"Period, comma": "小数点、カンマ",
"Person": "ユーザー",
"Please enter a valid email": "有効なメールアドレスを入力してください",
"Progress": "進捗",
"Property options": "プロパティのオプション",
"Relative": "相対",
"Rename property": "プロパティ名を変更",
"Reset": "リセット",
"Save for everyone": "全員に保存",
"Select": "選択",
"Select a property": "プロパティを選択",
"Select all loaded rows": "読み込まれているすべての行を選択",
"Show all": "すべて表示",
"Sort": "並べ替え",
"Sort by": "並べ替え条件",
"Space, comma": "スペース、カンマ",
"Space, period": "スペース、小数点",
"This board has no grouping property yet.": "このボードにはまだグループ化プロパティがありません。",
"This month": "今月",
"This year": "今年",
"Thousands and decimal separators": "桁区切りと小数点区切り",
"Time format": "時刻形式",
"To Do": "To Do",
"Tomorrow": "明日",
"True": "真",
"Unsaved changes": "未保存の変更",
"Untitled base": "無題のベース",
"URL": "URL",
"Value": "値",
"View updated for everyone": "ビューが全員向けに更新されました",
"You have unsaved changes. Do you want to discard them?": "未保存の変更があります。破棄しますか?",
"{{count}} rows deleted_one": "1 row deleted",
"{{count}} rows deleted_other": "{{count}} rows deleted",
"{{count}} selected_one": "1 selected",
"{{count}} selected_other": "{{count}} selected",
"Compare": "比較",
"Compare versions": "バージョンを比較",
"Select version from {{date}}": "{{date}} のバージョンを選択",
"Version actions for {{date}}": "{{date}} のバージョンの操作",
"Comparing {{newer}} and {{older}}": "{{newer}} と {{older}} を比較中",
"Exit compare": "比較を終了",
"Search attachments...": "添付ファイルを検索…",
"Error loading attachments.": "添付ファイルの読み込み中にエラーが発生しました。",
"No attachments on this page yet.": "このページにはまだ添付ファイルがありません。",
"Uploaded by {{name}}": "アップロード者: {{name}}",
"Download {{name}}": "{{name}} をダウンロード",
"Access revoked": "アクセスが取り消されました",
"Authorize application": "アプリケーションを認可",
"{{name}} wants to access {{workspace}}": "{{name}} が {{workspace}} へのアクセスを求めています",
"Not you? Switch account": "あなたではありませんか? アカウントを切り替え",
"This application will be able to:": "このアプリケーションで可能なこと:",
"Write": "書き込み",
"Invalid authorization request": "無効な認可リクエストです",
"Authorize": "認可",
"Application": "アプリケーション",
"Permissions": "権限",
"Authorized": "認可済み",
"Revoke access": "アクセスを取り消す",
"Revoke access for {{name}}": "{{name}} のアクセスを取り消す",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "{{name}} のアクセスを取り消してもよろしいですか? このアプリケーションは今後あなたのアカウントにアクセスできなくなります。",
"Something went wrong. Please try again.": "問題が発生しました。もう一度お試しください。",
"Remove {{name}}": "{{name}} を削除",
"Make sure you trust this application before authorizing it.": "認可する前に、このアプリケーションを信頼できることを確認してください。",
"You will be redirected to": "次へリダイレクトされます",
"View content without making changes.": "変更を加えずにコンテンツを表示します。",
"Create and modify content.": "コンテンツを作成および変更します。",
"Applications and AI assistants you have authorized to access your account.": "あなたのアカウントへのアクセスを認可したアプリケーションと AI アシスタント。",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "このワークスペースでは MCP が有効になっています。OAuth を使用して AI アシスタントを Docmost アカウントに接続します。",
"Authorized apps": "認可済みアプリ",
"No authorized apps yet.": "認可済みアプリはまだありません。",
"Workspace knowledge only": "ワークスペースの知識のみ",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "AIチャットの回答を、ワークスペース内のページとアップロードされたファイルのみに制限します。外部の知識は使用されません。",
"Toggle workspace knowledge only": "「ワークスペースの知識のみ」を切り替え",
"Read-only mode": "読み取り専用モード",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AIチャットはワークスペースのコンテンツを検索および閲覧できますが、ページの作成や編集はできません。",
"Toggle AI Chat read-only mode": "AIチャットの読み取り専用モードを切り替え",
"Title only": "タイトルのみ",
"you": "あなた"
} }
+90 -343
View File
@@ -2,23 +2,23 @@
"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": "Space에 사용자 추가", "Add space members": "Space에 사용자 추가",
"Add to favorites": "즐겨찾기에 추가", "Add to favorites": "즐겨찾기에 추가",
"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.": "이 사용자를 Space에서 제거하시겠습니까? 사용자는 이 Space에 대한 모든 접근 권한을 잃게 됩니다.", "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.": "이 버전으로 복원하시겠습니까? 저장되지 않은 모든 변경사항이 손실됩니다.", "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": "Workspace 내 팀 및 Space의 사용자가 될 수 있습니다.",
"Can create and edit pages in space.": "Space에 페이지를 생성하고 편집할 수 있습니다.", "Can create and edit pages in space.": "Space에 페이지를 생성하고 편집할 수 있습니다.",
"Can edit": "편집할 수 있음", "Can edit": "편집할 수 있음",
"Can manage workspace": "워크스페이스를 관리할 수 있음", "Can manage workspace": "Workspace를 관리할 수 있음",
"Can manage workspace but cannot delete it": "워크스페이스를 관리할 수 있지만, 삭제할 수 없음", "Can manage workspace but cannot delete it": "Workspace를 관리할 수 있지만, 삭제는 불가능.",
"Can view": "볼 수 있음", "Can view": "볼 수 있음",
"Can view pages in space but not edit.": "Space의 페이지를 볼 수 있지만, 편집은 불가능.", "Can view pages in space but not edit.": "Space의 페이지를 볼 수 있지만, 편집은 불가능.",
"Cancel": "취소", "Cancel": "취소",
@@ -33,17 +33,15 @@
"Copy as Markdown": "Markdown으로 복사", "Copy as Markdown": "Markdown으로 복사",
"Copy link": "링크 복사", "Copy link": "링크 복사",
"Create": "생성", "Create": "생성",
"Create group": "그룹 생성", "Create group": " 생성",
"Create page": "페이지 생성", "Create page": "페이지 생성",
"Create space": "Space 생성", "Create space": "Space 생성",
"Create workspace": "워크스페이스 생성", "Create workspace": "Workspace 생성",
"Current password": "현재 비밀번호", "Current password": "현재 비밀번호",
"Dark": "어두운", "Dark": "어두운",
"Date": "날짜", "Date": "날짜",
"Delete": "삭제", "Delete": "삭제",
"Remove from page": "페이지에서 제거", "Delete group": "팀 삭제",
"Base options": "베이스 옵션",
"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": "세부사항",
@@ -58,7 +56,7 @@
"e.g Space for sales team to collaborate": "예: 영업 팀이 협업하는 스페이스", "e.g Space for sales team to collaborate": "예: 영업 팀이 협업하는 스페이스",
"Edit": "편집", "Edit": "편집",
"Read": "읽기", "Read": "읽기",
"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": "유효한 이메일 주소를 쉼표나 공백으로 구분하여 입력하세요 [최대: 50]", "Enter valid email addresses separated by comma or space max_50": "유효한 이메일 주소를 쉼표나 공백으로 구분하여 입력하세요 [최대: 50]",
@@ -78,24 +76,6 @@
"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": "데이터 갱신 실패",
"Failed to create base": "베이스를 만들지 못했습니다",
"Failed to update base": "베이스를 업데이트하지 못했습니다",
"Failed to delete base": "베이스를 삭제하지 못했습니다",
"Failed to create property": "속성을 만들지 못했습니다",
"Failed to update property": "속성을 업데이트하지 못했습니다",
"Failed to delete property": "속성을 삭제하지 못했습니다",
"Failed to reorder property": "속성 순서를 변경하지 못했습니다",
"Failed to create view": "보기를 만들지 못했습니다",
"Failed to update view": "보기를 업데이트하지 못했습니다",
"Failed to delete view": "보기를 삭제하지 못했습니다",
"Failed to create row": "행을 만들지 못했습니다",
"Failed to update row": "행을 업데이트하지 못했습니다",
"Failed to delete row": "행을 삭제하지 못했습니다",
"Failed to delete rows": "행을 삭제하지 못했습니다",
"Failed to reorder row": "행 순서를 변경하지 못했습니다",
"Failed to move card": "카드를 이동하지 못했습니다",
"Failed to add card": "카드를 추가하지 못했습니다",
"Failed to export CSV": "CSV를 내보내지 못했습니다",
"Favorite spaces": "즐겨찾는 스페이스", "Favorite spaces": "즐겨찾는 스페이스",
"Favorite spaces appear here": "즐겨찾는 스페이스가 여기에 표시됩니다", "Favorite spaces appear here": "즐겨찾는 스페이스가 여기에 표시됩니다",
"Favorites": "즐겨찾기", "Favorites": "즐겨찾기",
@@ -103,10 +83,10 @@
"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.": "Space 설정과 페이지에 대한 전체 접근 권한이 있습니다.", "Has full access to space settings and pages.": "Space 설정과 페이지에 대한 전체 접근 권한이 있습니다.",
"Home": "홈", "Home": "홈",
"Import pages": "페이지 가져오기", "Import pages": "페이지 가져오기",
@@ -119,14 +99,14 @@
"Invite new members": "새 사용자 초대", "Invite new members": "새 사용자 초대",
"Invite People": "사용자 초대", "Invite People": "사용자 초대",
"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": "초대된 사용자는 팀이 접근할 수 있는 Space에 대한 접근 권한을 받게 됩니다",
"Join the workspace": "워크스페이스 참여", "Join the workspace": "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": "사용자",
@@ -139,7 +119,7 @@
"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 shared pages": "공유된 페이지가 없습니다.", "No shared pages": "공유된 페이지가 없습니다.",
@@ -148,7 +128,7 @@
"Overview": "개요", "Overview": "개요",
"Owner": "소유자", "Owner": "소유자",
"page": "페이지", "page": "페이지",
"Page deleted successfully": "페이지 삭제", "Page deleted successfully": "페이지 삭제 완료",
"Page history": "페이지 기록", "Page history": "페이지 기록",
"Select version": "버전 선택", "Select version": "버전 선택",
"Highlight changes": "변경 사항 강조", "Highlight changes": "변경 사항 강조",
@@ -156,7 +136,7 @@
"Pages": "페이지", "Pages": "페이지",
"pages": "페이지", "pages": "페이지",
"Password": "비밀번호", "Password": "비밀번호",
"Password changed successfully": "비밀번호 변경", "Password changed successfully": "비밀번호 변경 완료",
"People": "사용자", "People": "사용자",
"Pending": "대기 중", "Pending": "대기 중",
"Please confirm your action": "작업을 확인해 주세요", "Please confirm your action": "작업을 확인해 주세요",
@@ -166,15 +146,15 @@
"Recently updated": "최근 업데이트", "Recently updated": "최근 업데이트",
"Remove": "제거", "Remove": "제거",
"Remove from favorites": "즐겨찾기에서 제거", "Remove from favorites": "즐겨찾기에서 제거",
"Remove group member": "그룹 멤버 제거", "Remove group member": "팀에서 사용자 제거",
"Remove space member": "Space에서 사용자 제거", "Remove space member": "Space에서 사용자 제거",
"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": "역할 선택",
@@ -199,8 +179,8 @@
"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": "시스템 설정",
"Templates": "템플릿", "Templates": "템플릿",
"Theme": "테마", "Theme": "테마",
@@ -209,7 +189,7 @@
"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": "워크스페이스 이름",
@@ -245,15 +225,15 @@
"Are you sure you want to delete this comment?": "이 댓글을 삭제하시겠습니까?", "Are you sure you want to delete this comment?": "이 댓글을 삭제하시겠습니까?",
"Delete chat": "채팅 삭제", "Delete chat": "채팅 삭제",
"Are you sure you want to delete '{{title}}'? This action cannot be undone.": "'{{title}}'을(를) 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", "Are you sure you want to delete '{{title}}'? This action cannot be undone.": "'{{title}}'을(를) 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"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": "댓글 처리 완료",
"Comment re-opened successfully": "댓글이 열린 상태로 표시됨", "Comment re-opened successfully": "댓글이 성공적으로 다시 열렸습니다",
"Comment unresolved successfully": "댓글이 미해결로 표시됨", "Comment unresolved successfully": "댓글 해결이 성공적으로 취소되었습니다",
"Failed to resolve comment": "댓글 처리 실패", "Failed to resolve comment": "댓글 처리 실패",
"Resolve comment": "댓글 해결", "Resolve comment": "댓글 해결",
"Unresolve comment": "댓글 해결 취소", "Unresolve comment": "댓글 해결 취소",
@@ -263,19 +243,19 @@
"Are you sure you want to unresolve this comment thread?": "이 댓글 스레드를 미해결로 변경하시겠습니까?", "Are you sure you want to unresolve this comment thread?": "이 댓글 스레드를 미해결로 변경하시겠습니까?",
"Resolved": "해결됨", "Resolved": "해결됨",
"No active comments.": "활성 댓글이 없습니다.", "No active comments.": "활성 댓글이 없습니다.",
"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.": "이 초대를 취소하시겠습니까? 사용자가 Workspace에 참여할 수 없게 됩니다.",
"Resend invitation": "초대 재전송", "Resend invitation": "초대 재전송",
"Anyone with this link can join this workspace.": "이 링크를 가진 모든 사용자가 이 워크스페이스에 참여할 수 있습니다.", "Anyone with this link can join this workspace.": "이 링크를 가진 모든 사용자가 이 Workspace에 참여할 수 있습니다.",
"Invite link": "초대 링크", "Invite link": "초대 링크",
"Copy": "복사", "Copy": "복사",
"Copy to space": "스페이스로 복사", "Copy to space": "스페이스로 복사",
"Copied": "복사됨", "Copied": "복사됨",
"Duplicate": "복제", "Duplicate": "복제",
"Select a user": "사용자 선택", "Select a user": "사용자 선택",
"Select a group": "그룹 선택", "Select a group": " 선택",
"Export all pages and attachments in this space.": "이 Space의 모든 페이지와 첨부파일을 내보냅니다.", "Export all pages and attachments in this space.": "이 Space의 모든 페이지와 첨부파일을 내보냅니다.",
"Delete space": "Space 삭제", "Delete space": "Space 삭제",
"Are you sure you want to delete this space?": "이 Space을 삭제하시겠습니까?", "Are you sure you want to delete this space?": "이 Space을 삭제하시겠습니까?",
@@ -290,11 +270,10 @@
"Export failed:": "내보내기 실패:", "Export failed:": "내보내기 실패:",
"export error": "내보내기 오류", "export error": "내보내기 오류",
"Export page": "페이지 내보내기", "Export page": "페이지 내보내기",
"Export successful": "내보내기 완료", "Export successful": "내보내기 성공",
"Export space": "Space 내보내기", "Export space": "Space 내보내기",
"Export {{type}}": "{{type}} 내보내기", "Export {{type}}": "{{type}} 내보내기",
"File exceeds the {{limit}} attachment limit": "첨부 파일 크기 제한 {{limit}}을 초과했습니다", "File exceeds the {{limit}} attachment limit": "첨부 파일 크기 제한 {{limit}}을 초과했습니다",
"Media": "미디어",
"Align left": "왼쪽 정렬", "Align left": "왼쪽 정렬",
"Align right": "오른쪽 정렬", "Align right": "오른쪽 정렬",
"Align center": "가운데 정렬", "Align center": "가운데 정렬",
@@ -388,8 +367,6 @@
"Insert horizontal rule divider": "가로 구분선 삽입", "Insert horizontal rule divider": "가로 구분선 삽입",
"Page break": "페이지 나누기", "Page break": "페이지 나누기",
"Insert a page break for printing.": "인쇄용 페이지 나누기를 삽입합니다.", "Insert a page break for printing.": "인쇄용 페이지 나누기를 삽입합니다.",
"Footnote": "각주",
"Insert a footnote reference.": "각주 참조를 삽입합니다.",
"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 audio from your device.": "기기에서 오디오를 업로드하세요.", "Upload any audio from your device.": "기기에서 오디오를 업로드하세요.",
@@ -421,8 +398,6 @@
"Insert mermaid diagram": "Mermaid 다이어그램 삽입", "Insert mermaid diagram": "Mermaid 다이어그램 삽입",
"Insert and design Drawio diagrams": "Drawio 다이어그램 삽입 및 편집", "Insert and design Drawio diagrams": "Drawio 다이어그램 삽입 및 편집",
"Insert current date": "현재 날짜 삽입", "Insert current date": "현재 날짜 삽입",
"Time": "시간",
"Insert current time": "현재 시간 삽입",
"Draw and sketch excalidraw diagrams": "Excalidraw 다이어그램 그리기 및 스케치", "Draw and sketch excalidraw diagrams": "Excalidraw 다이어그램 그리기 및 스케치",
"Multiple": "다중", "Multiple": "다중",
"Turn into": "변경하기", "Turn into": "변경하기",
@@ -449,13 +424,12 @@
"Names do not match": "이름이 일치하지 않습니다", "Names do not match": "이름이 일치하지 않습니다",
"Today, {{time}}": "오늘, {{time}}", "Today, {{time}}": "오늘, {{time}}",
"Yesterday, {{time}}": "어제, {{time}}", "Yesterday, {{time}}": "어제, {{time}}",
"now": "지금", "Space created successfully": "스페이스가 성공적으로 생성되었습니다",
"Space created successfully": "스페이스 생성됨", "Space updated successfully": "스페이스가 성공적으로 업데이트되었습니다",
"Space updated successfully": "스페이스 업데이트됨", "Space deleted successfully": "스페이스가 성공적으로 삭제되었습니다",
"Space deleted successfully": "스페이스 삭제됨", "Members added successfully": "멤버가 성공적으로 추가되었습니다",
"Members added successfully": "멤버 추가됨", "Member removed successfully": "멤버가 성공적으로 제거되었습니다",
"Member removed successfully": "멤버 삭제됨", "Member role updated successfully": "멤버 역할이 성공적으로 업데이트되었습니다",
"Member role updated successfully": "멤버 역할 업데이트됨",
"Created by: <b>{{creatorName}}</b>": "작성자: <b>{{creatorName}}</b>", "Created by: <b>{{creatorName}}</b>": "작성자: <b>{{creatorName}}</b>",
"Created at: {{time}}": "작성 시간: {{time}}", "Created at: {{time}}": "작성 시간: {{time}}",
"Edited by {{name}} {{time}}": "{{name}}님이 {{time}}에 편집함", "Edited by {{name}} {{time}}": "{{name}}님이 {{time}}에 편집함",
@@ -468,7 +442,7 @@
"Choose {{format}} file": "{{format}} 파일 선택", "Choose {{format}} file": "{{format}} 파일 선택",
"Reading": "읽기", "Reading": "읽기",
"Delete member": "멤버 삭제", "Delete member": "멤버 삭제",
"Member deleted successfully": "멤버 삭제됨", "Member deleted successfully": "멤버가 성공적으로 삭제되었습니다",
"Are you sure you want to delete this workspace member? This action is irreversible.": "이 워크스페이스 멤버를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", "Are you sure you want to delete this workspace member? This action is irreversible.": "이 워크스페이스 멤버를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"Deactivate member": "멤버 비활성화", "Deactivate member": "멤버 비활성화",
"Activate member": "멤버 활성화", "Activate member": "멤버 활성화",
@@ -501,32 +475,32 @@
"Delete share": "공유 삭제", "Delete share": "공유 삭제",
"Are you sure you want to delete this shared link?": "이 공유 링크를 삭제하시겠습니까?", "Are you sure you want to delete this shared link?": "이 공유 링크를 삭제하시겠습니까?",
"Publicly shared pages from spaces you are a member of will appear here": "회원으로 속한 스페이스의 공개 공유 페이지가 여기에 표시됩니다", "Publicly shared pages from spaces you are a member of will appear here": "회원으로 속한 스페이스의 공개 공유 페이지가 여기에 표시됩니다",
"Share deleted successfully": "공유 삭제됨", "Share deleted successfully": "공유가 성공적으로 삭제되었습니다",
"Share not found": "공유를 찾을 수 없습니다", "Share not found": "공유를 찾을 수 없습니다",
"Failed to share page": "페이지 공유에 실패했습니다", "Failed to share page": "페이지 공유에 실패했습니다",
"Disable public sharing": "공개 공유 비활성화", "Disable public sharing": "공유 비활성화",
"Prevent members from sharing pages publicly.": "멤버 페이지를 공개적으로 공유하지 못하게 합니다.", "Prevent members from sharing pages publicly.": "멤버들이 페이지를 공개적으로 공유하지 못하도록 방지하십시오.",
"Toggle public sharing": "공개 공유 전환", "Toggle public sharing": "공유 전환",
"Toggle space public sharing": "스페이스 공개 공유 전환", "Toggle space public sharing": "공간 공유 전환",
"Allow viewers to comment": "뷰어가 댓글을 달 수 있도록 허용", "Allow viewers to comment": "뷰어가 댓글을 달 수 있도록 허용",
"Allow viewers to add comments on pages in this space.": "이 공간 내 페이지에 뷰어가 댓글을 추가할 수 있도록 허용합니다.", "Allow viewers to add comments on pages in this space.": "이 공간 내 페이지에 뷰어가 댓글을 추가할 수 있도록 허용합니다.",
"Toggle viewer comments": "뷰어 댓글 전환", "Toggle viewer comments": "뷰어 댓글 전환",
"Public sharing is disabled at the workspace level": "공개 공유가 워크스페이스 수준에서 비활성화됨", "Public sharing is disabled at the workspace level": "워크스페이스 수준에서 공유가 비활성화되었습니다.",
"Prevent pages in this space from being shared publicly.": "이 스페이스의 페이지가 공개 공유되지 않도록 합니다.", "Prevent pages in this space from being shared publicly.": "이 공간의 페이지가 공개적으로 공유되지 않도록 방지하십시오.",
"Page permissions": "페이지 권한},{", "Page permissions": "페이지 권한},{",
"Control who can view and edit individual pages. Available with an enterprise license.": "개별 페이지의 조회 및 편집 권한을 제어합니다. 엔터프라이즈 라이선스에서 이용 가능합니다.", "Control who can view and edit individual pages. Available with an enterprise license.": "개별 페이지의 조회 및 편집 권한을 제어합니다. 엔터프라이즈 라이선스에서 이용 가능합니다.",
"Enable public sharing": "공개 공유 활성화", "Enable public sharing": "공유 활성화",
"Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "공개 공유를 활성화하시겠습니까? 멤버 페이지를 공개적으로 공유할 수 있게 됩니다.", "Are you sure you want to enable public sharing? Members will be able to share pages publicly.": "공유를 활성화하시겠습니까? 멤버들이 페이지를 공개적으로 공유할 수 있게 됩니다.",
"Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "공개 공유를 비활성화하시겠습니까? 이 워크스페이스의 기존 공유 링크가 모두 삭제됩니다.", "Are you sure you want to disable public sharing? All existing shared links in this workspace will be deleted.": "정말로 공유를 비활성화하시겠습니까? 이 워크스페이스의 모든 기존 공유 링크가 삭제됩니다.",
"Are you sure you want to enable public sharing for this space?": "이 스페이스에 대해 공개 공유를 활성화하시겠습니까?", "Are you sure you want to enable public sharing for this space?": "이 공간의 공유를 활성화하시겠습니까?",
"Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "공개 공유를 비활성화하시겠습니까? 이 스페이스의 기존 공유 링크가 모두 삭제됩니다.", "Are you sure you want to disable public sharing? All existing shared links in this space will be deleted.": "정말로 공유를 비활성화하시겠습니까? 이 공간의 모든 기존 공유 링크가 삭제됩니다.",
"Public sharing is disabled": "공개 공유가 비활성화", "Public sharing is disabled": "공유가 비활성화되었습니다.",
"Public sharing has been disabled at the workspace level.": "공개 공유가 워크스페이스 수준에서 비활성화되었습니다.", "Public sharing has been disabled at the workspace level.": "워크스페이스 수준에서 공유가 비활성화되었습니다.",
"Public sharing has been disabled for this space.": "이 스페이스의 공개 공유가 비활성화되었습니다.", "Public sharing has been disabled for this space.": "이 공간의 공유가 비활성화되었습니다.",
"Copy page": "페이지 복사", "Copy page": "페이지 복사",
"Copy page to a different space.": "다른 공간으로 페이지 복사하기.", "Copy page to a different space.": "다른 공간으로 페이지 복사하기.",
"Page copied successfully": "페이지 복사됨", "Page copied successfully": "페이지가 성공적으로 복사되었습니다",
"Page duplicated successfully": "페이지 복제됨", "Page duplicated successfully": "페이지가 성공적으로 복제되었습니다",
"Find": "찾기", "Find": "찾기",
"Not found": "찾을 수 없음", "Not found": "찾을 수 없음",
"Previous Match (Shift+Enter)": "이전 일치 항목 (Shift+Enter)", "Previous Match (Shift+Enter)": "이전 일치 항목 (Shift+Enter)",
@@ -542,13 +516,13 @@
"Error": "오류", "Error": "오류",
"Failed to disable MFA": "MFA 비활성화에 실패했습니다", "Failed to disable MFA": "MFA 비활성화에 실패했습니다",
"Disable two-factor authentication": "2단계 인증 비활성화", "Disable two-factor authentication": "2단계 인증 비활성화",
"Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "2단계 인증을 비활성화하면 계정의 보안이 낮아집니다. 로그인 시 비밀번호만 필요하게 됩니다.", "Disabling two-factor authentication will make your account less secure. You'll only need your password to sign in.": "이중 인증을 비활성화하면 계정의 보안이 낮아집니다. 로그인 시 비밀번호만 필요하게 됩니다.",
"Please enter your password to disable two-factor authentication:": "2단계 인증 비활성화를 위해 비밀번호를 입력하세요:", "Please enter your password to disable two-factor authentication:": "이중 인증 비활성화를 위해 비밀번호를 입력하세요:",
"Two-factor authentication has been enabled": "2단계 인증이 활성화", "Two-factor authentication has been enabled": "2단계 인증이 활성화되었습니다",
"Two-factor authentication has been disabled": "2단계 인증이 비활성화", "Two-factor authentication has been disabled": "2단계 인증이 비활성화되었습니다",
"2-step verification": "2단계 인증", "2-step verification": "2단계 인증",
"Protect your account with an additional verification layer when signing in.": "로그인 시 추가 인증 단계를 통해 계정을 보호하세요.", "Protect your account with an additional verification layer when signing in.": "로그인 시 추가 인증 단계를 통해 계정을 보호하세요.",
"Two-factor authentication is active on your account.": "2단계 인증이 계정에 활성화되어 있습니다.", "Two-factor authentication is active on your account.": "이중 인증이 계정에 활성화되어 있습니다.",
"Add 2FA method": "2FA 방법 추가", "Add 2FA method": "2FA 방법 추가",
"Backup codes": "백업 코드", "Backup codes": "백업 코드",
"Disable": "비활성화", "Disable": "비활성화",
@@ -578,13 +552,13 @@
"Save your backup codes": "백업 코드를 저장하세요", "Save your backup codes": "백업 코드를 저장하세요",
"These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "인증 앱에 대한 접근 권한을 잃은 경우, 이 코드를 사용하여 귀하의 계정에 접근할 수 있습니다. 각 코드는 한 번만 사용할 수 있습니다.", "These codes can be used to access your account if you lose access to your authenticator app. Each code can only be used once.": "인증 앱에 대한 접근 권한을 잃은 경우, 이 코드를 사용하여 귀하의 계정에 접근할 수 있습니다. 각 코드는 한 번만 사용할 수 있습니다.",
"Print": "인쇄", "Print": "인쇄",
"Two-factor authentication has been set up. Please log in again.": "2단계 인증이 설정되었습니다. 다시 로그인해 주세요.", "Two-factor authentication has been set up. Please log in again.": "이중 인증이 설정되었습니다. 다시 로그인해 주세요.",
"Two-Factor authentication required": "2단계 인증 필요", "Two-Factor authentication required": "2단계 인증 필요합니다",
"Your workspace requires two-factor authentication for all users": "이 워크스페이스는 모든 사용자에게 2단계 인증을 요구합니다", "Your workspace requires two-factor authentication for all users": "이 워크스페이스는 모든 사용자에게 2단계 인증을 요구합니다",
"To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "워크스페이스에 계속 액세스하려면 2단계 인증을 설정해야 합니다. 이는 계정의 보안을 한층 강화합니다.", "To continue accessing your workspace, you must set up two-factor authentication. This adds an extra layer of security to your account.": "워크스페이스 접근을 계속하려면 이중 인증을 설정해야 합니다. 이는 계정에 추가 보안 계층을 추가합니다.",
"Set up two-factor authentication": "2단계 인증 설정", "Set up two-factor authentication": "2단계 인증 설정",
"Cancel and logout": "취소하고 로그아웃", "Cancel and logout": "취소하고 로그아웃",
"Your workspace requires two-factor authentication. Please set it up to continue.": "워크스페이스에서는 2단계 인증이 필요합니다. 계속하려면 설정해 주세요.", "Your workspace requires two-factor authentication. Please set it up to continue.": "워크스페이스에서는 이중 인증이 필요합니다. 계속하려면 설정해 주세요.",
"This adds an extra layer of security to your account by requiring a verification code from your authenticator app.": "인증앱에서 얻은 인증 코드를 요구하여 계정의 보안에 추가적인 계층을 추가합니다.", "This adds an extra layer of security to your account by requiring a verification code from your authenticator app.": "인증앱에서 얻은 인증 코드를 요구하여 계정의 보안에 추가적인 계층을 추가합니다.",
"Password is required": "비밀번호는 필수입니다", "Password is required": "비밀번호는 필수입니다",
"Password must be at least 8 characters": "비밀번호는 8자 이상이어야 합니다", "Password must be at least 8 characters": "비밀번호는 8자 이상이어야 합니다",
@@ -593,10 +567,10 @@
"Enter the 6-digit code found in your authenticator app": "인증 앱에 표시된 6자리 코드를 입력하세요", "Enter the 6-digit code found in your authenticator app": "인증 앱에 표시된 6자리 코드를 입력하세요",
"Need help authenticating?": "인증에 도움이 필요하십니까?", "Need help authenticating?": "인증에 도움이 필요하십니까?",
"MFA QR Code": "MFA QR 코드", "MFA QR Code": "MFA QR 코드",
"Account created successfully. Please log in to set up two-factor authentication.": "계정 생성됨. 2단계 인증을 설정하려면 로그인해 주세요.", "Account created successfully. Please log in to set up two-factor authentication.": "계정이 성공적으로 생성되었습니다. 이중 인증을 설정하려면 로그인해 주세요.",
"Password reset successful. Please log in with your new password and complete two-factor authentication.": "비밀번호 재설정. 새 비밀번호로 로그인하여 2단계 인증을 완료하세요.", "Password reset successful. Please log in with your new password and complete two-factor authentication.": "비밀번호 재설정 성공. 새 비밀번호로 로그인하여 이중 인증을 완료하세요.",
"Password reset successful. Please log in with your new password to set up two-factor authentication.": "비밀번호 재설정. 새 비밀번호로 로그인하여 2단계 인증을 설정하세요.", "Password reset successful. Please log in with your new password to set up two-factor authentication.": "비밀번호 재설정 성공. 새 비밀번호로 로그인하여 이중 인증을 설정하세요.",
"Password reset was successful. Please log in with your new password.": "비밀번호 재설정. 새 비밀번호로 로그인하세요.", "Password reset was successful. Please log in with your new password.": "비밀번호 재설정이 성공적으로 완료되었습니다. 새 비밀번호로 로그인하세요.",
"Two-factor authentication": "2단계 인증", "Two-factor authentication": "2단계 인증",
"Use authenticator app instead": "대신 인증 앱 사용", "Use authenticator app instead": "대신 인증 앱 사용",
"Verify backup code": "백업 코드 확인", "Verify backup code": "백업 코드 확인",
@@ -618,12 +592,10 @@
"Permanently delete": "영구 삭제", "Permanently delete": "영구 삭제",
"<b>{{name}}</b> moved this page to Trash {{time}}.": "<b>{{name}}</b>님이 {{time}}에 이 페이지를 휴지통으로 이동했습니다.", "<b>{{name}}</b> moved this page to Trash {{time}}.": "<b>{{name}}</b>님이 {{time}}에 이 페이지를 휴지통으로 이동했습니다.",
"Page moved to trash": "페이지가 휴지통으로 이동되었습니다", "Page moved to trash": "페이지가 휴지통으로 이동되었습니다",
"Page restored successfully": "페이지 복원됨", "Page restored successfully": "페이지가 성공적으로 복원되었습니다",
"Deleted by": "삭제한 사람", "Deleted by": "삭제한 사람",
"Deleted at": "삭제 시간", "Deleted at": "삭제 시간",
"Preview": "미리보기", "Preview": "미리보기",
"Base preview unavailable": "베이스 미리보기를 사용할 수 없음",
"Restore this base to view its contents.": "이 베이스를 복원하여 내용을 보세요.",
"Subpages": "하위 페이지", "Subpages": "하위 페이지",
"Failed to load subpages": "하위 페이지를 불러오지 못했습니다", "Failed to load subpages": "하위 페이지를 불러오지 못했습니다",
"No subpages": "하위 페이지가 없습니다", "No subpages": "하위 페이지가 없습니다",
@@ -641,7 +613,7 @@
"Only users with email addresses from these domains can signup via SSO.": "이 도메인의 이메일 주소를 가진 사용자만 SSO를 통해 가입할 수 있습니다.", "Only users with email addresses from these domains can signup via SSO.": "이 도메인의 이메일 주소를 가진 사용자만 SSO를 통해 가입할 수 있습니다.",
"Enter valid domain names separated by comma or space": "쉼표 또는 공백으로 구분된 유효한 도메인 이름을 입력하세요", "Enter valid domain names separated by comma or space": "쉼표 또는 공백으로 구분된 유효한 도메인 이름을 입력하세요",
"Enforce two-factor authentication": "2단계 인증 강제", "Enforce two-factor authentication": "2단계 인증 강제",
"Once enforced, all members must enable two-factor authentication to access the workspace.": "시행되면 모든 멤버가 워크스페이스에 액세스하기 위해 2단계 인증을 활성화해야 합니다.", "Once enforced, all members must enable two-factor authentication to access the workspace.": "시행되면 모든 멤버가 작업 공간에 액세스하기 위해 이중 인증을 활성화해야 합니다.",
"Toggle MFA enforcement": "MFA 강제 설정 전환", "Toggle MFA enforcement": "MFA 강제 설정 전환",
"Display name": "표시 이름", "Display name": "표시 이름",
"Allow signup": "가입 허용", "Allow signup": "가입 허용",
@@ -660,7 +632,7 @@
"Remove image": "이미지 제거", "Remove image": "이미지 제거",
"Failed to remove image": "이미지 제거 실패", "Failed to remove image": "이미지 제거 실패",
"Image exceeds 10MB limit.": "이미지가 10MB 용량 제한을 초과합니다.", "Image exceeds 10MB limit.": "이미지가 10MB 용량 제한을 초과합니다.",
"Image removed successfully": "이미지 삭제됨", "Image removed successfully": "이미지가 성공적으로 제거되었습니다",
"API key": "API 키", "API key": "API 키",
"API keys": "API 키", "API keys": "API 키",
"API management": "API 관리", "API management": "API 관리",
@@ -672,7 +644,7 @@
"Last use": "최근 사용", "Last use": "최근 사용",
"No API keys found": "API 키를 찾을 수 없습니다", "No API keys found": "API 키를 찾을 수 없습니다",
"No expiration": "유효기간 없음", "No expiration": "유효기간 없음",
"Revoked successfully": "폐기 완료", "Revoked successfully": "성공적으로 취소되었습니다",
"Select expiration date": "만료일 선택", "Select expiration date": "만료일 선택",
"This action cannot be undone. Any applications using this API key will stop working.": "이 작업은 되돌릴 수 없습니다. 이 API 키를 사용하는 모든 응용 프로그램이 작동을 멈출 것입니다.", "This action cannot be undone. Any applications using this API key will stop working.": "이 작업은 되돌릴 수 없습니다. 이 API 키를 사용하는 모든 응용 프로그램이 작동을 멈출 것입니다.",
"Update": "업데이트", "Update": "업데이트",
@@ -699,7 +671,7 @@
"Upgrade your plan": "요금제를 업그레이드하세요", "Upgrade your plan": "요금제를 업그레이드하세요",
"Available with a paid license": "유료 라이선스에서만 사용 가능합니다", "Available with a paid license": "유료 라이선스에서만 사용 가능합니다",
"Upgrade your license tier.": "라이선스 등급을 업그레이드하세요.", "Upgrade your license tier.": "라이선스 등급을 업그레이드하세요.",
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "AI는 Docmost 유료 에디션에서 제공됩니다. sales@docmost.com으로 문의하세요.", "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI는 Docmost 엔터프라이즈 에디션에서 제공됩니다. sales@docmost.com으로 문의하세요.",
"AI & MCP": "AI 및 MCP", "AI & MCP": "AI 및 MCP",
"AI": "AI", "AI": "AI",
"MCP": "MCP", "MCP": "MCP",
@@ -707,11 +679,9 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "AI 어시스턴트와 도구가 워크스페이스 콘텐츠와 상호작용할 수 있도록 MCP 서버를 활성화하세요.", "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "AI 어시스턴트와 도구가 워크스페이스 콘텐츠와 상호작용할 수 있도록 MCP 서버를 활성화하세요.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP는 Docmost 엔터프라이즈 에디션에서만 제공됩니다. sales@docmost.com으로 문의하세요.", "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP는 Docmost 엔터프라이즈 에디션에서만 제공됩니다. sales@docmost.com으로 문의하세요.",
"MCP Server URL": "MCP 서버 URL", "MCP Server URL": "MCP 서버 URL",
"Connect AI assistants with your Docmost account via OAuth.": "OAuth를 통해 AI 도우미를 Docmost 계정에 연결합니다.", "Use your API key for authentication. You can manage API keys in your account settings.": "인증을 위해 API 키를 사용하세요. API 키는 계정 설정에서 관리할 수 있습니다.",
"Enforce OAuth": "Enforce OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI 도우미는 OAuth를 통해 Docmost 계정에 연결해야 합니다. MCP 서버에서는 API 키를 사용할 수 없습니다.",
"Toggle enforce OAuth for MCP": "Toggle enforce OAuth for MCP",
"Supported tools": "지원되는 도구", "Supported tools": "지원되는 도구",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "워크스페이스에 MCP가 활성화되어 있습니다. AI 어시스턴트를 연결하려면 API 키를 사용하세요.",
"MCP server URL:": "MCP 서버 URL:", "MCP server URL:": "MCP 서버 URL:",
"Learn more": "자세히 알아보기", "Learn more": "자세히 알아보기",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "워크스페이스의 모든 사용자를 위한 API 키를 관리하세요. 사용 방법은 <anchor>API 문서</anchor>를 참고하세요.", "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "워크스페이스의 모든 사용자를 위한 API 키를 관리하세요. 사용 방법은 <anchor>API 문서</anchor>를 참고하세요.",
@@ -944,8 +914,8 @@
"Try again": "다시 시도", "Try again": "다시 시도",
"Untitled chat": "제목 없는 채팅", "Untitled chat": "제목 없는 채팅",
"What can I help you with?": "무엇을 도와드릴까요?", "What can I help you with?": "무엇을 도와드릴까요?",
"Are you sure you want to revoke this {{credential}}": "이 {{credential}}을(를) 폐기하시겠습니까?", "Are you sure you want to revoke this {{credential}}": "이 {{credential}}을 취소하시겠습니까?",
"Automatically provision users and groups from your identity provider via SCIM.": "SCIM을 통해 ID 공급자로부터 사용자와 그룹을 자동으로 프로비저닝합니다.", "Automatically provision users and groups from your identity provider via SCIM.": "SCIM을 통해 ID 공급자에서 사용자와 그룹을 자동으로 프로비저닝합니다.",
"Configure your identity provider with this URL to provision users and groups.": "사용자와 그룹을 프로비저닝할 수 있도록 이 URL로 ID 공급자를 구성하세요.", "Configure your identity provider with this URL to provision users and groups.": "사용자와 그룹을 프로비저닝할 수 있도록 이 URL로 ID 공급자를 구성하세요.",
"Create {{credential}}": "{{credential}} 만들기", "Create {{credential}}": "{{credential}} 만들기",
"{{credential}} created": "{{credential}} 생성됨", "{{credential}} created": "{{credential}} 생성됨",
@@ -958,7 +928,7 @@
"Important": "중요", "Important": "중요",
"Make sure to copy your {{credential}} now. You won't be able to see it again!": "지금 {{credential}}를 복사해 두세요. 다시는 볼 수 없습니다!", "Make sure to copy your {{credential}} now. You won't be able to see it again!": "지금 {{credential}}를 복사해 두세요. 다시는 볼 수 없습니다!",
"Never": "안 함", "Never": "안 함",
"Revoke {{credential}}": "{{credential}} 폐기", "Revoke {{credential}}": "{{credential}} 취소",
"SCIM endpoint URL": "SCIM 엔드포인트 URL", "SCIM endpoint URL": "SCIM 엔드포인트 URL",
"SCIM provisioning": "SCIM 프로비저닝", "SCIM provisioning": "SCIM 프로비저닝",
"SCIM takes precedence over SSO group sync while enabled.": "SCIM이 활성화되어 있는 동안에는 SSO 그룹 동기화보다 SCIM이 우선 적용됩니다.", "SCIM takes precedence over SSO group sync while enabled.": "SCIM이 활성화되어 있는 동안에는 SSO 그룹 동기화보다 SCIM이 우선 적용됩니다.",
@@ -995,8 +965,6 @@
"Are you sure you want to delete this template?": "이 템플릿을 삭제하시겠습니까?", "Are you sure you want to delete this template?": "이 템플릿을 삭제하시겠습니까?",
"Template scope updated": "템플릿 범위가 업데이트되었습니다", "Template scope updated": "템플릿 범위가 업데이트되었습니다",
"Choose which space this template belongs to": "이 템플릿이 속할 스페이스를 선택하세요", "Choose which space this template belongs to": "이 템플릿이 속할 스페이스를 선택하세요",
"Only workspace admins can create global templates.": "전역 템플릿은 워크스페이스 관리자만 만들 수 있습니다.",
"You need edit access to this space to create templates.": "템플릿을 만들려면 이 스페이스에 대한 편집 권한이 있어야 합니다.",
"Scope": "범위", "Scope": "범위",
"Select scope": "범위 선택", "Select scope": "범위 선택",
"Title": "제목", "Title": "제목",
@@ -1009,7 +977,7 @@
"Search pages and spaces...": "페이지와 스페이스 검색...", "Search pages and spaces...": "페이지와 스페이스 검색...",
"No results found": "결과를 찾을 수 없습니다", "No results found": "결과를 찾을 수 없습니다",
"You don't have permission to create pages here": "여기에서 페이지를 만들 권한이 없습니다", "You don't have permission to create pages here": "여기에서 페이지를 만들 권한이 없습니다",
"Chat menu for {{title}}": "{{title}}의 채팅 메뉴", "Chat menu": "채팅 메뉴",
"API key menu": "API 키 메뉴", "API key menu": "API 키 메뉴",
"Jump to comment selection": "댓글 선택으로 이동", "Jump to comment selection": "댓글 선택으로 이동",
"Slash commands": "슬래시 명령어", "Slash commands": "슬래시 명령어",
@@ -1095,7 +1063,7 @@
"Filter": "필터", "Filter": "필터",
"Page title": "페이지 제목", "Page title": "페이지 제목",
"Page content": "페이지 내용", "Page content": "페이지 내용",
"Member actions for {{name}}": "{{name}}에 대한 멤버 작업", "Member actions": "멤버 작업",
"Toggle password visibility": "비밀번호 표시 전환", "Toggle password visibility": "비밀번호 표시 전환",
"Send comment": "댓글 보내기", "Send comment": "댓글 보내기",
"Token actions": "토큰 작업", "Token actions": "토큰 작업",
@@ -1116,226 +1084,5 @@
"Added {{name}} to favorites": "{{name}} 즐겨찾기에 추가됨", "Added {{name}} to favorites": "{{name}} 즐겨찾기에 추가됨",
"Removed {{name}} from favorites": "{{name}} 즐겨찾기에서 제거됨", "Removed {{name}} from favorites": "{{name}} 즐겨찾기에서 제거됨",
"Page menu for {{name}}": "{{name}}의 페이지 메뉴", "Page menu for {{name}}": "{{name}}의 페이지 메뉴",
"Create subpage of {{name}}": "{{name}}의 하위 페이지 만들기", "Create subpage of {{name}}": "{{name}}의 하위 페이지 만들기"
"Allow personal spaces": "개인 공간 허용",
"Members can create their own personal space.": "멤버는 자신만의 개인 공간을 만들 수 있습니다.",
"Toggle allow personal spaces": "개인 공간 허용 전환",
"Create personal space": "개인 공간 만들기",
"Personal space": "개인 공간",
"{{name}}'s space": "{{name}}의 공간",
"Apply": "적용",
"Cells that aren't already a page reference will be cleared.": "이미 페이지 참조인 셀이 아닌 경우 지워집니다.",
"Cells that aren't a valid URL will be cleared.": "유효한 URL이 아닌 셀은 지워집니다.",
"Cells that aren't a valid email address will be cleared.": "유효한 이메일 주소가 아닌 셀은 지워집니다.",
"Cells that can't be parsed as a date will be cleared.": "날짜로 해석할 수 없는 셀은 지워집니다.",
"Cells that can't be parsed as a number will be cleared.": "숫자로 해석할 수 없는 셀은 지워집니다.",
"Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).": "셀 값은 강제로 변환됩니다(yes/true/1은 선택됨이 되고, 그 외의 모든 값은 선택 해제되거나 지워집니다).",
"Cells will be reinterpreted under the new type.": "셀은 새 유형에 따라 다시 해석됩니다.",
"Cells will be replaced with a comma-separated list of file names.": "셀은 쉼표로 구분된 파일 이름 목록으로 대체됩니다.",
"Cells will be replaced with a comma-separated list of option names.": "셀은 쉼표로 구분된 옵션 이름 목록으로 대체됩니다.",
"Cells will be replaced with the option name.": "셀은 옵션 이름으로 대체됩니다.",
"Cells will be replaced with the page title.": "셀은 페이지 제목으로 대체됩니다.",
"Cells will be replaced with the person's name.": "셀은 사람 이름으로 대체됩니다.",
"Change type": "유형 변경",
"Change type to {{label}}?": "유형을 {{label}}로 변경할까요?",
"Converting…": "변환 중…",
"Existing values become single-item lists. No data is lost.": "기존 값은 단일 항목 목록이 됩니다. 데이터는 손실되지 않습니다.",
"Only the first selected item per row will be kept; the rest will be discarded.": "각 행에서 처음 선택된 항목만 유지되며 나머지는 삭제됩니다.",
"Previous record": "이전 레코드",
"Next record": "다음 레코드",
"Record actions": "레코드 작업",
"Delete record": "레코드 삭제",
"Delete record?": "레코드를 삭제할까요?",
"This action cannot be undone.": "이 작업은 되돌릴 수 없습니다.",
"to navigate": "탐색하려면",
"to close": "닫으려면",
"Expand row {{number}}": "행 {{number}} 펼치기",
"Saving…": "저장 중…",
"Read-only": "읽기 전용",
"Loading…": "불러오는 중…",
"Updated {{when}}": "{{when}}에 업데이트됨",
"Add property": "속성 추가",
"Create property": "속성 만들기",
"Hide properties": "속성 숨기기",
"Find a property type": "속성 유형 찾기",
"Properties": "속성",
"A property with this name already exists": "이 이름의 속성이 이미 있습니다",
"Add card": "카드 추가",
"Add filter": "필터 추가",
"Add option": "옵션 추가",
"Add sort": "정렬 추가",
"Add view": "보기 추가",
"All data in this column will be lost.": "이 열의 모든 데이터가 손실됩니다.",
"Allow multiple people": "여러 사람 허용",
"Alphabetize": "가나다순",
"Are you sure you want to delete": "삭제하시겠습니까",
"Ascending": "오름차순",
"Base deleted": "베이스가 삭제됨",
"Base name": "베이스 이름",
"Base table": "베이스 테이블",
"Card properties": "카드 속성",
"Cells longer than the Text limit will be truncated and the extra content permanently lost.": "텍스트 제한보다 긴 셀은 잘리며, 초과된 콘텐츠는 영구적으로 손실됩니다.",
"Checkbox": "체크박스",
"Checked by default": "기본적으로 선택됨",
"Choose a property": "속성 선택",
"Clear selection": "선택 지우기",
"Column options": "열 옵션",
"Comma, period": "쉼표, 마침표",
"Complete": "완료",
"Contains": "포함",
"Copy link to view": "보기 링크 복사",
"Create a status property": "상태 속성 만들기",
"Created at": "작성 시간",
"Currency": "통화",
"Decimal places": "소수 자릿수",
"Default value": "기본값",
"Delete property": "속성 삭제",
"Delete view": "보기 삭제",
"Delete {{count}} rows?_one": "1개 행을 삭제하시겠습니까?",
"Delete {{count}} rows?_other": "{{count}}개 행을 삭제하시겠습니까?",
"Descending": "내림차순",
"Discard": "삭제",
"Doesn't contain": "포함하지 않음",
"Done": "완료",
"Edit formula": "수식 편집",
"Edit property": "속성 편집",
"Export CSV": "CSV 내보내기",
"Failed to load base": "베이스를 불러오지 못했습니다",
"False": "거짓",
"File": "파일",
"Filter and sort changes are visible only to you": "필터 및 정렬 변경 사항은 나에게만 표시됩니다",
"Filter by": "필터 기준",
"Formula": "수식",
"Get started with": "시작하기",
"Greater than": "보다 큼",
"Group by": "그룹화 기준",
"Group this board by a select or status property.": "선택 또는 상태 속성으로 이 보드를 그룹화하세요.",
"Hide all": "모두 숨기기",
"Hide group": "그룹 숨기기",
"In Progress": "진행 중",
"In progress": "진행 중",
"Include time": "시간 포함",
"Insert a kanban board on this page": "이 페이지에 칸반 보드 삽입",
"Insert an inline base on this page": "이 페이지에 인라인 베이스 삽입",
"Is": "같음",
"Is after": "이후",
"Is any of": "다음 중 하나와 같음",
"Is before": "이전",
"Is empty": "비어 있음",
"Is none of": "다음 중 어느 것과도 같지 않음",
"Is not": "같지 않음",
"Is not empty": "비어 있지 않음",
"Is on or after": "해당 날짜 또는 이후",
"Is on or before": "해당 날짜 또는 이전",
"Is within": "다음 범위 내",
"Keep editing": "계속 편집",
"Last edited at": "마지막 편집 시간",
"Last edited by": "마지막 편집자",
"Less than": "보다 작음",
"Link copied to clipboard": "링크가 클립보드에 복사됨",
"Local": "로컬",
"Long text": "긴 텍스트",
"Moved card to {{column}}": "카드를 {{column}}(으)로 이동함",
"Moved column to {{column}}": "열을 {{column}}(으)로 이동함",
"Multi-select": "다중 선택",
"New row": "새 행",
"Next month": "다음 달",
"Next week": "다음 주",
"Next year": "내년",
"No filters applied": "적용된 필터 없음",
"No match": "일치하는 항목 없음",
"No options for this property type": "이 속성 유형에는 옵션이 없습니다",
"No sorts applied": "적용된 정렬 없음",
"No value": "값 없음",
"None": "없음",
"Not started": "시작 전",
"Number": "숫자",
"One month ago": "한 달 전",
"One month from now": "한 달 후",
"One week ago": "일주일 전",
"One week from now": "일주일 후",
"Open as page": "페이지로 열기",
"Options": "옵션",
"Past month": "지난달",
"Past week": "지난주",
"Past year": "작년",
"Percent": "백분율",
"Period, comma": "마침표, 쉼표",
"Person": "사람",
"Please enter a valid email": "유효한 이메일을 입력하세요",
"Progress": "진행률",
"Property options": "속성 옵션",
"Relative": "상대값",
"Rename property": "속성 이름 변경",
"Reset": "재설정",
"Save for everyone": "모든 사용자에게 저장",
"Select": "선택",
"Select a property": "속성 선택",
"Select all loaded rows": "불러온 모든 행 선택",
"Show all": "모두 보기",
"Sort": "정렬",
"Sort by": "정렬 기준",
"Space, comma": "공백, 쉼표",
"Space, period": "공백, 마침표",
"This board has no grouping property yet.": "이 보드에는 아직 그룹화 속성이 없습니다.",
"This month": "이번 달",
"This year": "올해",
"Thousands and decimal separators": "천 단위 및 소수 구분 기호",
"Time format": "시간 형식",
"To Do": "할 일",
"Tomorrow": "내일",
"True": "참",
"Unsaved changes": "저장되지 않은 변경 사항",
"Untitled base": "제목 없는 베이스",
"URL": "URL",
"Value": "값",
"View updated for everyone": "보기가 모두에게 업데이트되었습니다",
"You have unsaved changes. Do you want to discard them?": "저장되지 않은 변경 사항이 있습니다. 버리시겠습니까?",
"{{count}} rows deleted_one": "1개 행 삭제 완료",
"{{count}} rows deleted_other": "{{count}}개 행 삭제 완료",
"{{count}} selected_one": "1개 선택됨",
"{{count}} selected_other": "{{count}}개 선택됨",
"Compare": "비교",
"Compare versions": "버전 비교",
"Select version from {{date}}": "{{date}}의 버전 선택",
"Version actions for {{date}}": "{{date}} 버전 작업",
"Comparing {{newer}} and {{older}}": "{{newer}} 및 {{older}} 비교 중",
"Exit compare": "비교 종료",
"Search attachments...": "첨부파일 검색...",
"Error loading attachments.": "첨부파일을 불러오는 중 오류가 발생했습니다.",
"No attachments on this page yet.": "이 페이지에는 아직 첨부파일이 없습니다.",
"Uploaded by {{name}}": "업로드한 사람: {{name}}",
"Download {{name}}": "{{name}} 다운로드",
"Access revoked": "액세스가 취소되었습니다",
"Authorize application": "애플리케이션 승인",
"{{name}} wants to access {{workspace}}": "{{name}}에서 {{workspace}}에 액세스하려고 합니다",
"Not you? Switch account": "본인이 아니신가요? 계정을 전환하세요",
"This application will be able to:": "이 애플리케이션이 수행할 수 있는 작업:",
"Write": "쓰기",
"Invalid authorization request": "잘못된 승인 요청",
"Authorize": "승인",
"Application": "애플리케이션",
"Permissions": "권한",
"Authorized": "승인됨",
"Revoke access": "액세스 취소",
"Revoke access for {{name}}": "{{name}}의 액세스 취소",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "정말로 {{name}}의 액세스를 취소하시겠습니까? 이 애플리케이션은 더 이상 회원님의 계정에 액세스할 수 없습니다.",
"Something went wrong. Please try again.": "문제가 발생했습니다. 다시 시도해 주세요.",
"Remove {{name}}": "{{name}} 제거",
"Make sure you trust this application before authorizing it.": "이 애플리케이션을 승인하기 전에 신뢰할 수 있는지 확인하세요.",
"You will be redirected to": "다음으로 리디렉션됩니다",
"View content without making changes.": "변경하지 않고 콘텐츠를 봅니다.",
"Create and modify content.": "콘텐츠를 생성하고 수정합니다.",
"Applications and AI assistants you have authorized to access your account.": "회원님의 계정에 액세스하도록 승인한 애플리케이션 및 AI 도우미입니다.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "워크스페이스에서 MCP가 활성화되어 있습니다. OAuth를 통해 AI 도우미를 Docmost 계정에 연결하세요.",
"Authorized apps": "승인된 앱",
"No authorized apps yet.": "아직 승인된 앱이 없습니다.",
"Workspace knowledge only": "워크스페이스 지식만 사용",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "AI 채팅이 워크스페이스 페이지와 업로드된 파일의 내용만 바탕으로 답변하도록 제한합니다. 외부 지식은 사용하지 않습니다.",
"Toggle workspace knowledge only": "워크스페이스 지식만 사용 전환",
"Read-only mode": "읽기 전용 모드",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI 채팅은 워크스페이스 콘텐츠를 검색하고 읽을 수 있지만, 페이지를 생성하거나 편집할 수는 없습니다.",
"Toggle AI Chat read-only mode": "AI 채팅 읽기 전용 모드 전환",
"Title only": "제목만",
"you": "회원님"
} }
@@ -41,8 +41,6 @@
"Dark": "Donker", "Dark": "Donker",
"Date": "Datum", "Date": "Datum",
"Delete": "Verwijderen", "Delete": "Verwijderen",
"Remove from page": "Van pagina verwijderen",
"Base options": "Basisopties",
"Delete group": "Groep verwijderen", "Delete group": "Groep verwijderen",
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Weet u zeker dat u deze pagina wilt verwijderen? Dit zal de subpagina's en paginageschiedenis verwijderen. Deze actie kan niet ongedaan gemaakt worden.", "Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Weet u zeker dat u deze pagina wilt verwijderen? Dit zal de subpagina's en paginageschiedenis verwijderen. Deze actie kan niet ongedaan gemaakt worden.",
"Description": "Beschrijving", "Description": "Beschrijving",
@@ -78,24 +76,6 @@
"Failed to import pages": "Pagina's importeren mislukt", "Failed to import pages": "Pagina's importeren mislukt",
"Failed to load page. An error occurred.": "Laden van pagina mislukt. Er is een fout opgetreden.", "Failed to load page. An error occurred.": "Laden van pagina mislukt. Er is een fout opgetreden.",
"Failed to update data": "Bijwerken van gegevens mislukt", "Failed to update data": "Bijwerken van gegevens mislukt",
"Failed to create base": "Kan basis niet aanmaken",
"Failed to update base": "Kan basis niet bijwerken",
"Failed to delete base": "Kan basis niet verwijderen",
"Failed to create property": "Kan eigenschap niet aanmaken",
"Failed to update property": "Kan eigenschap niet bijwerken",
"Failed to delete property": "Kan eigenschap niet verwijderen",
"Failed to reorder property": "Kan eigenschap niet opnieuw ordenen",
"Failed to create view": "Kan weergave niet aanmaken",
"Failed to update view": "Kan weergave niet bijwerken",
"Failed to delete view": "Kan weergave niet verwijderen",
"Failed to create row": "Kan rij niet aanmaken",
"Failed to update row": "Kan rij niet bijwerken",
"Failed to delete row": "Kan rij niet verwijderen",
"Failed to delete rows": "Kan rijen niet verwijderen",
"Failed to reorder row": "Kan rij niet opnieuw ordenen",
"Failed to move card": "Kan kaart niet verplaatsen",
"Failed to add card": "Kan kaart niet toevoegen",
"Failed to export CSV": "Kan CSV niet exporteren",
"Favorite spaces": "Favoriete ruimtes", "Favorite spaces": "Favoriete ruimtes",
"Favorite spaces appear here": "Favoriete ruimtes verschijnen hier", "Favorite spaces appear here": "Favoriete ruimtes verschijnen hier",
"Favorites": "Favorieten", "Favorites": "Favorieten",
@@ -294,7 +274,6 @@
"Export space": "Exporteer ruimte", "Export space": "Exporteer ruimte",
"Export {{type}}": "Exporteer {{type}}", "Export {{type}}": "Exporteer {{type}}",
"File exceeds the {{limit}} attachment limit": "Bestand overschrijdt de bijlagelimiet van {{limit}}", "File exceeds the {{limit}} attachment limit": "Bestand overschrijdt de bijlagelimiet van {{limit}}",
"Media": "Media",
"Align left": "Links uitlijnen", "Align left": "Links uitlijnen",
"Align right": "Rechts uitlijnen", "Align right": "Rechts uitlijnen",
"Align center": "Centreren", "Align center": "Centreren",
@@ -388,8 +367,6 @@
"Insert horizontal rule divider": "Horizontale lijn invoegen", "Insert horizontal rule divider": "Horizontale lijn invoegen",
"Page break": "Pagina-einde", "Page break": "Pagina-einde",
"Insert a page break for printing.": "Voeg een pagina-einde in voor het afdrukken.", "Insert a page break for printing.": "Voeg een pagina-einde in voor het afdrukken.",
"Footnote": "Voetnoot",
"Insert a footnote reference.": "Voeg een voetnootverwijzing in.",
"Upload any image from your device.": "Upload een afbeelding vanaf uw apparaat.", "Upload any image from your device.": "Upload een afbeelding vanaf uw apparaat.",
"Upload any video from your device.": "Upload een video vanaf uw apparaat.", "Upload any video from your device.": "Upload een video vanaf uw apparaat.",
"Upload any audio from your device.": "Upload een audio vanaf uw apparaat.", "Upload any audio from your device.": "Upload een audio vanaf uw apparaat.",
@@ -421,8 +398,6 @@
"Insert mermaid diagram": "Mermaid-diagram invoegen", "Insert mermaid diagram": "Mermaid-diagram invoegen",
"Insert and design Drawio diagrams": "Drawio-diagrammen invoegen en ontwerpen", "Insert and design Drawio diagrams": "Drawio-diagrammen invoegen en ontwerpen",
"Insert current date": "Huidige datum invoegen", "Insert current date": "Huidige datum invoegen",
"Time": "Tijd",
"Insert current time": "Huidige tijd invoegen",
"Draw and sketch excalidraw diagrams": "Excalidraw-diagrammen tekenen en schetsen", "Draw and sketch excalidraw diagrams": "Excalidraw-diagrammen tekenen en schetsen",
"Multiple": "Meerdere", "Multiple": "Meerdere",
"Turn into": "Omzetten naar", "Turn into": "Omzetten naar",
@@ -449,7 +424,6 @@
"Names do not match": "Namen komen niet overeen", "Names do not match": "Namen komen niet overeen",
"Today, {{time}}": "Vandaag, {{time}}", "Today, {{time}}": "Vandaag, {{time}}",
"Yesterday, {{time}}": "Gisteren, {{time}}", "Yesterday, {{time}}": "Gisteren, {{time}}",
"now": "nu",
"Space created successfully": "Ruimte succesvol aangemaakt", "Space created successfully": "Ruimte succesvol aangemaakt",
"Space updated successfully": "Ruimte succesvol bijgewerkt", "Space updated successfully": "Ruimte succesvol bijgewerkt",
"Space deleted successfully": "Ruimte succesvol verwijderd", "Space deleted successfully": "Ruimte succesvol verwijderd",
@@ -622,8 +596,6 @@
"Deleted by": "Verwijderd door", "Deleted by": "Verwijderd door",
"Deleted at": "Verwijderd op", "Deleted at": "Verwijderd op",
"Preview": "Voorbeeld", "Preview": "Voorbeeld",
"Base preview unavailable": "Voorbeeld van basis niet beschikbaar",
"Restore this base to view its contents.": "Herstel deze basis om de inhoud te bekijken.",
"Subpages": "Subpagina's", "Subpages": "Subpagina's",
"Failed to load subpages": "Subpagina's laden mislukt", "Failed to load subpages": "Subpagina's laden mislukt",
"No subpages": "Geen subpagina's", "No subpages": "Geen subpagina's",
@@ -699,7 +671,7 @@
"Upgrade your plan": "Upgrade je abonnement", "Upgrade your plan": "Upgrade je abonnement",
"Available with a paid license": "Beschikbaar met een betaalde licentie", "Available with a paid license": "Beschikbaar met een betaalde licentie",
"Upgrade your license tier.": "Upgrade je licentieniveau.", "Upgrade your license tier.": "Upgrade je licentieniveau.",
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "AI is beschikbaar in de betaalde edities van Docmost. Neem contact op met sales@docmost.com.", "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI is alleen beschikbaar in de Docmost Enterprise-editie. Neem contact op met sales@docmost.com.",
"AI & MCP": "AI & MCP", "AI & MCP": "AI & MCP",
"AI": "AI", "AI": "AI",
"MCP": "MCP", "MCP": "MCP",
@@ -707,11 +679,9 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Schakel de MCP-server in zodat AI-assistenten en tools kunnen interageren met de inhoud van uw werkruimte.", "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Schakel de MCP-server in zodat AI-assistenten en tools kunnen interageren met de inhoud van uw werkruimte.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP is alleen beschikbaar in de Docmost Enterprise-editie. Neem contact op met sales@docmost.com.", "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP is alleen beschikbaar in de Docmost Enterprise-editie. Neem contact op met sales@docmost.com.",
"MCP Server URL": "MCP-server-URL", "MCP Server URL": "MCP-server-URL",
"Connect AI assistants with your Docmost account via OAuth.": "Verbind AI-assistenten met je Docmost-account via OAuth.", "Use your API key for authentication. You can manage API keys in your account settings.": "Gebruik uw API-sleutel voor authenticatie. U kunt API-sleutels beheren in uw accountinstellingen.",
"Enforce OAuth": "Enforce OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI-assistenten moeten verbinding maken met een Docmost-account via OAuth. API-sleutels kunnen niet worden gebruikt met de MCP-server.",
"Toggle enforce OAuth for MCP": "Toggle enforce OAuth for MCP",
"Supported tools": "Ondersteunde tools", "Supported tools": "Ondersteunde tools",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "In uw werkruimte is MCP ingeschakeld. Gebruik uw API-sleutel om AI-assistenten te koppelen.",
"MCP server URL:": "MCP-server-URL:", "MCP server URL:": "MCP-server-URL:",
"Learn more": "Meer informatie", "Learn more": "Meer informatie",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Beheer API-sleutels voor alle gebruikers in de werkruimte. Bekijk de <anchor>API-documentatie</anchor> voor gebruiksdetails.", "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Beheer API-sleutels voor alle gebruikers in de werkruimte. Bekijk de <anchor>API-documentatie</anchor> voor gebruiksdetails.",
@@ -995,8 +965,6 @@
"Are you sure you want to delete this template?": "Weet je zeker dat je dit sjabloon wilt verwijderen?", "Are you sure you want to delete this template?": "Weet je zeker dat je dit sjabloon wilt verwijderen?",
"Template scope updated": "Bereik van sjabloon bijgewerkt", "Template scope updated": "Bereik van sjabloon bijgewerkt",
"Choose which space this template belongs to": "Kies bij welke werkruimte dit sjabloon hoort", "Choose which space this template belongs to": "Kies bij welke werkruimte dit sjabloon hoort",
"Only workspace admins can create global templates.": "Alleen workspace-beheerders kunnen globale sjablonen maken.",
"You need edit access to this space to create templates.": "Je hebt bewerkingstoegang tot deze ruimte nodig om sjablonen te maken.",
"Scope": "Bereik", "Scope": "Bereik",
"Select scope": "Bereik selecteren", "Select scope": "Bereik selecteren",
"Title": "Titel", "Title": "Titel",
@@ -1009,7 +977,7 @@
"Search pages and spaces...": "Pagina's en werkruimtes zoeken...", "Search pages and spaces...": "Pagina's en werkruimtes zoeken...",
"No results found": "Geen resultaten gevonden", "No results found": "Geen resultaten gevonden",
"You don't have permission to create pages here": "Je hebt geen toestemming om hier pagina's te maken", "You don't have permission to create pages here": "Je hebt geen toestemming om hier pagina's te maken",
"Chat menu for {{title}}": "Chatmenu voor {{title}}", "Chat menu": "Chatmenu",
"API key menu": "API-sleutelmenu", "API key menu": "API-sleutelmenu",
"Jump to comment selection": "Naar reactieselectie springen", "Jump to comment selection": "Naar reactieselectie springen",
"Slash commands": "Slash-opdrachten", "Slash commands": "Slash-opdrachten",
@@ -1095,7 +1063,7 @@
"Filter": "Filter", "Filter": "Filter",
"Page title": "Paginatitel", "Page title": "Paginatitel",
"Page content": "Pagina-inhoud", "Page content": "Pagina-inhoud",
"Member actions for {{name}}": "Lidacties voor {{name}}", "Member actions": "Lidacties",
"Toggle password visibility": "Wachtwoordzichtbaarheid in-/uitschakelen", "Toggle password visibility": "Wachtwoordzichtbaarheid in-/uitschakelen",
"Send comment": "Reactie verzenden", "Send comment": "Reactie verzenden",
"Token actions": "Tokenacties", "Token actions": "Tokenacties",
@@ -1116,226 +1084,5 @@
"Added {{name}} to favorites": "{{name}} toegevoegd aan favorieten", "Added {{name}} to favorites": "{{name}} toegevoegd aan favorieten",
"Removed {{name}} from favorites": "{{name}} verwijderd uit favorieten", "Removed {{name}} from favorites": "{{name}} verwijderd uit favorieten",
"Page menu for {{name}}": "Paginamenu voor {{name}}", "Page menu for {{name}}": "Paginamenu voor {{name}}",
"Create subpage of {{name}}": "Subpagina van {{name}} maken", "Create subpage of {{name}}": "Subpagina van {{name}} maken"
"Allow personal spaces": "Persoonlijke ruimtes toestaan",
"Members can create their own personal space.": "Leden kunnen hun eigen persoonlijke ruimte aanmaken.",
"Toggle allow personal spaces": "Persoonlijke ruimtes toestaan in- of uitschakelen",
"Create personal space": "Persoonlijke ruimte aanmaken",
"Personal space": "Persoonlijke ruimte",
"{{name}}'s space": "ruimte van {{name}}",
"Apply": "Toepassen",
"Cells that aren't already a page reference will be cleared.": "Cellen die nog geen paginaverwijzing zijn, worden leeggemaakt.",
"Cells that aren't a valid URL will be cleared.": "Cellen die geen geldige URL zijn, worden leeggemaakt.",
"Cells that aren't a valid email address will be cleared.": "Cellen die geen geldig e-mailadres zijn, worden leeggemaakt.",
"Cells that can't be parsed as a date will be cleared.": "Cellen die niet als een datum kunnen worden geïnterpreteerd, worden leeggemaakt.",
"Cells that can't be parsed as a number will be cleared.": "Cellen die niet als een getal kunnen worden geïnterpreteerd, worden leeggemaakt.",
"Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).": "Cellen worden geconverteerd (yes/true/1 worden aangevinkt; al het andere wordt uitgevinkt of leeggemaakt).",
"Cells will be reinterpreted under the new type.": "Cellen worden opnieuw geïnterpreteerd volgens het nieuwe type.",
"Cells will be replaced with a comma-separated list of file names.": "Cellen worden vervangen door een door komma's gescheiden lijst met bestandsnamen.",
"Cells will be replaced with a comma-separated list of option names.": "Cellen worden vervangen door een door komma's gescheiden lijst met optienamen.",
"Cells will be replaced with the option name.": "Cellen worden vervangen door de optienaam.",
"Cells will be replaced with the page title.": "Cellen worden vervangen door de paginatitel.",
"Cells will be replaced with the person's name.": "Cellen worden vervangen door de naam van de persoon.",
"Change type": "Type wijzigen",
"Change type to {{label}}?": "Type wijzigen naar {{label}}?",
"Converting…": "Converteren…",
"Existing values become single-item lists. No data is lost.": "Bestaande waarden worden lijsten met één item. Er gaan geen gegevens verloren.",
"Only the first selected item per row will be kept; the rest will be discarded.": "Alleen het eerste geselecteerde item per rij blijft behouden; de rest wordt verwijderd.",
"Previous record": "Vorige record",
"Next record": "Volgende record",
"Record actions": "Recordacties",
"Delete record": "Record verwijderen",
"Delete record?": "Record verwijderen?",
"This action cannot be undone.": "Deze actie kan niet ongedaan worden gemaakt.",
"to navigate": "om te navigeren",
"to close": "om te sluiten",
"Expand row {{number}}": "Rij {{number}} uitvouwen",
"Saving…": "Opslaan…",
"Read-only": "Alleen-lezen",
"Loading…": "Laden…",
"Updated {{when}}": "Bijgewerkt {{when}}",
"Add property": "Eigenschap toevoegen",
"Create property": "Eigenschap aanmaken",
"Hide properties": "Eigenschappen verbergen",
"Find a property type": "Een eigenschapstype zoeken",
"Properties": "Eigenschappen",
"A property with this name already exists": "Er bestaat al een eigenschap met deze naam",
"Add card": "Kaart toevoegen",
"Add filter": "Filter toevoegen",
"Add option": "Optie toevoegen",
"Add sort": "Sortering toevoegen",
"Add view": "Weergave toevoegen",
"All data in this column will be lost.": "Alle gegevens in deze kolom gaan verloren.",
"Allow multiple people": "Meerdere personen toestaan",
"Alphabetize": "Alfabetisch sorteren",
"Are you sure you want to delete": "Weet je zeker dat je wilt verwijderen",
"Ascending": "Oplopend",
"Base deleted": "Base verwijderd",
"Base name": "Basenaam",
"Base table": "Basistabel",
"Card properties": "Kaarteigenschappen",
"Cells longer than the Text limit will be truncated and the extra content permanently lost.": "Cellen die langer zijn dan de tekstlimiet worden afgekapt en de extra inhoud gaat permanent verloren.",
"Checkbox": "Selectievakje",
"Checked by default": "Standaard aangevinkt",
"Choose a property": "Kies een eigenschap",
"Clear selection": "Selectie wissen",
"Column options": "Kolomopties",
"Comma, period": "Komma, punt",
"Complete": "Voltooid",
"Contains": "Bevat",
"Copy link to view": "Link naar weergave kopiëren",
"Create a status property": "Een statuseigenschap maken",
"Created at": "Aangemaakt op",
"Currency": "Valuta",
"Decimal places": "Decimalen",
"Default value": "Standaardwaarde",
"Delete property": "Eigenschap verwijderen",
"Delete view": "Weergave verwijderen",
"Delete {{count}} rows?_one": "1 rij verwijderen?",
"Delete {{count}} rows?_other": "{{count}} rijen verwijderen?",
"Descending": "Aflopend",
"Discard": "Negeren",
"Doesn't contain": "Bevat niet",
"Done": "Klaar",
"Edit formula": "Formule bewerken",
"Edit property": "Eigenschap bewerken",
"Export CSV": "CSV exporteren",
"Failed to load base": "Base laden mislukt",
"False": "Onwaar",
"File": "Bestand",
"Filter and sort changes are visible only to you": "Wijzigingen in filters en sortering zijn alleen zichtbaar voor jou",
"Filter by": "Filteren op",
"Formula": "Formule",
"Get started with": "Aan de slag met",
"Greater than": "Groter dan",
"Group by": "Groeperen op",
"Group this board by a select or status property.": "Groepeer dit bord op een selectie- of statuseigenschap.",
"Hide all": "Alles verbergen",
"Hide group": "Groep verbergen",
"In Progress": "In uitvoering",
"In progress": "In uitvoering",
"Include time": "Tijd opnemen",
"Insert a kanban board on this page": "Voeg een kanbanbord in op deze pagina",
"Insert an inline base on this page": "Voeg een inline base in op deze pagina",
"Is": "Is",
"Is after": "Is na",
"Is any of": "Is een van",
"Is before": "Is vóór",
"Is empty": "Is leeg",
"Is none of": "Is geen van",
"Is not": "Is niet",
"Is not empty": "Is niet leeg",
"Is on or after": "Is op of na",
"Is on or before": "Is op of vóór",
"Is within": "Is binnen",
"Keep editing": "Doorgaan met bewerken",
"Last edited at": "Laatst bewerkt op",
"Last edited by": "Laatst bewerkt door",
"Less than": "Kleiner dan",
"Link copied to clipboard": "Link gekopieerd naar klembord",
"Local": "Lokaal",
"Long text": "Lange tekst",
"Moved card to {{column}}": "Kaart verplaatst naar {{column}}",
"Moved column to {{column}}": "Kolom verplaatst naar {{column}}",
"Multi-select": "Meervoudige selectie",
"New row": "Nieuwe rij",
"Next month": "Volgende maand",
"Next week": "Volgende week",
"Next year": "Volgend jaar",
"No filters applied": "Geen filters toegepast",
"No match": "Geen overeenkomst",
"No options for this property type": "Geen opties voor dit eigenschapstype",
"No sorts applied": "Geen sorteringen toegepast",
"No value": "Geen waarde",
"None": "Geen",
"Not started": "Niet gestart",
"Number": "Getal",
"One month ago": "Een maand geleden",
"One month from now": "Over een maand",
"One week ago": "Een week geleden",
"One week from now": "Over een week",
"Open as page": "Openen als pagina",
"Options": "Opties",
"Past month": "Afgelopen maand",
"Past week": "Afgelopen week",
"Past year": "Afgelopen jaar",
"Percent": "Procent",
"Period, comma": "Punt, komma",
"Person": "Persoon",
"Please enter a valid email": "Voer een geldig e-mailadres in",
"Progress": "Voortgang",
"Property options": "Eigenschapopties",
"Relative": "Relatief",
"Rename property": "Eigenschap hernoemen",
"Reset": "Opnieuw instellen",
"Save for everyone": "Opslaan voor iedereen",
"Select": "Selecteren",
"Select a property": "Selecteer een eigenschap",
"Select all loaded rows": "Alle geladen rijen selecteren",
"Show all": "Alles tonen",
"Sort": "Sorteren",
"Sort by": "Sorteren op",
"Space, comma": "Spatie, komma",
"Space, period": "Spatie, punt",
"This board has no grouping property yet.": "Dit bord heeft nog geen groeperingseigenschap.",
"This month": "Deze maand",
"This year": "Dit jaar",
"Thousands and decimal separators": "Scheidingstekens voor duizendtallen en decimalen",
"Time format": "Tijdnotatie",
"To Do": "Te doen",
"Tomorrow": "Morgen",
"True": "Waar",
"Unsaved changes": "Niet-opgeslagen wijzigingen",
"Untitled base": "Base zonder titel",
"URL": "URL",
"Value": "Waarde",
"View updated for everyone": "Weergave bijgewerkt voor iedereen",
"You have unsaved changes. Do you want to discard them?": "Je hebt niet-opgeslagen wijzigingen. Wil je deze negeren?",
"{{count}} rows deleted_one": "1 rij verwijderd",
"{{count}} rows deleted_other": "{{count}} rijen verwijderd",
"{{count}} selected_one": "1 geselecteerd",
"{{count}} selected_other": "{{count}} geselecteerd",
"Compare": "Vergelijken",
"Compare versions": "Versies vergelijken",
"Select version from {{date}}": "Selecteer versie van {{date}}",
"Version actions for {{date}}": "Versieacties voor {{date}}",
"Comparing {{newer}} and {{older}}": "{{newer}} en {{older}} vergelijken",
"Exit compare": "Vergelijken afsluiten",
"Search attachments...": "Bijlagen zoeken...",
"Error loading attachments.": "Fout bij het laden van bijlagen.",
"No attachments on this page yet.": "Er zijn nog geen bijlagen op deze pagina.",
"Uploaded by {{name}}": "Geüpload door {{name}}",
"Download {{name}}": "{{name}} downloaden",
"Access revoked": "Toegang ingetrokken",
"Authorize application": "Applicatie autoriseren",
"{{name}} wants to access {{workspace}}": "{{name}} wil toegang tot {{workspace}}",
"Not you? Switch account": "Ben jij dit niet? Wissel van account",
"This application will be able to:": "Deze applicatie kan het volgende doen:",
"Write": "Schrijven",
"Invalid authorization request": "Ongeldig autorisatieverzoek",
"Authorize": "Autoriseren",
"Application": "Applicatie",
"Permissions": "Machtigingen",
"Authorized": "Geautoriseerd",
"Revoke access": "Toegang intrekken",
"Revoke access for {{name}}": "Toegang voor {{name}} intrekken",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Weet je zeker dat je de toegang voor {{name}} wilt intrekken? De applicatie heeft dan geen toegang meer tot je account.",
"Something went wrong. Please try again.": "Er is iets misgegaan. Probeer het opnieuw.",
"Remove {{name}}": "{{name}} verwijderen",
"Make sure you trust this application before authorizing it.": "Zorg ervoor dat je deze applicatie vertrouwt voordat je haar autoriseert.",
"You will be redirected to": "Je wordt doorgestuurd naar",
"View content without making changes.": "Inhoud bekijken zonder wijzigingen aan te brengen.",
"Create and modify content.": "Inhoud maken en wijzigen.",
"Applications and AI assistants you have authorized to access your account.": "Applicaties en AI-assistenten die je hebt geautoriseerd om toegang te krijgen tot je account.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "MCP is ingeschakeld voor je werkruimte. Verbind AI-assistenten met je Docmost-account via OAuth.",
"Authorized apps": "Geautoriseerde apps",
"No authorized apps yet.": "Nog geen geautoriseerde apps.",
"Workspace knowledge only": "Alleen werkruimtek kennis",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Beperk AI Chat tot het beantwoorden op basis van alleen je werkruimtepagina's en geüploade bestanden. Er wordt geen externe kennis gebruikt.",
"Toggle workspace knowledge only": "Alleen werkruimtekennis in-/uitschakelen",
"Read-only mode": "Alleen-lezenmodus",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat kan werkruimte-inhoud doorzoeken en lezen, maar kan geen pagina's maken of bewerken.",
"Toggle AI Chat read-only mode": "Alleen-lezenmodus voor AI Chat in-/uitschakelen",
"Title only": "Alleen titel",
"you": "jij"
} }
@@ -41,8 +41,6 @@
"Dark": "Escuro", "Dark": "Escuro",
"Date": "Data", "Date": "Data",
"Delete": "Excluir", "Delete": "Excluir",
"Remove from page": "Remover da página",
"Base options": "Opções da base",
"Delete group": "Excluir grupo", "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.", "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", "Description": "Descrição",
@@ -78,24 +76,6 @@
"Failed to import pages": "Falha ao importar páginas", "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 load page. An error occurred.": "Falha ao carregar página. Ocorreu um erro.",
"Failed to update data": "Falha ao atualizar dados", "Failed to update data": "Falha ao atualizar dados",
"Failed to create base": "Falha ao criar base",
"Failed to update base": "Falha ao atualizar base",
"Failed to delete base": "Falha ao excluir base",
"Failed to create property": "Falha ao criar propriedade",
"Failed to update property": "Falha ao atualizar propriedade",
"Failed to delete property": "Falha ao excluir propriedade",
"Failed to reorder property": "Falha ao reordenar propriedade",
"Failed to create view": "Falha ao criar visualização",
"Failed to update view": "Falha ao atualizar visualização",
"Failed to delete view": "Falha ao excluir visualização",
"Failed to create row": "Falha ao criar linha",
"Failed to update row": "Falha ao atualizar linha",
"Failed to delete row": "Falha ao excluir linha",
"Failed to delete rows": "Falha ao excluir linhas",
"Failed to reorder row": "Falha ao reordenar linha",
"Failed to move card": "Falha ao mover cartão",
"Failed to add card": "Falha ao adicionar cartão",
"Failed to export CSV": "Falha ao exportar CSV",
"Favorite spaces": "Espaços favoritos", "Favorite spaces": "Espaços favoritos",
"Favorite spaces appear here": "Os espaços favoritos aparecem aqui", "Favorite spaces appear here": "Os espaços favoritos aparecem aqui",
"Favorites": "Favoritos", "Favorites": "Favoritos",
@@ -294,7 +274,6 @@
"Export space": "Exportar espaço", "Export space": "Exportar espaço",
"Export {{type}}": "Exportar para {{type}}", "Export {{type}}": "Exportar para {{type}}",
"File exceeds the {{limit}} attachment limit": "O arquivo excede o limite de anexos {{limit}}", "File exceeds the {{limit}} attachment limit": "O arquivo excede o limite de anexos {{limit}}",
"Media": "Mídia",
"Align left": "Alinhar à esquerda", "Align left": "Alinhar à esquerda",
"Align right": "Alinhar à direita", "Align right": "Alinhar à direita",
"Align center": "Alinhar ao centro", "Align center": "Alinhar ao centro",
@@ -388,8 +367,6 @@
"Insert horizontal rule divider": "Insira um divisor horizontal", "Insert horizontal rule divider": "Insira um divisor horizontal",
"Page break": "Quebra de página", "Page break": "Quebra de página",
"Insert a page break for printing.": "Insira uma quebra de página para impressão.", "Insert a page break for printing.": "Insira uma quebra de página para impressão.",
"Footnote": "Nota de rodapé",
"Insert a footnote reference.": "Inserir uma referência de nota de rodapé.",
"Upload any image from your device.": "Envie qualquer imagem do seu dispositivo.", "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 video from your device.": "Envie qualquer vídeo do seu dispositivo.",
"Upload any audio from your device.": "Envie qualquer áudio do seu dispositivo.", "Upload any audio from your device.": "Envie qualquer áudio do seu dispositivo.",
@@ -421,8 +398,6 @@
"Insert mermaid diagram": "Inserir diagrama Mermaid", "Insert mermaid diagram": "Inserir diagrama Mermaid",
"Insert and design Drawio diagrams": "Inserir e criar diagramas Drawio", "Insert and design Drawio diagrams": "Inserir e criar diagramas Drawio",
"Insert current date": "Inserir data atual", "Insert current date": "Inserir data atual",
"Time": "Horário",
"Insert current time": "Inserir horário atual",
"Draw and sketch excalidraw diagrams": "Desenhar e esboçar diagramas Excalidraw", "Draw and sketch excalidraw diagrams": "Desenhar e esboçar diagramas Excalidraw",
"Multiple": "Múltiplo", "Multiple": "Múltiplo",
"Turn into": "Transformar em", "Turn into": "Transformar em",
@@ -449,7 +424,6 @@
"Names do not match": "Os nomes não correspondem", "Names do not match": "Os nomes não correspondem",
"Today, {{time}}": "Hoje, {{time}}", "Today, {{time}}": "Hoje, {{time}}",
"Yesterday, {{time}}": "Ontem, {{time}}", "Yesterday, {{time}}": "Ontem, {{time}}",
"now": "agora",
"Space created successfully": "Espaço criado com sucesso", "Space created successfully": "Espaço criado com sucesso",
"Space updated successfully": "Espaço atualizado com sucesso", "Space updated successfully": "Espaço atualizado com sucesso",
"Space deleted successfully": "Espaço excluído com sucesso", "Space deleted successfully": "Espaço excluído com sucesso",
@@ -622,8 +596,6 @@
"Deleted by": "Excluído por", "Deleted by": "Excluído por",
"Deleted at": "Excluído em", "Deleted at": "Excluído em",
"Preview": "Visualização", "Preview": "Visualização",
"Base preview unavailable": "Pré-visualização da base indisponível",
"Restore this base to view its contents.": "Restaure esta base para visualizar seu conteúdo.",
"Subpages": "Subpáginas", "Subpages": "Subpáginas",
"Failed to load subpages": "Falha ao carregar as subpáginas", "Failed to load subpages": "Falha ao carregar as subpáginas",
"No subpages": "Nenhuma subpágina", "No subpages": "Nenhuma subpágina",
@@ -699,7 +671,7 @@
"Upgrade your plan": "Faça upgrade do seu plano", "Upgrade your plan": "Faça upgrade do seu plano",
"Available with a paid license": "Disponível com uma licença paga", "Available with a paid license": "Disponível com uma licença paga",
"Upgrade your license tier.": "Faça upgrade do seu nível de licença.", "Upgrade your license tier.": "Faça upgrade do seu nível de licença.",
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "A IA está disponível nas edições pagas do Docmost. Contate sales@docmost.com.", "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "A IA está disponível apenas na edição empresarial do Docmost. Contate sales@docmost.com.",
"AI & MCP": "IA e MCP", "AI & MCP": "IA e MCP",
"AI": "IA", "AI": "IA",
"MCP": "MCP", "MCP": "MCP",
@@ -707,11 +679,9 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Ative o servidor MCP para permitir que assistentes de IA e ferramentas interajam com o conteúdo do seu espaço de trabalho.", "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Ative o servidor MCP para permitir que assistentes de IA e ferramentas interajam com o conteúdo do seu espaço de trabalho.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "O MCP está disponível apenas na edição empresarial do Docmost. Contate sales@docmost.com.", "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "O MCP está disponível apenas na edição empresarial do Docmost. Contate sales@docmost.com.",
"MCP Server URL": "URL do servidor MCP", "MCP Server URL": "URL do servidor MCP",
"Connect AI assistants with your Docmost account via OAuth.": "Conecte assistentes de IA à sua conta do Docmost via OAuth.", "Use your API key for authentication. You can manage API keys in your account settings.": "Use sua chave de API para autenticação. Você pode gerenciar chaves de API nas configurações da sua conta.",
"Enforce OAuth": "Enforce OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "Os assistentes de IA devem se conectar com uma conta do Docmost via OAuth. Chaves de API não podem ser usadas com o servidor MCP.",
"Toggle enforce OAuth for MCP": "Toggle enforce OAuth for MCP",
"Supported tools": "Ferramentas compatíveis", "Supported tools": "Ferramentas compatíveis",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "Seu espaço de trabalho tem MCP habilitado. Use sua chave de API para conectar assistentes de IA.",
"MCP server URL:": "URL do servidor MCP:", "MCP server URL:": "URL do servidor MCP:",
"Learn more": "Saiba mais", "Learn more": "Saiba mais",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gerencie as chaves de API de todos os usuários do workspace. Veja a <anchor>documentação da API</anchor> para detalhes de uso.", "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Gerencie as chaves de API de todos os usuários do workspace. Veja a <anchor>documentação da API</anchor> para detalhes de uso.",
@@ -995,8 +965,6 @@
"Are you sure you want to delete this template?": "Tem certeza de que deseja excluir este modelo?", "Are you sure you want to delete this template?": "Tem certeza de que deseja excluir este modelo?",
"Template scope updated": "Escopo do modelo atualizado", "Template scope updated": "Escopo do modelo atualizado",
"Choose which space this template belongs to": "Escolha a qual espaço este modelo pertence", "Choose which space this template belongs to": "Escolha a qual espaço este modelo pertence",
"Only workspace admins can create global templates.": "Somente administradores do espaço de trabalho podem criar modelos globais.",
"You need edit access to this space to create templates.": "Você precisa de acesso de edição a este espaço para criar modelos.",
"Scope": "Escopo", "Scope": "Escopo",
"Select scope": "Selecionar escopo", "Select scope": "Selecionar escopo",
"Title": "Título", "Title": "Título",
@@ -1009,7 +977,7 @@
"Search pages and spaces...": "Pesquisar páginas e espaços...", "Search pages and spaces...": "Pesquisar páginas e espaços...",
"No results found": "Nenhum resultado encontrado", "No results found": "Nenhum resultado encontrado",
"You don't have permission to create pages here": "Você não tem permissão para criar páginas aqui", "You don't have permission to create pages here": "Você não tem permissão para criar páginas aqui",
"Chat menu for {{title}}": "Menu do chat para {{title}}", "Chat menu": "Menu do chat",
"API key menu": "Menu da chave de API", "API key menu": "Menu da chave de API",
"Jump to comment selection": "Ir para a seleção de comentários", "Jump to comment selection": "Ir para a seleção de comentários",
"Slash commands": "Comandos de barra", "Slash commands": "Comandos de barra",
@@ -1095,7 +1063,7 @@
"Filter": "Filtrar", "Filter": "Filtrar",
"Page title": "Título da página", "Page title": "Título da página",
"Page content": "Conteúdo da página", "Page content": "Conteúdo da página",
"Member actions for {{name}}": "Ações do membro para {{name}}", "Member actions": "Ações do membro",
"Toggle password visibility": "Alternar visibilidade da senha", "Toggle password visibility": "Alternar visibilidade da senha",
"Send comment": "Enviar comentário", "Send comment": "Enviar comentário",
"Token actions": "Ações do token", "Token actions": "Ações do token",
@@ -1116,226 +1084,5 @@
"Added {{name}} to favorites": "{{name}} adicionado aos favoritos", "Added {{name}} to favorites": "{{name}} adicionado aos favoritos",
"Removed {{name}} from favorites": "{{name}} removido dos favoritos", "Removed {{name}} from favorites": "{{name}} removido dos favoritos",
"Page menu for {{name}}": "Menu da página de {{name}}", "Page menu for {{name}}": "Menu da página de {{name}}",
"Create subpage of {{name}}": "Criar subpágina de {{name}}", "Create subpage of {{name}}": "Criar subpágina de {{name}}"
"Allow personal spaces": "Permitir espaços pessoais",
"Members can create their own personal space.": "Os membros podem criar seu próprio espaço pessoal.",
"Toggle allow personal spaces": "Ativar/desativar permissão para espaços pessoais",
"Create personal space": "Criar espaço pessoal",
"Personal space": "Espaço pessoal",
"{{name}}'s space": "Espaço de {{name}}",
"Apply": "Aplicar",
"Cells that aren't already a page reference will be cleared.": "As células que ainda não forem uma referência de página serão limpas.",
"Cells that aren't a valid URL will be cleared.": "As células que não forem uma URL válida serão limpas.",
"Cells that aren't a valid email address will be cleared.": "As células que não forem um endereço de e-mail válido serão limpas.",
"Cells that can't be parsed as a date will be cleared.": "As células que não puderem ser interpretadas como uma data serão limpas.",
"Cells that can't be parsed as a number will be cleared.": "As células que não puderem ser interpretadas como um número serão limpas.",
"Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).": "As células serão convertidas (yes/true/1 se tornam marcadas; todo o resto se torna desmarcado ou limpo).",
"Cells will be reinterpreted under the new type.": "As células serão reinterpretadas com o novo tipo.",
"Cells will be replaced with a comma-separated list of file names.": "As células serão substituídas por uma lista de nomes de arquivos separados por vírgula.",
"Cells will be replaced with a comma-separated list of option names.": "As células serão substituídas por uma lista de nomes de opções separados por vírgula.",
"Cells will be replaced with the option name.": "As células serão substituídas pelo nome da opção.",
"Cells will be replaced with the page title.": "As células serão substituídas pelo título da página.",
"Cells will be replaced with the person's name.": "As células serão substituídas pelo nome da pessoa.",
"Change type": "Alterar tipo",
"Change type to {{label}}?": "Alterar tipo para {{label}}?",
"Converting…": "Convertendo…",
"Existing values become single-item lists. No data is lost.": "Os valores existentes se tornam listas de item único. Nenhum dado é perdido.",
"Only the first selected item per row will be kept; the rest will be discarded.": "Apenas o primeiro item selecionado por linha será mantido; o restante será descartado.",
"Previous record": "Registro anterior",
"Next record": "Próximo registro",
"Record actions": "Ações do registro",
"Delete record": "Excluir registro",
"Delete record?": "Excluir registro?",
"This action cannot be undone.": "Esta ação não pode ser desfeita.",
"to navigate": "para navegar",
"to close": "para fechar",
"Expand row {{number}}": "Expandir linha {{number}}",
"Saving…": "Salvando…",
"Read-only": "Somente leitura",
"Loading…": "Carregando…",
"Updated {{when}}": "Atualizado {{when}}",
"Add property": "Adicionar propriedade",
"Create property": "Criar propriedade",
"Hide properties": "Ocultar propriedades",
"Find a property type": "Encontrar um tipo de propriedade",
"Properties": "Propriedades",
"A property with this name already exists": "Já existe uma propriedade com este nome",
"Add card": "Adicionar cartão",
"Add filter": "Adicionar filtro",
"Add option": "Adicionar opção",
"Add sort": "Adicionar ordenação",
"Add view": "Adicionar visualização",
"All data in this column will be lost.": "Todos os dados nesta coluna serão perdidos.",
"Allow multiple people": "Permitir várias pessoas",
"Alphabetize": "Ordenar alfabeticamente",
"Are you sure you want to delete": "Tem certeza de que deseja excluir",
"Ascending": "Crescente",
"Base deleted": "Base excluída",
"Base name": "Nome da base",
"Base table": "Tabela da base",
"Card properties": "Propriedades do cartão",
"Cells longer than the Text limit will be truncated and the extra content permanently lost.": "Células maiores do que o limite de Texto serão truncadas e o conteúdo extra será perdido permanentemente.",
"Checkbox": "Caixa de seleção",
"Checked by default": "Marcado por padrão",
"Choose a property": "Escolha uma propriedade",
"Clear selection": "Limpar seleção",
"Column options": "Opções da coluna",
"Comma, period": "Vírgula, ponto",
"Complete": "Concluído",
"Contains": "Contém",
"Copy link to view": "Copiar link da visualização",
"Create a status property": "Criar uma propriedade de status",
"Created at": "Criado em",
"Currency": "Moeda",
"Decimal places": "Casas decimais",
"Default value": "Valor padrão",
"Delete property": "Excluir propriedade",
"Delete view": "Excluir visualização",
"Delete {{count}} rows?_one": "Delete 1 row?",
"Delete {{count}} rows?_other": "Delete {{count}} rows?",
"Descending": "Decrescente",
"Discard": "Descartar",
"Doesn't contain": "Não contém",
"Done": "Concluído",
"Edit formula": "Editar fórmula",
"Edit property": "Editar propriedade",
"Export CSV": "Exportar CSV",
"Failed to load base": "Falha ao carregar a base",
"False": "Falso",
"File": "Arquivo",
"Filter and sort changes are visible only to you": "As alterações de filtro e ordenação são visíveis somente para você",
"Filter by": "Filtrar por",
"Formula": "Fórmula",
"Get started with": "Começar com",
"Greater than": "Maior que",
"Group by": "Agrupar por",
"Group this board by a select or status property.": "Agrupe este quadro por uma propriedade de seleção ou status.",
"Hide all": "Ocultar tudo",
"Hide group": "Ocultar grupo",
"In Progress": "Em andamento",
"In progress": "Em andamento",
"Include time": "Incluir hora",
"Insert a kanban board on this page": "Inserir um quadro kanban nesta página",
"Insert an inline base on this page": "Inserir uma base inline nesta página",
"Is": "É",
"Is after": "É depois de",
"Is any of": "É qualquer um de",
"Is before": "É antes de",
"Is empty": "Está vazio",
"Is none of": "Não é nenhum de",
"Is not": "Não é",
"Is not empty": "Não está vazio",
"Is on or after": "É em ou depois de",
"Is on or before": "É em ou antes de",
"Is within": "Está dentro de",
"Keep editing": "Continuar editando",
"Last edited at": "Última edição em",
"Last edited by": "Última edição por",
"Less than": "Menor que",
"Link copied to clipboard": "Link copiado para a área de transferência",
"Local": "Local",
"Long text": "Texto longo",
"Moved card to {{column}}": "Cartão movido para {{column}}",
"Moved column to {{column}}": "Coluna movida para {{column}}",
"Multi-select": "Seleção múltipla",
"New row": "Nova linha",
"Next month": "Próximo mês",
"Next week": "Próxima semana",
"Next year": "Próximo ano",
"No filters applied": "Nenhum filtro aplicado",
"No match": "Sem correspondência",
"No options for this property type": "Sem opções para este tipo de propriedade",
"No sorts applied": "Nenhuma ordenação aplicada",
"No value": "Sem valor",
"None": "Nenhum",
"Not started": "Não iniciado",
"Number": "Número",
"One month ago": "Há um mês",
"One month from now": "Daqui a um mês",
"One week ago": "Há uma semana",
"One week from now": "Daqui a uma semana",
"Open as page": "Abrir como página",
"Options": "Opções",
"Past month": "Mês passado",
"Past week": "Semana passada",
"Past year": "Ano passado",
"Percent": "Porcentagem",
"Period, comma": "Ponto, vírgula",
"Person": "Pessoa",
"Please enter a valid email": "Por favor, insira um e-mail válido",
"Progress": "Progresso",
"Property options": "Opções da propriedade",
"Relative": "Relativo",
"Rename property": "Renomear propriedade",
"Reset": "Redefinir",
"Save for everyone": "Salvar para todos",
"Select": "Seleção",
"Select a property": "Selecione uma propriedade",
"Select all loaded rows": "Selecionar todas as linhas carregadas",
"Show all": "Mostrar tudo",
"Sort": "Ordenar",
"Sort by": "Ordenar por",
"Space, comma": "Espaço, vírgula",
"Space, period": "Espaço, ponto",
"This board has no grouping property yet.": "Este quadro ainda não tem uma propriedade de agrupamento.",
"This month": "Este mês",
"This year": "Este ano",
"Thousands and decimal separators": "Separadores de milhares e decimais",
"Time format": "Formato de hora",
"To Do": "A fazer",
"Tomorrow": "Amanhã",
"True": "Verdadeiro",
"Unsaved changes": "Alterações não salvas",
"Untitled base": "Base sem título",
"URL": "URL",
"Value": "Valor",
"View updated for everyone": "Visualização atualizada para todos",
"You have unsaved changes. Do you want to discard them?": "Você tem alterações não salvas. Deseja descartá-las?",
"{{count}} rows deleted_one": "1 row deleted",
"{{count}} rows deleted_other": "{{count}} rows deleted",
"{{count}} selected_one": "1 selected",
"{{count}} selected_other": "{{count}} selected",
"Compare": "Comparar",
"Compare versions": "Comparar versões",
"Select version from {{date}}": "Selecionar versão de {{date}}",
"Version actions for {{date}}": "Ações da versão de {{date}}",
"Comparing {{newer}} and {{older}}": "Comparando {{newer}} e {{older}}",
"Exit compare": "Sair da comparação",
"Search attachments...": "Pesquisar anexos...",
"Error loading attachments.": "Erro ao carregar anexos.",
"No attachments on this page yet.": "Ainda não há anexos nesta página.",
"Uploaded by {{name}}": "Enviado por {{name}}",
"Download {{name}}": "Baixar {{name}}",
"Access revoked": "Acesso revogado",
"Authorize application": "Autorizar aplicativo",
"{{name}} wants to access {{workspace}}": "{{name}} quer acessar {{workspace}}",
"Not you? Switch account": "Não é você? Trocar conta",
"This application will be able to:": "Este aplicativo poderá:",
"Write": "Escrever",
"Invalid authorization request": "Solicitação de autorização inválida",
"Authorize": "Autorizar",
"Application": "Aplicativo",
"Permissions": "Permissões",
"Authorized": "Autorizado",
"Revoke access": "Revogar acesso",
"Revoke access for {{name}}": "Revogar acesso de {{name}}",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Tem certeza de que deseja revogar o acesso de {{name}}? O aplicativo não poderá mais acessar sua conta.",
"Something went wrong. Please try again.": "Algo deu errado. Tente novamente.",
"Remove {{name}}": "Remover {{name}}",
"Make sure you trust this application before authorizing it.": "Certifique-se de confiar neste aplicativo antes de autorizá-lo.",
"You will be redirected to": "Você será redirecionado para",
"View content without making changes.": "Visualizar conteúdo sem fazer alterações.",
"Create and modify content.": "Criar e modificar conteúdo.",
"Applications and AI assistants you have authorized to access your account.": "Aplicativos e assistentes de IA que você autorizou a acessar sua conta.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "Seu workspace tem o MCP ativado. Conecte assistentes de IA à sua conta do Docmost via OAuth.",
"Authorized apps": "Aplicativos autorizados",
"No authorized apps yet.": "Ainda não há aplicativos autorizados.",
"Workspace knowledge only": "Somente conhecimento do workspace",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Restrinja o AI Chat para responder apenas com base nas páginas do seu workspace e nos arquivos enviados. Ele não usará conhecimento externo.",
"Toggle workspace knowledge only": "Alternar somente conhecimento do workspace",
"Read-only mode": "Modo somente leitura",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "O AI Chat pode pesquisar e ler o conteúdo do workspace, mas não pode criar nem editar páginas.",
"Toggle AI Chat read-only mode": "Alternar modo somente leitura do AI Chat",
"Title only": "Somente título",
"you": "você"
} }
@@ -41,8 +41,6 @@
"Dark": "Темная", "Dark": "Темная",
"Date": "Дата", "Date": "Дата",
"Delete": "Удалить", "Delete": "Удалить",
"Remove from page": "Удалить со страницы",
"Base options": "Параметры базы",
"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": "Описание",
@@ -78,24 +76,6 @@
"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": "Не удалось обновить данные",
"Failed to create base": "Не удалось создать базу",
"Failed to update base": "Не удалось обновить базу",
"Failed to delete base": "Не удалось удалить базу",
"Failed to create property": "Не удалось создать свойство",
"Failed to update property": "Не удалось обновить свойство",
"Failed to delete property": "Не удалось удалить свойство",
"Failed to reorder property": "Не удалось изменить порядок свойств",
"Failed to create view": "Не удалось создать представление",
"Failed to update view": "Не удалось обновить представление",
"Failed to delete view": "Не удалось удалить представление",
"Failed to create row": "Не удалось создать строку",
"Failed to update row": "Не удалось обновить строку",
"Failed to delete row": "Не удалось удалить строку",
"Failed to delete rows": "Не удалось удалить строки",
"Failed to reorder row": "Не удалось изменить порядок строки",
"Failed to move card": "Не удалось переместить карточку",
"Failed to add card": "Не удалось добавить карточку",
"Failed to export CSV": "Не удалось экспортировать CSV",
"Favorite spaces": "Избранные пространства", "Favorite spaces": "Избранные пространства",
"Favorite spaces appear here": "Здесь отображаются избранные пространства", "Favorite spaces appear here": "Здесь отображаются избранные пространства",
"Favorites": "Избранное", "Favorites": "Избранное",
@@ -294,7 +274,6 @@
"Export space": "Экспорт пространства", "Export space": "Экспорт пространства",
"Export {{type}}": "Экспорт {{type}}", "Export {{type}}": "Экспорт {{type}}",
"File exceeds the {{limit}} attachment limit": "Файл превышает лимит вложений {{limit}}", "File exceeds the {{limit}} attachment limit": "Файл превышает лимит вложений {{limit}}",
"Media": "Медиа",
"Align left": "По левому краю", "Align left": "По левому краю",
"Align right": "По правому краю", "Align right": "По правому краю",
"Align center": "По центру", "Align center": "По центру",
@@ -388,8 +367,6 @@
"Insert horizontal rule divider": "Вставить горизонтальный разделитель", "Insert horizontal rule divider": "Вставить горизонтальный разделитель",
"Page break": "Разрыв страницы", "Page break": "Разрыв страницы",
"Insert a page break for printing.": "Вставить разрыв страницы для печати.", "Insert a page break for printing.": "Вставить разрыв страницы для печати.",
"Footnote": "Сноска",
"Insert a footnote reference.": "Вставить ссылку на сноску.",
"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 audio from your device.": "Загрузите любой аудиофайл с вашего устройства.", "Upload any audio from your device.": "Загрузите любой аудиофайл с вашего устройства.",
@@ -421,8 +398,6 @@
"Insert mermaid diagram": "Вставить диаграмму Mermaid", "Insert mermaid diagram": "Вставить диаграмму Mermaid",
"Insert and design Drawio diagrams": "Вставляйте и редактируйте диаграммы Drawio", "Insert and design Drawio diagrams": "Вставляйте и редактируйте диаграммы Drawio",
"Insert current date": "Вставить текущую дату", "Insert current date": "Вставить текущую дату",
"Time": "Время",
"Insert current time": "Вставить текущее время",
"Draw and sketch excalidraw diagrams": "Рисуйте и создавайте диаграммы Excalidraw", "Draw and sketch excalidraw diagrams": "Рисуйте и создавайте диаграммы Excalidraw",
"Multiple": "Несколько", "Multiple": "Несколько",
"Turn into": "Преобразовать в", "Turn into": "Преобразовать в",
@@ -449,7 +424,6 @@
"Names do not match": "Названия не совпадают", "Names do not match": "Названия не совпадают",
"Today, {{time}}": "Сегодня, {{time}}", "Today, {{time}}": "Сегодня, {{time}}",
"Yesterday, {{time}}": "Вчера, {{time}}", "Yesterday, {{time}}": "Вчера, {{time}}",
"now": "сейчас",
"Space created successfully": "Пространство успешно создано", "Space created successfully": "Пространство успешно создано",
"Space updated successfully": "Пространство успешно обновлено", "Space updated successfully": "Пространство успешно обновлено",
"Space deleted successfully": "Пространство успешно удалено", "Space deleted successfully": "Пространство успешно удалено",
@@ -622,8 +596,6 @@
"Deleted by": "Удалено пользователем", "Deleted by": "Удалено пользователем",
"Deleted at": "Удалено", "Deleted at": "Удалено",
"Preview": "Предпросмотр", "Preview": "Предпросмотр",
"Base preview unavailable": "Предпросмотр базы недоступен",
"Restore this base to view its contents.": "Восстановите эту базу, чтобы просмотреть её содержимое.",
"Subpages": "Подстраницы", "Subpages": "Подстраницы",
"Failed to load subpages": "Не удалось загрузить подстраницы", "Failed to load subpages": "Не удалось загрузить подстраницы",
"No subpages": "Нет подстраниц", "No subpages": "Нет подстраниц",
@@ -699,7 +671,7 @@
"Upgrade your plan": "Обновите свой тарифный план", "Upgrade your plan": "Обновите свой тарифный план",
"Available with a paid license": "Доступно с платной лицензией", "Available with a paid license": "Доступно с платной лицензией",
"Upgrade your license tier.": "Обновите уровень вашей лицензии.", "Upgrade your license tier.": "Обновите уровень вашей лицензии.",
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "ИИ доступен в платных версиях Docmost. Свяжитесь по адресу sales@docmost.com.", "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "ИИ доступен только в корпоративной версии Docmost. Свяжитесь по адресу sales@docmost.com.",
"AI & MCP": "ИИ и MCP", "AI & MCP": "ИИ и MCP",
"AI": "ИИ", "AI": "ИИ",
"MCP": "MCP", "MCP": "MCP",
@@ -707,11 +679,9 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Включите сервер MCP, чтобы ИИ-ассистенты и инструменты могли взаимодействовать с содержимым вашего рабочего пространства.", "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Включите сервер MCP, чтобы ИИ-ассистенты и инструменты могли взаимодействовать с содержимым вашего рабочего пространства.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP доступен только в корпоративной версии Docmost. Свяжитесь по адресу sales@docmost.com.", "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP доступен только в корпоративной версии Docmost. Свяжитесь по адресу sales@docmost.com.",
"MCP Server URL": "URL сервера MCP", "MCP Server URL": "URL сервера MCP",
"Connect AI assistants with your Docmost account via OAuth.": "Подключайте AI-помощников к вашей учетной записи Docmost через OAuth.", "Use your API key for authentication. You can manage API keys in your account settings.": "Используйте ваш API-ключ для аутентификации. Управлять API-ключами можно в настройках аккаунта.",
"Enforce OAuth": "Enforce OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI-помощники должны подключаться к учетной записи Docmost через OAuth. Ключи API нельзя использовать с MCP-сервером.",
"Toggle enforce OAuth for MCP": "Toggle enforce OAuth for MCP",
"Supported tools": "Поддерживаемые инструменты", "Supported tools": "Поддерживаемые инструменты",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "В вашем рабочем пространстве включён MCP. Используйте свой API-ключ для подключения ИИ-ассистентов.",
"MCP server URL:": "URL сервера MCP:", "MCP server URL:": "URL сервера MCP:",
"Learn more": "Подробнее", "Learn more": "Подробнее",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Управляйте API-ключами для всех пользователей в рабочем пространстве. Смотрите <anchor>документацию по API</anchor> для получения информации об использовании.", "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Управляйте API-ключами для всех пользователей в рабочем пространстве. Смотрите <anchor>документацию по API</anchor> для получения информации об использовании.",
@@ -995,8 +965,6 @@
"Are you sure you want to delete this template?": "Вы уверены, что хотите удалить этот шаблон?", "Are you sure you want to delete this template?": "Вы уверены, что хотите удалить этот шаблон?",
"Template scope updated": "Область действия шаблона обновлена", "Template scope updated": "Область действия шаблона обновлена",
"Choose which space this template belongs to": "Выберите, к какому пространству относится этот шаблон", "Choose which space this template belongs to": "Выберите, к какому пространству относится этот шаблон",
"Only workspace admins can create global templates.": "Только администраторы рабочего пространства могут создавать глобальные шаблоны.",
"You need edit access to this space to create templates.": "Чтобы создавать шаблоны, вам нужен доступ на редактирование этого пространства.",
"Scope": "Область действия", "Scope": "Область действия",
"Select scope": "Выберите область действия", "Select scope": "Выберите область действия",
"Title": "Заголовок", "Title": "Заголовок",
@@ -1009,7 +977,7 @@
"Search pages and spaces...": "Поиск страниц и пространств...", "Search pages and spaces...": "Поиск страниц и пространств...",
"No results found": "Результаты не найдены", "No results found": "Результаты не найдены",
"You don't have permission to create pages here": "У вас нет прав на создание страниц здесь", "You don't have permission to create pages here": "У вас нет прав на создание страниц здесь",
"Chat menu for {{title}}": "Меню чата для {{title}}", "Chat menu": "Меню чата",
"API key menu": "Меню API-ключа", "API key menu": "Меню API-ключа",
"Jump to comment selection": "Перейти к выбору комментария", "Jump to comment selection": "Перейти к выбору комментария",
"Slash commands": "Команды со слешем", "Slash commands": "Команды со слешем",
@@ -1095,7 +1063,7 @@
"Filter": "Фильтр", "Filter": "Фильтр",
"Page title": "Заголовок страницы", "Page title": "Заголовок страницы",
"Page content": "Содержимое страницы", "Page content": "Содержимое страницы",
"Member actions for {{name}}": "Действия с участниками для {{name}}", "Member actions": "Действия с участником",
"Toggle password visibility": "Переключить видимость пароля", "Toggle password visibility": "Переключить видимость пароля",
"Send comment": "Отправить комментарий", "Send comment": "Отправить комментарий",
"Token actions": "Действия с токеном", "Token actions": "Действия с токеном",
@@ -1116,226 +1084,5 @@
"Added {{name}} to favorites": "{{name}} добавлено в избранное", "Added {{name}} to favorites": "{{name}} добавлено в избранное",
"Removed {{name}} from favorites": "{{name}} удалено из избранного", "Removed {{name}} from favorites": "{{name}} удалено из избранного",
"Page menu for {{name}}": "Меню страницы для {{name}}", "Page menu for {{name}}": "Меню страницы для {{name}}",
"Create subpage of {{name}}": "Создать подстраницу для {{name}}", "Create subpage of {{name}}": "Создать подстраницу для {{name}}"
"Allow personal spaces": "Разрешить личные пространства",
"Members can create their own personal space.": "Участники могут создавать свои собственные личные пространства.",
"Toggle allow personal spaces": "Переключить разрешение личных пространств",
"Create personal space": "Создать личное пространство",
"Personal space": "Личное пространство",
"{{name}}'s space": "Пространство {{name}}",
"Apply": "Применить",
"Cells that aren't already a page reference will be cleared.": "Ячейки, которые ещё не являются ссылкой на страницу, будут очищены.",
"Cells that aren't a valid URL will be cleared.": "Ячейки, которые не содержат допустимый URL, будут очищены.",
"Cells that aren't a valid email address will be cleared.": "Ячейки, которые не содержат допустимый адрес электронной почты, будут очищены.",
"Cells that can't be parsed as a date will be cleared.": "Ячейки, которые нельзя распознать как дату, будут очищены.",
"Cells that can't be parsed as a number will be cleared.": "Ячейки, которые нельзя распознать как число, будут очищены.",
"Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).": "Значения в ячейках будут приведены к типу (yes/true/1 станут отмеченными; всё остальное станет неотмеченным или будет очищено).",
"Cells will be reinterpreted under the new type.": "Значения в ячейках будут заново интерпретированы в соответствии с новым типом.",
"Cells will be replaced with a comma-separated list of file names.": "Ячейки будут заменены списком имён файлов, разделённых запятыми.",
"Cells will be replaced with a comma-separated list of option names.": "Ячейки будут заменены списком названий параметров, разделённых запятыми.",
"Cells will be replaced with the option name.": "Ячейки будут заменены названием параметра.",
"Cells will be replaced with the page title.": "Ячейки будут заменены заголовком страницы.",
"Cells will be replaced with the person's name.": "Ячейки будут заменены именем человека.",
"Change type": "Изменить тип",
"Change type to {{label}}?": "Изменить тип на {{label}}?",
"Converting…": "Преобразование…",
"Existing values become single-item lists. No data is lost.": "Существующие значения станут одноэлементными списками. Данные не будут потеряны.",
"Only the first selected item per row will be kept; the rest will be discarded.": "Для каждой строки будет сохранён только первый выбранный элемент; остальные будут отброшены.",
"Previous record": "Предыдущая запись",
"Next record": "Следующая запись",
"Record actions": "Действия с записью",
"Delete record": "Удалить запись",
"Delete record?": "Удалить запись?",
"This action cannot be undone.": "Это действие нельзя отменить.",
"to navigate": "для навигации",
"to close": "для закрытия",
"Expand row {{number}}": "Развернуть строку {{number}}",
"Saving…": "Сохранение…",
"Read-only": "Только для чтения",
"Loading…": "Загрузка…",
"Updated {{when}}": "Обновлено {{when}}",
"Add property": "Добавить свойство",
"Create property": "Создать свойство",
"Hide properties": "Скрыть свойства",
"Find a property type": "Найти тип свойства",
"Properties": "Свойства",
"A property with this name already exists": "Свойство с таким именем уже существует",
"Add card": "Добавить карточку",
"Add filter": "Добавить фильтр",
"Add option": "Добавить вариант",
"Add sort": "Добавить сортировку",
"Add view": "Добавить представление",
"All data in this column will be lost.": "Все данные в этом столбце будут потеряны.",
"Allow multiple people": "Разрешить нескольких пользователей",
"Alphabetize": "По алфавиту",
"Are you sure you want to delete": "Вы уверены, что хотите удалить",
"Ascending": "По возрастанию",
"Base deleted": "База удалена",
"Base name": "Название базы",
"Base table": "Таблица базы",
"Card properties": "Свойства карточки",
"Cells longer than the Text limit will be truncated and the extra content permanently lost.": "Ячейки, длина которых превышает ограничение для текста, будут обрезаны, а лишнее содержимое будет безвозвратно потеряно.",
"Checkbox": "Флажок",
"Checked by default": "Отмечено по умолчанию",
"Choose a property": "Выберите свойство",
"Clear selection": "Очистить выбор",
"Column options": "Параметры столбца",
"Comma, period": "Запятая, точка",
"Complete": "Завершено",
"Contains": "Содержит",
"Copy link to view": "Скопировать ссылку на представление",
"Create a status property": "Создать свойство статуса",
"Created at": "Создано",
"Currency": "Валюта",
"Decimal places": "Десятичные знаки",
"Default value": "Значение по умолчанию",
"Delete property": "Удалить свойство",
"Delete view": "Удалить представление",
"Delete {{count}} rows?_one": "Удалить 1 строку?",
"Delete {{count}} rows?_other": "Удалить {{count}} строк?",
"Descending": "По убыванию",
"Discard": "Отменить",
"Doesn't contain": "Не содержит",
"Done": "Готово",
"Edit formula": "Редактировать формулу",
"Edit property": "Редактировать свойство",
"Export CSV": "Экспорт CSV",
"Failed to load base": "Не удалось загрузить базу",
"False": "Ложь",
"File": "Файл",
"Filter and sort changes are visible only to you": "Изменения фильтра и сортировки видны только вам",
"Filter by": "Фильтр по",
"Formula": "Формула",
"Get started with": "Начать работу с",
"Greater than": "Больше чем",
"Group by": "Группировать по",
"Group this board by a select or status property.": "Сгруппируйте эту доску по свойству выбора или статуса.",
"Hide all": "Скрыть все",
"Hide group": "Скрыть группу",
"In Progress": "В процессе",
"In progress": "В процессе",
"Include time": "Включить время",
"Insert a kanban board on this page": "Вставить канбан-доску на эту страницу",
"Insert an inline base on this page": "Вставить встроенную базу на эту страницу",
"Is": "Равно",
"Is after": "После",
"Is any of": "Совпадает с любым из",
"Is before": "До",
"Is empty": "Пусто",
"Is none of": "Не совпадает ни с одним из",
"Is not": "Не равно",
"Is not empty": "Не пусто",
"Is on or after": "В этот день или позже",
"Is on or before": "В этот день или раньше",
"Is within": "Находится в пределах",
"Keep editing": "Продолжить редактирование",
"Last edited at": "Последнее изменение",
"Last edited by": "Последний изменивший",
"Less than": "Меньше чем",
"Link copied to clipboard": "Ссылка скопирована в буфер обмена",
"Local": "Локальный",
"Long text": "Длинный текст",
"Moved card to {{column}}": "Карточка перемещена в {{column}}",
"Moved column to {{column}}": "Столбец перемещён в {{column}}",
"Multi-select": "Множественный выбор",
"New row": "Новая строка",
"Next month": "Следующий месяц",
"Next week": "Следующая неделя",
"Next year": "Следующий год",
"No filters applied": "Фильтры не применены",
"No match": "Нет совпадений",
"No options for this property type": "Для этого типа свойства нет параметров",
"No sorts applied": "Сортировка не применена",
"No value": "Нет значения",
"None": "Нет",
"Not started": "Не начато",
"Number": "Число",
"One month ago": "Месяц назад",
"One month from now": "Через месяц",
"One week ago": "Неделю назад",
"One week from now": "Через неделю",
"Open as page": "Открыть как страницу",
"Options": "Параметры",
"Past month": "Прошлый месяц",
"Past week": "Прошлая неделя",
"Past year": "Прошлый год",
"Percent": "Процент",
"Period, comma": "Точка, запятая",
"Person": "Пользователь",
"Please enter a valid email": "Пожалуйста, введите корректный адрес электронной почты",
"Progress": "Прогресс",
"Property options": "Параметры свойства",
"Relative": "Относительный",
"Rename property": "Переименовать свойство",
"Reset": "Сбросить",
"Save for everyone": "Сохранить для всех",
"Select": "Выбор",
"Select a property": "Выберите свойство",
"Select all loaded rows": "Выбрать все загруженные строки",
"Show all": "Показать все",
"Sort": "Сортировка",
"Sort by": "Сортировать по",
"Space, comma": "Пробел, запятая",
"Space, period": "Пробел, точка",
"This board has no grouping property yet.": "У этой доски пока нет свойства для группировки.",
"This month": "Этот месяц",
"This year": "Этот год",
"Thousands and decimal separators": "Разделители тысяч и десятичных знаков",
"Time format": "Формат времени",
"To Do": "К выполнению",
"Tomorrow": "Завтра",
"True": "Истина",
"Unsaved changes": "Несохранённые изменения",
"Untitled base": "База без названия",
"URL": "URL",
"Value": "Значение",
"View updated for everyone": "Представление обновлено для всех",
"You have unsaved changes. Do you want to discard them?": "У вас есть несохранённые изменения. Хотите их отменить?",
"{{count}} rows deleted_one": "1 строка удалена",
"{{count}} rows deleted_other": "{{count}} строк удалено",
"{{count}} selected_one": "1 выбрано",
"{{count}} selected_other": "{{count}} выбрано",
"Compare": "Сравнить",
"Compare versions": "Сравнить версии",
"Select version from {{date}}": "Выбрать версию от {{date}}",
"Version actions for {{date}}": "Действия с версией от {{date}}",
"Comparing {{newer}} and {{older}}": "Сравнение {{newer}} и {{older}}",
"Exit compare": "Выйти из режима сравнения",
"Search attachments...": "Поиск вложений...",
"Error loading attachments.": "Ошибка при загрузке вложений.",
"No attachments on this page yet.": "На этой странице пока нет вложений.",
"Uploaded by {{name}}": "Загружено пользователем {{name}}",
"Download {{name}}": "Скачать {{name}}",
"Access revoked": "Доступ отозван",
"Authorize application": "Авторизовать приложение",
"{{name}} wants to access {{workspace}}": "{{name}} запрашивает доступ к {{workspace}}",
"Not you? Switch account": "Не вы? Сменить аккаунт",
"This application will be able to:": "Это приложение сможет:",
"Write": "Запись",
"Invalid authorization request": "Недопустимый запрос на авторизацию",
"Authorize": "Авторизовать",
"Application": "Приложение",
"Permissions": "Разрешения",
"Authorized": "Авторизовано",
"Revoke access": "Отозвать доступ",
"Revoke access for {{name}}": "Отозвать доступ для {{name}}",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Вы уверены, что хотите отозвать доступ для {{name}}? Приложение больше не сможет получить доступ к вашей учетной записи.",
"Something went wrong. Please try again.": "Что-то пошло не так. Пожалуйста, попробуйте еще раз.",
"Remove {{name}}": "Удалить {{name}}",
"Make sure you trust this application before authorizing it.": "Прежде чем авторизовать это приложение, убедитесь, что вы ему доверяете.",
"You will be redirected to": "Вы будете перенаправлены на",
"View content without making changes.": "Просматривать содержимое без внесения изменений.",
"Create and modify content.": "Создавать и изменять содержимое.",
"Applications and AI assistants you have authorized to access your account.": "Приложения и AI-помощники, которым вы разрешили доступ к своей учетной записи.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "В вашем рабочем пространстве включен MCP. Подключайте AI-помощников к вашей учетной записи Docmost через OAuth.",
"Authorized apps": "Авторизованные приложения",
"No authorized apps yet.": "Пока нет авторизованных приложений.",
"Workspace knowledge only": "Только знания рабочего пространства",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Ограничить AI Chat ответами только на основе страниц вашего рабочего пространства и загруженных файлов. Внешние знания использоваться не будут.",
"Toggle workspace knowledge only": "Переключить режим \"Только знания рабочего пространства\"",
"Read-only mode": "Режим только для чтения",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat может искать и читать содержимое рабочего пространства, но не может создавать или редактировать страницы.",
"Toggle AI Chat read-only mode": "Переключить режим только для чтения для AI Chat",
"Title only": "Только заголовок",
"you": "вы"
} }
@@ -41,8 +41,6 @@
"Dark": "Темна", "Dark": "Темна",
"Date": "Дата", "Date": "Дата",
"Delete": "Видалити", "Delete": "Видалити",
"Remove from page": "Видалити зі сторінки",
"Base options": "Параметри бази",
"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": "Опис",
@@ -78,24 +76,6 @@
"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": "Не вдалося оновити дані",
"Failed to create base": "Не вдалося створити базу",
"Failed to update base": "Не вдалося оновити базу",
"Failed to delete base": "Не вдалося видалити базу",
"Failed to create property": "Не вдалося створити властивість",
"Failed to update property": "Не вдалося оновити властивість",
"Failed to delete property": "Не вдалося видалити властивість",
"Failed to reorder property": "Не вдалося змінити порядок властивості",
"Failed to create view": "Не вдалося створити подання",
"Failed to update view": "Не вдалося оновити подання",
"Failed to delete view": "Не вдалося видалити подання",
"Failed to create row": "Не вдалося створити рядок",
"Failed to update row": "Не вдалося оновити рядок",
"Failed to delete row": "Не вдалося видалити рядок",
"Failed to delete rows": "Не вдалося видалити рядки",
"Failed to reorder row": "Не вдалося змінити порядок рядка",
"Failed to move card": "Не вдалося перемістити картку",
"Failed to add card": "Не вдалося додати картку",
"Failed to export CSV": "Не вдалося експортувати CSV",
"Favorite spaces": "Обрані простори", "Favorite spaces": "Обрані простори",
"Favorite spaces appear here": "Тут відображаються обрані простори", "Favorite spaces appear here": "Тут відображаються обрані простори",
"Favorites": "Обране", "Favorites": "Обране",
@@ -294,7 +274,6 @@
"Export space": "Експорт простору", "Export space": "Експорт простору",
"Export {{type}}": "Експорт {{type}}", "Export {{type}}": "Експорт {{type}}",
"File exceeds the {{limit}} attachment limit": "Файл перевищує ліміт вкладень {{limit}}", "File exceeds the {{limit}} attachment limit": "Файл перевищує ліміт вкладень {{limit}}",
"Media": "Медіа",
"Align left": "По лівому краю", "Align left": "По лівому краю",
"Align right": "По правому краю", "Align right": "По правому краю",
"Align center": "По центру", "Align center": "По центру",
@@ -388,8 +367,6 @@
"Insert horizontal rule divider": "Вставити горизонтальний роздільник", "Insert horizontal rule divider": "Вставити горизонтальний роздільник",
"Page break": "Розрив сторінки", "Page break": "Розрив сторінки",
"Insert a page break for printing.": "Вставте розрив сторінки для друку.", "Insert a page break for printing.": "Вставте розрив сторінки для друку.",
"Footnote": "Виноска",
"Insert a footnote reference.": "Вставити посилання на виноску.",
"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 audio from your device.": "Завантажте будь-який аудіофайл зі свого пристрою.", "Upload any audio from your device.": "Завантажте будь-який аудіофайл зі свого пристрою.",
@@ -421,8 +398,6 @@
"Insert mermaid diagram": "Вставити діаграму Mermaid", "Insert mermaid diagram": "Вставити діаграму Mermaid",
"Insert and design Drawio diagrams": "Вставляйте та створюйте діаграми Drawio", "Insert and design Drawio diagrams": "Вставляйте та створюйте діаграми Drawio",
"Insert current date": "Вставити поточну дату", "Insert current date": "Вставити поточну дату",
"Time": "Час",
"Insert current time": "Вставити поточний час",
"Draw and sketch excalidraw diagrams": "Створюйте та кресліть діаграми Excalidraw", "Draw and sketch excalidraw diagrams": "Створюйте та кресліть діаграми Excalidraw",
"Multiple": "Кілька", "Multiple": "Кілька",
"Turn into": "Перетворити", "Turn into": "Перетворити",
@@ -449,7 +424,6 @@
"Names do not match": "Назви не збігаються", "Names do not match": "Назви не збігаються",
"Today, {{time}}": "Сьогодні, {{time}}", "Today, {{time}}": "Сьогодні, {{time}}",
"Yesterday, {{time}}": "Вчора, {{time}}", "Yesterday, {{time}}": "Вчора, {{time}}",
"now": "зараз",
"Space created successfully": "Простір успішно створено", "Space created successfully": "Простір успішно створено",
"Space updated successfully": "Простір успішно оновлено", "Space updated successfully": "Простір успішно оновлено",
"Space deleted successfully": "Простір успішно видалено", "Space deleted successfully": "Простір успішно видалено",
@@ -622,8 +596,6 @@
"Deleted by": "Видалив", "Deleted by": "Видалив",
"Deleted at": "Видалено", "Deleted at": "Видалено",
"Preview": "Попередній перегляд", "Preview": "Попередній перегляд",
"Base preview unavailable": "Попередній перегляд бази недоступний",
"Restore this base to view its contents.": "Відновіть цю базу, щоб переглянути її вміст.",
"Subpages": "Підсторінки", "Subpages": "Підсторінки",
"Failed to load subpages": "Не вдалося завантажити підсторінки", "Failed to load subpages": "Не вдалося завантажити підсторінки",
"No subpages": "Немає підсторінок", "No subpages": "Немає підсторінок",
@@ -699,7 +671,7 @@
"Upgrade your plan": "Оновіть свій тарифний план", "Upgrade your plan": "Оновіть свій тарифний план",
"Available with a paid license": "Доступно за платною ліцензією", "Available with a paid license": "Доступно за платною ліцензією",
"Upgrade your license tier.": "Оновіть рівень своєї ліцензії.", "Upgrade your license tier.": "Оновіть рівень своєї ліцензії.",
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "ШІ доступний у платних редакціях Docmost. Зверніться до sales@docmost.com.", "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "ШІ доступний лише в корпоративній редакції Docmost. Зверніться до sales@docmost.com.",
"AI & MCP": "ШІ та MCP", "AI & MCP": "ШІ та MCP",
"AI": "ШІ", "AI": "ШІ",
"MCP": "MCP", "MCP": "MCP",
@@ -707,11 +679,9 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Увімкніть MCP‑сервер, щоб дозволити ШІ‑помічникам та інструментам взаємодіяти з вмістом вашого робочого простору.", "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "Увімкніть MCP‑сервер, щоб дозволити ШІ‑помічникам та інструментам взаємодіяти з вмістом вашого робочого простору.",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP доступний лише в корпоративній редакції Docmost. Зверніться до sales@docmost.com.", "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP доступний лише в корпоративній редакції Docmost. Зверніться до sales@docmost.com.",
"MCP Server URL": "URL сервера MCP", "MCP Server URL": "URL сервера MCP",
"Connect AI assistants with your Docmost account via OAuth.": "Підключайте AI-асистентів до свого облікового запису Docmost через OAuth.", "Use your API key for authentication. You can manage API keys in your account settings.": "Використовуйте свій API‑ключ для аутентифікації. Ви можете керувати API‑ключами в налаштуваннях облікового запису.",
"Enforce OAuth": "Enforce OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI-асистенти повинні підключатися до облікового запису Docmost через OAuth. Ключі API не можна використовувати з MCP-сервером.",
"Toggle enforce OAuth for MCP": "Toggle enforce OAuth for MCP",
"Supported tools": "Підтримувані інструменти", "Supported tools": "Підтримувані інструменти",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "У вашому робочому просторі MCP увімкнено. Використайте свій API‑ключ, щоб підключити ШІ‑помічників.",
"MCP server URL:": "URL сервера MCP:", "MCP server URL:": "URL сервера MCP:",
"Learn more": "Дізнатися більше", "Learn more": "Дізнатися більше",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Керуйте ключами API для всіх користувачів у робочому просторі. Перегляньте <anchor>документацію API</anchor> для деталей використання.", "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "Керуйте ключами API для всіх користувачів у робочому просторі. Перегляньте <anchor>документацію API</anchor> для деталей використання.",
@@ -995,8 +965,6 @@
"Are you sure you want to delete this template?": "Ви впевнені, що хочете видалити цей шаблон?", "Are you sure you want to delete this template?": "Ви впевнені, що хочете видалити цей шаблон?",
"Template scope updated": "Область дії шаблону оновлено", "Template scope updated": "Область дії шаблону оновлено",
"Choose which space this template belongs to": "Виберіть, до якого простору належить цей шаблон", "Choose which space this template belongs to": "Виберіть, до якого простору належить цей шаблон",
"Only workspace admins can create global templates.": "Лише адміністратори робочого простору можуть створювати глобальні шаблони.",
"You need edit access to this space to create templates.": "Щоб створювати шаблони, вам потрібен доступ до редагування цього простору.",
"Scope": "Область дії", "Scope": "Область дії",
"Select scope": "Вибрати область дії", "Select scope": "Вибрати область дії",
"Title": "Назва", "Title": "Назва",
@@ -1009,7 +977,7 @@
"Search pages and spaces...": "Шукати сторінки та простори...", "Search pages and spaces...": "Шукати сторінки та простори...",
"No results found": "Результати не знайдено", "No results found": "Результати не знайдено",
"You don't have permission to create pages here": "У вас немає дозволу на створення сторінок тут", "You don't have permission to create pages here": "У вас немає дозволу на створення сторінок тут",
"Chat menu for {{title}}": "Меню чату для {{title}}", "Chat menu": "Меню чату",
"API key menu": "Меню ключа API", "API key menu": "Меню ключа API",
"Jump to comment selection": "Перейти до вибору коментаря", "Jump to comment selection": "Перейти до вибору коментаря",
"Slash commands": "Слеш-команди", "Slash commands": "Слеш-команди",
@@ -1095,7 +1063,7 @@
"Filter": "Фільтр", "Filter": "Фільтр",
"Page title": "Назва сторінки", "Page title": "Назва сторінки",
"Page content": "Вміст сторінки", "Page content": "Вміст сторінки",
"Member actions for {{name}}": "Дії учасника для {{name}}", "Member actions": "Дії з учасником",
"Toggle password visibility": "Перемкнути видимість пароля", "Toggle password visibility": "Перемкнути видимість пароля",
"Send comment": "Надіслати коментар", "Send comment": "Надіслати коментар",
"Token actions": "Дії з токеном", "Token actions": "Дії з токеном",
@@ -1116,226 +1084,5 @@
"Added {{name}} to favorites": "{{name}} додано до обраного", "Added {{name}} to favorites": "{{name}} додано до обраного",
"Removed {{name}} from favorites": "{{name}} видалено з обраного", "Removed {{name}} from favorites": "{{name}} видалено з обраного",
"Page menu for {{name}}": "Меню сторінки для {{name}}", "Page menu for {{name}}": "Меню сторінки для {{name}}",
"Create subpage of {{name}}": "Створити підсторінку для {{name}}", "Create subpage of {{name}}": "Створити підсторінку для {{name}}"
"Allow personal spaces": "Дозволити особисті простори",
"Members can create their own personal space.": "Учасники можуть створювати власний особистий простір.",
"Toggle allow personal spaces": "Перемкнути дозвіл на особисті простори",
"Create personal space": "Створити особистий простір",
"Personal space": "Особистий простір",
"{{name}}'s space": "Простір {{name}}",
"Apply": "Застосувати",
"Cells that aren't already a page reference will be cleared.": "Клітинки, які ще не є посиланням на сторінку, буде очищено.",
"Cells that aren't a valid URL will be cleared.": "Клітинки, які не є дійсною URL-адресою, буде очищено.",
"Cells that aren't a valid email address will be cleared.": "Клітинки, які не є дійсною електронною адресою, буде очищено.",
"Cells that can't be parsed as a date will be cleared.": "Клітинки, які не вдається розпізнати як дату, буде очищено.",
"Cells that can't be parsed as a number will be cleared.": "Клітинки, які не вдається розпізнати як число, буде очищено.",
"Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).": "Клітинки буде приведено до потрібного формату (yes/true/1 стануть позначеними; усе інше стане непозначеним або буде очищено).",
"Cells will be reinterpreted under the new type.": "Клітинки буде повторно інтерпретовано відповідно до нового типу.",
"Cells will be replaced with a comma-separated list of file names.": "Клітинки буде замінено списком імен файлів, розділених комами.",
"Cells will be replaced with a comma-separated list of option names.": "Клітинки буде замінено списком назв параметрів, розділених комами.",
"Cells will be replaced with the option name.": "Клітинки буде замінено назвою параметра.",
"Cells will be replaced with the page title.": "Клітинки буде замінено заголовком сторінки.",
"Cells will be replaced with the person's name.": "Клітинки буде замінено іменем особи.",
"Change type": "Змінити тип",
"Change type to {{label}}?": "Змінити тип на {{label}}?",
"Converting…": "Перетворення…",
"Existing values become single-item lists. No data is lost.": "Наявні значення стануть одноелементними списками. Дані не буде втрачено.",
"Only the first selected item per row will be kept; the rest will be discarded.": "Для кожного рядка буде збережено лише перший вибраний елемент; решту буде відкинуто.",
"Previous record": "Попередній запис",
"Next record": "Наступний запис",
"Record actions": "Дії із записом",
"Delete record": "Видалити запис",
"Delete record?": "Видалити запис?",
"This action cannot be undone.": "Цю дію неможливо скасувати.",
"to navigate": "для навігації",
"to close": "для закриття",
"Expand row {{number}}": "Розгорнути рядок {{number}}",
"Saving…": "Збереження…",
"Read-only": "Лише читання",
"Loading…": "Завантаження…",
"Updated {{when}}": "Оновлено {{when}}",
"Add property": "Додати властивість",
"Create property": "Створити властивість",
"Hide properties": "Приховати властивості",
"Find a property type": "Знайти тип властивості",
"Properties": "Властивості",
"A property with this name already exists": "Властивість із такою назвою вже існує",
"Add card": "Додати картку",
"Add filter": "Додати фільтр",
"Add option": "Додати опцію",
"Add sort": "Додати сортування",
"Add view": "Додати подання",
"All data in this column will be lost.": "Усі дані в цьому стовпці буде втрачено.",
"Allow multiple people": "Дозволити кількох людей",
"Alphabetize": "За алфавітом",
"Are you sure you want to delete": "Ви впевнені, що хочете видалити",
"Ascending": "За зростанням",
"Base deleted": "Базу видалено",
"Base name": "Назва бази",
"Base table": "Таблиця бази",
"Card properties": "Властивості картки",
"Cells longer than the Text limit will be truncated and the extra content permanently lost.": "Клітинки, довші за обмеження для тексту, буде обрізано, а зайвий вміст буде безповоротно втрачено.",
"Checkbox": "Прапорець",
"Checked by default": "Позначено за замовчуванням",
"Choose a property": "Оберіть властивість",
"Clear selection": "Очистити вибір",
"Column options": "Параметри стовпця",
"Comma, period": "Кома, крапка",
"Complete": "Завершено",
"Contains": "Містить",
"Copy link to view": "Скопіювати посилання на подання",
"Create a status property": "Створити властивість статусу",
"Created at": "Створено",
"Currency": "Валюта",
"Decimal places": "Десяткові знаки",
"Default value": "Значення за замовчуванням",
"Delete property": "Видалити властивість",
"Delete view": "Видалити подання",
"Delete {{count}} rows?_one": "Delete 1 row?",
"Delete {{count}} rows?_other": "Delete {{count}} rows?",
"Descending": "За спаданням",
"Discard": "Скасувати",
"Doesn't contain": "Не містить",
"Done": "Готово",
"Edit formula": "Редагувати формулу",
"Edit property": "Редагувати властивість",
"Export CSV": "Експорт CSV",
"Failed to load base": "Не вдалося завантажити базу",
"False": "Хибність",
"File": "Файл",
"Filter and sort changes are visible only to you": "Зміни фільтрів і сортування видимі лише вам",
"Filter by": "Фільтрувати за",
"Formula": "Формула",
"Get started with": "Почніть із",
"Greater than": "Більше ніж",
"Group by": "Групувати за",
"Group this board by a select or status property.": "Згрупуйте цю дошку за властивістю вибору або статусу.",
"Hide all": "Приховати все",
"Hide group": "Приховати групу",
"In Progress": "У процесі",
"In progress": "У процесі",
"Include time": "Включити час",
"Insert a kanban board on this page": "Вставити канбан-дошку на цю сторінку",
"Insert an inline base on this page": "Вставити вбудовану базу на цю сторінку",
"Is": "Є",
"Is after": "Після",
"Is any of": "Є одним із",
"Is before": "До",
"Is empty": "Порожнє",
"Is none of": "Не є жодним із",
"Is not": "Не є",
"Is not empty": "Не порожнє",
"Is on or after": "У цей день або пізніше",
"Is on or before": "У цей день або раніше",
"Is within": "У межах",
"Keep editing": "Продовжити редагування",
"Last edited at": "Востаннє відредаговано",
"Last edited by": "Востаннє відредагував",
"Less than": "Менше ніж",
"Link copied to clipboard": "Посилання скопійовано в буфер обміну",
"Local": "Локальний",
"Long text": "Довгий текст",
"Moved card to {{column}}": "Картку переміщено до {{column}}",
"Moved column to {{column}}": "Стовпець переміщено до {{column}}",
"Multi-select": "Мультивибір",
"New row": "Новий рядок",
"Next month": "Наступного місяця",
"Next week": "Наступного тижня",
"Next year": "Наступного року",
"No filters applied": "Фільтри не застосовано",
"No match": "Немає збігів",
"No options for this property type": "Немає опцій для цього типу властивості",
"No sorts applied": "Сортування не застосовано",
"No value": "Немає значення",
"None": "Немає",
"Not started": "Не розпочато",
"Number": "Число",
"One month ago": "Місяць тому",
"One month from now": "Через місяць",
"One week ago": "Тиждень тому",
"One week from now": "Через тиждень",
"Open as page": "Відкрити як сторінку",
"Options": "Опції",
"Past month": "За минулий місяць",
"Past week": "За минулий тиждень",
"Past year": "За минулий рік",
"Percent": "Відсоток",
"Period, comma": "Крапка, кома",
"Person": "Особа",
"Please enter a valid email": "Будь ласка, введіть коректну електронну адресу",
"Progress": "Прогрес",
"Property options": "Параметри властивості",
"Relative": "Відносний",
"Rename property": "Перейменувати властивість",
"Reset": "Скинути",
"Save for everyone": "Зберегти для всіх",
"Select": "Вибір",
"Select a property": "Виберіть властивість",
"Select all loaded rows": "Вибрати всі завантажені рядки",
"Show all": "Показати все",
"Sort": "Сортування",
"Sort by": "Сортувати за",
"Space, comma": "Пробіл, кома",
"Space, period": "Пробіл, крапка",
"This board has no grouping property yet.": "Ця дошка ще не має властивості групування.",
"This month": "Цього місяця",
"This year": "Цього року",
"Thousands and decimal separators": "Роздільники тисяч і десяткових знаків",
"Time format": "Формат часу",
"To Do": "До виконання",
"Tomorrow": "Завтра",
"True": "Істина",
"Unsaved changes": "Незбережені зміни",
"Untitled base": "База без назви",
"URL": "URL",
"Value": "Значення",
"View updated for everyone": "Подання оновлено для всіх",
"You have unsaved changes. Do you want to discard them?": "У вас є незбережені зміни. Хочете їх скасувати?",
"{{count}} rows deleted_one": "1 row deleted",
"{{count}} rows deleted_other": "{{count}} rows deleted",
"{{count}} selected_one": "1 selected",
"{{count}} selected_other": "{{count}} selected",
"Compare": "Порівняти",
"Compare versions": "Порівняти версії",
"Select version from {{date}}": "Виберіть версію від {{date}}",
"Version actions for {{date}}": "Дії з версією від {{date}}",
"Comparing {{newer}} and {{older}}": "Порівняння {{newer}} і {{older}}",
"Exit compare": "Вийти з режиму порівняння",
"Search attachments...": "Пошук вкладень...",
"Error loading attachments.": "Помилка завантаження вкладень.",
"No attachments on this page yet.": "На цій сторінці ще немає вкладень.",
"Uploaded by {{name}}": "Завантажено користувачем {{name}}",
"Download {{name}}": "Завантажити {{name}}",
"Access revoked": "Доступ відкликано",
"Authorize application": "Авторизувати застосунок",
"{{name}} wants to access {{workspace}}": "{{name}} хоче отримати доступ до {{workspace}}",
"Not you? Switch account": "Не ви? Змінити обліковий запис",
"This application will be able to:": "Цей застосунок зможе:",
"Write": "Запис",
"Invalid authorization request": "Недійсний запит на авторизацію",
"Authorize": "Авторизувати",
"Application": "Застосунок",
"Permissions": "Дозволи",
"Authorized": "Авторизовано",
"Revoke access": "Відкликати доступ",
"Revoke access for {{name}}": "Відкликати доступ для {{name}}",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "Ви впевнені, що хочете відкликати доступ для {{name}}? Застосунок більше не зможе отримувати доступ до вашого облікового запису.",
"Something went wrong. Please try again.": "Щось пішло не так. Спробуйте ще раз.",
"Remove {{name}}": "Видалити {{name}}",
"Make sure you trust this application before authorizing it.": "Перш ніж авторизувати цей застосунок, переконайтеся, що ви йому довіряєте.",
"You will be redirected to": "Вас буде перенаправлено на",
"View content without making changes.": "Переглядати вміст без внесення змін.",
"Create and modify content.": "Створювати та змінювати вміст.",
"Applications and AI assistants you have authorized to access your account.": "Застосунки та AI-асистенти, яким ви надали дозвіл на доступ до свого облікового запису.",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "У вашому робочому просторі ввімкнено MCP. Підключайте AI-асистентів до свого облікового запису Docmost через OAuth.",
"Authorized apps": "Авторизовані застосунки",
"No authorized apps yet.": "Авторизованих застосунків ще немає.",
"Workspace knowledge only": "Лише знання робочого простору",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "Обмежити AI Chat відповідями лише на основі сторінок вашого робочого простору та завантажених файлів. Зовнішні знання не використовуватимуться.",
"Toggle workspace knowledge only": "Перемкнути режим лише знань робочого простору",
"Read-only mode": "Режим лише читання",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI Chat може шукати та читати вміст робочого простору, але не може створювати або редагувати сторінки.",
"Toggle AI Chat read-only mode": "Перемкнути режим лише читання для AI Chat",
"Title only": "Лише заголовок",
"you": "ви"
} }
@@ -41,8 +41,6 @@
"Dark": "深色", "Dark": "深色",
"Date": "日期", "Date": "日期",
"Delete": "删除", "Delete": "删除",
"Remove from page": "从页面中移除",
"Base options": "数据库选项",
"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": "描述",
@@ -78,24 +76,6 @@
"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": "数据更新失败",
"Failed to create base": "创建数据库失败",
"Failed to update base": "更新数据库失败",
"Failed to delete base": "删除数据库失败",
"Failed to create property": "创建属性失败",
"Failed to update property": "更新属性失败",
"Failed to delete property": "删除属性失败",
"Failed to reorder property": "重新排序属性失败",
"Failed to create view": "创建视图失败",
"Failed to update view": "更新视图失败",
"Failed to delete view": "删除视图失败",
"Failed to create row": "创建记录失败",
"Failed to update row": "更新记录失败",
"Failed to delete row": "删除记录失败",
"Failed to delete rows": "删除记录失败",
"Failed to reorder row": "重新排序记录失败",
"Failed to move card": "移动卡片失败",
"Failed to add card": "添加卡片失败",
"Failed to export CSV": "导出 CSV 失败",
"Favorite spaces": "收藏的空间", "Favorite spaces": "收藏的空间",
"Favorite spaces appear here": "收藏的空间会显示在这里", "Favorite spaces appear here": "收藏的空间会显示在这里",
"Favorites": "收藏", "Favorites": "收藏",
@@ -294,7 +274,6 @@
"Export space": "导出空间", "Export space": "导出空间",
"Export {{type}}": "导出为 {{type}}", "Export {{type}}": "导出为 {{type}}",
"File exceeds the {{limit}} attachment limit": "文件超出了 {{limit}} 类型附件限制", "File exceeds the {{limit}} attachment limit": "文件超出了 {{limit}} 类型附件限制",
"Media": "媒体",
"Align left": "靠左对齐", "Align left": "靠左对齐",
"Align right": "靠右对齐", "Align right": "靠右对齐",
"Align center": "居中对齐", "Align center": "居中对齐",
@@ -388,8 +367,6 @@
"Insert horizontal rule divider": "插入水平分割线", "Insert horizontal rule divider": "插入水平分割线",
"Page break": "分页符", "Page break": "分页符",
"Insert a page break for printing.": "插入一个用于打印的分页符。", "Insert a page break for printing.": "插入一个用于打印的分页符。",
"Footnote": "脚注",
"Insert a footnote reference.": "插入脚注引用。",
"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 audio from your device.": "从您的设备上传任意音频文件。", "Upload any audio from your device.": "从您的设备上传任意音频文件。",
@@ -421,8 +398,6 @@
"Insert mermaid diagram": "插入 Mermaid 图表", "Insert mermaid diagram": "插入 Mermaid 图表",
"Insert and design Drawio diagrams": "插入并设计 Drawio 图表", "Insert and design Drawio diagrams": "插入并设计 Drawio 图表",
"Insert current date": "插入当前日期", "Insert current date": "插入当前日期",
"Time": "时间",
"Insert current time": "插入当前时间",
"Draw and sketch excalidraw diagrams": "绘制和草绘 Excalidraw 图表", "Draw and sketch excalidraw diagrams": "绘制和草绘 Excalidraw 图表",
"Multiple": "多个", "Multiple": "多个",
"Turn into": "变成", "Turn into": "变成",
@@ -449,7 +424,6 @@
"Names do not match": "名称不匹配", "Names do not match": "名称不匹配",
"Today, {{time}}": "今天,{{time}}", "Today, {{time}}": "今天,{{time}}",
"Yesterday, {{time}}": "昨天,{{time}}", "Yesterday, {{time}}": "昨天,{{time}}",
"now": "现在",
"Space created successfully": "空间创建成功", "Space created successfully": "空间创建成功",
"Space updated successfully": "空间更新成功", "Space updated successfully": "空间更新成功",
"Space deleted successfully": "空间删除成功", "Space deleted successfully": "空间删除成功",
@@ -622,8 +596,6 @@
"Deleted by": "删除者", "Deleted by": "删除者",
"Deleted at": "删除于", "Deleted at": "删除于",
"Preview": "预览", "Preview": "预览",
"Base preview unavailable": "无法预览数据库",
"Restore this base to view its contents.": "恢复此数据库以查看其内容。",
"Subpages": "子页面", "Subpages": "子页面",
"Failed to load subpages": "加载子页面失败", "Failed to load subpages": "加载子页面失败",
"No subpages": "没有子页面", "No subpages": "没有子页面",
@@ -699,7 +671,7 @@
"Upgrade your plan": "升级您的方案", "Upgrade your plan": "升级您的方案",
"Available with a paid license": "需付费许可才可用", "Available with a paid license": "需付费许可才可用",
"Upgrade your license tier.": "升级您的许可等级。", "Upgrade your license tier.": "升级您的许可等级。",
"AI is available in the Docmost paid editions. Contact sales@docmost.com.": "AI 在 Docmost 付费版中提供。请联系 sales@docmost.com。", "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "AI 在 Docmost 企业版中提供。请联系 sales@docmost.com。",
"AI & MCP": "AI 与 MCP", "AI & MCP": "AI 与 MCP",
"AI": "AI", "AI": "AI",
"MCP": "MCP", "MCP": "MCP",
@@ -707,11 +679,9 @@
"Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "启用 MCP 服务器以允许 AI 助手和工具与您的工作区内容交互。", "Enable the MCP server to allow AI assistants and tools to interact with your workspace content.": "启用 MCP 服务器以允许 AI 助手和工具与您的工作区内容交互。",
"MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP 仅在 Docmost 企业版中提供。请联系 sales@docmost.com。", "MCP is only available in the Docmost enterprise edition. Contact sales@docmost.com.": "MCP 仅在 Docmost 企业版中提供。请联系 sales@docmost.com。",
"MCP Server URL": "MCP 服务器 URL", "MCP Server URL": "MCP 服务器 URL",
"Connect AI assistants with your Docmost account via OAuth.": "通过 OAuth 将 AI 助手连接到你的 Docmost 账户。", "Use your API key for authentication. You can manage API keys in your account settings.": "使用您的 API 密钥进行身份验证。您可以在账户设置中管理 API 密钥。",
"Enforce OAuth": "Enforce OAuth",
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.": "AI 助手必须通过 OAuth 使用 Docmost 账户连接。MCP 服务器不能使用 API 密钥。",
"Toggle enforce OAuth for MCP": "Toggle enforce OAuth for MCP",
"Supported tools": "支持的工具", "Supported tools": "支持的工具",
"Your workspace has MCP enabled. Use your API key to connect AI assistants.": "您的工作区已启用 MCP。使用您的 API 密钥连接 AI 助手。",
"MCP server URL:": "MCP 服务器 URL", "MCP server URL:": "MCP 服务器 URL",
"Learn more": "了解更多", "Learn more": "了解更多",
"Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "为工作区内所有用户管理 API 密钥。有关使用详情,请查阅<anchor>API 文档</anchor>。", "Manage API keys for all users in the workspace. View the <anchor>API documentation</anchor> for usage details.": "为工作区内所有用户管理 API 密钥。有关使用详情,请查阅<anchor>API 文档</anchor>。",
@@ -995,8 +965,6 @@
"Are you sure you want to delete this template?": "你确定要删除此模板吗?", "Are you sure you want to delete this template?": "你确定要删除此模板吗?",
"Template scope updated": "模板范围已更新", "Template scope updated": "模板范围已更新",
"Choose which space this template belongs to": "选择此模板所属的空间", "Choose which space this template belongs to": "选择此模板所属的空间",
"Only workspace admins can create global templates.": "只有工作区管理员才能创建全局模板。",
"You need edit access to this space to create templates.": "您需要拥有此空间的编辑权限才能创建模板。",
"Scope": "范围", "Scope": "范围",
"Select scope": "选择范围", "Select scope": "选择范围",
"Title": "标题", "Title": "标题",
@@ -1009,7 +977,7 @@
"Search pages and spaces...": "搜索页面和空间……", "Search pages and spaces...": "搜索页面和空间……",
"No results found": "未找到结果", "No results found": "未找到结果",
"You don't have permission to create pages here": "你无权在此处创建页面", "You don't have permission to create pages here": "你无权在此处创建页面",
"Chat menu for {{title}}": "{{title}} 的聊天菜单", "Chat menu": "聊天菜单",
"API key menu": "API 密钥菜单", "API key menu": "API 密钥菜单",
"Jump to comment selection": "跳转到评论选择", "Jump to comment selection": "跳转到评论选择",
"Slash commands": "斜杠命令", "Slash commands": "斜杠命令",
@@ -1095,7 +1063,7 @@
"Filter": "筛选", "Filter": "筛选",
"Page title": "页面标题", "Page title": "页面标题",
"Page content": "页面内容", "Page content": "页面内容",
"Member actions for {{name}}": "{{name}} 的成员操作", "Member actions": "成员操作",
"Toggle password visibility": "切换密码可见性", "Toggle password visibility": "切换密码可见性",
"Send comment": "发送评论", "Send comment": "发送评论",
"Token actions": "令牌操作", "Token actions": "令牌操作",
@@ -1116,226 +1084,5 @@
"Added {{name}} to favorites": "已将 {{name}} 添加到收藏", "Added {{name}} to favorites": "已将 {{name}} 添加到收藏",
"Removed {{name}} from favorites": "已将 {{name}} 从收藏中移除", "Removed {{name}} from favorites": "已将 {{name}} 从收藏中移除",
"Page menu for {{name}}": "{{name}} 的页面菜单", "Page menu for {{name}}": "{{name}} 的页面菜单",
"Create subpage of {{name}}": "创建 {{name}} 的子页面", "Create subpage of {{name}}": "创建 {{name}} 的子页面"
"Allow personal spaces": "允许个人空间",
"Members can create their own personal space.": "成员可以创建自己的个人空间。",
"Toggle allow personal spaces": "切换是否允许个人空间",
"Create personal space": "创建个人空间",
"Personal space": "个人空间",
"{{name}}'s space": "{{name}} 的空间",
"Apply": "应用",
"Cells that aren't already a page reference will be cleared.": "尚未是页面引用的单元格将被清空。",
"Cells that aren't a valid URL will be cleared.": "不是有效 URL 的单元格将被清空。",
"Cells that aren't a valid email address will be cleared.": "不是有效电子邮件地址的单元格将被清空。",
"Cells that can't be parsed as a date will be cleared.": "无法解析为日期的单元格将被清空。",
"Cells that can't be parsed as a number will be cleared.": "无法解析为数字的单元格将被清空。",
"Cells will be coerced (yes/true/1 become checked; everything else becomes unchecked or cleared).": "单元格将被强制转换(yes/true/1 将变为选中;其他所有值将变为未选中或被清空)。",
"Cells will be reinterpreted under the new type.": "单元格将在新类型下重新解释。",
"Cells will be replaced with a comma-separated list of file names.": "单元格将被替换为以逗号分隔的文件名列表。",
"Cells will be replaced with a comma-separated list of option names.": "单元格将被替换为以逗号分隔的选项名称列表。",
"Cells will be replaced with the option name.": "单元格将被替换为选项名称。",
"Cells will be replaced with the page title.": "单元格将被替换为页面标题。",
"Cells will be replaced with the person's name.": "单元格将被替换为此人的姓名。",
"Change type": "更改类型",
"Change type to {{label}}?": "将类型更改为 {{label}}",
"Converting…": "正在转换…",
"Existing values become single-item lists. No data is lost.": "现有值将变为只包含一项的列表。不会丢失数据。",
"Only the first selected item per row will be kept; the rest will be discarded.": "每行仅保留第一个已选项目;其余项目将被丢弃。",
"Previous record": "上一条记录",
"Next record": "下一条记录",
"Record actions": "记录操作",
"Delete record": "删除记录",
"Delete record?": "删除记录?",
"This action cannot be undone.": "此操作无法撤销。",
"to navigate": "用于导航",
"to close": "用于关闭",
"Expand row {{number}}": "展开第 {{number}} 行",
"Saving…": "正在保存…",
"Read-only": "只读",
"Loading…": "正在加载…",
"Updated {{when}}": "更新于 {{when}}",
"Add property": "添加属性",
"Create property": "创建属性",
"Hide properties": "隐藏属性",
"Find a property type": "查找属性类型",
"Properties": "属性",
"A property with this name already exists": "已存在同名属性",
"Add card": "添加卡片",
"Add filter": "添加筛选",
"Add option": "添加选项",
"Add sort": "添加排序",
"Add view": "添加视图",
"All data in this column will be lost.": "此列中的所有数据都将丢失。",
"Allow multiple people": "允许多人",
"Alphabetize": "按字母顺序",
"Are you sure you want to delete": "您确定要删除",
"Ascending": "升序",
"Base deleted": "资料库已删除",
"Base name": "资料库名称",
"Base table": "资料库表",
"Card properties": "卡片属性",
"Cells longer than the Text limit will be truncated and the extra content permanently lost.": "超出文本长度限制的单元格内容将被截断,额外内容将永久丢失。",
"Checkbox": "复选框",
"Checked by default": "默认选中",
"Choose a property": "选择一个属性",
"Clear selection": "清除选择",
"Column options": "列选项",
"Comma, period": "逗号、句点",
"Complete": "完成",
"Contains": "包含",
"Copy link to view": "复制视图链接",
"Create a status property": "创建状态属性",
"Created at": "创建于",
"Currency": "货币",
"Decimal places": "小数位数",
"Default value": "默认值",
"Delete property": "删除属性",
"Delete view": "删除视图",
"Delete {{count}} rows?_one": "删除 1 行?",
"Delete {{count}} rows?_other": "删除 {{count}} 行?",
"Descending": "降序",
"Discard": "放弃",
"Doesn't contain": "不包含",
"Done": "完成",
"Edit formula": "编辑公式",
"Edit property": "编辑属性",
"Export CSV": "导出 CSV",
"Failed to load base": "加载资料库失败",
"False": "假",
"File": "文件",
"Filter and sort changes are visible only to you": "筛选和排序更改仅对您可见",
"Filter by": "筛选条件",
"Formula": "公式",
"Get started with": "开始使用",
"Greater than": "大于",
"Group by": "分组依据",
"Group this board by a select or status property.": "按选择或状态属性对此看板进行分组。",
"Hide all": "全部隐藏",
"Hide group": "隐藏分组",
"In Progress": "进行中",
"In progress": "进行中",
"Include time": "包含时间",
"Insert a kanban board on this page": "在此页面插入一个看板",
"Insert an inline base on this page": "在此页面插入一个内联资料库",
"Is": "是",
"Is after": "在此之后",
"Is any of": "是其中之一",
"Is before": "在此之前",
"Is empty": "为空",
"Is none of": "都不是",
"Is not": "不是",
"Is not empty": "不为空",
"Is on or after": "在此日期或之后",
"Is on or before": "在此日期或之前",
"Is within": "在范围内",
"Keep editing": "继续编辑",
"Last edited at": "最后编辑于",
"Last edited by": "最后编辑者",
"Less than": "小于",
"Link copied to clipboard": "链接已复制到剪贴板",
"Local": "本地",
"Long text": "长文本",
"Moved card to {{column}}": "已将卡片移动到 {{column}}",
"Moved column to {{column}}": "已将列移动到 {{column}}",
"Multi-select": "多选",
"New row": "新行",
"Next month": "下个月",
"Next week": "下周",
"Next year": "明年",
"No filters applied": "未应用筛选",
"No match": "无匹配项",
"No options for this property type": "此属性类型没有可用选项",
"No sorts applied": "未应用排序",
"No value": "无值",
"None": "无",
"Not started": "未开始",
"Number": "数字",
"One month ago": "一个月前",
"One month from now": "从现在起一个月后",
"One week ago": "一周前",
"One week from now": "从现在起一周后",
"Open as page": "作为页面打开",
"Options": "选项",
"Past month": "过去一个月",
"Past week": "过去一周",
"Past year": "过去一年",
"Percent": "百分比",
"Period, comma": "句点、逗号",
"Person": "人员",
"Please enter a valid email": "请输入有效的电子邮件地址",
"Progress": "进度",
"Property options": "属性选项",
"Relative": "相对",
"Rename property": "重命名属性",
"Reset": "重置",
"Save for everyone": "保存给所有人",
"Select": "选择",
"Select a property": "选择一个属性",
"Select all loaded rows": "选择所有已加载的行",
"Show all": "显示全部",
"Sort": "排序",
"Sort by": "排序依据",
"Space, comma": "空格、逗号",
"Space, period": "空格、句点",
"This board has no grouping property yet.": "此看板尚无分组属性。",
"This month": "本月",
"This year": "今年",
"Thousands and decimal separators": "千位分隔符和小数分隔符",
"Time format": "时间格式",
"To Do": "待办",
"Tomorrow": "明天",
"True": "真",
"Unsaved changes": "未保存的更改",
"Untitled base": "未命名资料库",
"URL": "网址",
"Value": "值",
"View updated for everyone": "已为所有人更新视图",
"You have unsaved changes. Do you want to discard them?": "您有未保存的更改。要放弃这些更改吗?",
"{{count}} rows deleted_one": "已删除 1 行",
"{{count}} rows deleted_other": "已删除 {{count}} 行",
"{{count}} selected_one": "已选择 1 项",
"{{count}} selected_other": "已选择 {{count}} 项",
"Compare": "比较",
"Compare versions": "比较版本",
"Select version from {{date}}": "选择 {{date}} 的版本",
"Version actions for {{date}}": "{{date}} 的版本操作",
"Comparing {{newer}} and {{older}}": "正在比较 {{newer}} 和 {{older}}",
"Exit compare": "退出比较",
"Search attachments...": "搜索附件……",
"Error loading attachments.": "加载附件时出错。",
"No attachments on this page yet.": "此页面上还没有附件。",
"Uploaded by {{name}}": "由 {{name}} 上传",
"Download {{name}}": "下载 {{name}}",
"Access revoked": "访问权限已撤销",
"Authorize application": "授权应用程序",
"{{name}} wants to access {{workspace}}": "{{name}} 想要访问 {{workspace}}",
"Not you? Switch account": "不是你?切换账户",
"This application will be able to:": "此应用程序将能够:",
"Write": "写入",
"Invalid authorization request": "无效的授权请求",
"Authorize": "授权",
"Application": "应用程序",
"Permissions": "权限",
"Authorized": "已授权",
"Revoke access": "撤销访问权限",
"Revoke access for {{name}}": "撤销 {{name}} 的访问权限",
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.": "你确定要撤销 {{name}} 的访问权限吗?该应用程序将无法再访问你的账户。",
"Something went wrong. Please try again.": "出了点问题。请重试。",
"Remove {{name}}": "移除 {{name}}",
"Make sure you trust this application before authorizing it.": "在授权之前,请确保你信任此应用程序。",
"You will be redirected to": "你将被重定向到",
"View content without making changes.": "查看内容而不进行更改。",
"Create and modify content.": "创建和修改内容。",
"Applications and AI assistants you have authorized to access your account.": "你已授权访问你账户的应用程序和 AI 助手。",
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.": "你的工作区已启用 MCP。通过 OAuth 将 AI 助手连接到你的 Docmost 账户。",
"Authorized apps": "已授权的应用",
"No authorized apps yet.": "尚无已授权的应用。",
"Workspace knowledge only": "仅限工作区知识",
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.": "将 AI 聊天限制为仅根据你的工作区页面和已上传文件进行回答。它不会使用外部知识。",
"Toggle workspace knowledge only": "切换“仅限工作区知识”",
"Read-only mode": "只读模式",
"AI Chat can search and read workspace content, but cannot create or edit pages.": "AI 聊天可以搜索和读取工作区内容,但不能创建或编辑页面。",
"Toggle AI Chat read-only mode": "切换 AI 聊天只读模式",
"Title only": "仅标题",
"you": "你"
} }
+45 -84
View File
@@ -1,94 +1,60 @@
import { lazy, Suspense, useEffect } from "react";
import { Navigate, Route, Routes } from "react-router-dom"; import { Navigate, Route, Routes } from "react-router-dom";
import Layout from "@/components/layouts/global/layout.tsx"; import SetupWorkspace from "@/pages/auth/setup-workspace.tsx";
import LoginPage from "@/pages/auth/login";
import Home from "@/pages/dashboard/home";
import Page from "@/pages/page/page";
import AccountSettings from "@/pages/settings/account/account-settings";
import WorkspaceMembers from "@/pages/settings/workspace/workspace-members";
import WorkspaceSettings from "@/pages/settings/workspace/workspace-settings";
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 { Error404 } from "@/components/ui/error-404.tsx";
import AccountPreferences from "@/pages/settings/account/account-preferences.tsx";
import SpaceHome from "@/pages/space/space-home.tsx";
import PageRedirect from "@/pages/page/page-redirect.tsx";
import Layout from "@/components/layouts/global/layout.tsx";
import InviteSignup from "@/pages/auth/invite-signup.tsx";
import ForgotPassword from "@/pages/auth/forgot-password.tsx";
import PasswordReset from "./pages/auth/password-reset";
import Billing from "@/ee/billing/pages/billing.tsx";
import CloudLogin from "@/ee/pages/cloud-login.tsx";
import CreateWorkspace from "@/ee/pages/create-workspace.tsx";
import { isCloud } from "@/lib/config.ts"; import { isCloud } from "@/lib/config.ts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Security from "@/ee/security/pages/security.tsx";
import License from "@/ee/licence/pages/license.tsx";
import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.tsx"; import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.tsx";
import SharedPage from "@/pages/share/shared-page.tsx";
import PdfRenderPage from "@/ee/pdf-export/pdf-render-page.tsx";
import Shares from "@/pages/settings/shares/shares.tsx";
import ShareLayout from "@/features/share/components/share-layout.tsx";
import ShareRedirect from "@/pages/share/share-redirect.tsx";
import { useTrackOrigin } from "@/hooks/use-track-origin"; import { useTrackOrigin } from "@/hooks/use-track-origin";
import SpacesPage from "@/pages/spaces/spaces.tsx";
import { MfaChallengePage } from "@/ee/mfa/pages/mfa-challenge-page";
const SetupWorkspace = lazy(() => import("@/pages/auth/setup-workspace.tsx")); import { MfaSetupRequiredPage } from "@/ee/mfa/pages/mfa-setup-required-page";
const LoginPage = lazy(() => import("@/pages/auth/login")); import SpaceTrash from "@/pages/space/space-trash.tsx";
const Home = lazy(() => import("@/pages/dashboard/home")); import UserApiKeys from "@/ee/api-key/pages/user-api-keys";
const Page = lazy(() => import("@/pages/page/page")); import WorkspaceApiKeys from "@/ee/api-key/pages/workspace-api-keys";
const AccountSettings = lazy( import AiSettings from "@/ee/ai/pages/ai-settings.tsx";
() => import("@/pages/settings/account/account-settings"), import BasePage from "@/ee/base/pages/base-page.tsx";
); import AuditLogs from "@/ee/audit/pages/audit-logs.tsx";
const WorkspaceMembers = lazy( import VerifiedPages from "@/ee/page-verification/pages/verified-pages.tsx";
() => import("@/pages/settings/workspace/workspace-members"), import TemplateList from "@/ee/template/pages/template-list";
); import TemplateEditor from "@/ee/template/pages/template-editor";
const WorkspaceSettings = lazy( import FavoritesPage from "@/pages/favorites/favorites-page";
() => import("@/pages/settings/workspace/workspace-settings"), import AiChat from "@/ee/ai-chat/pages/ai-chat.tsx";
); import VerifyEmail from "@/ee/pages/verify-email.tsx";
const Groups = lazy(() => import("@/pages/settings/group/groups")); import LabelPage from "@/pages/label/label-page";
const GroupInfo = lazy(() => import("./pages/settings/group/group-info"));
const Spaces = lazy(() => import("@/pages/settings/space/spaces.tsx"));
const AccountPreferences = lazy(
() => import("@/pages/settings/account/account-preferences.tsx"),
);
const SpaceHome = lazy(() => import("@/pages/space/space-home.tsx"));
const PageRedirect = lazy(() => import("@/pages/page/page-redirect.tsx"));
const InviteSignup = lazy(() => import("@/pages/auth/invite-signup.tsx"));
const ForgotPassword = lazy(() => import("@/pages/auth/forgot-password.tsx"));
const PasswordReset = lazy(() => import("./pages/auth/password-reset"));
const Billing = lazy(() => import("@/ee/billing/pages/billing.tsx"));
const CloudLogin = lazy(() => import("@/ee/pages/cloud-login.tsx"));
const CreateWorkspace = lazy(() => import("@/ee/pages/create-workspace.tsx"));
const Security = lazy(() => import("@/ee/security/pages/security.tsx"));
const License = lazy(() => import("@/ee/licence/pages/license.tsx"));
const SharedPage = lazy(() => import("@/pages/share/shared-page.tsx"));
const PdfRenderPage = lazy(() => import("@/ee/pdf-export/pdf-render-page.tsx"));
const Shares = lazy(() => import("@/pages/settings/shares/shares.tsx"));
const ShareLayout = lazy(
() => import("@/features/share/components/share-layout.tsx"),
);
const ShareRedirect = lazy(() => import("@/pages/share/share-redirect.tsx"));
const SpacesPage = lazy(() => import("@/pages/spaces/spaces.tsx"));
const MfaChallengePage = lazy(() =>
import("@/ee/mfa/pages/mfa-challenge-page").then((m) => ({
default: m.MfaChallengePage,
})),
);
const MfaSetupRequiredPage = lazy(() =>
import("@/ee/mfa/pages/mfa-setup-required-page").then((m) => ({
default: m.MfaSetupRequiredPage,
})),
);
const SpaceTrash = lazy(() => import("@/pages/space/space-trash.tsx"));
const UserApiKeys = lazy(() => import("@/ee/api-key/pages/user-api-keys"));
const WorkspaceApiKeys = lazy(
() => import("@/ee/api-key/pages/workspace-api-keys"),
);
const AiSettings = lazy(() => import("@/ee/ai/pages/ai-settings.tsx"));
const BasePage = lazy(() => import("@/ee/base/pages/base-page.tsx"));
const AuditLogs = lazy(() => import("@/ee/audit/pages/audit-logs.tsx"));
const VerifiedPages = lazy(
() => import("@/ee/page-verification/pages/verified-pages.tsx"),
);
const TemplateList = lazy(() => import("@/ee/template/pages/template-list"));
const TemplateEditor = lazy(
() => import("@/ee/template/pages/template-editor"),
);
const FavoritesPage = lazy(() => import("@/pages/favorites/favorites-page"));
const AiChat = lazy(() => import("@/ee/ai-chat/pages/ai-chat.tsx"));
const VerifyEmail = lazy(() => import("@/ee/pages/verify-email.tsx"));
const LabelPage = lazy(() => import("@/pages/label/label-page"));
const OAuthConsent = lazy(() => import("@/ee/oauth/pages/oauth-consent.tsx"));
export default function App() { export default function App() {
const { t } = useTranslation(); const { t } = useTranslation();
useRedirectToCloudSelect(); useRedirectToCloudSelect();
useTrackOrigin(); useTrackOrigin();
useEffect(() => {
// warm the editor chunk so opening a page doesn't wait on the network
const timer = setTimeout(() => import("@/pages/page/page"), 3000);
return () => clearTimeout(timer);
}, []);
return ( return (
<Suspense fallback={null}> <>
<Routes> <Routes>
<Route index element={<Navigate to="/home" />} /> <Route index element={<Navigate to="/home" />} />
<Route path={"/login"} element={<LoginPage />} /> <Route path={"/login"} element={<LoginPage />} />
@@ -97,7 +63,6 @@ export default function App() {
<Route path={"/password-reset"} element={<PasswordReset />} /> <Route path={"/password-reset"} element={<PasswordReset />} />
<Route path={"/login/mfa"} element={<MfaChallengePage />} /> <Route path={"/login/mfa"} element={<MfaChallengePage />} />
<Route path={"/login/mfa/setup"} element={<MfaSetupRequiredPage />} /> <Route path={"/login/mfa/setup"} element={<MfaSetupRequiredPage />} />
<Route path={"/oauth/consent"} element={<OAuthConsent />} />
{!isCloud() && ( {!isCloud() && (
<Route path={"/setup/register"} element={<SetupWorkspace />} /> <Route path={"/setup/register"} element={<SetupWorkspace />} />
@@ -151,10 +116,6 @@ export default function App() {
element={<AccountPreferences />} element={<AccountPreferences />}
/> />
<Route path={"account/api-keys"} element={<UserApiKeys />} /> <Route path={"account/api-keys"} element={<UserApiKeys />} />
<Route
path={"account/api-keys/authorized-apps"}
element={<UserApiKeys />}
/>
<Route path={"workspace"} element={<WorkspaceSettings />} /> <Route path={"workspace"} element={<WorkspaceSettings />} />
<Route path={"members"} element={<WorkspaceMembers />} /> <Route path={"members"} element={<WorkspaceMembers />} />
<Route path={"api-keys"} element={<WorkspaceApiKeys />} /> <Route path={"api-keys"} element={<WorkspaceApiKeys />} />
@@ -174,6 +135,6 @@ export default function App() {
<Route path="*" element={<Error404 />} /> <Route path="*" element={<Error404 />} />
</Routes> </Routes>
</Suspense> </>
); );
} }
@@ -1,30 +1,17 @@
import { ActionIcon, Box, Group, ScrollArea, Title, Tooltip } from "@mantine/core"; import { ActionIcon, Box, Group, ScrollArea, Title, Tooltip } from "@mantine/core";
import { IconX } from "@tabler/icons-react"; import { IconX } from "@tabler/icons-react";
import CommentListWithTabs from "@/features/comment/components/comment-list-with-tabs.tsx";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
import React, { lazy, ReactNode, Suspense, useEffect } from "react"; import React, { ReactNode, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TableOfContents } from "@/features/editor/components/table-of-contents/table-of-contents.tsx";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms.ts"; import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms.ts";
import AsideChatPanel from "@/ee/ai-chat/components/aside-chat-panel";
import { PageDetailsAside } from "@/features/page-details/components/page-details-aside.tsx";
import { ASIDE_PANEL_ID } from "@/hooks/use-toggle-aside.tsx"; import { ASIDE_PANEL_ID } from "@/hooks/use-toggle-aside.tsx";
const CommentListWithTabs = lazy(
() => import("@/features/comment/components/comment-list-with-tabs.tsx"),
);
const TableOfContents = lazy(() =>
import(
"@/features/editor/components/table-of-contents/table-of-contents.tsx"
).then((m) => ({ default: m.TableOfContents })),
);
const AsideChatPanel = lazy(
() => import("@/ee/ai-chat/components/aside-chat-panel"),
);
const PageDetailsAside = lazy(() =>
import("@/features/page-details/components/page-details-aside.tsx").then(
(m) => ({ default: m.PageDetailsAside }),
),
);
export default function Aside() { export default function Aside() {
const [{ tab, isAsideOpen }, setAsideState] = useAtom(asideStateAtom); const [{ tab, isAsideOpen }, setAsideState] = useAtom(asideStateAtom);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -81,19 +68,17 @@ export default function Aside() {
</Group> </Group>
)} )}
<Suspense fallback={null}> {tab === "comments" || tab === "chat" ? (
{tab === "comments" || tab === "chat" ? ( component
component ) : (
) : ( <ScrollArea
<ScrollArea style={{ height: "85vh" }}
style={{ height: "85vh" }} scrollbarSize={5}
scrollbarSize={5} type="scroll"
type="scroll" >
> <div style={{ paddingBottom: "200px" }}>{component}</div>
<div style={{ paddingBottom: "200px" }}>{component}</div> </ScrollArea>
</ScrollArea> )}
)}
</Suspense>
</> </>
)} )}
</Box> </Box>
@@ -11,10 +11,7 @@ import {
sidebarWidthAtom, sidebarWidthAtom,
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx"; import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx";
import AiChatSidebar from "@/ee/ai-chat/components/ai-chat-sidebar.tsx";
const AiChatSidebar = React.lazy(
() => import("@/ee/ai-chat/components/ai-chat-sidebar.tsx"),
);
import { AppHeader } from "@/components/layouts/global/app-header.tsx"; import { AppHeader } from "@/components/layouts/global/app-header.tsx";
import Aside from "@/components/layouts/global/aside.tsx"; import Aside from "@/components/layouts/global/aside.tsx";
import classes from "./app-shell.module.css"; import classes from "./app-shell.module.css";
@@ -129,11 +126,7 @@ export default function GlobalAppShell({
)} )}
{isSpaceRoute && <SpaceSidebar />} {isSpaceRoute && <SpaceSidebar />}
{isSettingsRoute && <SettingsSidebar />} {isSettingsRoute && <SettingsSidebar />}
{isAiRoute && ( {isAiRoute && <AiChatSidebar />}
<React.Suspense fallback={null}>
<AiChatSidebar />
</React.Suspense>
)}
{showGlobalSidebar && <GlobalSidebar />} {showGlobalSidebar && <GlobalSidebar />}
</AppShell.Navbar> </AppShell.Navbar>
<AppShell.Main id={MAIN_CONTENT_ID} tabIndex={-1}> <AppShell.Main id={MAIN_CONTENT_ID} tabIndex={-1}>
@@ -1,12 +0,0 @@
import { UnstyledButton } from "@mantine/core";
import { type ComponentPropsWithoutRef, forwardRef } from "react";
// Menu.Item hard-codes role="menuitem"; use as its `component` to restore role="menuitemcheckbox" so aria-checked works.
export const CheckboxMenuItem = forwardRef<
HTMLButtonElement,
ComponentPropsWithoutRef<"button">
>((props, ref) => (
<UnstyledButton ref={ref} {...props} role="menuitemcheckbox" />
));
CheckboxMenuItem.displayName = "CheckboxMenuItem";
@@ -1,45 +0,0 @@
import { describe, expect, it, beforeEach } from "vitest";
import { render } from "@testing-library/react";
import { HelmetProvider } from "react-helmet-async";
import { DocumentTitle } from "./document-title.tsx";
const renderTitle = (ui: React.ReactNode) =>
render(<HelmetProvider>{ui}</HelmetProvider>);
describe("DocumentTitle", () => {
beforeEach(() => {
document.head.innerHTML = "<title>Docmost</title>";
});
it("appends the app name", () => {
renderTitle(<DocumentTitle title="Home" />);
expect(document.title).toBe("Home - Docmost");
});
it("omits the app name when asked", () => {
renderTitle(<DocumentTitle title="My page" withAppName={false} />);
expect(document.title).toBe("My page");
});
it("falls back to the app name without a title", () => {
renderTitle(<DocumentTitle />);
expect(document.title).toBe("Docmost");
});
it("never renders an empty title", () => {
renderTitle(<DocumentTitle title="Spaces" />);
const titles = Array.from(document.querySelectorAll("head > title"));
expect(titles.every((node) => node.textContent !== "")).toBe(true);
});
it("renders extra head children", () => {
renderTitle(
<DocumentTitle title="Shared">
<meta name="robots" content="noindex" />
</DocumentTitle>,
);
expect(
document.querySelector('head > meta[name="robots"]')?.getAttribute("content"),
).toBe("noindex");
});
});
@@ -1,29 +0,0 @@
import React from "react";
import { Helmet } from "react-helmet-async";
import { getAppName } from "@/lib/config.ts";
type DocumentTitleProps = {
title?: string;
withAppName?: boolean;
children?: React.ReactNode;
};
export function DocumentTitle({
title,
withAppName = true,
children,
}: DocumentTitleProps) {
const appName = getAppName();
let documentTitle = appName;
if (title) {
documentTitle = withAppName ? `${title} - ${appName}` : title;
}
return (
<Helmet>
<title>{documentTitle}</title>
{children}
</Helmet>
);
}
+4 -2
View File
@@ -1,15 +1,17 @@
import { Title, Text, Button, Container, Group } from "@mantine/core"; import { Title, Text, Button, Container, Group } from "@mantine/core";
import classes from "./error-404.module.css"; import classes from "./error-404.module.css";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DocumentTitle } from "@/components/ui/document-title.tsx";
export function Error404() { export function Error404() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <>
<DocumentTitle title={t("404 page not found")} /> <Helmet>
<title>{t("404 page not found")} - Docmost</title>
</Helmet>
<Container className={classes.root}> <Container className={classes.root}>
<Title className={classes.title}>{t("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}> <Text c="dimmed" size="lg" ta="center" className={classes.description}>
@@ -1,74 +0,0 @@
import { Badge, Group, Text, Switch, Tooltip } from "@mantine/core";
import { useAtom } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
import { notifications } from "@mantine/notifications";
import { useHasFeature } from "@/ee/hooks/use-feature";
import { Feature } from "@/ee/features";
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
export default function AiChatReadOnly() {
const { t } = useTranslation();
const hasAccess = useHasFeature(Feature.AI_CONTROLS);
return (
<Group justify="space-between" wrap="nowrap" gap="xl">
<div>
<Group gap="xs" align="center">
<Text size="md">{t("Read-only mode")}</Text>
{!hasAccess && (
<Badge variant="light" size="sm" radius="sm">
{t("Enterprise")}
</Badge>
)}
</Group>
<Text size="sm" c="dimmed">
{t(
"AI Chat can search and read workspace content, but cannot create or edit pages.",
)}
</Text>
</div>
<AiChatReadOnlyToggle />
</Group>
);
}
function AiChatReadOnlyToggle() {
const { t } = useTranslation();
const [workspace, setWorkspace] = useAtom(workspaceAtom);
const [checked, setChecked] = useState(
workspace?.settings?.ai?.chatReadOnly,
);
const hasAccess = useHasFeature(Feature.AI_CONTROLS);
const upgradeLabel = useUpgradeLabel();
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.currentTarget.checked;
try {
const updatedWorkspace = await updateWorkspace({
aiChatReadOnly: value,
});
setChecked(value);
setWorkspace(updatedWorkspace);
} catch (err: any) {
notifications.show({
message: err?.response?.data?.message,
color: "red",
});
}
};
return (
<Tooltip label={upgradeLabel} disabled={hasAccess} refProp="rootRef">
<Switch
defaultChecked={checked}
onChange={handleChange}
disabled={!hasAccess}
aria-label={t("Toggle AI Chat read-only mode")}
/>
</Tooltip>
);
}
@@ -1,74 +0,0 @@
import { Badge, Group, Text, Switch, Tooltip } from "@mantine/core";
import { useAtom } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
import { notifications } from "@mantine/notifications";
import { useHasFeature } from "@/ee/hooks/use-feature";
import { Feature } from "@/ee/features";
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
export default function AiChatWorkspaceKnowledgeOnly() {
const { t } = useTranslation();
const hasAccess = useHasFeature(Feature.AI_CONTROLS);
return (
<Group justify="space-between" wrap="nowrap" gap="xl">
<div>
<Group gap="xs" align="center">
<Text size="md">{t("Workspace knowledge only")}</Text>
{!hasAccess && (
<Badge variant="light" size="sm" radius="sm">
{t("Enterprise")}
</Badge>
)}
</Group>
<Text size="sm" c="dimmed">
{t(
"Restrict AI Chat to answering from your workspace pages and uploaded files only. It will not use outside knowledge.",
)}
</Text>
</div>
<AiChatWorkspaceKnowledgeOnlyToggle />
</Group>
);
}
function AiChatWorkspaceKnowledgeOnlyToggle() {
const { t } = useTranslation();
const [workspace, setWorkspace] = useAtom(workspaceAtom);
const [checked, setChecked] = useState(
workspace?.settings?.ai?.chatWorkspaceKnowledgeOnly,
);
const hasAccess = useHasFeature(Feature.AI_CONTROLS);
const upgradeLabel = useUpgradeLabel();
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.currentTarget.checked;
try {
const updatedWorkspace = await updateWorkspace({
aiChatWorkspaceKnowledgeOnly: value,
});
setChecked(value);
setWorkspace(updatedWorkspace);
} catch (err: any) {
notifications.show({
message: err?.response?.data?.message,
color: "red",
});
}
};
return (
<Tooltip label={upgradeLabel} disabled={hasAccess} refProp="rootRef">
<Switch
defaultChecked={checked}
onChange={handleChange}
disabled={!hasAccess}
aria-label={t("Toggle workspace knowledge only")}
/>
</Tooltip>
);
}
@@ -6,10 +6,7 @@ import {
IconFileText, IconFileText,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAtomValue } from "jotai"; import ChatInput from "./chat-input";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import { useRef } from "react";
import ChatInput, { ChatInputHandle } from "./chat-input";
import type { ChatAttachment, PageMention } from "../types/ai-chat.types"; import type { ChatAttachment, PageMention } from "../types/ai-chat.types";
import classes from "../styles/ai-chat.module.css"; import classes from "../styles/ai-chat.module.css";
@@ -17,7 +14,6 @@ type Suggestion = {
icon: React.ReactNode; icon: React.ReactNode;
text: string; text: string;
prompt: string; prompt: string;
write?: boolean;
}; };
const SUGGESTIONS: Suggestion[] = [ const SUGGESTIONS: Suggestion[] = [
@@ -30,7 +26,6 @@ const SUGGESTIONS: Suggestion[] = [
icon: <IconFilePlus size={16} />, icon: <IconFilePlus size={16} />,
text: "Create a new page", text: "Create a new page",
prompt: "Create a new page titled ", prompt: "Create a new page titled ",
write: true,
}, },
{ {
icon: <IconFileText size={16} />, icon: <IconFileText size={16} />,
@@ -41,7 +36,6 @@ const SUGGESTIONS: Suggestion[] = [
icon: <IconEdit size={16} />, icon: <IconEdit size={16} />,
text: "Update page content", text: "Update page content",
prompt: "Update the page @", prompt: "Update the page @",
write: true,
}, },
]; ];
@@ -53,13 +47,9 @@ type Props = {
export default function ChatEmptyState({ isStreaming, onSend, onStop }: Props) { export default function ChatEmptyState({ isStreaming, onSend, onStop }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
const workspace = useAtomValue(workspaceAtom);
const writesDisabled = workspace?.settings?.ai?.chatReadOnly === true;
const inputRef = useRef<ChatInputHandle>(null);
const handleSuggestionClick = (prompt: string) => { const handleSuggestionClick = (prompt: string) => {
inputRef.current?.prefill(prompt); onSend(prompt, [], []);
}; };
return ( return (
@@ -72,7 +62,6 @@ export default function ChatEmptyState({ isStreaming, onSend, onStop }: Props) {
<div className={classes.emptyStateInput}> <div className={classes.emptyStateInput}>
<ChatInput <ChatInput
ref={inputRef}
isStreaming={isStreaming} isStreaming={isStreaming}
onSend={onSend} onSend={onSend}
onStop={onStop} onStop={onStop}
@@ -84,7 +73,7 @@ export default function ChatEmptyState({ isStreaming, onSend, onStop }: Props) {
<div className={classes.suggestionsSection}> <div className={classes.suggestionsSection}>
<h2 className={classes.suggestionsLabel}>{t("Get started")}</h2> <h2 className={classes.suggestionsLabel}>{t("Get started")}</h2>
<div className={classes.suggestionsGrid}> <div className={classes.suggestionsGrid}>
{SUGGESTIONS.filter((s) => !writesDisabled || !s.write).map((s) => ( {SUGGESTIONS.map((s) => (
<button <button
key={s.text} key={s.text}
type="button" type="button"
@@ -1,12 +1,4 @@
import { import { useCallback, useId, useRef, useEffect, useState } from "react";
forwardRef,
useCallback,
useId,
useImperativeHandle,
useRef,
useEffect,
useState,
} from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { IconArrowUp, IconPaperclip, IconPlayerStopFilled, IconX, IconFile, IconPhoto, IconPlus, IconAt, IconFileText } from "@tabler/icons-react"; import { IconArrowUp, IconPaperclip, IconPlayerStopFilled, IconX, IconFile, IconPhoto, IconPlus, IconAt, IconFileText } from "@tabler/icons-react";
import { Popover } from "@mantine/core"; import { Popover } from "@mantine/core";
@@ -43,10 +35,6 @@ type Props = {
chatId?: string; chatId?: string;
}; };
export type ChatInputHandle = {
prefill: (text: string) => void;
};
function extractMentions(json: any): PageMention[] { function extractMentions(json: any): PageMention[] {
const mentions: PageMention[] = []; const mentions: PageMention[] = [];
const seen = new Set<string>(); const seen = new Set<string>();
@@ -101,7 +89,7 @@ function editorJsonToText(json: any): string {
return text; return text;
} }
const ChatInput = forwardRef<ChatInputHandle, Props>(function ChatInput({ export default function ChatInput({
isStreaming, isStreaming,
onSend, onSend,
onStop, onStop,
@@ -112,7 +100,7 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(function ChatInput({
variant = "card", variant = "card",
showDisclaimer = true, showDisclaimer = true,
chatId, chatId,
}: Props, ref) { }: Props) {
const chatIdRef = useRef(chatId); const chatIdRef = useRef(chatId);
chatIdRef.current = chatId; chatIdRef.current = chatId;
const { t } = useTranslation(); const { t } = useTranslation();
@@ -267,7 +255,6 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(function ChatInput({
}, },
content: "", content: "",
editable: true, editable: true,
textDirection: "auto",
immediatelyRender: true, immediatelyRender: true,
shouldRerenderOnTransaction: false, shouldRerenderOnTransaction: false,
autofocus: autofocus ? "end" : false, autofocus: autofocus ? "end" : false,
@@ -282,19 +269,6 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(function ChatInput({
} }
}, [editor]); }, [editor]);
useImperativeHandle(
ref,
() => ({
prefill: (text: string) => {
if (!editor || editor.isDestroyed) return;
editor.commands.clearContent();
editor.commands.insertContent(text);
editor.commands.focus();
},
}),
[editor],
);
const hasContent = !isEmpty || pendingAttachments.some((a) => !a.uploading) || (contextPages?.length ?? 0) > 0; const hasContent = !isEmpty || pendingAttachments.some((a) => !a.uploading) || (contextPages?.length ?? 0) > 0;
const wrapperClass = variant === "flat" ? classes.inputWrapperFlat : classes.inputWrapper; const wrapperClass = variant === "flat" ? classes.inputWrapperFlat : classes.inputWrapper;
@@ -454,6 +428,4 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(function ChatInput({
)} )}
</> </>
); );
}); }
export default ChatInput;
@@ -1,6 +1,5 @@
import { import {
Anchor, Anchor,
Badge,
Group, Group,
List, List,
Text, Text,
@@ -108,11 +107,11 @@ export default function McpSettings() {
</CopyButton> </CopyButton>
</Group> </Group>
<Text size="sm" c="dimmed" mt="xs"> <Text size="sm" c="dimmed" mt="xs">
{t("Connect AI assistants with your Docmost account via OAuth.")} {t(
"Use your API key for authentication. You can manage API keys in your account settings.",
)}
</Text> </Text>
<McpEnforceOauthSetting />
<div> <div>
<Text size="sm" fw={500} mt="md" mb={4}> <Text size="sm" fw={500} mt="md" mb={4}>
{t("Supported tools")} {t("Supported tools")}
@@ -155,56 +154,3 @@ export default function McpSettings() {
</Stack> </Stack>
); );
} }
function McpEnforceOauthSetting() {
const { t } = useTranslation();
const [workspace, setWorkspace] = useAtom(workspaceAtom);
const [checked, setChecked] = useState(workspace?.settings?.ai?.enforceMcpOauth);
const hasAccess = useHasFeature(Feature.MCP_CONTROLS);
const upgradeLabel = useUpgradeLabel();
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.currentTarget.checked;
try {
const updatedWorkspace = await updateWorkspace({ enforceMcpOauth: value });
setChecked(value);
setWorkspace(updatedWorkspace);
} catch (err) {
notifications.show({
message: err?.response?.data?.message,
color: "red",
});
}
};
return (
<Group justify="space-between" wrap="nowrap" gap="xl" mt="md">
<div>
<Group gap="xs" align="center">
<Text size="sm" fw={500}>
{t("Enforce OAuth")}
</Text>
{!hasAccess && (
<Badge variant="light" size="sm" radius="sm">
{t("Enterprise")}
</Badge>
)}
</Group>
<Text size="sm" c="dimmed">
{t(
"AI assistants must connect with a Docmost account via OAuth. API keys cannot be used with the MCP server.",
)}
</Text>
</div>
<Tooltip label={upgradeLabel} disabled={hasAccess} refProp="rootRef">
<Switch
defaultChecked={checked}
onChange={handleChange}
disabled={!hasAccess}
aria-label={t("Toggle enforce OAuth for MCP")}
/>
</Tooltip>
</Group>
);
}
+7 -24
View File
@@ -1,3 +1,5 @@
import { Helmet } from "react-helmet-async";
import { getAppName } from "@/lib/config.ts";
import SettingsTitle from "@/components/settings/settings-title.tsx"; import SettingsTitle from "@/components/settings/settings-title.tsx";
import React from "react"; import React from "react";
import useUserRole from "@/hooks/use-user-role.tsx"; import useUserRole from "@/hooks/use-user-role.tsx";
@@ -5,27 +7,20 @@ import { useTranslation } from "react-i18next";
import EnableAiSearch from "@/ee/ai/components/enable-ai-search.tsx"; import EnableAiSearch from "@/ee/ai/components/enable-ai-search.tsx";
import EnableGenerativeAi from "@/ee/ai/components/enable-generative-ai.tsx"; import EnableGenerativeAi from "@/ee/ai/components/enable-generative-ai.tsx";
import EnableAiChat from "@/ee/ai-chat/components/enable-ai-chat.tsx"; import EnableAiChat from "@/ee/ai-chat/components/enable-ai-chat.tsx";
import AiChatReadOnly from "@/ee/ai-chat/components/ai-chat-read-only.tsx";
import AiChatWorkspaceKnowledgeOnly from "@/ee/ai-chat/components/ai-chat-workspace-knowledge-only.tsx";
import McpSettings from "@/ee/ai/components/mcp-settings.tsx"; import McpSettings from "@/ee/ai/components/mcp-settings.tsx";
import { Alert, Collapse, Stack, Tabs } from "@mantine/core"; import { Alert, Stack, Tabs } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react"; import { IconInfoCircle } from "@tabler/icons-react";
import { useHasFeature } from "@/ee/hooks/use-feature"; import { useHasFeature } from "@/ee/hooks/use-feature";
import { Feature } from "@/ee/features"; import { Feature } from "@/ee/features";
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label"; import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
import { isCloud } from "@/lib/config.ts"; import { isCloud } from "@/lib/config.ts";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { DocumentTitle } from "@/components/ui/document-title.tsx";
import { useAtomValue } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
export default function AiSettings() { export default function AiSettings() {
const { t } = useTranslation(); const { t } = useTranslation();
const { isAdmin } = useUserRole(); const { isAdmin } = useUserRole();
const hasAccess = useHasFeature(Feature.AI); const hasAccess = useHasFeature(Feature.AI);
const upgradeLabel = useUpgradeLabel(); const upgradeLabel = useUpgradeLabel();
const workspace = useAtomValue(workspaceAtom);
const aiChatEnabled = workspace?.settings?.ai?.chat === true;
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -45,7 +40,9 @@ export default function AiSettings() {
return ( return (
<> <>
<DocumentTitle title="AI settings" /> <Helmet>
<title>AI settings - {getAppName()}</title>
</Helmet>
<SettingsTitle title={t("AI settings")} /> <SettingsTitle title={t("AI settings")} />
<Tabs color="dark" value={activeTab} onChange={handleTabChange}> <Tabs color="dark" value={activeTab} onChange={handleTabChange}>
@@ -67,7 +64,7 @@ export default function AiSettings() {
mb="lg" mb="lg"
> >
{t( {t(
"AI is available in the Docmost paid editions. Contact sales@docmost.com.", "AI is only available in the Docmost enterprise edition. Contact sales@docmost.com.",
)} )}
</Alert> </Alert>
)} )}
@@ -76,20 +73,6 @@ export default function AiSettings() {
{!isCloud() && <EnableAiSearch />} {!isCloud() && <EnableAiSearch />}
<EnableGenerativeAi /> <EnableGenerativeAi />
<EnableAiChat /> <EnableAiChat />
<Collapse expanded={aiChatEnabled}>
<Stack
gap="md"
pl="md"
ml="xs"
style={{
borderLeft:
"2px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))",
}}
>
<AiChatReadOnly />
<AiChatWorkspaceKnowledgeOnly />
</Stack>
</Collapse>
</Stack> </Stack>
</Tabs.Panel> </Tabs.Panel>
@@ -15,14 +15,6 @@ export interface IAiSearchResponse {
}>; }>;
} }
export async function hintVectorCache(): Promise<void> {
try {
await api.post("/ai/vector-cache-hint");
} catch {
// best-effort cache hint
}
}
export async function aiAnswers( export async function aiAnswers(
params: IPageSearchParams, params: IPageSearchParams,
onChunk?: (chunk: { content?: string; sources?: any[] }) => void, onChunk?: (chunk: { content?: string; sources?: any[] }) => void,
@@ -1,9 +1,10 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Anchor, Alert, Button, Group, Space, Tabs, Text } from "@mantine/core"; import { Anchor, Alert, Button, Group, Space, Text } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react"; import { IconInfoCircle } from "@tabler/icons-react";
import { Helmet } from "react-helmet-async";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import SettingsTitle from "@/components/settings/settings-title"; import SettingsTitle from "@/components/settings/settings-title";
import { getAppUrl } from "@/lib/config"; import { getAppName, getAppUrl } from "@/lib/config";
import { ApiKeyTable } from "@/ee/api-key/components/api-key-table"; import { ApiKeyTable } from "@/ee/api-key/components/api-key-table";
import { CreateApiKeyModal } from "@/ee/api-key/components/create-api-key-modal"; import { CreateApiKeyModal } from "@/ee/api-key/components/create-api-key-modal";
import { ApiKeyCreatedModal } from "@/ee/api-key/components/api-key-created-modal"; import { ApiKeyCreatedModal } from "@/ee/api-key/components/api-key-created-modal";
@@ -16,9 +17,6 @@ import { IApiKey } from "@/ee/api-key";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
import useUserRole from "@/hooks/use-user-role.tsx"; import useUserRole from "@/hooks/use-user-role.tsx";
import { DocumentTitle } from "@/components/ui/document-title.tsx";
import { useLocation, useNavigate } from "react-router-dom";
import { AuthorizedAppsPanel } from "@/ee/oauth/components/authorized-apps-panel.tsx";
export default function UserApiKeys() { export default function UserApiKeys() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -31,11 +29,6 @@ export default function UserApiKeys() {
const { data, isLoading } = useGetApiKeysQuery({ cursor }); const { data, isLoading } = useGetApiKeysQuery({ cursor });
const [workspace] = useAtom(workspaceAtom); const [workspace] = useAtom(workspaceAtom);
const { isAdmin } = useUserRole(); const { isAdmin } = useUserRole();
const location = useLocation();
const navigate = useNavigate();
const activeTab = location.pathname.endsWith("/authorized-apps")
? "authorized-apps"
: "api-keys";
const mcpEnabled = workspace?.settings?.ai?.mcp === true; const mcpEnabled = workspace?.settings?.ai?.mcp === true;
const restrictToAdmins = workspace?.settings?.api?.restrictToAdmins === true; const restrictToAdmins = workspace?.settings?.api?.restrictToAdmins === true;
const canCreate = !restrictToAdmins || isAdmin; const canCreate = !restrictToAdmins || isAdmin;
@@ -54,27 +47,30 @@ export default function UserApiKeys() {
setRevokeModalOpened(true); setRevokeModalOpened(true);
}; };
const handleTabChange = (value: string | null) => {
navigate(
value === "authorized-apps"
? "/settings/account/api-keys/authorized-apps"
: "/settings/account/api-keys",
);
};
return ( return (
<> <>
<DocumentTitle <Helmet>
title={activeTab === "authorized-apps" ? t("Authorized apps") : t("API keys")} <title>
/> {t("API keys")} - {getAppName()}
</title>
</Helmet>
<SettingsTitle title={t("API keys")} /> <SettingsTitle title={t("API keys")} />
{mcpEnabled && ( <Text size="sm" c="dimmed" mb="md">
<Trans
i18nKey="View the <anchor>API documentation</anchor> for usage details."
components={{
anchor: <Anchor href="https://docmost.com/api-docs" target="_blank" size="sm" />,
}}
/>
</Text>
{mcpEnabled && canCreate && (
<Alert variant="light" color="blue" mb="md" p="sm" icon={<IconInfoCircle />}> <Alert variant="light" color="blue" mb="md" p="sm" icon={<IconInfoCircle />}>
<Text size="sm"> <Text size="sm">
{t( {t(
"Your workspace has MCP enabled. Connect AI assistants with your Docmost account via OAuth.", "Your workspace has MCP enabled. Use your API key to connect AI assistants.",
)}{" "} )}{" "}
<Anchor <Anchor
href="https://docmost.com/docs/user-guide/mcp" href="https://docmost.com/docs/user-guide/mcp"
@@ -93,70 +89,37 @@ export default function UserApiKeys() {
</Alert> </Alert>
)} )}
<Tabs color="dark" value={activeTab} onChange={handleTabChange}> {canCreate ? (
<Tabs.List> <Group justify="flex-end" mb="md">
<Tabs.Tab fw={500} value="api-keys"> <Button onClick={() => setCreateModalOpened(true)}>
{t("API keys")} {t("Create API Key")}
</Tabs.Tab> </Button>
<Tabs.Tab fw={500} value="authorized-apps">
{t("Authorized apps")}
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="api-keys" pt="md">
<Group justify="space-between" align="center" mb="md">
<Text size="sm" c="dimmed">
<Trans
i18nKey="View the <anchor>API documentation</anchor> for usage details."
components={{
anchor: <Anchor href="https://docmost.com/api-docs" target="_blank" size="sm" />,
}}
/>
</Text>
{canCreate && (
<Button
onClick={() => setCreateModalOpened(true)}
style={{ flexShrink: 0 }}
>
{t("Create API Key")}
</Button>
)}
</Group> </Group>
) : restrictToAdmins ? (
<Alert variant="light" color="yellow" mb="md" p="sm" icon={<IconInfoCircle />}>
<Text size="sm">
{t("API key creation is restricted to admins by your workspace administrator.")}
</Text>
</Alert>
) : null}
{!canCreate && restrictToAdmins && ( <ApiKeyTable
<Alert variant="light" color="yellow" mb="md" p="sm" icon={<IconInfoCircle />}> apiKeys={data?.items || []}
<Text size="sm"> isLoading={isLoading}
{t("API key creation is restricted to admins by your workspace administrator.")} onUpdate={handleUpdate}
</Text> onRevoke={handleRevoke}
</Alert> />
)}
<ApiKeyTable <Space h="md" />
apiKeys={data?.items || []}
isLoading={isLoading} {data?.items.length > 0 && (
onUpdate={handleUpdate} <Paginate
onRevoke={handleRevoke} hasPrevPage={data?.meta?.hasPrevPage}
hasNextPage={data?.meta?.hasNextPage}
onNext={() => goNext(data?.meta?.nextCursor)}
onPrev={goPrev}
/> />
)}
<Space h="md" />
{data?.items.length > 0 && (
<Paginate
hasPrevPage={data?.meta?.hasPrevPage}
hasNextPage={data?.meta?.hasNextPage}
onNext={() => goNext(data?.meta?.nextCursor)}
onPrev={goPrev}
/>
)}
</Tabs.Panel>
<Tabs.Panel value="authorized-apps" pt="md">
<AuthorizedAppsPanel />
</Tabs.Panel>
</Tabs>
<CreateApiKeyModal <CreateApiKeyModal
opened={createModalOpened} opened={createModalOpened}
@@ -1,7 +1,9 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Anchor, Button, Divider, Group, Space, Text } from "@mantine/core"; import { Anchor, Button, Divider, Group, Space, Text } from "@mantine/core";
import { Helmet } from "react-helmet-async";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import SettingsTitle from "@/components/settings/settings-title"; import SettingsTitle from "@/components/settings/settings-title";
import { getAppName } from "@/lib/config";
import { ApiKeyTable } from "@/ee/api-key/components/api-key-table"; import { ApiKeyTable } from "@/ee/api-key/components/api-key-table";
import { CreateApiKeyModal } from "@/ee/api-key/components/create-api-key-modal"; import { CreateApiKeyModal } from "@/ee/api-key/components/create-api-key-modal";
import { ApiKeyCreatedModal } from "@/ee/api-key/components/api-key-created-modal"; import { ApiKeyCreatedModal } from "@/ee/api-key/components/api-key-created-modal";
@@ -13,7 +15,6 @@ import { useGetApiKeysQuery } from "@/ee/api-key/queries/api-key-query.ts";
import { IApiKey } from "@/ee/api-key"; import { IApiKey } from "@/ee/api-key";
import useUserRole from '@/hooks/use-user-role.tsx'; import useUserRole from '@/hooks/use-user-role.tsx';
import RestrictApiToAdmins from "@/ee/api-key/components/restrict-api-to-admins"; import RestrictApiToAdmins from "@/ee/api-key/components/restrict-api-to-admins";
import { DocumentTitle } from "@/components/ui/document-title.tsx";
export default function WorkspaceApiKeys() { export default function WorkspaceApiKeys() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -46,7 +47,11 @@ export default function WorkspaceApiKeys() {
return ( return (
<> <>
<DocumentTitle title={t("API management")} /> <Helmet>
<title>
{t("API management")} - {getAppName()}
</title>
</Helmet>
<SettingsTitle title={t("API management")} /> <SettingsTitle title={t("API management")} />
@@ -10,9 +10,11 @@ import {
Text, Text,
Tooltip, Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { IconSettings } from "@tabler/icons-react"; import { IconSettings } from "@tabler/icons-react";
import SettingsTitle from "@/components/settings/settings-title"; import SettingsTitle from "@/components/settings/settings-title";
import { getAppName } from "@/lib/config";
import Paginate from "@/components/common/paginate"; import Paginate from "@/components/common/paginate";
import { useCursorPaginate } from "@/hooks/use-cursor-paginate"; import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
import { import {
@@ -24,7 +26,6 @@ import { IAuditLogParams } from "@/ee/audit/types/audit.types";
import { eventFilterOptions } from "@/ee/audit/lib/audit-event-labels"; import { eventFilterOptions } from "@/ee/audit/lib/audit-event-labels";
import AuditLogsTable from "@/ee/audit/components/audit-logs-table"; import AuditLogsTable from "@/ee/audit/components/audit-logs-table";
import useUserRole from "@/hooks/use-user-role"; import useUserRole from "@/hooks/use-user-role";
import { DocumentTitle } from "@/components/ui/document-title.tsx";
type RetentionUnit = "days" | "months" | "years"; type RetentionUnit = "days" | "months" | "years";
@@ -96,7 +97,11 @@ export default function AuditLogs() {
return ( return (
<> <>
<DocumentTitle title={t("Audit log")} /> <Helmet>
<title>
{t("Audit log")} - {getAppName()}
</title>
</Helmet>
<SettingsTitle title={t("Audit log")} /> <SettingsTitle title={t("Audit log")} />
@@ -94,21 +94,21 @@ export const GridCell = memo(function GridCell({
const handleMouseDown = useCallback( const handleMouseDown = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => { (e: React.MouseEvent<HTMLDivElement>) => {
if (!property || e.button !== 0 || isEditing) return; if (!property || e.button !== 0) return;
setFocusedCell({ rowId, propertyId: property.id }); setFocusedCell({ rowId, propertyId: property.id });
}, },
[property, rowId, setFocusedCell, isEditing], [property, rowId, setFocusedCell],
); );
const handleClick = useCallback( const handleClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => { (e: React.MouseEvent<HTMLDivElement>) => {
if (!property || isEditing) return; if (!property) return;
setFocusedCell({ rowId, propertyId: property.id }); setFocusedCell({ rowId, propertyId: property.id });
(e.currentTarget.closest('[role="grid"]') as HTMLElement | null)?.focus({ (e.currentTarget.closest('[role="grid"]') as HTMLElement | null)?.focus({
preventScroll: true, preventScroll: true,
}); });
}, },
[property, rowId, setFocusedCell, isEditing], [property, rowId, setFocusedCell],
); );
const cellReadOnly = property const cellReadOnly = property
@@ -30,7 +30,7 @@ type BaseKanbanProps = {
export function BaseKanban({ base, view, pageId, embedded, editable, viewFilter }: BaseKanbanProps) { export function BaseKanban({ base, view, pageId, embedded, editable, viewFilter }: BaseKanbanProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { groupByPropertyId, groupByProperty, columns, hasValidGroupBy } = useKanbanColumns(base, view); const { groupByPropertyId, columns, hasValidGroupBy } = useKanbanColumns(base, view);
const updateView = useUpdateViewMutation(); const updateView = useUpdateViewMutation();
const moveCard = useKanbanMoveCardMutation(); const moveCard = useKanbanMoveCardMutation();
const { openRow } = useRowDetailModal(pageId); const { openRow } = useRowDetailModal(pageId);
@@ -201,7 +201,6 @@ export function BaseKanban({ base, view, pageId, embedded, editable, viewFilter
column={column} column={column}
viewFilter={viewFilter} viewFilter={viewFilter}
groupByPropertyId={groupByPropertyId!} groupByPropertyId={groupByPropertyId!}
groupByProperty={groupByProperty}
canEdit={editable} canEdit={editable}
onOpenRow={handleOpenRow} onOpenRow={handleOpenRow}
onHide={hideColumn} onHide={hideColumn}
@@ -1,27 +1,24 @@
import { useRef } from "react"; import { useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ActionIcon, Text } from "@mantine/core"; import { ActionIcon, Menu, Text } from "@mantine/core";
import { IconPlus, IconGripVertical } from "@tabler/icons-react"; import { IconDots, IconPlus, IconGripVertical } from "@tabler/icons-react";
import clsx from "clsx"; import clsx from "clsx";
import { IBaseProperty, KanbanColumn } from "@/ee/base/types/base.types"; import { KanbanColumn } from "@/ee/base/types/base.types";
import { choiceColor } from "@/ee/base/components/cells/choice-color"; import { choiceColor } from "@/ee/base/components/cells/choice-color";
import { useKanbanColumnDnd } from "@/ee/base/hooks/use-kanban-column-dnd"; import { useKanbanColumnDnd } from "@/ee/base/hooks/use-kanban-column-dnd";
import { BaseDropEdgeIndicator } from "@/ee/base/components/grid/base-drop-edge-indicator"; import { BaseDropEdgeIndicator } from "@/ee/base/components/grid/base-drop-edge-indicator";
import { KanbanColumnTitle } from "@/ee/base/components/kanban/kanban-column-title";
import { KanbanColumnMenu } from "@/ee/base/components/kanban/kanban-column-menu";
import classes from "@/ee/base/styles/kanban.module.css"; import classes from "@/ee/base/styles/kanban.module.css";
type KanbanColumnHeaderProps = { type KanbanColumnHeaderProps = {
column: KanbanColumn; column: KanbanColumn;
pageId: string; pageId: string;
property: IBaseProperty | undefined;
count?: string; count?: string;
canEdit: boolean; canEdit: boolean;
onHide: () => void; onHide: () => void;
onAddCard: () => void; onAddCard: () => void;
}; };
export function KanbanColumnHeader({ column, pageId, property, count, canEdit, onHide, onAddCard }: KanbanColumnHeaderProps) { export function KanbanColumnHeader({ column, pageId, count, canEdit, onHide, onAddCard }: KanbanColumnHeaderProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const dotColor = column.color const dotColor = column.color
? choiceColor(column.color).color as string ? choiceColor(column.color).color as string
@@ -52,13 +49,22 @@ export function KanbanColumnHeader({ column, pageId, property, count, canEdit, o
background: dotColor, background: dotColor,
}} }}
/> />
<KanbanColumnTitle column={column} property={property} pageId={pageId} canEdit={canEdit} /> <Text fw={600} size="sm" flex={1} truncate>
{column.isNoValue ? t("No value") : column.name}
</Text>
{count !== undefined && <Text className={classes.count}>{count}</Text>} {count !== undefined && <Text className={classes.count}>{count}</Text>}
{canEdit && ( {canEdit && (
<> <>
{property && ( <Menu position="bottom-end" withinPortal>
<KanbanColumnMenu property={property} pageId={pageId} onHide={onHide} /> <Menu.Target>
)} <ActionIcon variant="subtle" size="sm" color="gray" aria-label={t("Column options")}>
<IconDots size={14} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item onClick={onHide}>{t("Hide group")}</Menu.Item>
</Menu.Dropdown>
</Menu>
<ActionIcon variant="subtle" size="sm" color="gray" aria-label={t("Add card")} onClick={onAddCard}> <ActionIcon variant="subtle" size="sm" color="gray" aria-label={t("Add card")} onClick={onAddCard}>
<IconPlus size={14} /> <IconPlus size={14} />
</ActionIcon> </ActionIcon>
@@ -1,119 +0,0 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { ActionIcon, Popover, Stack } from "@mantine/core";
import { IconDots, IconEyeOff, IconSettings } from "@tabler/icons-react";
import { useAtom } from "jotai";
import {
propertyMenuCloseRequestAtomFamily,
propertyMenuDirtyAtomFamily,
} from "@/ee/base/atoms/base-atoms";
import { IBaseProperty } from "@/ee/base/types/base.types";
import {
MenuItem,
PropertyMenuContent,
} from "@/ee/base/components/property/property-menu";
type KanbanColumnMenuProps = {
property: IBaseProperty;
pageId: string;
onHide: () => void;
};
export function KanbanColumnMenu({ property, pageId, onHide }: KanbanColumnMenuProps) {
const { t } = useTranslation();
const [opened, setOpened] = useState(false);
const [view, setView] = useState<"menu" | "property">("menu");
const [dirty, setDirty] = useAtom(propertyMenuDirtyAtomFamily(pageId)) as unknown as [boolean, (val: boolean) => void];
const [closeRequest, setCloseRequest] = useAtom(propertyMenuCloseRequestAtomFamily(pageId)) as unknown as [number, (val: number) => void];
const handleClose = useCallback(() => {
setOpened(false);
setView("menu");
}, []);
const wasOpenedRef = useRef(opened);
useEffect(() => {
if (wasOpenedRef.current && !opened) setDirty(false);
wasOpenedRef.current = opened;
}, [opened, setDirty]);
const handleOpenChange = useCallback(
(next: boolean) => {
if (next) return;
if (dirty) {
setCloseRequest(closeRequest + 1);
} else {
handleClose();
}
},
[dirty, closeRequest, setCloseRequest, handleClose],
);
const toggle = useCallback(() => {
if (opened) {
handleOpenChange(false);
} else if (!dirty) {
setOpened(true);
}
}, [opened, dirty, handleOpenChange]);
return (
<Popover
opened={opened}
onChange={handleOpenChange}
onClose={handleClose}
position="bottom-end"
shadow="md"
width={260}
trapFocus
returnFocus
withinPortal
closeOnClickOutside
closeOnEscape
>
<Popover.Target>
<ActionIcon
variant="subtle"
size="sm"
color="gray"
aria-label={t("Column options")}
onClick={toggle}
>
<IconDots size={14} />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown
p={0}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
{view === "menu" ? (
<Stack gap={0} p={4}>
<MenuItem
icon={<IconSettings size={14} />}
label={t("Edit property")}
onClick={() => setView("property")}
/>
<MenuItem
icon={<IconEyeOff size={14} />}
label={t("Hide group")}
onClick={() => {
handleClose();
onHide();
}}
/>
</Stack>
) : (
<PropertyMenuContent
property={property}
opened={opened}
onClose={handleClose}
onDirtyChange={setDirty}
pageId={pageId}
initialPanel={property.pendingType ? "main" : "options"}
/>
)}
</Popover.Dropdown>
</Popover>
);
}
@@ -1,113 +0,0 @@
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Group, Popover, Text, TextInput, UnstyledButton } from "@mantine/core";
import { IBaseProperty, KanbanColumn, SelectTypeOptions } from "@/ee/base/types/base.types";
import { useUpdatePropertyMutation } from "@/ee/base/queries/base-property-query";
import classes from "@/ee/base/styles/kanban.module.css";
type KanbanColumnTitleProps = {
column: KanbanColumn;
property: IBaseProperty | undefined;
pageId: string;
canEdit: boolean;
};
export function KanbanColumnTitle({ column, property, pageId, canEdit }: KanbanColumnTitleProps) {
const { t } = useTranslation();
const [opened, setOpened] = useState(false);
const [draft, setDraft] = useState("");
const updateProperty = useUpdatePropertyMutation();
const commit = useCallback(() => {
setOpened(false);
const name = draft.trim();
const options = property?.typeOptions as SelectTypeOptions | undefined;
if (!property || !options || !name || name === column.name) return;
if (!options.choices.some((c) => c.id === column.key)) return;
updateProperty.mutate({
propertyId: property.id,
pageId,
typeOptions: {
...options,
choices: options.choices.map((c) =>
c.id === column.key ? { ...c, name } : c,
),
},
});
}, [draft, property, column.name, column.key, pageId, updateProperty]);
const toggle = useCallback(() => {
if (opened) {
commit();
} else {
setDraft(column.name);
setOpened(true);
}
}, [opened, commit, column.name]);
const cancel = useCallback(() => setOpened(false), []);
if (!canEdit || column.isNoValue || !property) {
return (
<Text fw={600} size="sm" flex={1} truncate>
{column.isNoValue ? t("No value") : column.name}
</Text>
);
}
return (
<Popover
opened={opened}
onChange={(next) => {
if (!next) commit();
}}
position="bottom-start"
shadow="md"
width={240}
withinPortal
trapFocus
returnFocus
closeOnClickOutside
closeOnEscape={false}
>
<Popover.Target>
<UnstyledButton className={classes.columnTitleButton} onClick={toggle}>
<Text fw={600} size="sm" truncate flex={1} ta="left">
{column.name}
</Text>
</UnstyledButton>
</Popover.Target>
<Popover.Dropdown
p="xs"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
cancel();
}
}}
>
<Group gap="xs" wrap="nowrap">
<TextInput
size="xs"
flex={1}
value={draft}
data-autofocus
onFocus={(e) => e.currentTarget.select()}
onChange={(e) => setDraft(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
commit();
}
}}
/>
<Button size="xs" onClick={commit}>
{t("Done")}
</Button>
</Group>
</Popover.Dropdown>
</Popover>
);
}
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef } from "react"; import { useCallback, useEffect, useMemo, useRef } from "react";
import { generateJitteredKeyBetween } from "fractional-indexing-jittered"; import { generateJitteredKeyBetween } from "fractional-indexing-jittered";
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
import { type IBase, type IBaseProperty, type IBaseRow, type IBaseView, type FilterGroup, type KanbanColumn as KanbanColumnType, KANBAN_CARD_DRAG_TYPE } from "@/ee/base/types/base.types"; import { type IBase, type IBaseRow, type IBaseView, type FilterGroup, type KanbanColumn as KanbanColumnType, KANBAN_CARD_DRAG_TYPE } from "@/ee/base/types/base.types";
import { buildColumnFilter } from "@/ee/base/services/kanban-column-filter"; import { buildColumnFilter } from "@/ee/base/services/kanban-column-filter";
import { formatKanbanCount } from "@/ee/base/services/format-kanban-count"; import { formatKanbanCount } from "@/ee/base/services/format-kanban-count";
import { useKanbanColumnAutoScroll } from "@/ee/base/hooks/use-kanban-autoscroll"; import { useKanbanColumnAutoScroll } from "@/ee/base/hooks/use-kanban-autoscroll";
@@ -19,7 +19,6 @@ type KanbanColumnProps = {
column: KanbanColumnType; column: KanbanColumnType;
viewFilter: FilterGroup | undefined; viewFilter: FilterGroup | undefined;
groupByPropertyId: string; groupByPropertyId: string;
groupByProperty: IBaseProperty | undefined;
canEdit: boolean; canEdit: boolean;
onOpenRow: (rowId: string) => void; onOpenRow: (rowId: string) => void;
onHide: (columnKey: string) => void; onHide: (columnKey: string) => void;
@@ -34,7 +33,6 @@ export function KanbanColumn({
column, column,
viewFilter, viewFilter,
groupByPropertyId, groupByPropertyId,
groupByProperty,
canEdit, canEdit,
onOpenRow, onOpenRow,
onHide, onHide,
@@ -141,7 +139,6 @@ export function KanbanColumn({
<KanbanColumnHeader <KanbanColumnHeader
column={column} column={column}
pageId={pageId} pageId={pageId}
property={groupByProperty}
count={count} count={count}
canEdit={canEdit} canEdit={canEdit}
onHide={() => onHide(column.key)} onHide={() => onHide(column.key)}
@@ -53,7 +53,6 @@ type PropertyMenuContentProps = {
onDirtyChange?: (dirty: boolean) => void; onDirtyChange?: (dirty: boolean) => void;
onEditFormula?: () => void; onEditFormula?: () => void;
pageId: string; pageId: string;
initialPanel?: "main" | "options";
}; };
type MenuPanel = type MenuPanel =
@@ -97,10 +96,9 @@ export function PropertyMenuContent({
onDirtyChange, onDirtyChange,
onEditFormula, onEditFormula,
pageId, pageId,
initialPanel,
}: PropertyMenuContentProps) { }: PropertyMenuContentProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [panel, setPanel] = useState<MenuPanel>(initialPanel ?? "main"); const [panel, setPanel] = useState<MenuPanel>("main");
const [renameValue, setRenameValue] = useState(property.name); const [renameValue, setRenameValue] = useState(property.name);
const renameInputRef = useRef<HTMLInputElement>(null); const renameInputRef = useRef<HTMLInputElement>(null);
const [optionsDirty, setOptionsDirty] = useState(false); const [optionsDirty, setOptionsDirty] = useState(false);
@@ -119,12 +117,12 @@ export function PropertyMenuContent({
useEffect(() => { useEffect(() => {
if (opened) { if (opened) {
setPanel(initialPanel ?? "main"); setPanel("main");
setRenameValue(property.name); setRenameValue(property.name);
setOptionsDirty(false); setOptionsDirty(false);
setPendingTargetType(null); setPendingTargetType(null);
} }
}, [opened, property.name, initialPanel]); }, [opened, property.name]);
useEffect(() => { useEffect(() => {
if (panel === "rename") { if (panel === "rename") {
@@ -465,7 +463,7 @@ export function PropertyMenuContent({
PropertyMenuContent.displayName = "PropertyMenuContent"; PropertyMenuContent.displayName = "PropertyMenuContent";
export function MenuItem({ function MenuItem({
icon, icon,
label, label,
rightIcon, rightIcon,
@@ -9,7 +9,6 @@ import {
Text, Text,
UnstyledButton, UnstyledButton,
Button, Button,
MultiSelect,
} from "@mantine/core"; } from "@mantine/core";
import { IconPlus, IconTrash } from "@tabler/icons-react"; import { IconPlus, IconTrash } from "@tabler/icons-react";
import { import {
@@ -53,9 +52,6 @@ const NO_VALUE_OPERATORS: FilterOperator[] = ["isEmpty", "isNotEmpty"];
// stored value so a stale shape isn't sent to the engine. // stored value so a stale shape isn't sent to the engine.
function valueClass(op: FilterOperator, inputKind: string): string { function valueClass(op: FilterOperator, inputKind: string): string {
if (NO_VALUE_OPERATORS.includes(op)) return "none"; if (NO_VALUE_OPERATORS.includes(op)) return "none";
if (inputKind === "choices") {
return op === "any" || op === "none" ? "choicesMulti" : "choicesSingle";
}
if (inputKind === "person") { if (inputKind === "person") {
return op === "any" || op === "none" ? "personMulti" : "personSingle"; return op === "any" || op === "none" ? "personMulti" : "personSingle";
} }
@@ -74,10 +70,6 @@ function getOperatorsForType(type: string): FilterOperator[] {
DEFAULT_FILTER_OPERATORS) as FilterOperator[]; DEFAULT_FILTER_OPERATORS) as FilterOperator[];
} }
function isMultiChoice(op: FilterCondition["op"]): boolean {
return op === "any" || op === "none";
}
function FilterValueInput({ function FilterValueInput({
condition, condition,
property, property,
@@ -129,32 +121,6 @@ function FilterValueInput({
const typeOptions = property.typeOptions as SelectTypeOptions | undefined; const typeOptions = property.typeOptions as SelectTypeOptions | undefined;
const choices = typeOptions?.choices ?? []; const choices = typeOptions?.choices ?? [];
const choiceOptions = choices.map((c) => ({ value: c.id, label: c.name })); const choiceOptions = choices.map((c) => ({ value: c.id, label: c.name }));
if (isMultiChoice(condition.op)) {
const { value } = condition;
const selected = (
Array.isArray(value) ? value : value ? [value] : []
).filter((id) => choices.some((c) => c.id === id));
return (
<MultiSelect
size="xs"
data={choiceOptions}
comboboxProps={{ withinPortal: false }}
value={selected}
onChange={(values) => onChange(values)}
w={160}
styles={{
pillsList: {
maxHeight: 70,
overflowY: "auto",
},
}}
maxDropdownHeight={220}
/>
);
}
return ( return (
<Select <Select
size="xs" size="xs"
@@ -233,18 +199,11 @@ export function ViewFilterConfigPopover({
label: p.name, label: p.name,
})); }));
const [unSaved, setUnSaved] = useState(false)
const [draft, setDraft] = useState<FilterCondition | null>(null); const [draft, setDraft] = useState<FilterCondition | null>(null);
const [draftConditions, setDraftConditions] =
useState<FilterCondition[]>(conditions);
useEffect(() => { useEffect(() => {
if (opened) { if (!opened) setDraft(null);
setDraftConditions(conditions); }, [opened]);
setDraft(null);
setUnSaved(false)
}
}, [opened, conditions]);
const handleStartDraft = useCallback(() => { const handleStartDraft = useCallback(() => {
const firstProperty = properties[0]; const firstProperty = properties[0];
@@ -257,21 +216,14 @@ export function ViewFilterConfigPopover({
}, [properties]); }, [properties]);
const handleSaveDraft = useCallback(() => { const handleSaveDraft = useCallback(() => {
const nextConditions = draft if (!draft) return;
? [...draftConditions, draft] onChange([...conditions, draft]);
: draftConditions;
onChange(nextConditions);
setDraft(null); setDraft(null);
setUnSaved(false) }, [draft, conditions, onChange]);
}, [draft, draftConditions, onChange]);
const handleCancelDraft = useCallback(() => { const handleCancelDraft = useCallback(() => {
setDraftConditions(conditions)
setDraft(null); setDraft(null);
setUnSaved(false) }, []);
}, [conditions]);
const handleDraftPropertyChange = useCallback( const handleDraftPropertyChange = useCallback(
(propertyId: string | null) => { (propertyId: string | null) => {
@@ -320,19 +272,17 @@ export function ViewFilterConfigPopover({
const handleRemove = useCallback( const handleRemove = useCallback(
(index: number) => { (index: number) => {
setUnSaved(true); onChange(conditions.filter((_, i) => i !== index));
setDraftConditions((current) => current.filter((_, i) => i !== index));
}, },
[], [conditions, onChange],
); );
const handlePropertyChange = useCallback( const handlePropertyChange = useCallback(
(index: number, propertyId: string | null) => { (index: number, propertyId: string | null) => {
if (!propertyId) return; if (!propertyId) return;
const newProperty = properties.find((p) => p.id === propertyId); const newProperty = properties.find((p) => p.id === propertyId);
setUnSaved(true) onChange(
setDraftConditions((current) => conditions.map((f, i) => {
current.map((f, i) => {
if (i !== index) return f; if (i !== index) return f;
if (newProperty) { if (newProperty) {
const validOperators = getOperatorsForType(newProperty.type); const validOperators = getOperatorsForType(newProperty.type);
@@ -352,16 +302,15 @@ export function ViewFilterConfigPopover({
}), }),
); );
}, },
[properties], [conditions, properties, onChange],
); );
const handleOperatorChange = useCallback( const handleOperatorChange = useCallback(
(index: number, operator: string | null) => { (index: number, operator: string | null) => {
if (!operator) return; if (!operator) return;
const op = operator as FilterOperator; const op = operator as FilterOperator;
setUnSaved(true) onChange(
setDraftConditions((current) => conditions.map((f, i) => {
current.map((f, i) => {
if (i !== index) return f; if (i !== index) return f;
const kind = inputKindForProperty( const kind = inputKindForProperty(
properties.find((p) => p.id === f.propertyId), properties.find((p) => p.id === f.propertyId),
@@ -371,17 +320,16 @@ export function ViewFilterConfigPopover({
}), }),
); );
}, },
[properties], [conditions, properties, onChange],
); );
const handleValueChange = useCallback( const handleValueChange = useCallback(
(index: number, value: unknown) => { (index: number, value: unknown) => {
setUnSaved(true) onChange(
setDraftConditions((current) => conditions.map((f, i) => (i === index ? { ...f, value } : f)),
current.map((f, i) => (i === index ? { ...f, value } : f)),
); );
}, },
[], [conditions, onChange],
); );
return ( return (
@@ -414,13 +362,13 @@ export function ViewFilterConfigPopover({
{t("Filter by")} {t("Filter by")}
</Text> </Text>
{draftConditions.length === 0 && !draft && ( {conditions.length === 0 && !draft && (
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
{t("No filters applied")} {t("No filters applied")}
</Text> </Text>
)} )}
{draftConditions.map((condition, index) => { {conditions.map((condition, index) => {
const needsValue = !NO_VALUE_OPERATORS.includes(condition.op); const needsValue = !NO_VALUE_OPERATORS.includes(condition.op);
const property = properties.find( const property = properties.find(
(p) => p.id === condition.propertyId, (p) => p.id === condition.propertyId,
@@ -523,6 +471,14 @@ export function ViewFilterConfigPopover({
/> />
)} )}
</Group> </Group>
<Group justify="flex-end" gap="xs">
<Button variant="default" size="xs" onClick={handleCancelDraft}>
{t("Cancel")}
</Button>
<Button size="xs" onClick={handleSaveDraft}>
{t("Save")}
</Button>
</Group>
</Stack> </Stack>
); );
})()} })()}
@@ -536,20 +492,6 @@ export function ViewFilterConfigPopover({
{t("Add filter")} {t("Add filter")}
</UnstyledButton> </UnstyledButton>
)} )}
<Group justify="flex-end" gap="xs">
<Button
variant="default"
size="xs"
onClick={handleCancelDraft}
disabled={!draft && !unSaved}
>
{t("Cancel")}
</Button>
<Button size="xs" onClick={handleSaveDraft} disabled={!draft && !unSaved}>
{t("Save")}
</Button>
</Group>
</Stack> </Stack>
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>
@@ -1,5 +1,5 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { IBase, IBaseProperty, IBaseView, KanbanColumn, NO_VALUE_CHOICE_ID, SelectTypeOptions } from "@/ee/base/types/base.types"; import { IBase, IBaseView, KanbanColumn, NO_VALUE_CHOICE_ID, SelectTypeOptions } from "@/ee/base/types/base.types";
export type KanbanGroup = KanbanColumn & { hidden: boolean }; export type KanbanGroup = KanbanColumn & { hidden: boolean };
@@ -8,7 +8,6 @@ export function useKanbanColumns(
view: IBaseView | undefined, view: IBaseView | undefined,
): { ): {
groupByPropertyId: string | undefined; groupByPropertyId: string | undefined;
groupByProperty: IBaseProperty | undefined;
columns: KanbanColumn[]; columns: KanbanColumn[];
allGroups: KanbanGroup[]; allGroups: KanbanGroup[];
hasValidGroupBy: boolean; hasValidGroupBy: boolean;
@@ -19,7 +18,7 @@ export function useKanbanColumns(
const groupable = prop && (prop.type === "select" || prop.type === "status"); const groupable = prop && (prop.type === "select" || prop.type === "status");
if (!groupable || !prop || !view) { if (!groupable || !prop || !view) {
return { groupByPropertyId, groupByProperty: undefined, columns: [], allGroups: [], hasValidGroupBy: false }; return { groupByPropertyId, columns: [], allGroups: [], hasValidGroupBy: false };
} }
const typeOptions = prop.typeOptions as SelectTypeOptions; const typeOptions = prop.typeOptions as SelectTypeOptions;
@@ -48,6 +47,6 @@ export function useKanbanColumns(
}); });
const columns: KanbanColumn[] = allGroups.filter((g) => !g.hidden); const columns: KanbanColumn[] = allGroups.filter((g) => !g.hidden);
return { groupByPropertyId, groupByProperty: prop, columns, allGroups, hasValidGroupBy: true }; return { groupByPropertyId, columns, allGroups, hasValidGroupBy: true };
}, [base, view]); }, [base, view]);
} }
@@ -76,21 +76,6 @@
cursor: grabbing; cursor: grabbing;
} }
.columnTitleButton {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
padding: 2px 6px;
margin: -2px -6px;
border-radius: 6px;
cursor: pointer;
}
.columnTitleButton:hover {
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
}
.cardList { .cardList {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+5 -2
View File
@@ -1,3 +1,5 @@
import { Helmet } from "react-helmet-async";
import { getAppName } from "@/lib/config.ts";
import SettingsTitle from "@/components/settings/settings-title.tsx"; import SettingsTitle from "@/components/settings/settings-title.tsx";
import BillingPlans from "@/ee/billing/components/billing-plans.tsx"; import BillingPlans from "@/ee/billing/components/billing-plans.tsx";
import BillingTrial from "@/ee/billing/components/billing-trial.tsx"; import BillingTrial from "@/ee/billing/components/billing-trial.tsx";
@@ -7,7 +9,6 @@ import React from "react";
import BillingDetails from "@/ee/billing/components/billing-details.tsx"; import BillingDetails from "@/ee/billing/components/billing-details.tsx";
import { useBillingQuery } from "@/ee/billing/queries/billing-query.ts"; import { useBillingQuery } from "@/ee/billing/queries/billing-query.ts";
import useUserRole from "@/hooks/use-user-role.tsx"; import useUserRole from "@/hooks/use-user-role.tsx";
import { DocumentTitle } from "@/components/ui/document-title.tsx";
export default function Billing() { export default function Billing() {
const { data: billing, isError: isBillingError } = useBillingQuery(); const { data: billing, isError: isBillingError } = useBillingQuery();
@@ -19,7 +20,9 @@ export default function Billing() {
return ( return (
<> <>
<DocumentTitle title="Billing" /> <Helmet>
<title>Billing - {getAppName()}</title>
</Helmet>
<SettingsTitle title="Billing" /> <SettingsTitle title="Billing" />
<BillingTrial /> <BillingTrial />
-3
View File
@@ -22,7 +22,4 @@ export const Feature = {
PERSONAL_SPACES: 'spaces:personal', PERSONAL_SPACES: 'spaces:personal',
DOCX_EXPORT: 'export:docx', DOCX_EXPORT: 'export:docx',
BASES: 'bases', BASES: 'bases',
OAUTH: 'oauth',
AI_CONTROLS: 'ai:controls',
MCP_CONTROLS: 'mcp:controls',
} as const; } as const;
+5 -2
View File
@@ -1,3 +1,5 @@
import { Helmet } from "react-helmet-async";
import { getAppName } from "@/lib/config.ts";
import SettingsTitle from "@/components/settings/settings-title.tsx"; import SettingsTitle from "@/components/settings/settings-title.tsx";
import React from "react"; import React from "react";
import useUserRole from "@/hooks/use-user-role.tsx"; import useUserRole from "@/hooks/use-user-role.tsx";
@@ -7,7 +9,6 @@ import InstallationDetails from "@/ee/licence/components/installation-details.ts
import OssDetails from "@/ee/licence/components/oss-details.tsx"; import OssDetails from "@/ee/licence/components/oss-details.tsx";
import { useAtom } from "jotai/index"; import { useAtom } from "jotai/index";
import { entitlementAtom } from "@/ee/entitlement/entitlement-atom"; import { entitlementAtom } from "@/ee/entitlement/entitlement-atom";
import { DocumentTitle } from "@/components/ui/document-title.tsx";
export default function License() { export default function License() {
const [entitlements] = useAtom(entitlementAtom); const [entitlements] = useAtom(entitlementAtom);
@@ -20,7 +21,9 @@ export default function License() {
return ( return (
<> <>
<DocumentTitle title="License" /> <Helmet>
<title>License - {getAppName()}</title>
</Helmet>
<SettingsTitle title="License" /> <SettingsTitle title="License" />
<ActivateLicenseForm /> <ActivateLicenseForm />
@@ -47,6 +47,7 @@ interface MfaSetupModalProps {
} }
interface SetupData { interface SetupData {
secret: string;
qrCode: string; qrCode: string;
manualKey: string; manualKey: string;
} }
@@ -100,6 +101,7 @@ export function MfaSetupModal({
const enableMutation = useMutation({ const enableMutation = useMutation({
mutationFn: (verificationCode: string) => mutationFn: (verificationCode: string) =>
enableMfa({ enableMfa({
secret: setupData!.secret,
verificationCode, verificationCode,
}), }),
onSuccess: (data) => { onSuccess: (data) => {
@@ -31,10 +31,12 @@ export interface MfaSetupRequest {
export interface MfaSetupResponse { export interface MfaSetupResponse {
method: string; method: string;
qrCode: string; qrCode: string;
secret: string;
manualKey: string; manualKey: string;
} }
export interface MfaEnableRequest { export interface MfaEnableRequest {
secret: string;
verificationCode: string; verificationCode: string;
} }
@@ -1,19 +0,0 @@
import { Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { OAuthGrantsTable } from "@/ee/oauth/components/oauth-grants-table";
import { useOAuthGrantsQuery } from "@/ee/oauth/queries/oauth-query";
export function AuthorizedAppsPanel() {
const { t } = useTranslation();
const { data, isLoading } = useOAuthGrantsQuery();
return (
<>
<Text size="sm" c="dimmed" mb="md">
{t("Applications and AI assistants you have authorized to access your account.")}
</Text>
<OAuthGrantsTable grants={data || []} isLoading={isLoading} />
</>
);
}
@@ -1,178 +0,0 @@
import { ActionIcon, Badge, Group, Skeleton, Table, Text, Tooltip } from "@mantine/core";
import { IconTrash } from "@tabler/icons-react";
import { modals } from "@mantine/modals";
import { useTranslation } from "react-i18next";
import NoTableResults from "@/components/common/no-table-results";
import { formatLocalized, useDateFnsLocale } from "@/lib/date-locale.ts";
import { IOAuthGrant } from "@/ee/oauth/types/oauth.types";
import { useRevokeOAuthGrantMutation } from "@/ee/oauth/queries/oauth-query";
// Callback hosts identify the app; the full URL belongs on the consent screen.
function callbackHosts(redirectUris: string[] = []): string[] {
const hosts = redirectUris.map((uri) => {
try {
const url = new URL(uri);
return url.protocol === "http:" || url.protocol === "https:"
? url.host
: url.protocol;
} catch {
return uri;
}
});
return Array.from(new Set(hosts));
}
type OAuthGrantsTableProps = {
grants: IOAuthGrant[];
isLoading?: boolean;
};
function TableSkeleton() {
return (
<>
{Array.from({ length: 3 }).map((_, i) => (
<Table.Tr key={i}>
<Table.Td>
<Skeleton height={14} width={140} />
</Table.Td>
<Table.Td>
<Group gap={4}>
<Skeleton height={20} width={50} />
<Skeleton height={20} width={55} />
</Group>
</Table.Td>
<Table.Td>
<Skeleton height={14} width={90} />
</Table.Td>
<Table.Td>
<Skeleton height={14} width={90} />
</Table.Td>
<Table.Td>
<Skeleton height={28} width={28} circle />
</Table.Td>
</Table.Tr>
))}
</>
);
}
export function OAuthGrantsTable({ grants, isLoading }: OAuthGrantsTableProps) {
const { t } = useTranslation();
const locale = useDateFnsLocale();
const revokeMutation = useRevokeOAuthGrantMutation();
const scopeMeta: Record<string, { color: string; label: string }> = {
read: { color: "blue", label: t("Read") },
write: { color: "orange", label: t("Write") },
};
const formatDate = (date: string | null) => {
if (!date) return t("Never");
return formatLocalized(date, "MMM dd, yyyy", "PP", locale);
};
const openRevokeModal = (grant: IOAuthGrant) =>
modals.openConfirmModal({
title: t("Revoke access"),
centered: true,
children: (
<Text size="sm">
{t(
"Are you sure you want to revoke access for {{name}}? The application will no longer be able to access your account.",
{ name: grant.clientName },
)}
</Text>
),
labels: { confirm: t("Revoke access"), cancel: t("Cancel") },
confirmProps: { color: "red" },
onConfirm: () => revokeMutation.mutate(grant.id),
});
return (
<Table.ScrollContainer minWidth={600}>
<Table highlightOnHover verticalSpacing="sm">
<Table.Thead>
<Table.Tr>
<Table.Th>{t("Application")}</Table.Th>
<Table.Th>{t("Permissions")}</Table.Th>
<Table.Th>{t("Authorized")}</Table.Th>
<Table.Th>{t("Last used")}</Table.Th>
<Table.Th aria-label={t("Action")} />
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{isLoading ? (
<TableSkeleton />
) : grants.length > 0 ? (
grants.map((grant) => (
<Table.Tr key={grant.id}>
<Table.Td>
<Text fz="sm" fw={500}>
{grant.clientName}
</Text>
<Text
fz="xs"
c="dimmed"
title={grant.redirectUris?.join("\n")}
style={{ overflowWrap: "anywhere" }}
>
{callbackHosts(grant.redirectUris).join(", ")}
</Text>
</Table.Td>
<Table.Td>
<Group gap={4}>
{grant.scopes.map((scope) => {
const meta = scopeMeta[scope];
if (!meta) return null;
return (
<Badge
key={scope}
variant="light"
color={meta.color}
size="sm"
>
{meta.label}
</Badge>
);
})}
</Group>
</Table.Td>
<Table.Td>
<Text fz="sm" style={{ whiteSpace: "nowrap" }}>
{formatDate(grant.createdAt)}
</Text>
</Table.Td>
<Table.Td>
<Text fz="sm" style={{ whiteSpace: "nowrap" }}>
{formatDate(grant.lastUsedAt)}
</Text>
</Table.Td>
<Table.Td>
<Tooltip label={t("Revoke access")}>
<ActionIcon
variant="subtle"
color="red"
aria-label={t("Revoke access for {{name}}", {
name: grant.clientName,
})}
onClick={() => openRevokeModal(grant)}
>
<IconTrash size={16} />
</ActionIcon>
</Tooltip>
</Table.Td>
</Table.Tr>
))
) : (
<NoTableResults colSpan={5} text={t("No authorized apps yet.")} />
)}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
);
}
@@ -1,333 +0,0 @@
import React, { useEffect, useMemo, useState } from "react";
import {
Alert,
Anchor,
Box,
Button,
Center,
Checkbox,
Container,
Divider,
Group,
Loader,
Paper,
Stack,
Text,
ThemeIcon,
Title,
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import {
IconAlertTriangle,
IconEye,
IconPencil,
} from "@tabler/icons-react";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { RESET } from "jotai/utils";
import { useTranslation } from "react-i18next";
import { useLocation, useSearchParams } from "react-router-dom";
import { AuthLayout } from "@/features/auth/components/auth-layout.tsx";
import classes from "@/features/auth/components/auth.module.css";
import { DocumentTitle } from "@/components/ui/document-title.tsx";
import { UserInfo } from "@/components/common/user-info.tsx";
import useCurrentUser from "@/features/user/hooks/use-current-user";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
import { logout } from "@/features/auth/services/auth-service";
import { ICurrentUser } from "@/features/user/types/user.types";
import APP_ROUTE from "@/lib/app-route.ts";
import {
approveOAuthAuthorization,
getOAuthAuthorizeInfo,
} from "@/ee/oauth/services/oauth-service";
import {
IApproveAuthorizationPayload,
IAuthorizeParams,
IOAuthAuthorizeInfo,
} from "@/ee/oauth/types/oauth.types";
function loginRedirectUrl(pathname: string, search: string): string {
return `${APP_ROUTE.AUTH.LOGIN}?redirect=${encodeURIComponent(pathname + search)}`;
}
function errorStatus(error: any): number | undefined {
return error?.response?.status;
}
function errorText(error: any): string | undefined {
const data = error?.response?.data;
if (typeof data?.error_description === "string") return data.error_description;
if (Array.isArray(data?.message)) return data.message.join(", ");
return data?.message;
}
const OAUTH_PARAM_NAMES = [
"response_type",
"client_id",
"redirect_uri",
"state",
"code_challenge",
"code_challenge_method",
"scope",
"resource",
];
function pickOAuthParams(params: IAuthorizeParams): IAuthorizeParams {
const picked: IAuthorizeParams = {};
for (const name of OAUTH_PARAM_NAMES) {
if (params[name] !== undefined) {
picked[name] = params[name];
}
}
return picked;
}
export default function OAuthConsent() {
const { t } = useTranslation();
const location = useLocation();
const [searchParams] = useSearchParams();
const params = useMemo<IAuthorizeParams>(
() => Object.fromEntries(searchParams.entries()),
[searchParams],
);
const currentUserQuery = useCurrentUser();
const infoQuery = useQuery({
queryKey: ["oauth-authorize-info", params],
queryFn: () => getOAuthAuthorizeInfo(params),
});
const isUnauthenticated =
errorStatus(currentUserQuery.error) === 401 ||
errorStatus(infoQuery.error) === 401;
useEffect(() => {
if (isUnauthenticated) {
window.location.replace(
loginRedirectUrl(location.pathname, location.search),
);
}
}, [isUnauthenticated, location.pathname, location.search]);
const isLoading =
isUnauthenticated || currentUserQuery.isLoading || infoQuery.isLoading;
return (
<AuthLayout>
<DocumentTitle title={t("Authorize application")} />
<Container size={460} className={classes.container}>
<Box p="xl">
{isLoading ? (
<Center mih={200}>
<Loader />
</Center>
) : infoQuery.data && currentUserQuery.data ? (
<ConsentCard
info={infoQuery.data}
currentUser={currentUserQuery.data}
params={params}
/>
) : (
<InvalidRequestCard
description={errorText(infoQuery.error ?? currentUserQuery.error)}
/>
)}
</Box>
</Container>
</AuthLayout>
);
}
function InvalidRequestCard({ description }: { description?: string }) {
const { t } = useTranslation();
return (
<Stack align="center" gap="sm">
<ThemeIcon size={48} radius="xl" variant="light" color="red">
<IconAlertTriangle size={26} stroke={1.5} />
</ThemeIcon>
<Title order={3} ta="center" fw={600}>
{t("Invalid authorization request")}
</Title>
{description && (
<Text size="sm" c="dimmed" ta="center">
{description}
</Text>
)}
</Stack>
);
}
type ConsentCardProps = {
info: IOAuthAuthorizeInfo;
currentUser: ICurrentUser;
params: IAuthorizeParams;
};
function ConsentCard({ info, currentUser, params }: ConsentCardProps) {
const { t } = useTranslation();
const location = useLocation();
const [, setCurrentUser] = useAtom(currentUserAtom);
const [approvedScopes, setApprovedScopes] = useState<string[]>(
info.scopes.filter((scope) => scope === "read" || scope === "write"),
);
const [submitting, setSubmitting] = useState<"approve" | "deny" | null>(null);
const scopeRows = [
{
scope: "read",
icon: <IconEye size={16} stroke={1.5} />,
label: t("Read"),
description: t("View data in your workspace without making changes."),
},
{
scope: "write",
icon: <IconPencil size={16} stroke={1.5} />,
label: t("Write"),
description: t("Create and modify data in your workspace."),
},
];
function toggleScope(scope: string, checked: boolean) {
setApprovedScopes((prev) =>
checked ? [...prev, scope] : prev.filter((item) => item !== scope),
);
}
async function submitDecision(approved: boolean) {
setSubmitting(approved ? "approve" : "deny");
const oauthParams = pickOAuthParams(params);
const payload: IApproveAuthorizationPayload = approved
? { ...oauthParams, approved: true, approvedScopes }
: { ...oauthParams, approved: false };
try {
const res = await approveOAuthAuthorization(payload);
window.location.replace(res.redirectUrl);
} catch (err) {
if (errorStatus(err) === 401) {
window.location.replace(
loginRedirectUrl(location.pathname, location.search),
);
return;
}
setSubmitting(null);
notifications.show({
message: errorText(err) || t("Something went wrong. Please try again."),
color: "red",
});
}
}
async function switchAccount() {
if (submitting !== null) {
return;
}
setCurrentUser(RESET);
try {
await logout();
} finally {
window.location.replace(
loginRedirectUrl(location.pathname, location.search),
);
}
}
return (
<Stack gap="lg">
<Title order={3} ta="center" fw={600}>
{t("{{name}} wants to access {{workspace}}", {
name: info.clientName,
workspace: currentUser.workspace.name,
})}
</Title>
<Paper withBorder radius="md" p="sm">
<Group justify="space-between" wrap="nowrap">
<UserInfo user={currentUser.user} />
<Anchor
component="button"
type="button"
size="xs"
c="dimmed"
disabled={submitting !== null}
onClick={switchAccount}
style={{ whiteSpace: "nowrap" }}
>
{t("Not you? Switch account")}
</Anchor>
</Group>
</Paper>
<Divider />
<Stack gap="sm">
<Text size="sm" fw={500}>
{t("This application will be able to:")}
</Text>
{scopeRows.map((row) => {
const requested = info.scopes.includes(row.scope);
return (
<Checkbox
key={row.scope}
size="sm"
checked={approvedScopes.includes(row.scope)}
disabled={!requested || submitting !== null}
onChange={(event) =>
toggleScope(row.scope, event.currentTarget.checked)
}
label={
<Group gap={6} wrap="nowrap">
{row.icon}
<Text size="sm" fw={500}>
{row.label}
</Text>
</Group>
}
description={row.description}
/>
);
})}
</Stack>
<div>
<Text size="xs" c="dimmed">
{t("You will be redirected to")}
</Text>
<Text size="xs" ff="monospace" style={{ overflowWrap: "anywhere" }}>
{info.redirectUri}
</Text>
</div>
{!info.verified && (
<Alert
variant="light"
color="yellow"
py="xs"
icon={<IconAlertTriangle size={16} />}
>
{t("Make sure you trust this application before authorizing it.")}
</Alert>
)}
<Group grow>
<Button
variant="default"
onClick={() => submitDecision(false)}
loading={submitting === "deny"}
disabled={submitting === "approve"}
>
{t("Cancel")}
</Button>
<Button
onClick={() => submitDecision(true)}
loading={submitting === "approve"}
disabled={approvedScopes.length === 0 || submitting === "deny"}
>
{t("Authorize")}
</Button>
</Group>
</Stack>
);
}
@@ -1,45 +0,0 @@
import {
useMutation,
useQuery,
useQueryClient,
UseQueryResult,
} from "@tanstack/react-query";
import {
getOAuthGrants,
revokeOAuthGrant,
} from "@/ee/oauth/services/oauth-service";
import { IOAuthGrant } from "@/ee/oauth/types/oauth.types";
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
export function useOAuthGrantsQuery(): UseQueryResult<IOAuthGrant[], Error> {
return useQuery({
queryKey: ["oauth-grants"],
queryFn: () => getOAuthGrants(),
staleTime: 0,
gcTime: 0,
});
}
export function useRevokeOAuthGrantMutation() {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation<void, Error, string>({
mutationFn: (grantId) => revokeOAuthGrant(grantId),
onSuccess: () => {
notifications.show({ message: t("Access revoked") });
queryClient.invalidateQueries({
predicate: (item) =>
["oauth-grants"].includes(item.queryKey[0] as string),
});
},
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
notifications.show({
message: errorMessage || t("Something went wrong. Please try again."),
color: "red",
});
},
});
}
@@ -1,36 +0,0 @@
import api from "@/lib/api-client";
import {
IApproveAuthorizationPayload,
IAuthorizeParams,
IOAuthAuthorizeInfo,
IOAuthGrant,
} from "@/ee/oauth/types/oauth.types";
export async function getOAuthAuthorizeInfo(
params: IAuthorizeParams,
): Promise<IOAuthAuthorizeInfo> {
const req = await api.post<IOAuthAuthorizeInfo>(
"/oauth/authorize-info",
params,
);
return req.data;
}
export async function approveOAuthAuthorization(
payload: IApproveAuthorizationPayload,
): Promise<{ redirectUrl: string }> {
const req = await api.post<{ redirectUrl: string }>(
"/oauth/authorize",
payload,
);
return req.data;
}
export async function getOAuthGrants(): Promise<IOAuthGrant[]> {
const req = await api.post<IOAuthGrant[]>("/oauth/grants", {});
return req.data;
}
export async function revokeOAuthGrant(grantId: string): Promise<void> {
await api.post("/oauth/grants/revoke", { grantId });
}
@@ -1,24 +0,0 @@
export interface IOAuthAuthorizeInfo {
clientName: string;
redirectUri: string;
scopes: string[];
clientCreatedAt: string;
verified: boolean;
}
export interface IOAuthGrant {
id: string;
clientName: string;
redirectUris: string[];
scopes: string[];
createdAt: string;
lastUsedAt: string | null;
}
export type IAuthorizeParams = Record<string, string>;
export type IApproveAuthorizationPayload = {
[param: string]: unknown;
approved: boolean;
approvedScopes?: string[];
};
@@ -1,16 +1,17 @@
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import { Group, MultiSelect, Select, Space, TextInput } from "@mantine/core"; import { Group, MultiSelect, Select, Space, TextInput } from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { IconSearch } from "@tabler/icons-react"; import { IconSearch } from "@tabler/icons-react";
import SettingsTitle from "@/components/settings/settings-title"; import SettingsTitle from "@/components/settings/settings-title";
import { getAppName } from "@/lib/config";
import Paginate from "@/components/common/paginate"; import Paginate from "@/components/common/paginate";
import { useCursorPaginate } from "@/hooks/use-cursor-paginate"; import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
import { useVerificationListQuery } from "@/ee/page-verification/queries/page-verification-query"; import { useVerificationListQuery } from "@/ee/page-verification/queries/page-verification-query";
import { IVerificationListParams } from "@/ee/page-verification/types/page-verification.types"; import { IVerificationListParams } from "@/ee/page-verification/types/page-verification.types";
import VerificationListTable from "@/ee/page-verification/components/verification-list-table"; import VerificationListTable from "@/ee/page-verification/components/verification-list-table";
import { useGetSpacesQuery } from "@/features/space/queries/space-query"; import { useGetSpacesQuery } from "@/features/space/queries/space-query";
import { DocumentTitle } from "@/components/ui/document-title.tsx";
export default function VerifiedPages() { export default function VerifiedPages() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -67,7 +68,11 @@ export default function VerifiedPages() {
return ( return (
<> <>
<DocumentTitle title={t("Verified pages")} /> <Helmet>
<title>
{t("Verified pages")} - {getAppName()}
</title>
</Helmet>
<SettingsTitle title={t("Verified pages")} /> <SettingsTitle title={t("Verified pages")} />
+7 -2
View File
@@ -1,13 +1,18 @@
import { Helmet } from "react-helmet-async";
import { getAppName } from "@/lib/config.ts";
import { CloudLoginForm } from "@/ee/components/cloud-login-form.tsx"; import { CloudLoginForm } from "@/ee/components/cloud-login-form.tsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DocumentTitle } from "@/components/ui/document-title.tsx";
export default function CloudLogin() { export default function CloudLogin() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <>
<DocumentTitle title={t("Login")} /> <Helmet>
<title>
{t("Login")} - {getAppName()}
</title>
</Helmet>
<CloudLoginForm /> <CloudLoginForm />
</> </>
@@ -1,11 +1,14 @@
import { SetupWorkspaceForm } from "@/features/auth/components/setup-workspace-form.tsx"; import { SetupWorkspaceForm } from "@/features/auth/components/setup-workspace-form.tsx";
import { Helmet } from "react-helmet-async";
import React from "react"; import React from "react";
import { DocumentTitle } from "@/components/ui/document-title.tsx"; import { getAppName } from "@/lib/config.ts";
export default function CreateWorkspace() { export default function CreateWorkspace() {
return ( return (
<> <>
<DocumentTitle title="Create Workspace" /> <Helmet>
<title>Create Workspace - {getAppName()}</title>
</Helmet>
<SetupWorkspaceForm /> <SetupWorkspaceForm />
</> </>
); );
@@ -1,4 +1,5 @@
import { isCloud } from "@/lib/config.ts"; import { Helmet } from "react-helmet-async";
import { getAppName, isCloud } from "@/lib/config.ts";
import SettingsTitle from "@/components/settings/settings-title.tsx"; import SettingsTitle from "@/components/settings/settings-title.tsx";
import { import {
Alert, Alert,
@@ -36,7 +37,6 @@ import EnableScim from "@/ee/scim/components/enable-scim";
import { useCursorPaginate } from "@/hooks/use-cursor-paginate"; import { useCursorPaginate } from "@/hooks/use-cursor-paginate";
import Paginate from "@/components/common/paginate"; import Paginate from "@/components/common/paginate";
import { IScimToken } from "@/ee/scim/types/scim-token.types"; import { IScimToken } from "@/ee/scim/types/scim-token.types";
import { DocumentTitle } from "@/components/ui/document-title.tsx";
const SCIM_TOKEN_LIMIT = 5; const SCIM_TOKEN_LIMIT = 5;
@@ -64,7 +64,9 @@ export default function Security() {
return ( return (
<> <>
<DocumentTitle title="Security" /> <Helmet>
<title>Security - {getAppName()}</title>
</Helmet>
<SettingsTitle title={t("Security")} /> <SettingsTitle title={t("Security")} />
<EnforceMfa /> <EnforceMfa />
@@ -41,7 +41,6 @@ export default function ReadonlyTemplateEditor({
<EditorProvider <EditorProvider
editable={false} editable={false}
immediatelyRender={true} immediatelyRender={true}
textDirection="auto"
extensions={extensions} extensions={extensions}
content={template.content} content={template.content}
/> />
@@ -22,6 +22,8 @@ import { useTranslation } from "react-i18next";
import { useDisclosure, useWindowEvent } from "@mantine/hooks"; import { useDisclosure, useWindowEvent } from "@mantine/hooks";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { Link, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
import { Helmet } from "react-helmet-async";
import { getAppName } from "@/lib/config";
import { useEditor, EditorContent } from "@tiptap/react"; import { useEditor, EditorContent } from "@tiptap/react";
import { templateExtensions } from "@/features/editor/extensions/extensions"; import { templateExtensions } from "@/features/editor/extensions/extensions";
import { import {
@@ -36,13 +38,8 @@ import { FixedToolbar } from "@/features/editor/components/fixed-toolbar/fixed-t
import { EditorLinkMenu } from "@/features/editor/components/link/link-menu"; import { EditorLinkMenu } from "@/features/editor/components/link/link-menu";
import { EditorBubbleMenu } from "@/features/editor/components/bubble-menu/bubble-menu"; import { EditorBubbleMenu } from "@/features/editor/components/bubble-menu/bubble-menu";
import { EditorAiMenu } from "@/ee/ai/components/editor/ai-menu/ai-menu"; import { EditorAiMenu } from "@/ee/ai/components/editor/ai-menu/ai-menu";
import TableMenu from "@/features/editor/components/table/table-menu.tsx";
import { TableHandlesLayer } from "@/features/editor/components/table/handle/table-handles-layer";
import CalloutMenu from "@/features/editor/components/callout/callout-menu.tsx";
import ColumnsMenu from "@/features/editor/components/columns/columns-menu.tsx";
import classes from "./template-editor.module.css"; import classes from "./template-editor.module.css";
import { DocumentTitle } from "@/components/ui/document-title.tsx";
export default function TemplateEditor() { export default function TemplateEditor() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -87,10 +84,7 @@ export default function TemplateEditor() {
const editor = useEditor({ const editor = useEditor({
extensions: templateExtensions, extensions: templateExtensions,
content: "", content: "",
textDirection: "auto",
editorProps: { editorProps: {
scrollThreshold: 80,
scrollMargin: 80,
handleDOMEvents: { handleDOMEvents: {
keydown: (_view, event) => { keydown: (_view, event) => {
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) { if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
@@ -247,7 +241,11 @@ export default function TemplateEditor() {
return ( return (
<> <>
<DocumentTitle title={t("Edit template")} /> <Helmet>
<title>
{t("Edit template")} - {getAppName()}
</title>
</Helmet>
{editorToolbarEnabled && editor && ( {editorToolbarEnabled && editor && (
<FixedToolbar editor={editor} templateMode /> <FixedToolbar editor={editor} templateMode />
@@ -400,18 +398,9 @@ export default function TemplateEditor() {
<EditorAiMenu editor={editor} /> <EditorAiMenu editor={editor} />
<EditorBubbleMenu editor={editor} templateMode /> <EditorBubbleMenu editor={editor} templateMode />
<EditorLinkMenu editor={editor} /> <EditorLinkMenu editor={editor} />
<TableMenu editor={editor} />
<TableHandlesLayer editor={editor} />
<CalloutMenu editor={editor} />
<ColumnsMenu editor={editor} />
</> </>
)} )}
<div <div style={{ paddingBottom: "20vh" }} />
onClick={() => {
if (editor && !editor.isDestroyed) editor.commands.focus("end");
}}
style={{ paddingBottom: "20vh" }}
/>
</Container> </Container>
</> </>
); );
@@ -13,9 +13,11 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import { modals } from "@mantine/modals"; import { modals } from "@mantine/modals";
import { IconPlus } from "@tabler/icons-react"; import { IconPlus } from "@tabler/icons-react";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import { getAppName } from "@/lib/config";
import { import {
useGetTemplatesQuery, useGetTemplatesQuery,
useDeleteTemplateMutation, useDeleteTemplateMutation,
@@ -27,18 +29,11 @@ import UseTemplateModal from "@/ee/template/components/use-template-modal";
import TemplatePreviewModal from "@/ee/template/components/template-preview-modal"; import TemplatePreviewModal from "@/ee/template/components/template-preview-modal";
import useUserRole from "@/hooks/use-user-role"; import useUserRole from "@/hooks/use-user-role";
import CreateTemplateModal from "@/ee/template/components/create-template-modal"; import CreateTemplateModal from "@/ee/template/components/create-template-modal";
import { useAtomValue } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom";
import { DocumentTitle } from "@/components/ui/document-title.tsx";
export default function TemplateList() { export default function TemplateList() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { isAdmin: isWorkspaceAdmin } = useUserRole(); const { isAdmin: isWorkspaceAdmin } = useUserRole();
const workspace = useAtomValue(workspaceAtom);
const canCreateTemplate =
isWorkspaceAdmin ||
workspace?.settings?.templates?.allowMemberTemplates === true;
const [spaceFilter, setSpaceFilter] = useState<string | null>(null); const [spaceFilter, setSpaceFilter] = useState<string | null>(null);
const [selectedTemplate, setSelectedTemplate] = useState<ITemplate | null>( const [selectedTemplate, setSelectedTemplate] = useState<ITemplate | null>(
null, null,
@@ -101,12 +96,16 @@ export default function TemplateList() {
return ( return (
<> <>
<DocumentTitle title={t("Templates")} /> <Helmet>
<title>
{t("Templates")} - {getAppName()}
</title>
</Helmet>
<Container size="900" pt="xl"> <Container size="900" pt="xl">
<Group justify="space-between" mb="xl"> <Group justify="space-between" mb="xl">
<Title order={3}>{t("Templates")}</Title> <Title order={3}>{t("Templates")}</Title>
{canCreateTemplate && ( {isWorkspaceAdmin && (
<Button <Button
leftSection={<IconPlus size={16} />} leftSection={<IconPlus size={16} />}
onClick={openCreateModal} onClick={openCreateModal}
@@ -76,7 +76,7 @@ export function useCreateTemplateMutation() {
onError: (error) => { onError: (error) => {
const errorMessage = error["response"]?.data?.message; const errorMessage = error["response"]?.data?.message;
notifications.show({ notifications.show({
message: errorMessage ? t(errorMessage) : t("Failed to create template"), message: errorMessage || t("Failed to create template"),
color: "red", color: "red",
}); });
}, },
@@ -117,7 +117,7 @@ export function useUpdateTemplateMutation() {
onError: (error) => { onError: (error) => {
const errorMessage = error["response"]?.data?.message; const errorMessage = error["response"]?.data?.message;
notifications.show({ notifications.show({
message: errorMessage ? t(errorMessage) : t("Failed to update template"), message: errorMessage || t("Failed to update template"),
color: "red", color: "red",
}); });
}, },
@@ -1,59 +0,0 @@
import { ThemeIcon } from "@mantine/core";
import {
IconFile,
IconFileTypeCsv,
IconFileTypeDocx,
IconFileTypePdf,
IconFileTypePpt,
IconFileTypeXls,
IconFileZip,
IconMovie,
IconMusic,
IconPhoto,
type Icon,
} from "@tabler/icons-react";
const EXT_ICONS: Record<string, { icon: Icon; color: string }> = {
".pdf": { icon: IconFileTypePdf, color: "red" },
".doc": { icon: IconFileTypeDocx, color: "blue" },
".docx": { icon: IconFileTypeDocx, color: "blue" },
".xls": { icon: IconFileTypeXls, color: "teal" },
".xlsx": { icon: IconFileTypeXls, color: "teal" },
".csv": { icon: IconFileTypeCsv, color: "teal" },
".ppt": { icon: IconFileTypePpt, color: "orange" },
".pptx": { icon: IconFileTypePpt, color: "orange" },
".zip": { icon: IconFileZip, color: "gray" },
".rar": { icon: IconFileZip, color: "gray" },
".7z": { icon: IconFileZip, color: "gray" },
".tar": { icon: IconFileZip, color: "gray" },
".gz": { icon: IconFileZip, color: "gray" },
};
const MIME_ICONS: Array<{ prefix: string; icon: Icon; color: string }> = [
{ prefix: "image/", icon: IconPhoto, color: "grape" },
{ prefix: "video/", icon: IconMovie, color: "violet" },
{ prefix: "audio/", icon: IconMusic, color: "pink" },
];
interface AttachmentFileIconProps {
fileExt?: string;
mimeType?: string;
}
export function AttachmentFileIcon({
fileExt,
mimeType,
}: AttachmentFileIconProps) {
const byExt = fileExt ? EXT_ICONS[fileExt.toLowerCase()] : undefined;
const byMime = mimeType
? MIME_ICONS.find((entry) => mimeType.startsWith(entry.prefix))
: undefined;
const { icon: FileIcon, color } = byExt ??
byMime ?? { icon: IconFile, color: "gray" };
return (
<ThemeIcon variant="light" color={color} size={40} radius="md">
<FileIcon size={22} stroke={1.5} />
</ThemeIcon>
);
}
@@ -1,191 +0,0 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
ActionIcon,
Anchor,
Center,
Group,
Loader,
Modal,
ScrollArea,
Text,
Tooltip,
} from "@mantine/core";
import { IconDownload } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { SearchInput } from "@/components/common/search-input.tsx";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import { usePageAttachmentsQuery } from "@/features/attachments/queries/attachment-query.ts";
import { IPageAttachment } from "@/features/attachments/types/attachment.types.ts";
import { AttachmentFileIcon } from "@/features/attachments/components/attachment-file-icon.tsx";
import { formatBytes } from "@/lib";
import { getFileUrl } from "@/lib/config.ts";
import { formattedDate } from "@/lib/time.ts";
interface PageAttachmentsModalProps {
pageId: string;
open: boolean;
onClose: () => void;
}
export default function PageAttachmentsModal({
pageId,
open,
onClose,
}: PageAttachmentsModalProps) {
const { t } = useTranslation();
return (
<Modal
opened={open}
onClose={onClose}
title={t("Attachments")}
size={800}
closeButtonProps={{ "aria-label": t("Close") }}
>
<PageAttachmentsList pageId={pageId} />
</Modal>
);
}
function PageAttachmentsList({ pageId }: { pageId: string }) {
const { t } = useTranslation();
const [search, setSearch] = useState("");
const {
data,
isLoading,
isError,
isFetching,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = usePageAttachmentsQuery(pageId, search);
const attachments = useMemo(
() => data?.pages.flatMap((page) => page.items) ?? [],
[data],
);
const loadMoreRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const sentinel = loadMoreRef.current;
if (!sentinel || !hasNextPage) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !isFetching) {
fetchNextPage();
}
},
{ threshold: 0.1 },
);
observer.observe(sentinel);
return () => observer.disconnect();
}, [fetchNextPage, hasNextPage, isFetching]);
const handleSearch = useCallback((value: string) => setSearch(value), []);
return (
<>
<SearchInput
onSearch={handleSearch}
placeholder={t("Search attachments...")}
/>
{isLoading ? (
<Center py="xl">
<Loader size="sm" />
</Center>
) : isError ? (
<Center py="xl">
<Text size="sm" c="dimmed">
{t("Error loading attachments.")}
</Text>
</Center>
) : attachments.length === 0 ? (
<Center py="xl">
<Text size="sm" c="dimmed">
{search
? t("No results found")
: t("No attachments on this page yet.")}
</Text>
</Center>
) : (
<ScrollArea.Autosize mah={480} type="scroll" scrollbarSize={5}>
{attachments.map((attachment) => (
<AttachmentRow key={attachment.id} attachment={attachment} />
))}
{hasNextPage && <div ref={loadMoreRef} style={{ height: 1 }} />}
{isFetchingNextPage && (
<Center py="sm">
<Loader size="sm" />
</Center>
)}
</ScrollArea.Autosize>
)}
</>
);
}
function AttachmentRow({ attachment }: { attachment: IPageAttachment }) {
const { t } = useTranslation();
const fileUrl = getFileUrl(attachment.url);
return (
<Group wrap="nowrap" gap="md" py="sm" pr="xs">
<AttachmentFileIcon
fileExt={attachment.fileExt}
mimeType={attachment.mimeType}
/>
<div style={{ flex: 1, minWidth: 0 }}>
<Anchor
href={fileUrl}
target="_blank"
rel="noopener noreferrer"
size="sm"
fw={500}
c="inherit"
truncate="end"
style={{ display: "block" }}
>
{attachment.fileName}
</Anchor>
<Text size="xs" c="dimmed" mt={2} truncate="end">
{formatBytes(Number(attachment.fileSize))}
{" · "}
{formattedDate(new Date(attachment.createdAt))}
</Text>
</div>
{attachment.creator && (
<Tooltip
label={t("Uploaded by {{name}}", { name: attachment.creator.name })}
withArrow
>
<CustomAvatar
avatarUrl={attachment.creator.avatarUrl}
name={attachment.creator.name}
size="sm"
/>
</Tooltip>
)}
<Tooltip label={t("Download attachment")} withArrow>
<ActionIcon
component="a"
href={fileUrl}
download={attachment.fileName}
target="_blank"
rel="noopener noreferrer"
variant="subtle"
color="gray"
aria-label={t("Download {{name}}", { name: attachment.fileName })}
>
<IconDownload size={18} />
</ActionIcon>
</Tooltip>
</Group>
);
}
@@ -1,25 +0,0 @@
import {
InfiniteData,
keepPreviousData,
useInfiniteQuery,
UseInfiniteQueryResult,
} from "@tanstack/react-query";
import { getPageAttachments } from "@/features/attachments/services/attachment-service.ts";
import { IPageAttachment } from "@/features/attachments/types/attachment.types.ts";
import { IPagination } from "@/lib/types.ts";
export function usePageAttachmentsQuery(
pageId: string,
search?: string,
): UseInfiniteQueryResult<InfiniteData<IPagination<IPageAttachment>, unknown>> {
return useInfiniteQuery({
queryKey: ["page-attachments", pageId, search],
queryFn: ({ pageParam }) =>
getPageAttachments(pageId, { cursor: pageParam, query: search }),
enabled: !!pageId,
gcTime: 0,
placeholderData: keepPreviousData,
initialPageParam: undefined,
getNextPageParam: (lastPage) => lastPage.meta?.nextCursor ?? undefined,
});
}
@@ -3,17 +3,7 @@ import loadImage from "blueimp-load-image";
import { import {
AvatarIconType, AvatarIconType,
IAttachment, IAttachment,
IPageAttachment,
} from "@/features/attachments/types/attachment.types.ts"; } from "@/features/attachments/types/attachment.types.ts";
import { IPagination, QueryParams } from "@/lib/types.ts";
export async function getPageAttachments(
pageId: string,
params?: QueryParams,
): Promise<IPagination<IPageAttachment>> {
const req = await api.post("/pages/attachments", { pageId, ...params });
return req.data;
}
async function compressAndResizeIcon( async function compressAndResizeIcon(
file: File, file: File,
@@ -1,5 +1,4 @@
export { export {
getPageAttachments,
uploadIcon, uploadIcon,
uploadUserAvatar, uploadUserAvatar,
uploadSpaceIcon, uploadSpaceIcon,
@@ -15,15 +15,6 @@ export interface IAttachment {
deletedAt: string | null; deletedAt: string | null;
} }
export interface IPageAttachment extends IAttachment {
url: string;
creator: {
id: string;
name: string;
avatarUrl: string | null;
} | null;
}
export enum AvatarIconType { export enum AvatarIconType {
AVATAR = "avatar", AVATAR = "avatar",
SPACE_ICON = "space-icon", SPACE_ICON = "space-icon",
@@ -103,7 +103,6 @@ const CommentEditor = forwardRef(
}, },
content: defaultContent, content: defaultContent,
editable, editable,
textDirection: "auto",
immediatelyRender: true, immediatelyRender: true,
shouldRerenderOnTransaction: false, shouldRerenderOnTransaction: false,
autofocus: (autofocus && "end") || false, autofocus: (autofocus && "end") || false,
@@ -16,14 +16,6 @@ export const showAiMenuAtom = atom(false);
export const showLinkMenuAtom = atom(false); export const showLinkMenuAtom = atom(false);
export type LightboxRequest = {
src: string;
type: "image" | "video";
} | null;
const initialLightboxRequest: LightboxRequest = null;
export const lightboxRequestAtom = atom(initialLightboxRequest);
// Current page's edit mode — initialized from the user's saved preference on // Current page's edit mode — initialized from the user's saved preference on
// first load, can be toggled locally without persisting to the server. // first load, can be toggled locally without persisting to the server.
export const currentPageEditModeAtom = atom<PageEditMode>(PageEditMode.Edit); export const currentPageEditModeAtom = atom<PageEditMode>(PageEditMode.Edit);
@@ -1,46 +0,0 @@
import {
HocuspocusProviderWebsocket,
WebSocketStatus,
} from "@hocuspocus/provider";
import { getCollaborationUrl } from "@/lib/config.ts";
const RELEASE_GRACE_MS = 5000;
let socket: HocuspocusProviderWebsocket | null = null;
let editorCount = 0;
let releaseTimer: ReturnType<typeof setTimeout> | null = null;
export function getCollabSocket(): HocuspocusProviderWebsocket {
if (!socket) {
socket = new HocuspocusProviderWebsocket({
url: getCollaborationUrl(),
autoConnect: false,
});
}
return socket;
}
export function acquireCollabSocket(): void {
editorCount++;
if (releaseTimer) {
clearTimeout(releaseTimer);
releaseTimer = null;
}
const collabSocket = getCollabSocket();
collabSocket.shouldConnect = true;
if (collabSocket.status === WebSocketStatus.Disconnected) {
collabSocket.connect();
}
}
export function releaseCollabSocket(): void {
editorCount--;
if (editorCount > 0) return;
if (releaseTimer) clearTimeout(releaseTimer);
releaseTimer = setTimeout(() => {
releaseTimer = null;
if (editorCount === 0) {
socket?.disconnect();
}
}, RELEASE_GRACE_MS);
}
@@ -2,8 +2,6 @@ import type { Editor, Range } from "@tiptap/core";
import { v7 as uuid7 } from "uuid"; import { v7 as uuid7 } from "uuid";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import api from "@/lib/api-client"; import api from "@/lib/api-client";
import i18n from "@/i18n.ts";
import { getApiErrorMessage } from "@/lib/api-error";
function findBaseEmbedPlaceholderPos( function findBaseEmbedPlaceholderPos(
editor: Editor, editor: Editor,
@@ -52,7 +50,7 @@ export async function insertBaseEmbedBlock(
return true; return true;
}) })
.run(); .run();
} catch (err) { } catch {
const pos = findBaseEmbedPlaceholderPos(editor, pendingKey); const pos = findBaseEmbedPlaceholderPos(editor, pendingKey);
if (pos !== null) { if (pos !== null) {
editor editor
@@ -64,9 +62,6 @@ export async function insertBaseEmbedBlock(
}) })
.run(); .run();
} }
notifications.show({ notifications.show({ message: "Failed to create base", color: "red" });
message: getApiErrorMessage(err, i18n.t("Failed to create base")),
color: "red",
});
} }
} }
@@ -23,21 +23,11 @@ import {
} from "@/features/comment/atoms/comment-atom"; } from "@/features/comment/atoms/comment-atom";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import { v7 as uuid7 } from "uuid"; import { v7 as uuid7 } from "uuid";
import { import { isCellSelection, isEditorReady, isTextSelected } from "@docmost/editor-ext";
isCellSelection,
isEditorReady,
isTextSelected,
} from "@docmost/editor-ext";
import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx"; import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import { showAiMenuAtom, showLinkMenuAtom } from "@/features/editor/atoms/editor-atoms";
showAiMenuAtom, import { userAtom, workspaceAtom } from "@/features/user/atoms/current-user-atom";
showLinkMenuAtom,
} from "@/features/editor/atoms/editor-atoms";
import {
userAtom,
workspaceAtom,
} from "@/features/user/atoms/current-user-atom";
export interface BubbleMenuItem { export interface BubbleMenuItem {
name: string; name: string;
@@ -227,12 +217,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
<ActionIcon.Group> <ActionIcon.Group>
{items.map((item, index) => ( {items.map((item, index) => (
<Tooltip <Tooltip key={index} label={t(item.name)} withArrow>
key={index}
label={t(item.name)}
withArrow
withinPortal={false}
>
<ActionIcon <ActionIcon
key={index} key={index}
variant="default" variant="default"
@@ -241,9 +226,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
aria-label={t(item.name)} aria-label={t(item.name)}
className={clsx({ [classes.active]: item.isActive() })} className={clsx({ [classes.active]: item.isActive() })}
style={{ border: "none" }} style={{ border: "none" }}
onClick={() => onClick={() => isEditorReady(props.editor) && item.command()}
isEditorReady(props.editor) && item.command()
}
> >
<item.icon style={{ width: rem(16) }} stroke={2} /> <item.icon style={{ width: rem(16) }} stroke={2} />
</ActionIcon> </ActionIcon>
@@ -273,9 +256,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
radius="6px" radius="6px"
aria-label={t(commentItem.name)} aria-label={t(commentItem.name)}
style={{ border: "none" }} style={{ border: "none" }}
onClick={() => onClick={() => isEditorReady(props.editor) && commentItem.command()}
isEditorReady(props.editor) && commentItem.command()
}
> >
<IconMessage size={16} stroke={2} /> <IconMessage size={16} stroke={2} />
</ActionIcon> </ActionIcon>
@@ -129,7 +129,8 @@ function handleColorKeyNav(
grid: "text" | "highlight", grid: "text" | "highlight",
) { ) {
const cols = COLOR_GRID_COLS; const cols = COLOR_GRID_COLS;
const total = grid === "text" ? TEXT_COLORS.length : HIGHLIGHT_COLORS.length; const total =
grid === "text" ? TEXT_COLORS.length : HIGHLIGHT_COLORS.length;
const col = index % cols; const col = index % cols;
if (e.key === "ArrowRight") { if (e.key === "ArrowRight") {
@@ -162,7 +163,8 @@ function handleColorKeyNav(
if (prev >= 0) { if (prev >= 0) {
focusSwatch(grid, prev); focusSwatch(grid, prev);
} else if (grid === "highlight") { } else if (grid === "highlight") {
const lastRowStart = Math.floor((TEXT_COLORS.length - 1) / cols) * cols; const lastRowStart =
Math.floor((TEXT_COLORS.length - 1) / cols) * cols;
focusSwatch("text", Math.min(lastRowStart + col, TEXT_COLORS.length - 1)); focusSwatch("text", Math.min(lastRowStart + col, TEXT_COLORS.length - 1));
} }
return; return;
@@ -220,7 +222,7 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
withArrow withArrow
> >
<Popover.Target> <Popover.Target>
<Tooltip label={t("Text color")} withArrow withinPortal={false}> <Tooltip label={t("Text color")} withArrow>
<Button <Button
variant="default" variant="default"
radius="0" radius="0"
@@ -245,26 +247,26 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
<Popover.Dropdown onMouseDown={(e) => e.preventDefault()}> <Popover.Dropdown onMouseDown={(e) => e.preventDefault()}>
<Stack gap="md" p="2px"> <Stack gap="md" p="2px">
<Box> <Box>
<Text size="sm" fw={600} mb="xs"> <Text size="sm" fw={600} mb="xs">
{t("Text color")} {t("Text color")}
</Text> </Text>
<SimpleGrid cols={5} spacing="xs"> <SimpleGrid cols={5} spacing="xs">
{TEXT_COLORS.map(({ name, color }, index) => { {TEXT_COLORS.map(({ name, color }, index) => {
const applyTextColor = () => { const applyTextColor = () => {
if (!isEditorReady(editor)) return; if (!isEditorReady(editor)) return;
if (name === "Default") { if (name === "Default") {
editor.commands.unsetColor(); editor.commands.unsetColor();
} else { } else {
editor editor
.chain() .chain()
.focus() .focus()
.setColor(color || "") .setColor(color || "")
.run(); .run();
} }
setIsOpen(false); setIsOpen(false);
}; };
return ( return (
<Tooltip key={index} label={t(name)} withArrow> <Tooltip key={index} label={t(name)} withArrow>
<Box <Box
role="button" role="button"
@@ -304,34 +306,34 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
A A
</Box> </Box>
</Tooltip> </Tooltip>
); );
})} })}
</SimpleGrid> </SimpleGrid>
</Box> </Box>
<Box> <Box>
<Text size="sm" fw={600} mb="xs"> <Text size="sm" fw={600} mb="xs">
{t("Highlight color")} {t("Highlight color")}
</Text> </Text>
<SimpleGrid cols={5} spacing="xs"> <SimpleGrid cols={5} spacing="xs">
{HIGHLIGHT_COLORS.map(({ name, color }, index) => { {HIGHLIGHT_COLORS.map(({ name, color }, index) => {
const applyHighlight = () => { const applyHighlight = () => {
if (!isEditorReady(editor)) return; if (!isEditorReady(editor)) return;
if (name === "Default") { if (name === "Default") {
editor.commands.unsetHighlight(); editor.commands.unsetHighlight();
} else { } else {
editor editor
.chain() .chain()
.focus() .focus()
.toggleMark("highlight", { .toggleMark("highlight", {
color: color || "", color: color || "",
colorName: name.toLowerCase() || "", colorName: name.toLowerCase() || "",
}) })
.run(); .run();
} }
setIsOpen(false); setIsOpen(false);
}; };
return ( return (
<Tooltip key={index} label={t(name)} withArrow> <Tooltip key={index} label={t(name)} withArrow>
<Box <Box
role="button" role="button"
@@ -376,36 +378,37 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
)} )}
</Box> </Box>
</Tooltip> </Tooltip>
); );
})} })}
</SimpleGrid> </SimpleGrid>
</Box> </Box>
<Button <Button
variant="default" variant="default"
fullWidth fullWidth
data-color-grid="remove" data-color-grid="remove"
className={classes.removeColor} className={classes.removeColor}
onClick={() => { onClick={() => {
if (isEditorReady(editor)) { if (isEditorReady(editor)) {
editor.commands.unsetColor(); editor.commands.unsetColor();
editor.commands.unsetHighlight(); editor.commands.unsetHighlight();
} }
setIsOpen(false); setIsOpen(false);
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "ArrowUp") { if (e.key === "ArrowUp") {
e.preventDefault(); e.preventDefault();
const lastRowStart = const lastRowStart =
Math.floor((HIGHLIGHT_COLORS.length - 1) / COLOR_GRID_COLS) * Math.floor(
COLOR_GRID_COLS; (HIGHLIGHT_COLORS.length - 1) / COLOR_GRID_COLS,
focusSwatch("highlight", lastRowStart); ) * COLOR_GRID_COLS;
} focusSwatch("highlight", lastRowStart);
}} }
> }}
{t("Remove color")} >
</Button> {t("Remove color")}
</Stack> </Button>
</Stack>
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>
); );
@@ -10,7 +10,7 @@ export const LinkSelector: FC = () => {
const setShowLinkMenu = useSetAtom(showLinkMenuAtom); const setShowLinkMenu = useSetAtom(showLinkMenuAtom);
return ( return (
<Tooltip label={t("Add link")} withArrow withinPortal={false}> <Tooltip label={t("Add link")} withArrow>
<ActionIcon <ActionIcon
variant="default" variant="default"
size="lg" size="lg"
@@ -91,12 +91,7 @@ export const TextAlignmentSelector: FC<TextAlignmentProps> = ({
onChange={setIsOpen} onChange={setIsOpen}
> >
<Menu.Target> <Menu.Target>
<Tooltip <Tooltip label={t("Text align")} withArrow disabled={isOpen}>
label={t("Text align")}
withArrow
disabled={isOpen}
withinPortal={false}
>
<Button <Button
variant="default" variant="default"
style={{ border: "none", height: "34px" }} style={{ border: "none", height: "34px" }}
@@ -1,188 +0,0 @@
import type { Editor } from "@tiptap/react";
import type { Node as PMNode } from "@tiptap/pm/model";
import Lightbox, { type Slide } from "yet-another-react-lightbox";
import type { LightboxRequest } from "@/features/editor/atoms/editor-atoms";
import { getFileUrl } from "@/lib/config.ts";
import "yet-another-react-lightbox/styles.css";
import Download from "yet-another-react-lightbox/plugins/download";
import Fullscreen from "yet-another-react-lightbox/plugins/fullscreen";
import Video from "yet-another-react-lightbox/plugins/video";
import Zoom from "yet-another-react-lightbox/plugins/zoom";
import { useEffect, useMemo, useState } from "react";
import i18n from "@/i18n.ts";
import { useTranslation } from "react-i18next";
type LightboxViewProps = {
editor: Editor;
open: boolean;
src: string;
type: "image" | "video";
onClose: () => void;
};
function getVideoMimeType(src: string) {
const extension = src.split(/[?#]/, 1)[0].split(".").pop()?.toLowerCase();
switch (extension) {
case "webm":
return "video/webm";
case "ogv":
return "video/ogg";
case "mov":
return "video/quicktime";
case "m4v":
return "video/x-m4v";
default:
return "video/mp4";
}
}
function getFilename(src: string) {
const filename = src.split(/[?#]/, 1)[0].split("/").pop();
if (!filename) return i18n.t("Media");
try {
return decodeURIComponent(filename);
} catch {
return filename;
}
}
function getMedia(rawSrc: string, type?: string, alt?: string): Slide {
const src = getFileUrl(rawSrc);
const filename = getFilename(rawSrc);
if (type === "video") {
return {
type: "video",
sources: [{ src, type: getVideoMimeType(rawSrc) }],
download: { url: src, filename },
};
} else {
return {
type: "image",
src,
alt: alt || undefined,
download: { url: src, filename },
};
}
}
const LIGHTBOX_NODE_TYPES: Record<string, "image" | "video"> = {
image: "image",
video: "video",
drawio: "image",
excalidraw: "image",
};
// video is excluded: clicks there operate the native controls
const CLICK_TO_EXPAND_NODE_TYPES = new Set(["image", "drawio", "excalidraw"]);
export function getLightboxClickRequest(node: PMNode): LightboxRequest {
if (!CLICK_TO_EXPAND_NODE_TYPES.has(node.type.name)) return null;
const src = typeof node.attrs.src === "string" ? node.attrs.src : "";
if (!src) return null;
return { src: getFileUrl(src), type: "image" };
}
function getPageMedia(editor: Editor): Slide[] {
const media: Slide[] = [];
editor.state.doc.descendants((node) => {
const type = LIGHTBOX_NODE_TYPES[node.type.name];
if (!type) return;
const rawSrc = typeof node.attrs.src === "string" ? node.attrs.src : "";
if (!rawSrc) return;
media.push(getMedia(rawSrc, type, node.attrs.alt));
});
return media;
}
export default function LightboxView({
editor,
open,
src,
type,
onClose,
}: LightboxViewProps) {
const { i18n: i18nInstance } = useTranslation();
const selectedSlide = useMemo(
() => getMedia(src, type),
[src, type, i18nInstance.language]
);
const [pageSlides, setPageSlides] = useState<Slide[]>([]);
const [loadedMediaKey, setLoadedMediaKey] = useState<string | null>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
if (!open) setIsFullscreen(false);
}, [open]);
useEffect(() => {
if (!open) return;
setLoadedMediaKey(null);
const frame = requestAnimationFrame(() => {
const slides = getPageMedia(editor);
setPageSlides(slides);
setLoadedMediaKey(`${type}:${src}`);
});
return () => cancelAnimationFrame(frame);
}, [editor, open, type, src]);
const slides = loadedMediaKey === `${type}:${src}` ? pageSlides : [selectedSlide];
const index = useMemo(() => {
if (!(pageSlides.length > 0)) {
return 0;
}
const idx = slides.findIndex((slide) =>
type === "video"
? "sources" in slide && slide.sources.some((s) => s.src === src)
: "src" in slide && slide.src === src
);
return idx >= 0 ? idx : 0;
}, [slides, src, type]);
return (
<Lightbox
open={open}
close={onClose}
index={index}
slides={slides}
plugins={[Download, Fullscreen, Video, Zoom]}
styles={{
container: { backgroundColor: "rgba(0, 0, 0, 0.8)" },
icon: { width: 24, height: 24 },
toolbar: {
margin: 8,
borderRadius: 8,
backgroundColor: "rgba(0, 0, 0, 0.5)",
},
}}
controller={{ closeOnBackdropClick: !isFullscreen }}
on={{
enterFullscreen: () => setIsFullscreen(true),
exitFullscreen: () => setIsFullscreen(false),
}}
video={{ controls: true, playsInline: true }}
zoom={{
scrollToZoom: true,
maxZoomPixelRatio: 4,
maxZoom: 4,
supports: ["video"],
}}
/>
);
}
@@ -1,7 +1,6 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useSetAtom } from "jotai";
import { Node as PMNode } from "@tiptap/pm/model"; import { Node as PMNode } from "@tiptap/pm/model";
import { isEditorReady } from "@docmost/editor-ext"; import { isEditorReady } from "@docmost/editor-ext";
import { import {
@@ -25,7 +24,6 @@ import {
IconDownload, IconDownload,
IconEdit, IconEdit,
IconTrash, IconTrash,
IconZoomIn,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { getDrawioUrl, getFileUrl } from "@/lib/config.ts"; import { getDrawioUrl, getFileUrl } from "@/lib/config.ts";
@@ -41,12 +39,10 @@ import { decodeBase64ToSvgString, svgStringToFile } from "@/lib/utils";
import { IAttachment } from "@/features/attachments/types/attachment.types"; import { IAttachment } from "@/features/attachments/types/attachment.types";
import { modals } from "@mantine/modals"; import { modals } from "@mantine/modals";
import { useAltTextControl } from "@/features/editor/components/common/use-alt-text-control.tsx"; import { useAltTextControl } from "@/features/editor/components/common/use-alt-text-control.tsx";
import { lightboxRequestAtom } from "@/features/editor/atoms/editor-atoms";
import classes from "../common/toolbar-menu.module.css"; import classes from "../common/toolbar-menu.module.css";
export function DrawioMenu({ editor }: EditorMenuProps) { export function DrawioMenu({ editor }: EditorMenuProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const setLightboxRequest = useSetAtom(lightboxRequestAtom);
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const [initialXML, setInitialXML] = useState<string>(""); const [initialXML, setInitialXML] = useState<string>("");
const drawioRef = useRef<DrawIoEmbedRef>(null); const drawioRef = useRef<DrawIoEmbedRef>(null);
@@ -332,23 +328,6 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
<div className={classes.divider} /> <div className={classes.divider} />
<Tooltip position="top" label={t("Expand")} withinPortal={false}>
<ActionIcon
onClick={() =>
editorState?.src &&
setLightboxRequest({
src: getFileUrl(editorState.src),
type: "image",
})
}
size="lg"
aria-label={t("Expand")}
variant="subtle"
>
<IconZoomIn size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Edit")} withinPortal={false}> <Tooltip position="top" label={t("Edit")} withinPortal={false}>
<ActionIcon <ActionIcon
onClick={handleOpen} onClick={handleOpen}
@@ -1,7 +1,6 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react"; import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react";
import { useSetAtom } from "jotai";
import { Node as PMNode } from "@tiptap/pm/model"; import { Node as PMNode } from "@tiptap/pm/model";
import { isEditorReady } from "@docmost/editor-ext"; import { isEditorReady } from "@docmost/editor-ext";
import { import {
@@ -26,7 +25,6 @@ import {
IconDownload, IconDownload,
IconEdit, IconEdit,
IconTrash, IconTrash,
IconZoomIn,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { getFileUrl } from "@/lib/config.ts"; import { getFileUrl } from "@/lib/config.ts";
@@ -39,7 +37,6 @@ import ReactClearModal from "react-clear-modal";
import { useHandleLibrary } from "@excalidraw/excalidraw"; import { useHandleLibrary } from "@excalidraw/excalidraw";
import { localStorageLibraryAdapter } from "@/features/editor/components/excalidraw/excalidraw-utils.ts"; import { localStorageLibraryAdapter } from "@/features/editor/components/excalidraw/excalidraw-utils.ts";
import { useAltTextControl } from "@/features/editor/components/common/use-alt-text-control.tsx"; import { useAltTextControl } from "@/features/editor/components/common/use-alt-text-control.tsx";
import { lightboxRequestAtom } from "@/features/editor/atoms/editor-atoms";
import classes from "../common/toolbar-menu.module.css"; import classes from "../common/toolbar-menu.module.css";
const ExcalidrawComponent = lazy(() => const ExcalidrawComponent = lazy(() =>
@@ -50,7 +47,6 @@ const ExcalidrawComponent = lazy(() =>
export function ExcalidrawMenu({ editor }: EditorMenuProps) { export function ExcalidrawMenu({ editor }: EditorMenuProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const setLightboxRequest = useSetAtom(lightboxRequestAtom);
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const [excalidrawAPI, setExcalidrawAPI] = const [excalidrawAPI, setExcalidrawAPI] =
useState<ExcalidrawImperativeAPI>(null); useState<ExcalidrawImperativeAPI>(null);
@@ -363,23 +359,6 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
<div className={classes.divider} /> <div className={classes.divider} />
<Tooltip position="top" label={t("Expand")} withinPortal={false}>
<ActionIcon
onClick={() =>
editorState?.src &&
setLightboxRequest({
src: getFileUrl(editorState.src),
type: "image",
})
}
size="lg"
aria-label={t("Expand")}
variant="subtle"
>
<IconZoomIn size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Edit")} withinPortal={false}> <Tooltip position="top" label={t("Edit")} withinPortal={false}>
<ActionIcon <ActionIcon
onClick={handleOpen} onClick={handleOpen}
@@ -6,7 +6,6 @@
z-index: 99; z-index: 99;
display: flex; display: flex;
align-items: center; align-items: center;
min-height: 45px;
background: var(--mantine-color-body); background: var(--mantine-color-body);
border-bottom: 1px solid border-bottom: 1px solid
light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
@@ -31,6 +31,8 @@ export const FixedToolbar: FC<FixedToolbarProps> = ({
const workspace = useAtomValue(workspaceAtom); const workspace = useAtomValue(workspaceAtom);
const isGenerativeAiEnabled = workspace?.settings?.ai?.generative === true; const isGenerativeAiEnabled = workspace?.settings?.ai?.generative === true;
if (!editor || !state) return null;
return ( return (
<> <>
<div <div
@@ -47,26 +49,22 @@ export const FixedToolbar: FC<FixedToolbarProps> = ({
<div className={classes.divider} /> <div className={classes.divider} />
</> </>
)} */} )} */}
{editor && state && ( <BlockTypeGroup editor={editor} />
<> <div className={classes.divider} />
<BlockTypeGroup editor={editor} /> <InlineMarksGroup editor={editor} state={state} />
<div className={classes.divider} /> <div className={classes.divider} />
<InlineMarksGroup editor={editor} state={state} /> <ColorGroup editor={editor} />
<div className={classes.divider} /> <div className={classes.divider} />
<ColorGroup editor={editor} /> <ListsGroup editor={editor} state={state} />
<div className={classes.divider} /> <div className={classes.divider} />
<ListsGroup editor={editor} state={state} /> <AlignmentGroup editor={editor} />
<div className={classes.divider} /> <div className={classes.divider} />
<AlignmentGroup editor={editor} /> <MediaGroup editor={editor} templateMode={templateMode} />
<div className={classes.divider} /> <div className={classes.divider} />
<MediaGroup editor={editor} templateMode={templateMode} /> <QuickInsertsGroup editor={editor} />
<div className={classes.divider} /> <MoreInsertsGroup editor={editor} templateMode={templateMode} />
<QuickInsertsGroup editor={editor} /> <div className={classes.divider} />
<MoreInsertsGroup editor={editor} templateMode={templateMode} /> <HistoryGroup editor={editor} state={state} />
<div className={classes.divider} />
<HistoryGroup editor={editor} state={state} />
</>
)}
</div> </div>
</div> </div>
<div className={classes.spacer} aria-hidden /> <div className={classes.spacer} aria-hidden />
@@ -1,6 +1,6 @@
import { FC } from "react"; import { FC } from "react";
import type { Editor } from "@tiptap/react"; import type { Editor } from "@tiptap/react";
import { ActionIcon, Badge, Menu, Tooltip } from "@mantine/core"; import { ActionIcon, Menu, Tooltip } from "@mantine/core";
import { import {
IconAppWindow, IconAppWindow,
IconCalendar, IconCalendar,
@@ -12,7 +12,6 @@ import {
IconMathFunction, IconMathFunction,
IconRotate2, IconRotate2,
IconSitemap, IconSitemap,
IconSuperscript,
IconTable, IconTable,
IconTag, IconTag,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
@@ -33,9 +32,6 @@ import {
} from "@/components/icons"; } from "@/components/icons";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { insertBaseEmbedBlock } from "@/features/editor/components/base-embed/insert-base-embed"; import { insertBaseEmbedBlock } from "@/features/editor/components/base-embed/insert-base-embed";
import { useHasFeature } from "@/ee/hooks/use-feature";
import { Feature } from "@/ee/features";
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
interface Props { interface Props {
editor: Editor; editor: Editor;
@@ -44,8 +40,6 @@ interface Props {
export const MoreInsertsGroup: FC<Props> = ({ editor, templateMode }) => { export const MoreInsertsGroup: FC<Props> = ({ editor, templateMode }) => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const hasBases = useHasFeature(Feature.BASES);
const upgradeLabel = useUpgradeLabel();
const setEmbed = (provider: string) => const setEmbed = (provider: string) =>
editor.chain().focus().setEmbed({ provider }).run(); editor.chain().focus().setEmbed({ provider }).run();
@@ -112,48 +106,20 @@ export const MoreInsertsGroup: FC<Props> = ({ editor, templateMode }) => {
</Menu.Item> </Menu.Item>
)} )}
{!templateMode && ( {!templateMode && (
<Tooltip label={upgradeLabel} disabled={hasBases} position="right"> <Menu.Item
<Menu.Item leftSection={<IconTable size={16} />}
leftSection={<IconTable size={16} />} onClick={() => insertBaseEmbedBlock(editor)}
aria-disabled={!hasBases} >
closeMenuOnClick={hasBases} {t("Base (Inline)")}
style={{ opacity: hasBases ? undefined : 0.7 }} </Menu.Item>
rightSection={
!hasBases && (
<Badge size="xs" variant="light" color="gray">
{t("Upgrade")}
</Badge>
)
}
onClick={() => {
if (hasBases) insertBaseEmbedBlock(editor);
}}
>
{t("Base (Inline)")}
</Menu.Item>
</Tooltip>
)} )}
{!templateMode && ( {!templateMode && (
<Tooltip label={upgradeLabel} disabled={hasBases} position="right"> <Menu.Item
<Menu.Item leftSection={<IconLayoutKanban size={16} />}
leftSection={<IconLayoutKanban size={16} />} onClick={() => insertBaseEmbedBlock(editor, { template: "kanban" })}
aria-disabled={!hasBases} >
closeMenuOnClick={hasBases} {t("Kanban")}
style={{ opacity: hasBases ? undefined : 0.7 }} </Menu.Item>
rightSection={
!hasBases && (
<Badge size="xs" variant="light" color="gray">
{t("Upgrade")}
</Badge>
)
}
onClick={() => {
if (hasBases) insertBaseEmbedBlock(editor, { template: "kanban" });
}}
>
{t("Kanban")}
</Menu.Item>
</Tooltip>
)} )}
<Menu.Divider /> <Menu.Divider />
@@ -271,12 +237,6 @@ export const MoreInsertsGroup: FC<Props> = ({ editor, templateMode }) => {
> >
{t("Math block")} {t("Math block")}
</Menu.Item> </Menu.Item>
<Menu.Item
leftSection={<IconSuperscript size={16} />}
onClick={() => editor.chain().focus().addFootnote().run()}
>
{t("Footnote")}
</Menu.Item>
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
); );
@@ -1,7 +1,6 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import React, { useCallback, useRef } from "react"; import React, { useCallback, useRef } from "react";
import { useSetAtom } from "jotai";
import { Node as PMNode } from "@tiptap/pm/model"; import { Node as PMNode } from "@tiptap/pm/model";
import { isEditorReady } from "@docmost/editor-ext"; import { isEditorReady } from "@docmost/editor-ext";
import { import {
@@ -17,19 +16,16 @@ import {
IconDownload, IconDownload,
IconRefresh, IconRefresh,
IconTrash, IconTrash,
IconZoomIn,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { getFileUrl } from "@/lib/config.ts"; import { getFileUrl } from "@/lib/config.ts";
import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx"; import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx";
import { useAltTextControl } from "@/features/editor/components/common/use-alt-text-control.tsx"; import { useAltTextControl } from "@/features/editor/components/common/use-alt-text-control.tsx";
import { lightboxRequestAtom } from "@/features/editor/atoms/editor-atoms";
import classes from "../common/toolbar-menu.module.css"; import classes from "../common/toolbar-menu.module.css";
export function ImageMenu({ editor }: EditorMenuProps) { export function ImageMenu({ editor }: EditorMenuProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const setLightboxRequest = useSetAtom(lightboxRequestAtom);
const editorState = useEditorState({ const editorState = useEditorState({
editor, editor,
@@ -211,23 +207,6 @@ export function ImageMenu({ editor }: EditorMenuProps) {
<div className={classes.divider} /> <div className={classes.divider} />
<Tooltip position="top" label={t("Expand")} withinPortal={false}>
<ActionIcon
onClick={() =>
editorState?.src &&
setLightboxRequest({
src: getFileUrl(editorState.src),
type: "image",
})
}
size="lg"
aria-label={t("Expand")}
variant="subtle"
>
<IconZoomIn size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Download")} withinPortal={false}> <Tooltip position="top" label={t("Download")} withinPortal={false}>
<ActionIcon <ActionIcon
onClick={handleDownload} onClick={handleDownload}
@@ -10,7 +10,6 @@ import {
Paper, Paper,
ScrollArea, ScrollArea,
Text, Text,
Tooltip,
UnstyledButton, UnstyledButton,
VisuallyHidden, VisuallyHidden,
} from "@mantine/core"; } from "@mantine/core";
@@ -19,7 +18,6 @@ import clsx from "clsx";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useHasFeature } from "@/ee/hooks/use-feature"; import { useHasFeature } from "@/ee/hooks/use-feature";
import { Feature } from "@/ee/features"; import { Feature } from "@/ee/features";
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
const CommandList = ({ const CommandList = ({
items, items,
@@ -39,12 +37,11 @@ const CommandList = ({
const [selectionAnnouncement, setSelectionAnnouncement] = useState(""); const [selectionAnnouncement, setSelectionAnnouncement] = useState("");
const hasBases = useHasFeature(Feature.BASES); const hasBases = useHasFeature(Feature.BASES);
const upgradeLabel = useUpgradeLabel(); // Title must match the "Base (Inline)" item in menu-items.ts. Without the
// Without the bases entitlement the item stays visible but inert; an // bases entitlement the item stays visible but disabled; an expired license
// expired license the client can't detect falls through to a handled // the client can't detect falls through to a handled create failure.
// create failure.
const isItemDisabled = (item: SlashMenuItemType) => const isItemDisabled = (item: SlashMenuItemType) =>
!hasBases && item.requiresBases === true; !hasBases && item.title === "Base (Inline)";
const flatItems = useMemo(() => { const flatItems = useMemo(() => {
return Object.values(items).flat(); return Object.values(items).flat();
@@ -155,22 +152,18 @@ const CommandList = ({
const itemIndex = flatIndex; const itemIndex = flatIndex;
const disabled = isItemDisabled(item); const disabled = isItemDisabled(item);
return ( return (
<Tooltip
key={itemIndex}
label={upgradeLabel}
disabled={!disabled}
position="right"
>
<UnstyledButton <UnstyledButton
data-item-index={itemIndex} data-item-index={itemIndex}
key={itemIndex}
id={`slash-command-option-${itemIndex}`} id={`slash-command-option-${itemIndex}`}
role="option" role="option"
aria-selected={itemIndex === selectedIndex} aria-selected={itemIndex === selectedIndex}
aria-disabled={disabled} aria-disabled={disabled}
disabled={disabled}
onClick={() => selectItem(itemIndex)} onClick={() => selectItem(itemIndex)}
className={clsx(classes.menuBtn, { className={clsx(classes.menuBtn, {
[classes.selectedItem]: itemIndex === selectedIndex, [classes.selectedItem]: itemIndex === selectedIndex,
[classes.gatedItem]: disabled, [classes.disabledItem]: disabled,
})} })}
> >
<Group wrap="nowrap"> <Group wrap="nowrap">
@@ -195,7 +188,6 @@ const CommandList = ({
)} )}
</Group> </Group>
</UnstyledButton> </UnstyledButton>
</Tooltip>
); );
})} })}
</div> </div>
@@ -30,7 +30,6 @@ import {
IconTag, IconTag,
IconMoodSmile, IconMoodSmile,
IconRotate2, IconRotate2,
IconSuperscript,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { import {
CommandProps, CommandProps,
@@ -178,16 +177,6 @@ const CommandGroups: SlashMenuGroupedItemsType = {
command: ({ editor, range }: CommandProps) => command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).setPageBreak().run(), editor.chain().focus().deleteRange(range).setPageBreak().run(),
}, },
{
title: "Footnote",
description: "Insert a footnote reference.",
searchTerms: ["footnote", "reference", "citation", "note"],
icon: IconSuperscript,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).run();
editor.commands.addFootnote();
},
},
{ {
title: "Image", title: "Image",
description: "Upload any image from your device.", description: "Upload any image from your device.",
@@ -377,7 +366,6 @@ const CommandGroups: SlashMenuGroupedItemsType = {
description: "Insert an inline base on this page", description: "Insert an inline base on this page",
searchTerms: ["base", "database", "table", "grid", "spreadsheet"], searchTerms: ["base", "database", "table", "grid", "spreadsheet"],
icon: IconTable, icon: IconTable,
requiresBases: true,
command: ({ editor, range }: CommandProps) => { command: ({ editor, range }: CommandProps) => {
insertBaseEmbedBlock(editor, { range }); insertBaseEmbedBlock(editor, { range });
}, },
@@ -387,7 +375,6 @@ const CommandGroups: SlashMenuGroupedItemsType = {
description: "Insert a kanban board on this page", description: "Insert a kanban board on this page",
searchTerms: ["kanban", "board", "cards", "status", "task", "database"], searchTerms: ["kanban", "board", "cards", "status", "task", "database"],
icon: IconLayoutKanban, icon: IconLayoutKanban,
requiresBases: true,
command: ({ editor, range }: CommandProps) => { command: ({ editor, range }: CommandProps) => {
insertBaseEmbedBlock(editor, { range, template: "kanban" }); insertBaseEmbedBlock(editor, { range, template: "kanban" });
}, },
@@ -26,6 +26,7 @@
} }
} }
.gatedItem { .disabledItem {
opacity: 0.7; opacity: 0.45;
cursor: not-allowed;
} }
@@ -21,7 +21,6 @@ export type SlashMenuItemType = {
searchTerms: string[]; searchTerms: string[];
command: (props: CommandProps) => void; command: (props: CommandProps) => void;
disable?: (editor: ReturnType<typeof useEditor>) => boolean; disable?: (editor: ReturnType<typeof useEditor>) => boolean;
requiresBases?: true;
}; };
export type SlashMenuGroupedItemsType = { export type SlashMenuGroupedItemsType = {
@@ -13,21 +13,14 @@ import {
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { sortPositionKeys } from "@/features/page/tree/utils/utils"; import { sortPositionKeys } from "@/features/page/tree/utils/utils";
import { useSharedPageSubpages } from "@/features/share/hooks/use-shared-page-subpages"; import { useSharedPageSubpages } from "@/features/share/hooks/use-shared-page-subpages";
import { extractPageSlugId } from "@/lib";
export default function SubpagesView(props: NodeViewProps) { export default function SubpagesView(props: NodeViewProps) {
const { editor } = props; const { editor } = props;
const { spaceSlug, shareId, pageSlug } = useParams(); const { spaceSlug, shareId } = useParams();
const { t } = useTranslation(); const { t } = useTranslation();
// @ts-ignore //@ts-ignore
const storagePageId = editor.storage.pageId; const currentPageId = editor.storage.pageId;
const routePageId = extractPageSlugId(pageSlug);
let currentPageId = storagePageId;
if (shareId){
currentPageId = routePageId;
}
// Get subpages from shared tree if we're in a shared context // Get subpages from shared tree if we're in a shared context
const sharedSubpages = useSharedPageSubpages(currentPageId); const sharedSubpages = useSharedPageSubpages(currentPageId);
@@ -40,7 +40,6 @@ export default function TransclusionContent({ content }: Props) {
<EditorProvider <EditorProvider
editable={false} editable={false}
immediatelyRender={true} immediatelyRender={true}
textDirection="auto"
extensions={extensions} extensions={extensions}
content={content as any} content={content as any}
/> />
@@ -1,7 +1,6 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import { useCallback } from "react"; import { useCallback } from "react";
import { useSetAtom } from "jotai";
import { Node as PMNode } from "@tiptap/pm/model"; import { Node as PMNode } from "@tiptap/pm/model";
import { isEditorReady } from "@docmost/editor-ext"; import { isEditorReady } from "@docmost/editor-ext";
import { import {
@@ -16,17 +15,14 @@ import {
IconLayoutAlignRight, IconLayoutAlignRight,
IconDownload, IconDownload,
IconTrash, IconTrash,
IconZoomIn,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { getFileUrl } from "@/lib/config.ts"; import { getFileUrl } from "@/lib/config.ts";
import { useAltTextControl } from "@/features/editor/components/common/use-alt-text-control.tsx"; import { useAltTextControl } from "@/features/editor/components/common/use-alt-text-control.tsx";
import { lightboxRequestAtom } from "@/features/editor/atoms/editor-atoms";
import classes from "../common/toolbar-menu.module.css"; import classes from "../common/toolbar-menu.module.css";
export function VideoMenu({ editor }: EditorMenuProps) { export function VideoMenu({ editor }: EditorMenuProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const setLightboxRequest = useSetAtom(lightboxRequestAtom);
const editorState = useEditorState({ const editorState = useEditorState({
editor, editor,
@@ -187,23 +183,6 @@ export function VideoMenu({ editor }: EditorMenuProps) {
<div className={classes.divider} /> <div className={classes.divider} />
<Tooltip position="top" label={t("Expand")} withinPortal={false}>
<ActionIcon
onClick={() =>
editorState?.src &&
setLightboxRequest({
src: getFileUrl(editorState.src),
type: "video",
})
}
size="lg"
aria-label={t("Expand")}
variant="subtle"
>
<IconZoomIn size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Download")} withinPortal={false}> <Tooltip position="top" label={t("Download")} withinPortal={false}>
<ActionIcon <ActionIcon
onClick={handleDownload} onClick={handleDownload}
@@ -1,11 +1,9 @@
import { markInputRule } from "@tiptap/core"; import { markInputRule } from "@tiptap/core";
import { StarterKit } from "@tiptap/starter-kit"; import { StarterKit } from "@tiptap/starter-kit";
import { Document } from "@tiptap/extension-document";
import { Code } from "@tiptap/extension-code"; import { Code } from "@tiptap/extension-code";
import { TextAlign } from "@tiptap/extension-text-align"; import { TextAlign } from "@tiptap/extension-text-align";
import { TaskList, TaskItem } from "@tiptap/extension-list"; import { TaskList, TaskItem } from "@tiptap/extension-list";
import { CharacterCount, UndoRedo } from "@tiptap/extensions"; import { Placeholder, CharacterCount, UndoRedo } from "@tiptap/extensions";
import { Placeholder } from "@/features/editor/extensions/placeholder";
import { Superscript } from "@tiptap/extension-superscript"; import { Superscript } from "@tiptap/extension-superscript";
import SubScript from "@tiptap/extension-subscript"; import SubScript from "@tiptap/extension-subscript";
import { Typography } from "@tiptap/extension-typography"; import { Typography } from "@tiptap/extension-typography";
@@ -64,9 +62,6 @@ import {
TransclusionReference, TransclusionReference,
TableView, TableView,
BaseEmbed as BaseEmbedNode, BaseEmbed as BaseEmbedNode,
Footnotes,
Footnote,
FootnoteReference,
} from "@docmost/editor-ext"; } from "@docmost/editor-ext";
import { import {
randomElement, randomElement,
@@ -136,7 +131,6 @@ lowlight.register("scala", scala);
// @ts-ignore // @ts-ignore
export const mainExtensions = [ export const mainExtensions = [
StarterKit.configure({ StarterKit.configure({
document: false,
heading: false, heading: false,
undoRedo: false, undoRedo: false,
link: false, link: false,
@@ -148,9 +142,6 @@ export const mainExtensions = [
codeBlock: false, codeBlock: false,
code: false, code: false,
}), }),
Document.extend({
content: "block+ footnotes?",
}),
// Override TipTap's Code extension to fix the inline code input rule. // Override TipTap's Code extension to fix the inline code input rule.
// The upstream regex /(^|[^`])`([^`]+)`(?!`)$/ captures the character // The upstream regex /(^|[^`])`([^`]+)`(?!`)$/ captures the character
// before the opening backtick as part of the match, causing markInputRule // before the opening backtick as part of the match, causing markInputRule
@@ -203,19 +194,16 @@ export const mainExtensions = [
return i18n.t("Toggle title"); return i18n.t("Toggle title");
} }
if (node.type.name === "paragraph") { if (node.type.name === "paragraph") {
const doc = editor.state.doc; const $pos = editor.state.doc.resolve(pos);
if (pos >= 0 && pos <= doc.content.size) { const parentName = $pos.parent.type.name;
const parentName = doc.resolve(pos).parent.type.name; if (
if ( parentName === "column" ||
parentName === "column" || parentName === "tableCell" ||
parentName === "tableCell" || parentName === "tableHeader" ||
parentName === "tableHeader" || parentName === "callout" ||
parentName === "callout" || parentName === "blockquote"
parentName === "blockquote" || ) {
parentName === "footnote" return i18n.t("Write...");
) {
return i18n.t("Write...");
}
} }
return i18n.t('Write anything. Enter "/" for commands'); return i18n.t('Write anything. Enter "/" for commands');
} }
@@ -426,9 +414,6 @@ export const mainExtensions = [
}).configure(), }).configure(),
Columns, Columns,
Column, Column,
Footnotes,
Footnote,
FootnoteReference,
AutoJoiner.configure({ AutoJoiner.configure({
elementsToJoin: [], elementsToJoin: [],
}), }),
@@ -1,64 +0,0 @@
import { isNodeEmpty } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { Placeholder as TiptapPlaceholder } from "@tiptap/extensions";
export const Placeholder = TiptapPlaceholder.extend({
addProseMirrorPlugins() {
const editor = this.editor;
const options = this.options;
const dataAttribute = `data-${options.dataAttribute || "placeholder"}`;
return [
new Plugin({
key: new PluginKey("docmostPlaceholder"),
props: {
decorations: (state) => {
if (options.showOnlyWhenEditable && !editor.isEditable) {
return null;
}
const { doc, selection } = state;
const { anchor } = selection;
const decorations: Decoration[] = [];
const isEmptyDoc = editor.isEmpty;
doc.descendants((node, pos) => {
if (!node.type.isTextblock) {
return options.includeChildren;
}
const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize;
const isEmpty = !node.isLeaf && isNodeEmpty(node);
if ((hasAnchor || !options.showOnlyCurrent) && isEmpty) {
const emptyNodeClass =
typeof options.emptyNodeClass === "function"
? options.emptyNodeClass({ editor, node, pos, hasAnchor })
: options.emptyNodeClass;
const classes = [emptyNodeClass];
if (isEmptyDoc) {
classes.push(options.emptyEditorClass);
}
decorations.push(
Decoration.node(pos, pos + node.nodeSize, {
class: classes.join(" "),
[dataAttribute]:
typeof options.placeholder === "function"
? options.placeholder({ editor, node, pos, hasAnchor })
: options.placeholder,
}),
);
}
return options.includeChildren;
});
return DecorationSet.create(doc, decorations);
},
},
}),
];
},
});
@@ -0,0 +1,7 @@
import { getCollaborationUrl } from "@/lib/config.ts";
const useCollaborationURL = (): string => {
return getCollaborationUrl();
};
export default useCollaborationURL;
+169 -218
View File
@@ -2,22 +2,20 @@ import "@/features/editor/styles/index.css";
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
useLayoutEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { IndexeddbPersistence } from "y-indexeddb"; import { IndexeddbPersistence } from "y-indexeddb";
import * as Y from "yjs";
import { import {
HocuspocusProvider,
onStatusParameters,
WebSocketStatus, WebSocketStatus,
HocuspocusProviderWebsocket,
onSyncedParameters,
onStatelessParameters, onStatelessParameters,
} from "@hocuspocus/provider"; } from "@hocuspocus/provider";
import {
HocuspocusProviderWebsocketComponent,
HocuspocusRoom,
useHocuspocusEvent,
useHocuspocusProvider,
} from "@hocuspocus/provider-react";
import { import {
Editor, Editor,
EditorContent, EditorContent,
@@ -30,10 +28,10 @@ import {
mainExtensions, mainExtensions,
} from "@/features/editor/extensions/extensions"; } from "@/features/editor/extensions/extensions";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import useCollaborationUrl from "@/features/editor/hooks/use-collaboration-url";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom"; import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
import { import {
currentPageEditModeAtom, currentPageEditModeAtom,
lightboxRequestAtom,
pageEditorAtom, pageEditorAtom,
yjsConnectionStatusAtom, yjsConnectionStatusAtom,
yjsSyncedAtom, yjsSyncedAtom,
@@ -54,9 +52,6 @@ import CalloutMenu from "@/features/editor/components/callout/callout-menu.tsx";
import VideoMenu from "@/features/editor/components/video/video-menu.tsx"; import VideoMenu from "@/features/editor/components/video/video-menu.tsx";
import PdfMenu from "@/features/editor/components/pdf/pdf-menu.tsx"; import PdfMenu from "@/features/editor/components/pdf/pdf-menu.tsx";
import SubpagesMenu from "@/features/editor/components/subpages/subpages-menu.tsx"; import SubpagesMenu from "@/features/editor/components/subpages/subpages-menu.tsx";
import LightboxView, {
getLightboxClickRequest,
} from "@/features/editor/components/common/lightbox-view";
import { import {
handleFileDrop, handleFileDrop,
handlePaste, handlePaste,
@@ -81,11 +76,6 @@ import { EditorLinkMenu } from "@/features/editor/components/link/link-menu";
import ColumnsMenu from "@/features/editor/components/columns/columns-menu.tsx"; import ColumnsMenu from "@/features/editor/components/columns/columns-menu.tsx";
import { TransclusionLookupProvider } from "@/features/editor/components/transclusion/transclusion-lookup-context"; import { TransclusionLookupProvider } from "@/features/editor/components/transclusion/transclusion-lookup-context";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import {
acquireCollabSocket,
getCollabSocket,
releaseCollabSocket,
} from "@/features/editor/collab-socket";
interface PageEditorProps { interface PageEditorProps {
pageId: string; pageId: string;
@@ -101,80 +91,7 @@ export default function PageEditor({
canComment, canComment,
}: PageEditorProps) { }: PageEditorProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { data: collabQuery, refetch: refetchCollabToken } = useCollabToken(); const collaborationURL = useCollaborationUrl();
const { pageSlug } = useParams();
const slugId = extractPageSlugId(pageSlug);
const [socket] = useState(getCollabSocket);
const hasCollabToken = !!collabQuery?.token;
useEffect(() => {
if (!hasCollabToken) return;
acquireCollabSocket();
return () => releaseCollabSocket();
}, [hasCollabToken]);
const handleStateless = ({ payload }: onStatelessParameters) => {
try {
const message = JSON.parse(payload);
if (message?.type !== "page.updated" || !message.updatedAt) return;
const pageData = queryClient.getQueryData<IPage>(["pages", slugId]);
if (pageData) {
queryClient.setQueryData(["pages", slugId], {
...pageData,
updatedAt: message.updatedAt,
...(message.lastUpdatedBy && {
lastUpdatedBy: message.lastUpdatedBy,
}),
});
}
} catch {
// ignore unrelated stateless messages
}
};
const handleAuthenticationFailed = () => {
const payload = jwtDecode(collabQuery?.token);
const now = Date.now().valueOf() / 1000;
const isTokenExpired = now >= payload.exp;
if (isTokenExpired) {
refetchCollabToken();
}
};
return (
<TransclusionLookupProvider>
{collabQuery?.token ? (
<HocuspocusProviderWebsocketComponent websocketProvider={socket}>
<HocuspocusRoom
name={`page.${pageId}`}
token={collabQuery.token}
flushDelay={500}
onStateless={handleStateless}
onAuthenticationFailed={handleAuthenticationFailed}
>
<CollabPageEditor
pageId={pageId}
editable={editable}
content={content}
canComment={canComment}
/>
</HocuspocusRoom>
</HocuspocusProviderWebsocketComponent>
) : (
<StaticPageEditor content={content} ariaLabel={t("Page content")} />
)}
</TransclusionLookupProvider>
);
}
function CollabPageEditor({
pageId,
editable,
content,
canComment,
}: PageEditorProps) {
const { t } = useTranslation();
const provider = useHocuspocusProvider();
const isComponentMounted = useRef(false); const isComponentMounted = useRef(false);
const editorRef = useRef<Editor | null>(null); const editorRef = useRef<Editor | null>(null);
@@ -188,7 +105,6 @@ function CollabPageEditor({
const [, setActiveCommentId] = useAtom(activeCommentIdAtom); const [, setActiveCommentId] = useAtom(activeCommentIdAtom);
const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom); const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom);
const [showReadOnlyCommentPopup] = useAtom(showReadOnlyCommentPopupAtom); const [showReadOnlyCommentPopup] = useAtom(showReadOnlyCommentPopupAtom);
const [lightboxRequest, setLightboxRequest] = useAtom(lightboxRequestAtom);
const [isLocalSynced, setIsLocalSynced] = useState(false); const [isLocalSynced, setIsLocalSynced] = useState(false);
const [isRemoteSynced, setIsRemoteSynced] = useState(false); const [isRemoteSynced, setIsRemoteSynced] = useState(false);
const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom( const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom(
@@ -196,6 +112,7 @@ function CollabPageEditor({
); );
const [, setYjsSynced] = useAtom(yjsSyncedAtom); const [, setYjsSynced] = useAtom(yjsSyncedAtom);
const menuContainerRef = useRef(null); const menuContainerRef = useRef(null);
const { data: collabQuery, refetch: refetchCollabToken } = useCollabToken();
const { isIdle, resetIdle } = useIdle(FIVE_MINUTES, { initialState: false }); const { isIdle, resetIdle } = useIdle(FIVE_MINUTES, { initialState: false });
const documentState = useDocumentVisibility(); const documentState = useDocumentVisibility();
const { pageSlug } = useParams(); const { pageSlug } = useParams();
@@ -206,24 +123,95 @@ function CollabPageEditor({
[isComponentMounted], [isComponentMounted],
); );
const { handleScrollTo } = useEditorScroll({ canScroll }); const { handleScrollTo } = useEditorScroll({ canScroll });
// Providers only created once per pageId
const providersRef = useRef<{
local: IndexeddbPersistence;
remote: HocuspocusProvider;
socket: HocuspocusProviderWebsocket;
} | null>(null);
const [providersReady, setProvidersReady] = useState(false);
useEffect(() => { useEffect(() => {
const local = new IndexeddbPersistence( if (!providersRef.current) {
provider.configuration.name, const documentName = `page.${pageId}`;
provider.document, const ydoc = new Y.Doc();
); const local = new IndexeddbPersistence(documentName, ydoc);
local.on("synced", () => setIsLocalSynced(true)); const socket = new HocuspocusProviderWebsocket({
return () => { url: collaborationURL,
local.destroy(); });
}; const onLocalSyncedHandler = () => {
}, [provider]); setIsLocalSynced(true);
};
const onStatusHandler = (event: onStatusParameters) => {
setYjsConnectionStatus(event.status);
};
const onSyncedHandler = (event: onSyncedParameters) => {
setIsRemoteSynced(event.state);
};
const onStatelessHandler = ({ payload }: onStatelessParameters) => {
try {
const message = JSON.parse(payload);
if (message?.type !== "page.updated" || !message.updatedAt) return;
const pageData = queryClient.getQueryData<IPage>(["pages", slugId]);
if (pageData) {
queryClient.setQueryData(["pages", slugId], {
...pageData,
updatedAt: message.updatedAt,
...(message.lastUpdatedBy && {
lastUpdatedBy: message.lastUpdatedBy,
}),
});
}
} catch {
// ignore unrelated stateless messages
}
};
const onAuthenticationFailedHandler = () => {
const payload = jwtDecode(collabQuery?.token);
const now = Date.now().valueOf() / 1000;
const isTokenExpired = now >= payload.exp;
if (isTokenExpired) {
refetchCollabToken().then((result) => {
if (result.data?.token) {
socket.disconnect();
setTimeout(() => {
remote.configuration.token = result.data.token;
socket.connect();
}, 100);
}
});
}
};
const remote = new HocuspocusProvider({
websocketProvider: socket,
name: documentName,
document: ydoc,
token: collabQuery?.token,
onAuthenticationFailed: onAuthenticationFailedHandler,
onStatus: onStatusHandler,
onSynced: onSyncedHandler,
onStateless: onStatelessHandler,
});
useHocuspocusEvent("synced", ({ state }) => setIsRemoteSynced(state)); local.on("synced", onLocalSyncedHandler);
useHocuspocusEvent("status", ({ status }) => setYjsConnectionStatus(status)); providersRef.current = { socket, local, remote };
setProvidersReady(true);
} else {
setProvidersReady(true);
}
// Only destroy on final unmount
return () => {
providersRef.current?.socket.destroy();
providersRef.current?.remote.destroy();
providersRef.current?.local.destroy();
providersRef.current = null;
};
}, [pageId]);
// Only connect/disconnect on tab/idle, not destroy // Only connect/disconnect on tab/idle, not destroy
useEffect(() => { useEffect(() => {
const socket = provider.configuration.websocketProvider; if (!providersReady || !providersRef.current) return;
const socket = providersRef.current.socket;
if ( if (
isIdle && isIdle &&
@@ -240,32 +228,28 @@ function CollabPageEditor({
resetIdle(); resetIdle();
socket.connect(); socket.connect();
} }
}, [isIdle, documentState, provider, resetIdle]); }, [isIdle, documentState, providersReady, resetIdle]);
// Attach here, to make sure the connection gets properly established
providersRef.current?.remote.attach();
const extensions = useMemo(() => { const extensions = useMemo(() => {
if (!currentUser?.user) { if (!providersReady || !providersRef.current || !currentUser?.user) {
return mainExtensions; return mainExtensions;
} }
return [...mainExtensions, ...collabExtensions(provider, currentUser.user)]; const remoteProvider = providersRef.current.remote;
}, [provider, currentUser?.user]);
const debouncedUpdateContent = useDebouncedCallback((newContent: any) => { return [
const pageData = queryClient.getQueryData<IPage>(["pages", slugId]); ...mainExtensions,
...collabExtensions(remoteProvider, currentUser?.user),
if (pageData) { ];
queryClient.setQueryData(["pages", slugId], { }, [providersReady, currentUser?.user]);
...pageData,
content: newContent,
});
}
}, 3000);
const editor = useEditor( const editor = useEditor(
{ {
extensions, extensions,
editable, editable,
textDirection: "auto",
immediatelyRender: true, immediatelyRender: true,
shouldRerenderOnTransaction: false, shouldRerenderOnTransaction: false,
editorProps: { editorProps: {
@@ -321,22 +305,6 @@ function CollabPageEditor({
return handleFileDrop(editorRef.current, event, moved, pageId); return handleFileDrop(editorRef.current, event, moved, pageId);
}, },
handleClickOn: (view, _pos, node) => {
if (view.editable) return false;
const request = getLightboxClickRequest(node);
if (!request) return false;
setLightboxRequest(request);
return true;
},
handleDoubleClickOn: (_view, _pos, node) => {
const request = getLightboxClickRequest(node);
if (!request) return false;
setLightboxRequest(request);
return true;
},
}, },
onCreate({ editor }) { onCreate({ editor }) {
if (editor) { if (editor) {
@@ -358,16 +326,6 @@ function CollabPageEditor({
[pageId, editable, extensions], [pageId, editable, extensions],
); );
useLayoutEffect(() => {
if (editor && !editor.isDestroyed) {
// @ts-ignore
setEditor(editor);
// @ts-ignore
editor.storage.pageId = pageId;
editorRef.current = editor;
}
}, [editor, pageId, setEditor]);
const editorIsEditable = useEditorState({ const editorIsEditable = useEditorState({
editor, editor,
selector: (ctx) => { selector: (ctx) => {
@@ -375,6 +333,17 @@ function CollabPageEditor({
}, },
}); });
const debouncedUpdateContent = useDebouncedCallback((newContent: any) => {
const pageData = queryClient.getQueryData<IPage>(["pages", slugId]);
if (pageData) {
queryClient.setQueryData(["pages", slugId], {
...pageData,
content: newContent,
});
}
}, 3000);
const handleActiveCommentEvent = (event) => { const handleActiveCommentEvent = (event) => {
const { commentId, resolved } = event.detail; const { commentId, resolved } = event.detail;
@@ -407,7 +376,6 @@ function CollabPageEditor({
setActiveCommentId(null); setActiveCommentId(null);
setShowCommentPopup(false); setShowCommentPopup(false);
setAsideState({ tab: "", isAsideOpen: false }); setAsideState({ tab: "", isAsideOpen: false });
setLightboxRequest(null);
}, [pageId]); }, [pageId]);
const isSynced = isLocalSynced && isRemoteSynced; const isSynced = isLocalSynced && isRemoteSynced;
@@ -448,82 +416,65 @@ function CollabPageEditor({
} }
}, [yjsConnectionStatus, isSynced]); }, [yjsConnectionStatus, isSynced]);
if (showStatic) {
return <StaticPageEditor content={content} ariaLabel={t("Page content")} />;
}
return ( return (
<div className="editor-container" style={{ position: "relative" }}> <TransclusionLookupProvider>
<div ref={menuContainerRef}> {showStatic ? (
<EditorContent editor={editor} /> <EditorProvider
editable={false}
immediatelyRender={true}
extensions={mainExtensions}
content={content}
editorProps={{
attributes: {
"aria-label": t("Page content"),
},
}}
/>
) : (
<div className="editor-container" style={{ position: "relative" }}>
<div ref={menuContainerRef}>
<EditorContent editor={editor} />
{editor && ( {editor && (
<SearchAndReplaceDialog editor={editor} editable={editable} /> <SearchAndReplaceDialog editor={editor} editable={editable} />
)} )}
{editor && editorIsEditable && ( {editor && editorIsEditable && (
<div> <div>
<EditorAiMenu editor={editor} /> <EditorAiMenu editor={editor} />
<EditorLinkMenu editor={editor} /> <EditorLinkMenu editor={editor} />
<EditorBubbleMenu editor={editor} /> <EditorBubbleMenu editor={editor} />
<TableMenu editor={editor} /> <TableMenu editor={editor} />
<TableHandlesLayer editor={editor} /> <TableHandlesLayer editor={editor} />
<ImageMenu editor={editor} /> <ImageMenu editor={editor} />
<VideoMenu editor={editor} /> <VideoMenu editor={editor} />
<PdfMenu editor={editor} /> <PdfMenu editor={editor} />
<CalloutMenu editor={editor} /> <CalloutMenu editor={editor} />
<SubpagesMenu editor={editor} /> <SubpagesMenu editor={editor} />
<ExcalidrawMenu editor={editor} /> <ExcalidrawMenu editor={editor} />
<DrawioMenu editor={editor} /> <DrawioMenu editor={editor} />
<ColumnsMenu editor={editor} /> <ColumnsMenu editor={editor} />
</div>
)}
{editor &&
!editorIsEditable &&
(editable || canComment) &&
providersRef.current && <ReadonlyBubbleMenu editor={editor} />}
{showCommentPopup && (
<CommentDialog editor={editor} pageId={pageId} />
)}
{showReadOnlyCommentPopup && (
<CommentDialog editor={editor} pageId={pageId} readOnly />
)}
</div> </div>
)} <div
{editor && !editorIsEditable && (editable || canComment) && ( onClick={() => {
<ReadonlyBubbleMenu editor={editor} /> if (editor && !editor.isDestroyed) editor.commands.focus("end");
)} }}
{editor && ( style={{ paddingBottom: "20vh" }}
<LightboxView ></div>
editor={editor} </div>
open={!!lightboxRequest} )}
src={lightboxRequest?.src ?? ""} </TransclusionLookupProvider>
type={lightboxRequest?.type ?? "image"}
onClose={() => setLightboxRequest(null)}
/>
)}
{showCommentPopup && <CommentDialog editor={editor} pageId={pageId} />}
{showReadOnlyCommentPopup && (
<CommentDialog editor={editor} pageId={pageId} readOnly />
)}
</div>
<div
onClick={() => {
if (editor && !editor.isDestroyed) editor.commands.focus("end");
}}
style={{ paddingBottom: "20vh" }}
></div>
</div>
);
}
function StaticPageEditor({
content,
ariaLabel,
}: {
content: any;
ariaLabel: string;
}) {
return (
<EditorProvider
editable={false}
immediatelyRender={true}
textDirection="auto"
extensions={mainExtensions}
content={content}
editorProps={{
attributes: {
"aria-label": ariaLabel,
},
}}
/>
); );
} }
@@ -1,27 +1,15 @@
import "@/features/editor/styles/index.css"; import "@/features/editor/styles/index.css";
import React, { import React, { useCallback, useEffect, useMemo, useRef } from "react";
useCallback, import { EditorProvider } from "@tiptap/react";
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Editor, EditorProvider } from "@tiptap/react";
import { mainExtensions } from "@/features/editor/extensions/extensions"; import { mainExtensions } from "@/features/editor/extensions/extensions";
import { Document } from "@tiptap/extension-document"; import { Document } from "@tiptap/extension-document";
import { Heading, UniqueID } from "@docmost/editor-ext"; import { Heading, UniqueID } from "@docmost/editor-ext";
import { Text } from "@tiptap/extension-text"; import { Text } from "@tiptap/extension-text";
import { Placeholder } from "@tiptap/extension-placeholder"; import { Placeholder } from "@tiptap/extension-placeholder";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { import { readOnlyEditorAtom } from "@/features/editor/atoms/editor-atoms.ts";
lightboxRequestAtom,
readOnlyEditorAtom,
} from "@/features/editor/atoms/editor-atoms.ts";
import { useEditorScroll } from "./hooks/use-editor-scroll"; import { useEditorScroll } from "./hooks/use-editor-scroll";
import { TransclusionLookupProvider } from "@/features/editor/components/transclusion/transclusion-lookup-context"; import { TransclusionLookupProvider } from "@/features/editor/components/transclusion/transclusion-lookup-context";
import LightboxView, {
getLightboxClickRequest,
} from "@/features/editor/components/common/lightbox-view";
interface PageEditorProps { interface PageEditorProps {
title: string; title: string;
@@ -45,8 +33,6 @@ export default function ReadonlyPageEditor({
shareId, shareId,
}: PageEditorProps) { }: PageEditorProps) {
const [, setReadOnlyEditor] = useAtom(readOnlyEditorAtom); const [, setReadOnlyEditor] = useAtom(readOnlyEditorAtom);
const [lightboxRequest, setLightboxRequest] = useAtom(lightboxRequestAtom);
const [contentEditor, setContentEditor] = useState<Editor | null>(null);
const isComponentMounted = useRef(false); const isComponentMounted = useRef(false);
const editorCreated = useRef(false); const editorCreated = useRef(false);
@@ -63,11 +49,6 @@ export default function ReadonlyPageEditor({
isComponentMounted.current = true; isComponentMounted.current = true;
}, []); }, []);
useEffect(() => {
if (!shareId) return;
setLightboxRequest(null);
}, [pageId, shareId]);
const extensions = useMemo(() => { const extensions = useMemo(() => {
const excludedExtensions = new Set([ const excludedExtensions = new Set([
"uniqueID", "uniqueID",
@@ -104,7 +85,6 @@ export default function ReadonlyPageEditor({
<EditorProvider <EditorProvider
editable={false} editable={false}
immediatelyRender={true} immediatelyRender={true}
textDirection="auto"
extensions={titleExtensions} extensions={titleExtensions}
content={title} content={title}
></EditorProvider> ></EditorProvider>
@@ -113,22 +93,8 @@ export default function ReadonlyPageEditor({
<EditorProvider <EditorProvider
editable={false} editable={false}
immediatelyRender={true} immediatelyRender={true}
textDirection="auto"
extensions={extensions} extensions={extensions}
content={content} content={content}
editorProps={
shareId
? {
handleClickOn: (_view, _pos, node) => {
const request = getLightboxClickRequest(node);
if (!request) return false;
setLightboxRequest(request);
return true;
},
}
: undefined
}
onCreate={({ editor }) => { onCreate={({ editor }) => {
if (editor) { if (editor) {
if (pageId) { if (pageId) {
@@ -137,22 +103,12 @@ export default function ReadonlyPageEditor({
} }
// @ts-ignore // @ts-ignore
setReadOnlyEditor(editor); setReadOnlyEditor(editor);
setContentEditor(editor);
handleScrollTo(editor); handleScrollTo(editor);
editorCreated.current = true; editorCreated.current = true;
} }
}} }}
></EditorProvider> ></EditorProvider>
{shareId && contentEditor && (
<LightboxView
editor={contentEditor}
open={!!lightboxRequest}
src={lightboxRequest?.src ?? ""}
type={lightboxRequest?.type ?? "image"}
onClose={() => setLightboxRequest(null)}
/>
)}
<div style={{ paddingBottom: "20vh" }}></div> <div style={{ paddingBottom: "20vh" }}></div>
</TransclusionLookupProvider> </TransclusionLookupProvider>
); );
@@ -1,26 +0,0 @@
.ProseMirror sup a.footnote-ref {
color: var(--mantine-primary-color-filled);
text-decoration: none;
cursor: pointer;
font-weight: 600;
}
.ProseMirror sup:has(a.footnote-ref) {
padding: 0 1px;
}
.ProseMirror ol.footnotes {
margin-top: 2rem;
padding-top: 0.75rem;
font-size: 0.875rem;
color: var(--mantine-color-dimmed);
list-style-type: decimal;
}
.ProseMirror ol.footnotes:has(li) {
border-top: 1px solid var(--mantine-color-default-border);
}
.ProseMirror ol.footnotes li p {
margin: 0.15rem 0;
}
@@ -18,4 +18,3 @@
@import "./columns.css"; @import "./columns.css";
@import "./status.css"; @import "./status.css";
@import "./base-embed.css"; @import "./base-embed.css";
@import "./footnotes.css";
@@ -38,60 +38,6 @@
line-height: var(--mantine-line-height-md); line-height: var(--mantine-line-height-md);
} }
.media-alt-badge {
position: absolute;
right: 8px;
bottom: 8px;
z-index: 1;
padding: 2px 8px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.03em;
cursor: default;
user-select: none;
background-color: var(--mantine-color-body);
@mixin light {
border: 1px solid var(--mantine-color-gray-3);
color: var(--mantine-color-gray-7);
}
@mixin dark {
border: 1px solid var(--mantine-color-dark-4);
color: var(--mantine-color-dark-1);
}
@media print {
display: none;
}
&::after {
content: attr(data-alt);
display: none;
position: absolute;
right: 0;
bottom: calc(100% + 6px);
width: max-content;
max-width: 320px;
padding: 6px 10px;
border-radius: var(--mantine-radius-default);
font-size: var(--mantine-font-size-sm);
font-weight: 400;
letter-spacing: normal;
line-height: var(--mantine-line-height-sm);
white-space: pre-wrap;
overflow-wrap: break-word;
text-align: start;
background-color: var(--mantine-color-gray-9);
color: var(--mantine-color-white);
}
&:hover::after {
display: block;
}
}
.media-pulse { .media-pulse {
animation: media-pulse 1.2s ease-in-out infinite; animation: media-pulse 1.2s ease-in-out infinite;
@@ -54,7 +54,7 @@
var(--mantine-color-dark-5) var(--mantine-color-dark-5)
); );
font-weight: bold; font-weight: bold;
text-align: start; text-align: left;
} }
.column-resize-handle { .column-resize-handle {
@@ -86,7 +86,6 @@ export function TitleEditor({
}, },
editable: editable, editable: editable,
content: title, content: title,
textDirection: "auto",
immediatelyRender: true, immediatelyRender: true,
shouldRerenderOnTransaction: false, shouldRerenderOnTransaction: false,
editorProps: { editorProps: {
@@ -39,7 +39,6 @@ export function useWorkspaceLabelsQuery(query: string, enabled: boolean) {
queryFn: () => getWorkspaceLabels({ type: "page", query, limit: 50 }), queryFn: () => getWorkspaceLabels({ type: "page", query, limit: 50 }),
enabled, enabled,
staleTime: 30 * 1000, staleTime: 30 * 1000,
placeholderData: keepPreviousData
}); });
} }

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