mirror of
https://github.com/docmost/docmost
synced 2025-03-28 21:13:28 +00:00
Compare commits
115 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a74d3feae4 | ||
![]() |
e40faf97ec | ||
![]() |
bbe4fe99f9 | ||
![]() |
8300c5b731 | ||
![]() |
13039cfacc | ||
![]() |
593f41a050 | ||
![]() |
f8ce160906 | ||
![]() |
c824b5b570 | ||
![]() |
37e760d76c | ||
![]() |
442fa23399 | ||
![]() |
2e5990d057 | ||
![]() |
15bdbf74cd | ||
![]() |
3d9a7d808b | ||
![]() |
f45bdddb23 | ||
![]() |
21c3ad0ecc | ||
![]() |
573457403e | ||
![]() |
d021d0a38f | ||
![]() |
96dfe9f817 | ||
![]() |
598361992e | ||
![]() |
210d1474ea | ||
![]() |
5f520689ed | ||
![]() |
2a535de29d | ||
![]() |
f45d9dc5a0 | ||
![]() |
f7a14e23cd | ||
![]() |
1f40e9b960 | ||
![]() |
fea6518352 | ||
![]() |
061a02ce51 | ||
![]() |
2205ce0c3b | ||
![]() |
a812cdcf15 | ||
![]() |
30acc6676a | ||
![]() |
5c9e0a2630 | ||
![]() |
fd36076ae7 | ||
![]() |
dd52eb15ca | ||
![]() |
6776e073b6 | ||
![]() |
7a47da9273 | ||
![]() |
e62bc6c250 | ||
![]() |
4f9e588494 | ||
![]() |
05a3dfa26d | ||
![]() |
8826cca539 | ||
![]() |
1988feb9ce | ||
![]() |
e9b7273489 | ||
![]() |
315afd6818 | ||
![]() |
93ea31feb0 | ||
![]() |
3b4e414c97 | ||
![]() |
d925c95fc9 | ||
![]() |
4511db1526 | ||
![]() |
56d9e46fd3 | ||
![]() |
cdea149ce7 | ||
![]() |
16254802e3 | ||
![]() |
a7dd9b9198 | ||
![]() |
b81c9ee10c | ||
![]() |
91596be70e | ||
![]() |
72f64e7b10 | ||
![]() |
3cfb17bb62 | ||
![]() |
fe5066c7b5 | ||
![]() |
e13be904cd | ||
![]() |
fda5c7d60f | ||
![]() |
7fc1a782a7 | ||
![]() |
54d27af76a | ||
![]() |
0065f29634 | ||
![]() |
7d034e8a8b | ||
![]() |
81b6c7ef69 | ||
![]() |
89f6b0a8c2 | ||
![]() |
ad1571b902 | ||
![]() |
4b9ab4f63c | ||
![]() |
08829ea721 | ||
![]() |
6c502b4749 | ||
![]() |
6b41538b60 | ||
![]() |
496f5d7384 | ||
![]() |
32c7a16d06 | ||
![]() |
64ecef09bc | ||
![]() |
3e5cb92621 | ||
![]() |
fd5ad2f576 | ||
![]() |
74a5360561 | ||
![]() |
7580e8d1fe | ||
![]() |
f92d63261d | ||
![]() |
4d51986250 | ||
![]() |
e209aaa272 | ||
![]() |
0ef6b1978a | ||
![]() |
ae842f94d0 | ||
![]() |
7121771f92 | ||
![]() |
040d6625df | ||
![]() |
33ddd92198 | ||
![]() |
54e8d60840 | ||
![]() |
db986038c2 | ||
![]() |
de0b5f0046 | ||
![]() |
638b811857 | ||
![]() |
d775a61c95 | ||
![]() |
0f74f03264 | ||
![]() |
f8b93ce93f | ||
![]() |
85d18b8cc8 | ||
![]() |
4d9fe6f804 | ||
![]() |
85159a2c95 | ||
![]() |
990612793f | ||
![]() |
f2235fd2a2 | ||
![]() |
2044cbb21c | ||
![]() |
3d52b82cd4 | ||
![]() |
89a2dd602b | ||
![]() |
3cb954db69 | ||
![]() |
71cfe3cd8e | ||
![]() |
f7efb6c2c9 | ||
![]() |
59b514fa26 | ||
![]() |
0c1f9304f4 | ||
![]() |
e876214eeb | ||
![]() |
5fece5fc68 | ||
![]() |
f3dbf7cc5d | ||
![]() |
f7ac6bb4bb | ||
![]() |
1f5ffe7f9d | ||
![]() |
95715421c6 | ||
![]() |
f5bc99b449 | ||
![]() |
287b833838 | ||
![]() |
0cbbcb8eb1 | ||
![]() |
670ee64179 | ||
![]() |
290b7d9d94 | ||
![]() |
2503bfd3a2 |
@ -2,7 +2,7 @@
|
||||
APP_URL=http://localhost:3000
|
||||
PORT=3000
|
||||
|
||||
# make sure to replace this.
|
||||
# minimum of 32 characters. Generate one with: openssl rand -hex 32
|
||||
APP_SECRET=REPLACE_WITH_LONG_SECRET
|
||||
|
||||
JWT_TOKEN_EXPIRES_IN=30d
|
||||
@ -41,4 +41,6 @@ SMTP_IGNORETLS=false
|
||||
POSTMARK_TOKEN=
|
||||
|
||||
# for custom drawio server
|
||||
DRAWIO_URL=
|
||||
DRAWIO_URL=
|
||||
|
||||
DISABLE_TELEMETRY=false
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,6 @@
|
||||
.env
|
||||
.env.dev
|
||||
.env.prod
|
||||
data
|
||||
# compiled output
|
||||
/dist
|
||||
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "apps/server/src/ee"]
|
||||
path = apps/server/src/ee
|
||||
url = https://github.com/docmost/ee
|
@ -1,4 +1,4 @@
|
||||
FROM node:21-alpine AS base
|
||||
FROM node:22-alpine AS base
|
||||
LABEL org.opencontainers.image.source="https://github.com/docmost/docmost"
|
||||
|
||||
FROM base AS builder
|
||||
@ -7,7 +7,7 @@ WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm install -g pnpm
|
||||
RUN npm install -g pnpm@10.4.0
|
||||
RUN pnpm install --frozen-lockfile
|
||||
RUN pnpm build
|
||||
|
||||
@ -33,7 +33,7 @@ COPY --from=builder /app/pnpm*.yaml /app/
|
||||
# Copy patches
|
||||
COPY --from=builder /app/patches /app/patches
|
||||
|
||||
RUN npm install -g pnpm
|
||||
RUN npm install -g pnpm@10.4.0
|
||||
|
||||
RUN chown -R node:node /app
|
||||
|
||||
|
21
README.md
21
README.md
@ -13,9 +13,11 @@
|
||||
> Docmost is currently in **beta**. We value your feedback as we progress towards a stable release.
|
||||
|
||||
## Getting started
|
||||
|
||||
To get started with Docmost, please refer to our [documentation](https://docmost.com/docs).
|
||||
|
||||
## Features
|
||||
|
||||
- Real-time collaboration
|
||||
- Diagrams (Draw.io, Excalidraw and Mermaid)
|
||||
- Spaces
|
||||
@ -24,13 +26,26 @@ To get started with Docmost, please refer to our [documentation](https://docmost
|
||||
- Comments
|
||||
- Page history
|
||||
- Search
|
||||
- File attachment
|
||||
- File attachments
|
||||
- Embeds (Airtable, Loom, Miro and more)
|
||||
- Translations (10+ languages)
|
||||
|
||||
### Screenshots
|
||||
|
||||
#### Screenshots
|
||||
<p align="center">
|
||||
<img alt="home" src="https://docmost.com/screenshots/home.png" width="70%">
|
||||
<img alt="editor" src="https://docmost.com/screenshots/editor.png" width="70%">
|
||||
</p>
|
||||
|
||||
### Contributing
|
||||
### License
|
||||
Docmost core is licensed under the open-source AGPL 3.0 license.
|
||||
Enterprise features are available under an enterprise license (Enterprise Edition).
|
||||
|
||||
All files in the following directories are licensed under the Docmost Enterprise license defined in `packages/ee/License`.
|
||||
- apps/server/src/ee
|
||||
- apps/client/src/ee
|
||||
- packages/ee
|
||||
|
||||
### Contributing
|
||||
|
||||
See the [development documentation](https://docmost.com/docs/self-hosting/development)
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "client",
|
||||
"private": true,
|
||||
"version": "0.6.1",
|
||||
"version": "0.9.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
@ -16,37 +16,41 @@
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@excalidraw/excalidraw": "^0.17.6",
|
||||
"@mantine/core": "^7.14.2",
|
||||
"@mantine/form": "^7.14.2",
|
||||
"@mantine/hooks": "^7.14.2",
|
||||
"@mantine/modals": "^7.14.2",
|
||||
"@mantine/notifications": "^7.14.2",
|
||||
"@mantine/spotlight": "^7.14.2",
|
||||
"@mantine/core": "^7.17.0",
|
||||
"@mantine/form": "^7.17.0",
|
||||
"@mantine/hooks": "^7.17.0",
|
||||
"@mantine/modals": "^7.17.0",
|
||||
"@mantine/notifications": "^7.17.0",
|
||||
"@mantine/spotlight": "^7.17.0",
|
||||
"@tabler/icons-react": "^3.22.0",
|
||||
"@tanstack/react-query": "^5.61.4",
|
||||
"axios": "^1.7.8",
|
||||
"@tiptap/extension-character-count": "^2.11.5",
|
||||
"axios": "^1.7.9",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"jotai": "^2.10.3",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next-http-backend": "^2.6.1",
|
||||
"jotai": "^2.12.1",
|
||||
"jotai-optics": "^0.4.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"katex": "^0.16.11",
|
||||
"katex": "0.16.21",
|
||||
"lowlight": "^3.2.0",
|
||||
"mermaid": "^11.4.0",
|
||||
"mermaid": "^11.4.1",
|
||||
"mitt": "^3.0.1",
|
||||
"react": "^18.3.1",
|
||||
"react-arborist": "^3.4.0",
|
||||
"react-arborist": "3.4.0",
|
||||
"react-clear-modal": "^2.0.11",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-drawio": "^1.0.1",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-router-dom": "^7.0.1",
|
||||
"semver": "^7.7.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tiptap-extension-global-drag-handle": "^0.1.16",
|
||||
"tiptap-extension-global-drag-handle": "^0.1.18",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -71,6 +75,6 @@
|
||||
"prettier": "^3.4.1",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.17.0",
|
||||
"vite": "^6.0.0"
|
||||
"vite": "^6.1.0"
|
||||
}
|
||||
}
|
||||
|
355
apps/client/public/locales/de-DE/translation.json
Normal file
355
apps/client/public/locales/de-DE/translation.json
Normal file
@ -0,0 +1,355 @@
|
||||
{
|
||||
"Account": "Konto",
|
||||
"Active": "Aktiv",
|
||||
"Add": "Hinzufügen",
|
||||
"Add group members": "Gruppenmitglieder hinzufügen",
|
||||
"Add groups": "Gruppen hinzufügen",
|
||||
"Add members": "Mitglieder hinzufügen",
|
||||
"Add to groups": "Zu Gruppen hinzufügen",
|
||||
"Add space members": "Bereichsmitglieder hinzufügen",
|
||||
"Admin": "Administrator",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Sind Sie sicher, dass Sie diese Gruppe löschen möchten? Mitglieder verlieren den Zugang zu den Ressourcen, auf die diese Gruppe zugreifen kann.",
|
||||
"Are you sure you want to delete this page?": "Sind Sie sicher, dass Sie diese Seite löschen möchten?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Sind Sie sicher, dass Sie diesen Benutzer aus der Gruppe entfernen möchten? Der Benutzer verliert den Zugang zu den Ressourcen, auf die diese Gruppe zugreifen kann.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Sind Sie sicher, dass Sie diesen Benutzer aus dem Bereich entfernen möchten? Der Benutzer verliert den gesamten Zugang zu diesem Bereich.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Sind Sie sicher, dass Sie diese Version wiederherstellen möchten? Alle nicht versionierten Änderungen gehen verloren.",
|
||||
"Can become members of groups and spaces in workspace": "Kann Mitglied von Gruppen und Bereichen im Arbeitsbereich werden",
|
||||
"Can create and edit pages in space.": "Kann Seiten im Bereich erstellen und bearbeiten.",
|
||||
"Can edit": "Kann bearbeiten",
|
||||
"Can manage workspace": "Kann Arbeitsbereich verwalten",
|
||||
"Can manage workspace but cannot delete it": "Kann Arbeitsbereich verwalten, aber nicht löschen",
|
||||
"Can view": "Kann anzeigen",
|
||||
"Can view pages in space but not edit.": "Kann Seiten im Bereich anzeigen, aber nicht bearbeiten.",
|
||||
"Cancel": "Abbrechen",
|
||||
"Change email": "E-Mail ändern",
|
||||
"Change password": "Passwort ändern",
|
||||
"Change photo": "Foto ändern",
|
||||
"Choose a role": "Wählen Sie eine Rolle",
|
||||
"Choose your preferred color scheme.": "Wählen Sie Ihr bevorzugtes Farbschema.",
|
||||
"Choose your preferred interface language.": "Wählen Sie Ihre bevorzugte Benutzersprache.",
|
||||
"Choose your preferred page width.": "Wählen Sie Ihre bevorzugte Seitenbreite.",
|
||||
"Confirm": "Bestätigen",
|
||||
"Copy link": "Link kopieren",
|
||||
"Create": "Erstellen",
|
||||
"Create group": "Gruppe erstellen",
|
||||
"Create page": "Seite erstellen",
|
||||
"Create space": "Bereich erstellen",
|
||||
"Create workspace": "Arbeitsbereich erstellen",
|
||||
"Current password": "Aktuelles Passwort",
|
||||
"Dark": "Dunkel",
|
||||
"Date": "Datum",
|
||||
"Delete": "Löschen",
|
||||
"Delete group": "Gruppe löschen",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sind Sie sicher, dass Sie diese Seite löschen möchten? Dadurch werden ihre Unterseiten und die Seitengeschichte gelöscht. Diese Aktion ist unwiderruflich.",
|
||||
"Description": "Beschreibung",
|
||||
"Details": "Einzelheiten",
|
||||
"e.g ACME": "z.B. ACME",
|
||||
"e.g ACME Inc": "z.B. ACME Inc.",
|
||||
"e.g Developers": "z.B. Entwickler",
|
||||
"e.g Group for developers": "z.B. Gruppe für Entwickler",
|
||||
"e.g product": "z.B. Produkt",
|
||||
"e.g Product Team": "z.B. Produktteam",
|
||||
"e.g Sales": "z.B. Vertrieb",
|
||||
"e.g Space for product team": "z.B. Bereich für das Produktteam",
|
||||
"e.g Space for sales team to collaborate": "z.B. Bereich für das Vertriebsteam zur Zusammenarbeit",
|
||||
"Edit": "Bearbeiten",
|
||||
"Edit group": "Gruppe bearbeiten",
|
||||
"Email": "E-Mail",
|
||||
"Enter a strong password": "Geben Sie ein starkes Passwort ein",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Geben Sie gültige E-Mail-Adressen ein, getrennt durch Kommas oder Leerzeichen [max: 50]",
|
||||
"enter valid emails addresses": "gültige E-Mail-Adressen eingeben",
|
||||
"Enter your current password": "Geben Sie Ihr aktuelles Passwort ein",
|
||||
"enter your full name": "Geben Sie Ihren vollständigen Namen ein",
|
||||
"Enter your new password": "Geben Sie Ihr neues Passwort ein",
|
||||
"Enter your new preferred email": "Geben Sie Ihre neue bevorzugte E-Mail ein",
|
||||
"Enter your password": "Geben Sie Ihr Passwort ein",
|
||||
"Error fetching page data.": "Fehler beim Abrufen der Seitendaten.",
|
||||
"Error loading page history.": "Fehler beim Laden der Seitengeschichte.",
|
||||
"Export": "Exportieren",
|
||||
"Failed to create page": "Erstellung der Seite fehlgeschlagen",
|
||||
"Failed to delete page": "Löschen der Seite fehlgeschlagen",
|
||||
"Failed to fetch recent pages": "Fehler beim Abrufen der letzten Seiten",
|
||||
"Failed to import pages": "Import der Seiten fehlgeschlagen",
|
||||
"Failed to load page. An error occurred.": "Seite konnte nicht geladen werden. Es ist ein Fehler aufgetreten.",
|
||||
"Failed to update data": "Aktualisierung der Daten fehlgeschlagen",
|
||||
"Full access": "Voller Zugriff",
|
||||
"Full page width": "Volle Seitenbreite",
|
||||
"Full width": "Volle Breite",
|
||||
"General": "Allgemein",
|
||||
"Group": "Gruppe",
|
||||
"Group description": "Gruppenbeschreibung",
|
||||
"Group name": "Gruppenname",
|
||||
"Groups": "Gruppen",
|
||||
"Has full access to space settings and pages.": "Hat vollen Zugriff auf die Bereichseinstellungen und Seiten.",
|
||||
"Home": "Startseite",
|
||||
"Import pages": "Seiten importieren",
|
||||
"Import pages & space settings": "Seiten und Bereichseinstellungen importieren",
|
||||
"Importing pages": "Seiten werden importiert",
|
||||
"invalid invitation link": "ungültiger Einladungslink",
|
||||
"Invitation signup": "Einladung zur Anmeldung",
|
||||
"Invite by email": "Einladen per E-Mail",
|
||||
"Invite members": "Mitglieder einladen",
|
||||
"Invite new members": "Neue Mitglieder einladen",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Eingeladene Mitglieder, die ihre Einladung noch nicht angenommen haben, werden hier angezeigt.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Eingeladene Mitglieder erhalten Zugriff auf die Bereiche, auf die die Gruppen zugreifen können",
|
||||
"Join the workspace": "Dem Arbeitsbereich beitreten",
|
||||
"Language": "Sprache",
|
||||
"Light": "Hell",
|
||||
"Link copied": "Link kopiert",
|
||||
"Login": "Anmelden",
|
||||
"Logout": "Abmelden",
|
||||
"Manage Group": "Gruppe verwalten",
|
||||
"Manage members": "Mitglieder verwalten",
|
||||
"member": "Mitglied",
|
||||
"Member": "Mitglied",
|
||||
"members": "Mitglieder",
|
||||
"Members": "Mitglieder",
|
||||
"My preferences": "Meine Vorlieben",
|
||||
"My Profile": "Mein Profil",
|
||||
"My profile": "Mein Profil",
|
||||
"Name": "Name",
|
||||
"New email": "Neue E-Mail",
|
||||
"New page": "Neue Seite",
|
||||
"New password": "Neues Passwort",
|
||||
"No group found": "Keine Gruppe gefunden",
|
||||
"No page history saved yet.": "Es wurde noch keine Seitengeschichte gespeichert.",
|
||||
"No pages yet": "Noch keine Seiten",
|
||||
"No results found...": "Keine Ergebnisse gefunden...",
|
||||
"No user found": "Kein Benutzer gefunden",
|
||||
"Overview": "Überblick",
|
||||
"Owner": "Besitzer",
|
||||
"page": "Seite",
|
||||
"Page deleted successfully": "Seite erfolgreich gelöscht",
|
||||
"Page history": "Seitengeschichte",
|
||||
"Page import is in progress. Please do not close this tab.": "Der Seitenimport läuft. Bitte schließen Sie diesen Tab nicht.",
|
||||
"Pages": "Seiten",
|
||||
"pages": "Seiten",
|
||||
"Password": "Passwort",
|
||||
"Password changed successfully": "Passwort erfolgreich geändert",
|
||||
"Pending": "Ausstehend",
|
||||
"Please confirm your action": "Bitte bestätigen Sie Ihre Aktion",
|
||||
"Preferences": "Vorlieben",
|
||||
"Print PDF": "PDF drucken",
|
||||
"Profile": "Profil",
|
||||
"Recently updated": "Kürzlich aktualisiert",
|
||||
"Remove": "Entfernen",
|
||||
"Remove group member": "Gruppenmitglied entfernen",
|
||||
"Remove space member": "Bereichsmitglied entfernen",
|
||||
"Restore": "Wiederherstellen",
|
||||
"Role": "Rolle",
|
||||
"Save": "Speichern",
|
||||
"Search": "Suche",
|
||||
"Search for groups": "Suche nach Gruppen",
|
||||
"Search for users": "Suche nach Benutzern",
|
||||
"Search for users and groups": "Suche nach Benutzern und Gruppen",
|
||||
"Search...": "Suche...",
|
||||
"Select language": "Sprache auswählen",
|
||||
"Select role": "Rolle auswählen",
|
||||
"Select role to assign to all invited members": "Rolle für alle eingeladenen Mitglieder auswählen",
|
||||
"Select theme": "Design auswählen",
|
||||
"Send invitation": "Einladung senden",
|
||||
"Invitation sent": "Einladung gesendet",
|
||||
"Settings": "Einstellungen",
|
||||
"Setup workspace": "Arbeitsbereich einrichten",
|
||||
"Sign In": "Anmelden",
|
||||
"Sign Up": "Registrieren",
|
||||
"Slug": "Slug",
|
||||
"Space": "Bereich",
|
||||
"Space description": "Bereichsbeschreibung",
|
||||
"Space menu": "Bereichsmenü",
|
||||
"Space name": "Bereichsname",
|
||||
"Space settings": "Bereichseinstellungen",
|
||||
"Space slug": "Slug des Bereichs",
|
||||
"Spaces": "Bereiche",
|
||||
"Spaces you belong to": "Bereiche, denen Sie angehören",
|
||||
"No space found": "Keine Bereiche gefunden",
|
||||
"Search for spaces": "Nach Bereichen suchen",
|
||||
"Start typing to search...": "Anfangen zu tippen, um zu suchen...",
|
||||
"Status": "Status",
|
||||
"Successfully imported": "Erfolgreich importiert",
|
||||
"Successfully restored": "Erfolgreich wiederhergestellt",
|
||||
"System settings": "Systemeinstellungen",
|
||||
"Theme": "Design",
|
||||
"To change your email, you have to enter your password and new email.": "Um Ihre E-Mail-Adresse zu ändern, müssen Sie Ihr Passwort und Ihre neue E-Mail-Adresse eingeben.",
|
||||
"Toggle full page width": "Volle Seitenbreite umschalten",
|
||||
"Unable to import pages. Please try again.": "Seiten konnten nicht importiert werden. Bitte versuchen Sie es erneut.",
|
||||
"untitled": "ohne Titel",
|
||||
"Untitled": "Ohne Titel",
|
||||
"Updated successfully": "Erfolgreich aktualisiert",
|
||||
"User": "Benutzer",
|
||||
"Workspace": "Arbeitsbereich",
|
||||
"Workspace Name": "Arbeitsbereichsname",
|
||||
"Workspace settings": "Arbeitsbereich-Einstellungen",
|
||||
"You can change your password here.": "Hier können Sie Ihr Passwort ändern.",
|
||||
"Your Email": "Ihre E-Mail",
|
||||
"Your import is complete.": "Ihr Import ist abgeschlossen.",
|
||||
"Your name": "Ihr Name",
|
||||
"Your Name": "Ihr Name",
|
||||
"Your password": "Ihr Passwort",
|
||||
"Your password must be a minimum of 8 characters.": "Ihr Passwort muss mindestens 8 Zeichen lang sein.",
|
||||
"Sidebar toggle": "Seitenleiste umschalten",
|
||||
"Comments": "Kommentare",
|
||||
"404 page not found": "404 Seite nicht gefunden",
|
||||
"Sorry, we can't find the page you are looking for.": "Entschuldigung, wir können die gesuchte Seite nicht finden.",
|
||||
"Take me back to homepage": "Zurück zur Startseite",
|
||||
"Forgot password": "Passwort vergessen",
|
||||
"Forgot your password?": "Passwort vergessen?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Ein Link zum Zurücksetzen des Passworts wurde an Ihre E-Mail gesendet. Bitte überprüfen Sie Ihren Posteingang.",
|
||||
"Send reset link": "Zurücksetzungslink senden",
|
||||
"Password reset": "Passwort zurücksetzen",
|
||||
"Your new password": "Ihr neues Passwort",
|
||||
"Set password": "Passwort festlegen",
|
||||
"Write a comment": "Einen Kommentar schreiben",
|
||||
"Reply...": "Antworten...",
|
||||
"Error loading comments.": "Fehler beim Laden der Kommentare.",
|
||||
"No comments yet.": "Noch keine Kommentare.",
|
||||
"Edit comment": "Kommentar bearbeiten",
|
||||
"Delete comment": "Kommentar löschen",
|
||||
"Are you sure you want to delete this comment?": "Sind Sie sicher, dass Sie diesen Kommentar löschen möchten?",
|
||||
"Comment created successfully": "Kommentar erfolgreich erstellt",
|
||||
"Error creating comment": "Fehler beim Erstellen des Kommentars",
|
||||
"Comment updated successfully": "Kommentar erfolgreich aktualisiert",
|
||||
"Failed to update comment": "Aktualisierung des Kommentars fehlgeschlagen",
|
||||
"Comment deleted successfully": "Kommentar erfolgreich gelöscht",
|
||||
"Failed to delete comment": "Löschen des Kommentars fehlgeschlagen",
|
||||
"Comment resolved successfully": "Kommentar erfolgreich gelöst",
|
||||
"Failed to resolve comment": "Lösen des Kommentars fehlgeschlagen",
|
||||
"Revoke invitation": "Einladung widerrufen",
|
||||
"Revoke": "Widerrufen",
|
||||
"Don't": "Nicht",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Sind Sie sicher, dass Sie diese Einladung widerrufen möchten? Der Benutzer kann dem Arbeitsbereich nicht beitreten.",
|
||||
"Resend invitation": "Einladung erneut senden",
|
||||
"Anyone with this link can join this workspace.": "Jeder mit diesem Link kann dem Arbeitsbereich beitreten.",
|
||||
"Invite link": "Einladungslink",
|
||||
"Copy": "Kopieren",
|
||||
"Copied": "Kopiert",
|
||||
"Select a user": "Benutzer auswählen",
|
||||
"Select a group": "Gruppe auswählen",
|
||||
"Export all pages and attachments in this space.": "Alle Seiten und Anhänge in diesem Bereich exportieren.",
|
||||
"Delete space": "Bereich löschen",
|
||||
"Are you sure you want to delete this space?": "Sind Sie sicher, dass Sie diesen Bereich löschen möchten?",
|
||||
"Delete this space with all its pages and data.": "Diesen Bereich mit allen Seiten und Daten löschen.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Alle Seiten, Kommentare, Anhänge und Berechtigungen in diesem Bereich werden unwiderruflich gelöscht.",
|
||||
"Confirm space name": "Bestätigen Sie den Namen des Arbeitsbereichs",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Geben Sie den Namen des Bereichs <b>{{spaceName}}</b> ein, um Ihre Aktion zu bestätigen.",
|
||||
"Format": "Format",
|
||||
"Include subpages": "Unterseiten einbeziehen",
|
||||
"Include attachments": "Anhänge einbeziehen",
|
||||
"Select export format": "Exportformat auswählen",
|
||||
"Export failed:": "Export fehlgeschlagen:",
|
||||
"export error": "Exportfehler",
|
||||
"Export page": "Seite exportieren",
|
||||
"Export space": "Bereich exportieren",
|
||||
"Export {{type}}": "Exportiere {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Datei überschreitet das Anhängelimit von {{limit}}",
|
||||
"Align left": "Links ausrichten",
|
||||
"Align right": "Rechts ausrichten",
|
||||
"Align center": "Zentrieren",
|
||||
"Justify": "Blocksatz",
|
||||
"Merge cells": "Zellen zusammenführen",
|
||||
"Split cell": "Zelle teilen",
|
||||
"Delete column": "Spalte löschen",
|
||||
"Delete row": "Zeile löschen",
|
||||
"Add left column": "Linke Spalte hinzufügen",
|
||||
"Add right column": "Rechte Spalte hinzufügen",
|
||||
"Add row above": "Zeile oben hinzufügen",
|
||||
"Add row below": "Zeile unten hinzufügen",
|
||||
"Delete table": "Tabelle löschen",
|
||||
"Info": "Info",
|
||||
"Success": "Erfolg",
|
||||
"Warning": "Warnung",
|
||||
"Danger": "Gefahr",
|
||||
"Mermaid diagram error:": "Fehler im Mermaid-Diagramm:",
|
||||
"Invalid Mermaid diagram": "Ungültiges Mermaid-Diagramm",
|
||||
"Double-click to edit Draw.io diagram": "Zum Bearbeiten des Draw.io-Diagramms doppelklicken",
|
||||
"Exit": "Beenden",
|
||||
"Save & Exit": "Speichern & Beenden",
|
||||
"Double-click to edit Excalidraw diagram": "Zum Bearbeiten des Excalidraw-Diagramms doppelklicken",
|
||||
"Paste link": "Link einfügen",
|
||||
"Edit link": "Link bearbeiten",
|
||||
"Remove link": "Link entfernen",
|
||||
"Add link": "Link hinzufügen",
|
||||
"Please enter a valid url": "Bitte geben Sie eine gültige URL ein",
|
||||
"Empty equation": "Leere Gleichung",
|
||||
"Invalid equation": "Ungültige Gleichung",
|
||||
"Color": "Farbe",
|
||||
"Text color": "Textfarbe",
|
||||
"Default": "Standard",
|
||||
"Blue": "Blau",
|
||||
"Green": "Grün",
|
||||
"Purple": "Lila",
|
||||
"Red": "Rot",
|
||||
"Yellow": "Gelb",
|
||||
"Orange": "Orange",
|
||||
"Pink": "Rosa",
|
||||
"Gray": "Grau",
|
||||
"Embed link": "Link einbetten",
|
||||
"Invalid {{provider}} embed link": "Ungültiger {{provider}}-Einbettungslink",
|
||||
"Embed {{provider}}": "{{provider}} einbetten",
|
||||
"Enter {{provider}} link to embed": "Geben Sie den Einbettungslink für {{provider}} ein",
|
||||
"Bold": "Fett",
|
||||
"Italic": "Kursiv",
|
||||
"Underline": "Unterstreichen",
|
||||
"Strike": "Durchstreichen",
|
||||
"Code": "Code",
|
||||
"Comment": "Kommentar",
|
||||
"Text": "Text",
|
||||
"Heading 1": "Überschrift 1",
|
||||
"Heading 2": "Überschrift 2",
|
||||
"Heading 3": "Überschrift 3",
|
||||
"To-do List": "To-do-Liste",
|
||||
"Bullet List": "Aufzählungsliste",
|
||||
"Numbered List": "Nummerierte Liste",
|
||||
"Blockquote": "Blockzitat",
|
||||
"Just start typing with plain text.": "Tippen Sie einfach mit normalem Text los.",
|
||||
"Track tasks with a to-do list.": "Verfolgen Sie Aufgaben mit einer To-do-Liste.",
|
||||
"Big section heading.": "Große Abschnittsüberschrift.",
|
||||
"Medium section heading.": "Mittlere Abschnittsüberschrift.",
|
||||
"Small section heading.": "Kleine Abschnittsüberschrift.",
|
||||
"Create a simple bullet list.": "Erstellen Sie eine einfache Aufzählungsliste.",
|
||||
"Create a list with numbering.": "Erstellen Sie eine nummerierte Liste.",
|
||||
"Create block quote.": "Erstellen Sie ein Blockzitat.",
|
||||
"Insert code snippet.": "Code-Snippet einfügen.",
|
||||
"Insert horizontal rule divider": "Horizontale Trennlinie einfügen",
|
||||
"Upload any image from your device.": "Laden Sie ein beliebiges Bild von Ihrem Gerät hoch.",
|
||||
"Upload any video from your device.": "Laden Sie ein beliebiges Video von Ihrem Gerät hoch.",
|
||||
"Upload any file from your device.": "Laden Sie eine beliebige Datei von Ihrem Gerät hoch.",
|
||||
"Table": "Tabelle",
|
||||
"Insert a table.": "Tabelle einfügen.",
|
||||
"Insert collapsible block.": "Einklappbaren Block einfügen.",
|
||||
"Video": "Video",
|
||||
"Divider": "Trennlinie",
|
||||
"Quote": "Zitat",
|
||||
"Image": "Bild",
|
||||
"File attachment": "Dateianhang",
|
||||
"Toggle block": "Block umschalten",
|
||||
"Callout": "Hinweisbox",
|
||||
"Insert callout notice.": "Hinweisbox einfügen.",
|
||||
"Math inline": "Mathe inline",
|
||||
"Insert inline math equation.": "Mathe-Gleichung inline einfügen.",
|
||||
"Math block": "Matheblock",
|
||||
"Insert math equation": "Mathe-Gleichung einfügen",
|
||||
"Mermaid diagram": "Mermaid-Diagramm",
|
||||
"Insert mermaid diagram": "Mermaid-Diagramm einfügen",
|
||||
"Insert and design Drawio diagrams": "Drawio-Diagramme einfügen und gestalten",
|
||||
"Insert current date": "Aktuelles Datum einfügen",
|
||||
"Draw and sketch excalidraw diagrams": "Excalidraw-Diagramme zeichnen und skizzieren",
|
||||
"Multiple": "Mehrere",
|
||||
"Heading {{level}}": "Überschrift {{level}}",
|
||||
"Toggle title": "Titel umschalten",
|
||||
"Write anything. Enter \"/\" for commands": "Schreiben Sie irgendetwas. Geben Sie \"/\" für Befehle ein",
|
||||
"Names do not match": "Namen stimmen nicht überein",
|
||||
"Today, {{time}}": "Heute, {{time}}",
|
||||
"Yesterday, {{time}}": "Gestern, {{time}}",
|
||||
"Space created successfully": "Der Bereich wurde erfolgreich erstellt",
|
||||
"Space updated successfully": "Der Bereich wurde erfolgreich aktualisiert",
|
||||
"Space deleted successfully": "Der Bereich wurde erfolgreich gelöscht",
|
||||
"Members added successfully": "Mitglieder erfolgreich hinzugefügt",
|
||||
"Member removed successfully": "Mitglied erfolgreich entfernt",
|
||||
"Member role updated successfully": "Mitgliederrolle erfolgreich aktualisiert",
|
||||
"Created by: <b>{{creatorName}}</b>": "Erstellt von: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Erstellt am: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Bearbeitet von {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Wortanzahl: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Zeichenzahl: {{characterCount}}"
|
||||
}
|
357
apps/client/public/locales/en-US/translation.json
Normal file
357
apps/client/public/locales/en-US/translation.json
Normal file
@ -0,0 +1,357 @@
|
||||
{
|
||||
"Account": "Account",
|
||||
"Active": "Active",
|
||||
"Add": "Add",
|
||||
"Add group members": "Add group members",
|
||||
"Add groups": "Add groups",
|
||||
"Add members": "Add members",
|
||||
"Add to groups": "Add to groups",
|
||||
"Add space members": "Add space members",
|
||||
"Admin": "Admin",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Are you sure you want to delete this group? Members will lose access to resources this group has access to.",
|
||||
"Are you sure you want to delete this page?": "Are you sure you want to delete this page?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Are you sure you want to remove this user from the space? The user will lose all access to this space.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Are you sure you want to restore this version? Any changes not versioned will be lost.",
|
||||
"Can become members of groups and spaces in workspace": "Can become members of groups and spaces in workspace",
|
||||
"Can create and edit pages in space.": "Can create and edit pages in space.",
|
||||
"Can edit": "Can edit",
|
||||
"Can manage workspace": "Can manage workspace",
|
||||
"Can manage workspace but cannot delete it": "Can manage workspace but cannot delete it",
|
||||
"Can view": "Can view",
|
||||
"Can view pages in space but not edit.": "Can view pages in space but not edit.",
|
||||
"Cancel": "Cancel",
|
||||
"Change email": "Change email",
|
||||
"Change password": "Change password",
|
||||
"Change photo": "Change photo",
|
||||
"Choose a role": "Choose a role",
|
||||
"Choose your preferred color scheme.": "Choose your preferred color scheme.",
|
||||
"Choose your preferred interface language.": "Choose your preferred interface language.",
|
||||
"Choose your preferred page width.": "Choose your preferred page width.",
|
||||
"Confirm": "Confirm",
|
||||
"Copy link": "Copy link",
|
||||
"Create": "Create",
|
||||
"Create group": "Create group",
|
||||
"Create page": "Create page",
|
||||
"Create space": "Create space",
|
||||
"Create workspace": "Create workspace",
|
||||
"Current password": "Current password",
|
||||
"Dark": "Dark",
|
||||
"Date": "Date",
|
||||
"Delete": "Delete",
|
||||
"Delete group": "Delete group",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.",
|
||||
"Description": "Description",
|
||||
"Details": "Details",
|
||||
"e.g ACME": "e.g ACME",
|
||||
"e.g ACME Inc": "e.g ACME Inc",
|
||||
"e.g Developers": "e.g Developers",
|
||||
"e.g Group for developers": "e.g Group for developers",
|
||||
"e.g product": "e.g product",
|
||||
"e.g Product Team": "e.g Product Team",
|
||||
"e.g Sales": "e.g Sales",
|
||||
"e.g Space for product team": "e.g Space for product team",
|
||||
"e.g Space for sales team to collaborate": "e.g Space for sales team to collaborate",
|
||||
"Edit": "Edit",
|
||||
"Edit group": "Edit group",
|
||||
"Email": "Email",
|
||||
"Enter a strong password": "Enter a strong password",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Enter valid email addresses separated by comma or space [max: 50]",
|
||||
"enter valid emails addresses": "enter valid emails addresses",
|
||||
"Enter your current password": "Enter your current password",
|
||||
"enter your full name": "enter your full name",
|
||||
"Enter your new password": "Enter your new password",
|
||||
"Enter your new preferred email": "Enter your new preferred email",
|
||||
"Enter your password": "Enter your password",
|
||||
"Error fetching page data.": "Error fetching page data.",
|
||||
"Error loading page history.": "Error loading page history.",
|
||||
"Export": "Export",
|
||||
"Failed to create page": "Failed to create page",
|
||||
"Failed to delete page": "Failed to delete page",
|
||||
"Failed to fetch recent pages": "Failed to fetch recent pages",
|
||||
"Failed to import pages": "Failed to import pages",
|
||||
"Failed to load page. An error occurred.": "Failed to load page. An error occurred.",
|
||||
"Failed to update data": "Failed to update data",
|
||||
"Full access": "Full access",
|
||||
"Full page width": "Full page width",
|
||||
"Full width": "Full width",
|
||||
"General": "General",
|
||||
"Group": "Group",
|
||||
"Group description": "Group description",
|
||||
"Group name": "Group name",
|
||||
"Groups": "Groups",
|
||||
"Has full access to space settings and pages.": "Has full access to space settings and pages.",
|
||||
"Home": "Home",
|
||||
"Import pages": "Import pages",
|
||||
"Import pages & space settings": "Import pages & space settings",
|
||||
"Importing pages": "Importing pages",
|
||||
"invalid invitation link": "invalid invitation link",
|
||||
"Invitation signup": "Invitation signup",
|
||||
"Invite by email": "Invite by email",
|
||||
"Invite members": "Invite members",
|
||||
"Invite new members": "Invite new members",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Invited members who are yet to accept their invitation will appear here.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Invited members will be granted access to spaces the groups can access",
|
||||
"Join the workspace": "Join the workspace",
|
||||
"Language": "Language",
|
||||
"Light": "Light",
|
||||
"Link copied": "Link copied",
|
||||
"Login": "Login",
|
||||
"Logout": "Logout",
|
||||
"Manage Group": "Manage Group",
|
||||
"Manage members": "Manage members",
|
||||
"member": "member",
|
||||
"Member": "Member",
|
||||
"members": "members",
|
||||
"Members": "Members",
|
||||
"My preferences": "My preferences",
|
||||
"My Profile": "My Profile",
|
||||
"My profile": "My profile",
|
||||
"Name": "Name",
|
||||
"New email": "New email",
|
||||
"New page": "New page",
|
||||
"New password": "New password",
|
||||
"No group found": "No group found",
|
||||
"No page history saved yet.": "No page history saved yet.",
|
||||
"No pages yet": "No pages yet",
|
||||
"No results found...": "No results found...",
|
||||
"No user found": "No user found",
|
||||
"Overview": "Overview",
|
||||
"Owner": "Owner",
|
||||
"page": "page",
|
||||
"Page deleted successfully": "Page deleted successfully",
|
||||
"Page history": "Page history",
|
||||
"Page import is in progress. Please do not close this tab.": "Page import is in progress. Please do not close this tab.",
|
||||
"Pages": "Pages",
|
||||
"pages": "pages",
|
||||
"Password": "Password",
|
||||
"Password changed successfully": "Password changed successfully",
|
||||
"Pending": "Pending",
|
||||
"Please confirm your action": "Please confirm your action",
|
||||
"Preferences": "Preferences",
|
||||
"Print PDF": "Print PDF",
|
||||
"Profile": "Profile",
|
||||
"Recently updated": "Recently updated",
|
||||
"Remove": "Remove",
|
||||
"Remove group member": "Remove group member",
|
||||
"Remove space member": "Remove space member",
|
||||
"Restore": "Restore",
|
||||
"Role": "Role",
|
||||
"Save": "Save",
|
||||
"Search": "Search",
|
||||
"Search for groups": "Search for groups",
|
||||
"Search for users": "Search for users",
|
||||
"Search for users and groups": "Search for users and groups",
|
||||
"Search...": "Search...",
|
||||
"Select language": "Select language",
|
||||
"Select role": "Select role",
|
||||
"Select role to assign to all invited members": "Select role to assign to all invited members",
|
||||
"Select theme": "Select theme",
|
||||
"Send invitation": "Send invitation",
|
||||
"Invitation sent": "Invitation sent",
|
||||
"Settings": "Settings",
|
||||
"Setup workspace": "Setup workspace",
|
||||
"Sign In": "Sign In",
|
||||
"Sign Up": "Sign Up",
|
||||
"Slug": "Slug",
|
||||
"Space": "Space",
|
||||
"Space description": "Space description",
|
||||
"Space menu": "Space menu",
|
||||
"Space name": "Space name",
|
||||
"Space settings": "Space settings",
|
||||
"Space slug": "Space slug",
|
||||
"Spaces": "Spaces",
|
||||
"Spaces you belong to": "Spaces you belong to",
|
||||
"No space found": "No space found",
|
||||
"Search for spaces": "Search for spaces",
|
||||
"Start typing to search...": "Start typing to search...",
|
||||
"Status": "Status",
|
||||
"Successfully imported": "Successfully imported",
|
||||
"Successfully restored": "Successfully restored",
|
||||
"System settings": "System settings",
|
||||
"Theme": "Theme",
|
||||
"To change your email, you have to enter your password and new email.": "To change your email, you have to enter your password and new email.",
|
||||
"Toggle full page width": "Toggle full page width",
|
||||
"Unable to import pages. Please try again.": "Unable to import pages. Please try again.",
|
||||
"untitled": "untitled",
|
||||
"Untitled": "Untitled",
|
||||
"Updated successfully": "Updated successfully",
|
||||
"User": "User",
|
||||
"Workspace": "Workspace",
|
||||
"Workspace Name": "Workspace Name",
|
||||
"Workspace settings": "Workspace settings",
|
||||
"You can change your password here.": "You can change your password here.",
|
||||
"Your Email": "Your Email",
|
||||
"Your import is complete.": "Your import is complete.",
|
||||
"Your name": "Your name",
|
||||
"Your Name": "Your Name",
|
||||
"Your password": "Your password",
|
||||
"Your password must be a minimum of 8 characters.": "Your password must be a minimum of 8 characters.",
|
||||
"Sidebar toggle": "Sidebar toggle",
|
||||
"Comments": "Comments",
|
||||
"404 page not found": "404 page not found",
|
||||
"Sorry, we can't find the page you are looking for.": "Sorry, we can't find the page you are looking for.",
|
||||
"Take me back to homepage": "Take me back to homepage",
|
||||
"Forgot password": "Forgot password",
|
||||
"Forgot your password?": "Forgot your password?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "A password reset link has been sent to your email. Please check your inbox.",
|
||||
"Send reset link": "Send reset link",
|
||||
"Password reset": "Password reset",
|
||||
"Your new password": "Your new password",
|
||||
"Set password": "Set password",
|
||||
"Write a comment": "Write a comment",
|
||||
"Reply...": "Reply...",
|
||||
"Error loading comments.": "Error loading comments.",
|
||||
"No comments yet.": "No comments yet.",
|
||||
"Edit comment": "Edit comment",
|
||||
"Delete comment": "Delete comment",
|
||||
"Are you sure you want to delete this comment?": "Are you sure you want to delete this comment?",
|
||||
"Comment created successfully": "Comment created successfully",
|
||||
"Error creating comment": "Error creating comment",
|
||||
"Comment updated successfully": "Comment updated successfully",
|
||||
"Failed to update comment": "Failed to update comment",
|
||||
"Comment deleted successfully": "Comment deleted successfully",
|
||||
"Failed to delete comment": "Failed to delete comment",
|
||||
"Comment resolved successfully": "Comment resolved successfully",
|
||||
"Failed to resolve comment": "Failed to resolve comment",
|
||||
"Revoke invitation": "Revoke invitation",
|
||||
"Revoke": "Revoke",
|
||||
"Don't": "Don't",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Are you sure you want to revoke this invitation? The user will not be able to join the workspace.",
|
||||
"Resend invitation": "Resend invitation",
|
||||
"Anyone with this link can join this workspace.": "Anyone with this link can join this workspace.",
|
||||
"Invite link": "Invite link",
|
||||
"Copy": "Copy",
|
||||
"Copied": "Copied",
|
||||
"Select a user": "Select a user",
|
||||
"Select a group": "Select a group",
|
||||
"Export all pages and attachments in this space.": "Export all pages and attachments in this space.",
|
||||
"Delete space": "Delete space",
|
||||
"Are you sure you want to delete this space?": "Are you sure you want to delete this space?",
|
||||
"Delete this space with all its pages and data.": "Delete this space with all its pages and data.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "All pages, comments, attachments and permissions in this space will be deleted irreversibly.",
|
||||
"Confirm space name": "Confirm space name",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Type the space name <b>{{spaceName}}</b> to confirm your action.",
|
||||
"Format": "Format",
|
||||
"Include subpages": "Include subpages",
|
||||
"Include attachments": "Include attachments",
|
||||
"Select export format": "Select export format",
|
||||
"Export failed:": "Export failed:",
|
||||
"export error": "export error",
|
||||
"Export page": "Export page",
|
||||
"Export space": "Export space",
|
||||
"Export {{type}}": "Export {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "File exceeds the {{limit}} attachment limit",
|
||||
"Align left": "Align left",
|
||||
"Align right": "Align right",
|
||||
"Align center": "Align center",
|
||||
"Justify": "Justify",
|
||||
"Merge cells": "Merge cells",
|
||||
"Split cell": "Split cell",
|
||||
"Delete column": "Delete column",
|
||||
"Delete row": "Delete row",
|
||||
"Add left column": "Add left column",
|
||||
"Add right column": "Add right column",
|
||||
"Add row above": "Add row above",
|
||||
"Add row below": "Add row below",
|
||||
"Delete table": "Delete table",
|
||||
"Info": "Info",
|
||||
"Success": "Success",
|
||||
"Warning": "Warning",
|
||||
"Danger": "Danger",
|
||||
"Mermaid diagram error:": "Mermaid diagram error:",
|
||||
"Invalid Mermaid diagram": "Invalid Mermaid diagram",
|
||||
"Double-click to edit Draw.io diagram": "Double-click to edit Draw.io diagram",
|
||||
"Exit": "Exit",
|
||||
"Save & Exit": "Save & Exit",
|
||||
"Double-click to edit Excalidraw diagram": "Double-click to edit Excalidraw diagram",
|
||||
"Paste link": "Paste link",
|
||||
"Edit link": "Edit link",
|
||||
"Remove link": "Remove link",
|
||||
"Add link": "Add link",
|
||||
"Please enter a valid url": "Please enter a valid url",
|
||||
"Empty equation": "Empty equation",
|
||||
"Invalid equation": "Invalid equation",
|
||||
"Color": "Color",
|
||||
"Text color": "Text color",
|
||||
"Default": "Default",
|
||||
"Blue": "Blue",
|
||||
"Green": "Green",
|
||||
"Purple": "Purple",
|
||||
"Red": "Red",
|
||||
"Yellow": "Yellow",
|
||||
"Orange": "Orange",
|
||||
"Pink": "Pink",
|
||||
"Gray": "Gray",
|
||||
"Embed link": "Embed link",
|
||||
"Invalid {{provider}} embed link": "Invalid {{provider}} embed link",
|
||||
"Embed {{provider}}": "Embed {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Enter {{provider}} link to embed",
|
||||
"Bold": "Bold",
|
||||
"Italic": "Italic",
|
||||
"Underline": "Underline",
|
||||
"Strike": "Strike",
|
||||
"Code": "Code",
|
||||
"Comment": "Comment",
|
||||
"Text": "Text",
|
||||
"Heading 1": "Heading 1",
|
||||
"Heading 2": "Heading 2",
|
||||
"Heading 3": "Heading 3",
|
||||
"To-do List": "To-do List",
|
||||
"Bullet List": "Bullet List",
|
||||
"Numbered List": "Numbered List",
|
||||
"Blockquote": "Blockquote",
|
||||
"Just start typing with plain text.": "Just start typing with plain text.",
|
||||
"Track tasks with a to-do list.": "Track tasks with a to-do list.",
|
||||
"Big section heading.": "Big section heading.",
|
||||
"Medium section heading.": "Medium section heading.",
|
||||
"Small section heading.": "Small section heading.",
|
||||
"Create a simple bullet list.": "Create a simple bullet list.",
|
||||
"Create a list with numbering.": "Create a list with numbering.",
|
||||
"Create block quote.": "Create block quote.",
|
||||
"Insert code snippet.": "Insert code snippet.",
|
||||
"Insert horizontal rule divider": "Insert horizontal rule divider",
|
||||
"Upload any image from your device.": "Upload any image from your device.",
|
||||
"Upload any video from your device.": "Upload any video from your device.",
|
||||
"Upload any file from your device.": "Upload any file from your device.",
|
||||
"Table": "Table",
|
||||
"Insert a table.": "Insert a table.",
|
||||
"Insert collapsible block.": "Insert collapsible block.",
|
||||
"Video": "Video",
|
||||
"Divider": "Divider",
|
||||
"Quote": "Quote",
|
||||
"Image": "Image",
|
||||
"File attachment": "File attachment",
|
||||
"Toggle block": "Toggle block",
|
||||
"Callout": "Callout",
|
||||
"Insert callout notice.": "Insert callout notice.",
|
||||
"Math inline": "Math inline",
|
||||
"Insert inline math equation.": "Insert inline math equation.",
|
||||
"Math block": "Math block",
|
||||
"Insert math equation": "Insert math equation",
|
||||
"Mermaid diagram": "Mermaid diagram",
|
||||
"Insert mermaid diagram": "Insert mermaid diagram",
|
||||
"Insert and design Drawio diagrams": "Insert and design Drawio diagrams",
|
||||
"Insert current date": "Insert current date",
|
||||
"Draw and sketch excalidraw diagrams": "Draw and sketch excalidraw diagrams",
|
||||
"Multiple": "Multiple",
|
||||
"Heading {{level}}": "Heading {{level}}",
|
||||
"Toggle title": "Toggle title",
|
||||
"Write anything. Enter \"/\" for commands": "Write anything. Enter \"/\" for commands",
|
||||
"Names do not match": "Names do not match",
|
||||
"Today, {{time}}": "Today, {{time}}",
|
||||
"Yesterday, {{time}}": "Yesterday, {{time}}",
|
||||
"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>": "Created by: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Created at: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Character count: {{characterCount}}",
|
||||
"New update": "New update",
|
||||
"{{latestVersion}} is available": "{{latestVersion}} is available"
|
||||
}
|
355
apps/client/public/locales/es-ES/translation.json
Normal file
355
apps/client/public/locales/es-ES/translation.json
Normal file
@ -0,0 +1,355 @@
|
||||
{
|
||||
"Account": "Cuenta",
|
||||
"Active": "Activo",
|
||||
"Add": "Agregar",
|
||||
"Add group members": "Agregar miembros del grupo",
|
||||
"Add groups": "Agregar grupos",
|
||||
"Add members": "Agregar miembros",
|
||||
"Add to groups": "Agregar a grupos",
|
||||
"Add space members": "Agregar miembros al espacio",
|
||||
"Admin": "Administrador",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "¿Estás seguro de que deseas eliminar este grupo? Los miembros perderán acceso a los recursos a los que este grupo tiene acceso.",
|
||||
"Are you sure you want to delete this page?": "¿Está seguro de que desea eliminar esta página?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "¿Está seguro de que desea eliminar a este usuario del grupo? El usuario perderá acceso a los recursos a los que tiene acceso este grupo.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "¿Está seguro de que desea eliminar a este usuario del espacio? El usuario perderá todo acceso a este espacio.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "¿Está seguro de que desea restaurar esta versión? Cualquier cambio no versionado se perderá.",
|
||||
"Can become members of groups and spaces in workspace": "Pueden convertirse en miembros de grupos y espacios en el espacio de trabajo",
|
||||
"Can create and edit pages in space.": "Puede crear y editar páginas en el espacio.",
|
||||
"Can edit": "Puede editar",
|
||||
"Can manage workspace": "Puede gestionar el espacio de trabajo",
|
||||
"Can manage workspace but cannot delete it": "Puede gestionar el espacio de trabajo pero no puede eliminarlo",
|
||||
"Can view": "Puede ver",
|
||||
"Can view pages in space but not edit.": "Puede ver páginas en el espacio pero no editarlas.",
|
||||
"Cancel": "Cancelar",
|
||||
"Change email": "Cambiar correo electrónico",
|
||||
"Change password": "Cambiar contraseña",
|
||||
"Change photo": "Cambiar foto",
|
||||
"Choose a role": "Seleccione un rol",
|
||||
"Choose your preferred color scheme.": "Elige tu esquema de color preferido.",
|
||||
"Choose your preferred interface language.": "Elige tu idioma de interfaz preferido.",
|
||||
"Choose your preferred page width.": "Elige el ancho de página que prefieras.",
|
||||
"Confirm": "Confirmar",
|
||||
"Copy link": "Copiar enlace",
|
||||
"Create": "Crear",
|
||||
"Create group": "Crear grupo",
|
||||
"Create page": "Crear página",
|
||||
"Create space": "Crear espacio",
|
||||
"Create workspace": "Crear espacio de trabajo",
|
||||
"Current password": "Contraseña actual",
|
||||
"Dark": "Oscuro",
|
||||
"Date": "Fecha",
|
||||
"Delete": "Eliminar",
|
||||
"Delete group": "Eliminar grupo",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "¿Está seguro de que desea eliminar esta página? Esto eliminará sus dependientes y el historial de la página. Esta acción es irreversible.",
|
||||
"Description": "Descripción",
|
||||
"Details": "Detalles",
|
||||
"e.g ACME": "ej: ACME",
|
||||
"e.g ACME Inc": "ej: ACME Inc",
|
||||
"e.g Developers": "ej: Desarrolladores",
|
||||
"e.g Group for developers": "ej: Grupo para desarrolladores",
|
||||
"e.g product": "ej: producto",
|
||||
"e.g Product Team": "ej: Equipo de Producto",
|
||||
"e.g Sales": "ej: Ventas",
|
||||
"e.g Space for product team": "ej: Espacio para el equipo de producto",
|
||||
"e.g Space for sales team to collaborate": "ej: Espacio para que el equipo de ventas colabore",
|
||||
"Edit": "Editar",
|
||||
"Edit group": "Editar grupo",
|
||||
"Email": "Correo electrónico",
|
||||
"Enter a strong password": "Introduce una contraseña fuerte",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Ingrese direcciones de correo electrónico válidas separadas por coma o espacio [max: 50]",
|
||||
"enter valid emails addresses": "introduce direcciones de correo electrónico válidas",
|
||||
"Enter your current password": "Introduce tu contraseña actual",
|
||||
"enter your full name": "introduzca su nombre completo",
|
||||
"Enter your new password": "Ingrese su nueva contraseña",
|
||||
"Enter your new preferred email": "Introduce tu nuevo correo electrónico preferido",
|
||||
"Enter your password": "Introduce tu contraseña",
|
||||
"Error fetching page data.": "Error al obtener los datos de la página.",
|
||||
"Error loading page history.": "Error al cargar el historial de la página.",
|
||||
"Export": "Exportar",
|
||||
"Failed to create page": "No se pudo crear la página",
|
||||
"Failed to delete page": "No se pudo eliminar la página",
|
||||
"Failed to fetch recent pages": "Error al obtener las páginas recientes",
|
||||
"Failed to import pages": "No se pudieron importar las páginas",
|
||||
"Failed to load page. An error occurred.": "Error al cargar la página. Se produjo un error.",
|
||||
"Failed to update data": "No se pudo actualizar los datos",
|
||||
"Full access": "Acceso completo",
|
||||
"Full page width": "Ancho de página completa",
|
||||
"Full width": "Ancho completo",
|
||||
"General": "General",
|
||||
"Group": "Grupo",
|
||||
"Group description": "Descripción del grupo",
|
||||
"Group name": "Nombre del grupo",
|
||||
"Groups": "Grupos",
|
||||
"Has full access to space settings and pages.": "Tiene acceso completo a la configuración y páginas del espacio.",
|
||||
"Home": "Inicio",
|
||||
"Import pages": "Importar páginas",
|
||||
"Import pages & space settings": "Importar páginas y configuraciones del espacio",
|
||||
"Importing pages": "Importando páginas",
|
||||
"invalid invitation link": "enlace de invitación no válido",
|
||||
"Invitation signup": "Registro por invitación",
|
||||
"Invite by email": "Invitar por correo electrónico",
|
||||
"Invite members": "Invitar a miembros",
|
||||
"Invite new members": "Invitar a nuevos miembros",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Los miembros invitados que aún no han aceptado su invitación aparecerán aquí.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Los miembros invitados recibirán acceso a los espacios a los que los grupos pueden acceder",
|
||||
"Join the workspace": "Unirse al espacio de trabajo",
|
||||
"Language": "Idioma",
|
||||
"Light": "Ligero",
|
||||
"Link copied": "Enlace copiado",
|
||||
"Login": "Iniciar sesión",
|
||||
"Logout": "Cerrar sesión",
|
||||
"Manage Group": "Gestionar Grupo",
|
||||
"Manage members": "Gestionar miembros",
|
||||
"member": "miembro",
|
||||
"Member": "Miembro",
|
||||
"members": "miembros",
|
||||
"Members": "Miembros",
|
||||
"My preferences": "Mis preferencias",
|
||||
"My Profile": "Mi Perfil",
|
||||
"My profile": "Mi perfil",
|
||||
"Name": "Nombre",
|
||||
"New email": "Nuevo correo electrónico",
|
||||
"New page": "Nueva página",
|
||||
"New password": "Nueva contraseña",
|
||||
"No group found": "No se encontró grupo",
|
||||
"No page history saved yet.": "No hay historial de la página guardado aún.",
|
||||
"No pages yet": "No hay páginas todavía",
|
||||
"No results found...": "No se encontraron resultados...",
|
||||
"No user found": "No se encontró usuario",
|
||||
"Overview": "Visión general",
|
||||
"Owner": "Propietario",
|
||||
"page": "página",
|
||||
"Page deleted successfully": "Página eliminada con éxito",
|
||||
"Page history": "Historial de la página",
|
||||
"Page import is in progress. Please do not close this tab.": "La importación de la página está en curso. Por favor, no cierre esta pestaña.",
|
||||
"Pages": "Páginas",
|
||||
"pages": "páginas",
|
||||
"Password": "Contraseña",
|
||||
"Password changed successfully": "Contraseña cambiada con éxito",
|
||||
"Pending": "Pendiente",
|
||||
"Please confirm your action": "Por favor, confirme su acción",
|
||||
"Preferences": "Preferencias",
|
||||
"Print PDF": "Imprimir PDF",
|
||||
"Profile": "Perfil",
|
||||
"Recently updated": "Recientemente actualizado",
|
||||
"Remove": "Eliminar",
|
||||
"Remove group member": "Eliminar miembro del grupo",
|
||||
"Remove space member": "Eliminar miembro del espacio",
|
||||
"Restore": "Restaurar",
|
||||
"Role": "Rol",
|
||||
"Save": "Guardar",
|
||||
"Search": "Buscar",
|
||||
"Search for groups": "Buscar grupos",
|
||||
"Search for users": "Buscar usuarios",
|
||||
"Search for users and groups": "Buscar usuarios y grupos",
|
||||
"Search...": "Buscar...",
|
||||
"Select language": "Seleccionar idioma",
|
||||
"Select role": "Seleccionar rol",
|
||||
"Select role to assign to all invited members": "Seleccionar rol para asignar a todos los miembros invitados",
|
||||
"Select theme": "Seleccionar tema",
|
||||
"Send invitation": "Enviar invitación",
|
||||
"Invitation sent": "Invitación enviada",
|
||||
"Settings": "Ajustes",
|
||||
"Setup workspace": "Configurar espacio de trabajo",
|
||||
"Sign In": "Iniciar sesión",
|
||||
"Sign Up": "Registrarse",
|
||||
"Slug": "Identificador",
|
||||
"Space": "Espacio",
|
||||
"Space description": "Descripción del espacio",
|
||||
"Space menu": "Menú de espacio",
|
||||
"Space name": "Nombre del espacio",
|
||||
"Space settings": "Configuración del espacio",
|
||||
"Space slug": "Identificador del espacio",
|
||||
"Spaces": "Espacios",
|
||||
"Spaces you belong to": "Espacios a los que perteneces",
|
||||
"No space found": "No se encontró espacio",
|
||||
"Search for spaces": "Buscar espacios",
|
||||
"Start typing to search...": "Empieza a escribir para buscar...",
|
||||
"Status": "Estado",
|
||||
"Successfully imported": "Importado con éxito",
|
||||
"Successfully restored": "Restaurado con éxito",
|
||||
"System settings": "Configuración del sistema",
|
||||
"Theme": "Tema",
|
||||
"To change your email, you have to enter your password and new email.": "Para cambiar tu correo electrónico, debes ingresar tu contraseña y nuevo correo electrónico.",
|
||||
"Toggle full page width": "Alternar el ancho de página completa",
|
||||
"Unable to import pages. Please try again.": "No se pueden importar las páginas. Por favor, inténtelo de nuevo.",
|
||||
"untitled": "sin título",
|
||||
"Untitled": "Sin título",
|
||||
"Updated successfully": "Actualizado con éxito",
|
||||
"User": "Usuario",
|
||||
"Workspace": "Espacio de trabajo",
|
||||
"Workspace Name": "Nombre del espacio de trabajo",
|
||||
"Workspace settings": "Configuración del espacio de trabajo",
|
||||
"You can change your password here.": "Puede cambiar su contraseña aquí.",
|
||||
"Your Email": "Su correo electrónico",
|
||||
"Your import is complete.": "Su importación está completa.",
|
||||
"Your name": "Tu nombre",
|
||||
"Your Name": "Tu Nombre",
|
||||
"Your password": "Tu contraseña",
|
||||
"Your password must be a minimum of 8 characters.": "Su contraseña debe tener un mínimo de 8 caracteres.",
|
||||
"Sidebar toggle": "Alternar barra lateral",
|
||||
"Comments": "Comentarios",
|
||||
"404 page not found": "404 página no encontrada",
|
||||
"Sorry, we can't find the page you are looking for.": "Lo sentimos, no podemos encontrar la página que buscas.",
|
||||
"Take me back to homepage": "Llévame de vuelta a la página de inicio",
|
||||
"Forgot password": "Olvidó la contraseña",
|
||||
"Forgot your password?": "¿Olvidó su contraseña?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Se ha enviado un enlace para restablecer la contraseña a tu correo electrónico. Por favor, revisa tu bandeja de entrada.",
|
||||
"Send reset link": "Enviar enlace de restablecimiento",
|
||||
"Password reset": "Restablecimiento de contraseña",
|
||||
"Your new password": "Tu nueva contraseña",
|
||||
"Set password": "Establecer contraseña",
|
||||
"Write a comment": "Escribir un comentario",
|
||||
"Reply...": "Responder...",
|
||||
"Error loading comments.": "Error al cargar comentarios.",
|
||||
"No comments yet.": "No hay comentarios todavía.",
|
||||
"Edit comment": "Editar comentario",
|
||||
"Delete comment": "Eliminar comentario",
|
||||
"Are you sure you want to delete this comment?": "¿Está seguro de que desea eliminar este comentario?",
|
||||
"Comment created successfully": "Comentario creado con éxito",
|
||||
"Error creating comment": "Error al crear comentario",
|
||||
"Comment updated successfully": "Comentario actualizado con éxito",
|
||||
"Failed to update comment": "No se pudo actualizar el comentario",
|
||||
"Comment deleted successfully": "Comentario eliminado con éxito",
|
||||
"Failed to delete comment": "No se pudo eliminar el comentario",
|
||||
"Comment resolved successfully": "Comentario resuelto con éxito",
|
||||
"Failed to resolve comment": "No se pudo resolver el comentario",
|
||||
"Revoke invitation": "Revocar invitación",
|
||||
"Revoke": "Revocar",
|
||||
"Don't": "No",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "¿Está seguro de que desea revocar esta invitación? El usuario no podrá unirse al espacio de trabajo.",
|
||||
"Resend invitation": "Reenviar invitación",
|
||||
"Anyone with this link can join this workspace.": "Cualquiera con este enlace puede unirse a este espacio de trabajo.",
|
||||
"Invite link": "Enlace de invitación",
|
||||
"Copy": "Copiar",
|
||||
"Copied": "Copiado",
|
||||
"Select a user": "Seleccionar un usuario",
|
||||
"Select a group": "Seleccionar un grupo",
|
||||
"Export all pages and attachments in this space.": "Exportar todas las páginas y archivos adjuntos en este espacio.",
|
||||
"Delete space": "Eliminar espacio",
|
||||
"Are you sure you want to delete this space?": "¿Está seguro de que desea eliminar este espacio?",
|
||||
"Delete this space with all its pages and data.": "Eliminar este espacio con todas sus páginas y datos.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Todas las páginas, comentarios, archivos adjuntos y permisos en este espacio se eliminarán de forma irreversible.",
|
||||
"Confirm space name": "Confirmar nombre del espacio",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Escribe el nombre del espacio <b>{{spaceName}}</b> para confirmar tu acción.",
|
||||
"Format": "Formato",
|
||||
"Include subpages": "Incluir subpáginas",
|
||||
"Include attachments": "Incluir adjuntos",
|
||||
"Select export format": "Seleccionar formato de exportación",
|
||||
"Export failed:": "Exportación fallida:",
|
||||
"export error": "error de exportación",
|
||||
"Export page": "Exportar página",
|
||||
"Export space": "Exportar espacio",
|
||||
"Export {{type}}": "Exportar {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "El archivo supera el límite de {{limit}} adjuntos",
|
||||
"Align left": "Alinear a la izquierda",
|
||||
"Align right": "Alinear a la derecha",
|
||||
"Align center": "Alinear al centro",
|
||||
"Justify": "Justificar",
|
||||
"Merge cells": "Combinar celdas",
|
||||
"Split cell": "Dividir celda",
|
||||
"Delete column": "Eliminar columna",
|
||||
"Delete row": "Eliminar fila",
|
||||
"Add left column": "Agregar columna izquierda",
|
||||
"Add right column": "Agregar columna derecha",
|
||||
"Add row above": "Agregar fila arriba",
|
||||
"Add row below": "Agregar fila debajo",
|
||||
"Delete table": "Eliminar tabla",
|
||||
"Info": "Información",
|
||||
"Success": "Satisfactorio",
|
||||
"Warning": "Advertencia",
|
||||
"Danger": "Peligro",
|
||||
"Mermaid diagram error:": "Error en diagrama de Mermaid:",
|
||||
"Invalid Mermaid diagram": "Diagrama de Mermaid no válido",
|
||||
"Double-click to edit Draw.io diagram": "Doble clic para editar el diagrama de Draw.io",
|
||||
"Exit": "Salir",
|
||||
"Save & Exit": "Guardar y Salir",
|
||||
"Double-click to edit Excalidraw diagram": "Doble clic para editar el diagrama de Excalidraw",
|
||||
"Paste link": "Pegar enlace",
|
||||
"Edit link": "Editar enlace",
|
||||
"Remove link": "Eliminar enlace",
|
||||
"Add link": "Agregar enlace",
|
||||
"Please enter a valid url": "Por favor, ingrese una URL válida",
|
||||
"Empty equation": "Ecuación vacía",
|
||||
"Invalid equation": "Ecuación no válida",
|
||||
"Color": "Color",
|
||||
"Text color": "Color del texto",
|
||||
"Default": "Predeterminado",
|
||||
"Blue": "Azul",
|
||||
"Green": "Verde",
|
||||
"Purple": "Morado",
|
||||
"Red": "Rojo",
|
||||
"Yellow": "Amarillo",
|
||||
"Orange": "Naranja",
|
||||
"Pink": "Rosa",
|
||||
"Gray": "Gris",
|
||||
"Embed link": "Enlace adjunto",
|
||||
"Invalid {{provider}} embed link": "Enlace incrustado {{provider}} no válido",
|
||||
"Embed {{provider}}": "Incrustar {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Introduzca el enlace de {{provider}} para incrustar",
|
||||
"Bold": "Negrita",
|
||||
"Italic": "Cursiva",
|
||||
"Underline": "Subrayar",
|
||||
"Strike": "Tachar",
|
||||
"Code": "Código",
|
||||
"Comment": "Comentario",
|
||||
"Text": "Texto",
|
||||
"Heading 1": "Encabezado 1",
|
||||
"Heading 2": "Encabezado 2",
|
||||
"Heading 3": "Encabezado 3",
|
||||
"To-do List": "Lista de cosas por hacer",
|
||||
"Bullet List": "Lista con viñetas",
|
||||
"Numbered List": "Lista numerada",
|
||||
"Blockquote": "Cita en bloque",
|
||||
"Just start typing with plain text.": "Simplemente comienza a escribir con texto sin formato.",
|
||||
"Track tasks with a to-do list.": "Administra tareas con una lista de tareas pendientes.",
|
||||
"Big section heading.": "Gran encabezado de sección.",
|
||||
"Medium section heading.": "Encabezado de sección mediano.",
|
||||
"Small section heading.": "Pequeño encabezado de sección.",
|
||||
"Create a simple bullet list.": "Crear una lista con viñetas simple.",
|
||||
"Create a list with numbering.": "Crear una lista con numeración.",
|
||||
"Create block quote.": "Crear una cita en bloque.",
|
||||
"Insert code snippet.": "Insertar fragmento de código.",
|
||||
"Insert horizontal rule divider": "Insertar regla horizontal",
|
||||
"Upload any image from your device.": "Sube cualquier imagen desde tu dispositivo.",
|
||||
"Upload any video from your device.": "Sube cualquier video desde tu dispositivo.",
|
||||
"Upload any file from your device.": "Sube cualquier archivo desde tu dispositivo.",
|
||||
"Table": "Tabla",
|
||||
"Insert a table.": "Insertar una tabla.",
|
||||
"Insert collapsible block.": "Insertar bloque desplegable.",
|
||||
"Video": "Vídeo",
|
||||
"Divider": "Divisor",
|
||||
"Quote": "Cita",
|
||||
"Image": "Imagen",
|
||||
"File attachment": "Adjunto de archivo",
|
||||
"Toggle block": "Alternar bloque",
|
||||
"Callout": "Aviso",
|
||||
"Insert callout notice.": "Insertar aviso de llamada.",
|
||||
"Math inline": "Matemáticas en línea",
|
||||
"Insert inline math equation.": "Insertar ecuación matemática en línea.",
|
||||
"Math block": "Bloque de matemáticas",
|
||||
"Insert math equation": "Insertar ecuación matemática",
|
||||
"Mermaid diagram": "Diagrama de Mermaid",
|
||||
"Insert mermaid diagram": "Insertar diagrama de Mermaid",
|
||||
"Insert and design Drawio diagrams": "Insertar y diseñar diagramas Drawio",
|
||||
"Insert current date": "Insertar fecha actual",
|
||||
"Draw and sketch excalidraw diagrams": "Dibujar y esbozar diagramas de Excalidraw",
|
||||
"Multiple": "Múltiple",
|
||||
"Heading {{level}}": "Encabezado {{level}}",
|
||||
"Toggle title": "Alternar título",
|
||||
"Write anything. Enter \"/\" for commands": "Escribe cualquier cosa. Ingresa \"/\" para comandos",
|
||||
"Names do not match": "Los nombres no coinciden",
|
||||
"Today, {{time}}": "Hoy, {{time}}",
|
||||
"Yesterday, {{time}}": "Ayer, {{time}}",
|
||||
"Space created successfully": "Espacio creado con éxito",
|
||||
"Space updated successfully": "Espacio actualizado con éxito",
|
||||
"Space deleted successfully": "Espacio eliminado con éxito",
|
||||
"Members added successfully": "Miembros añadidos con éxito",
|
||||
"Member removed successfully": "Miembro eliminado con éxito",
|
||||
"Member role updated successfully": "Rol de miembro actualizado con éxito",
|
||||
"Created by: <b>{{creatorName}}</b>": "Creado por: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Creado a: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Editado por {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Conteo de palabras: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Recuento de caracteres: {{characterCount}}"
|
||||
}
|
355
apps/client/public/locales/fr-FR/translation.json
Normal file
355
apps/client/public/locales/fr-FR/translation.json
Normal file
@ -0,0 +1,355 @@
|
||||
{
|
||||
"Account": "Compte",
|
||||
"Active": "Actif",
|
||||
"Add": "Ajouter",
|
||||
"Add group members": "Ajouter des membres au groupe",
|
||||
"Add groups": "Ajouter des groupes",
|
||||
"Add members": "Ajouter des membres",
|
||||
"Add to groups": "Ajouter aux groupes",
|
||||
"Add space members": "Ajouter des membres à l'espace",
|
||||
"Admin": "Admin",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Êtes-vous sûr de vouloir supprimer ce groupe ? Les membres perdront l'accès aux ressources auxquelles ce groupe a accès.",
|
||||
"Are you sure you want to delete this page?": "Êtes-vous sûr de vouloir supprimer cette page ?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Êtes-vous sûr de vouloir retirer cet utilisateur du groupe ? L'utilisateur perdra l'accès aux ressources auxquelles ce groupe a accès.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Êtes-vous sûr de vouloir retirer cet utilisateur de l'espace ? L'utilisateur perdra tout accès à cet espace.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Êtes-vous sûr de vouloir restaurer cette version ? Toutes les modifications non versionnées seront perdues.",
|
||||
"Can become members of groups and spaces in workspace": "Peut devenir membre de groupes et d'espaces dans l'espace de travail",
|
||||
"Can create and edit pages in space.": "Peut créer et modifier des pages dans l'espace.",
|
||||
"Can edit": "Peut modifier",
|
||||
"Can manage workspace": "Peut gérer l'espace de travail",
|
||||
"Can manage workspace but cannot delete it": "Peut gérer l'espace de travail mais ne peut pas le supprimer",
|
||||
"Can view": "Peut voir",
|
||||
"Can view pages in space but not edit.": "Peut voir les pages dans l'espace mais ne peut pas les modifier.",
|
||||
"Cancel": "Annuler",
|
||||
"Change email": "Changer l'email",
|
||||
"Change password": "Changer le mot de passe",
|
||||
"Change photo": "Changer la photo",
|
||||
"Choose a role": "Choisir un rôle",
|
||||
"Choose your preferred color scheme.": "Choisissez votre palette de couleurs préférée.",
|
||||
"Choose your preferred interface language.": "Choisissez votre langue d'interface préférée.",
|
||||
"Choose your preferred page width.": "Choisissez votre largeur de page préférée.",
|
||||
"Confirm": "Confirmer",
|
||||
"Copy link": "Copier le lien",
|
||||
"Create": "Créer",
|
||||
"Create group": "Créer groupe",
|
||||
"Create page": "Créer page",
|
||||
"Create space": "Créer espace",
|
||||
"Create workspace": "Créer espace de travail",
|
||||
"Current password": "Mot de passe actuel",
|
||||
"Dark": "Sombre",
|
||||
"Date": "Date",
|
||||
"Delete": "Supprimer",
|
||||
"Delete group": "Supprimer groupe",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Êtes-vous sûr de vouloir supprimer cette page ? Cela supprimera ses enfants et l'historique de la page. Cette action est irréversible.",
|
||||
"Description": "Description",
|
||||
"Details": "Détails",
|
||||
"e.g ACME": "par ex. ACME",
|
||||
"e.g ACME Inc": "par ex. ACME Inc",
|
||||
"e.g Developers": "par ex. Développeurs",
|
||||
"e.g Group for developers": "par ex. Groupe pour développeurs",
|
||||
"e.g product": "par ex. produit",
|
||||
"e.g Product Team": "par ex. Équipe Produit",
|
||||
"e.g Sales": "par ex. Ventes",
|
||||
"e.g Space for product team": "par ex. Espace pour l'équipe produit",
|
||||
"e.g Space for sales team to collaborate": "par ex. Espace pour l'équipe de vente pour collaborer",
|
||||
"Edit": "Modifier",
|
||||
"Edit group": "Modifier groupe",
|
||||
"Email": "Email",
|
||||
"Enter a strong password": "Entrez un mot de passe fort",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Entrez des adresses email valides séparées par une virgule ou un espace [max : 50]",
|
||||
"enter valid emails addresses": "entrez des adresses email valides",
|
||||
"Enter your current password": "Entrez votre mot de passe actuel",
|
||||
"enter your full name": "entrez votre nom complet",
|
||||
"Enter your new password": "Entrez votre nouveau mot de passe",
|
||||
"Enter your new preferred email": "Entrez votre nouvel email préféré",
|
||||
"Enter your password": "Entrez votre mot de passe",
|
||||
"Error fetching page data.": "Erreur lors de la récupération des données de la page.",
|
||||
"Error loading page history.": "Erreur lors du chargement de l'historique de la page.",
|
||||
"Export": "Exporter",
|
||||
"Failed to create page": "Échec de la création de la page",
|
||||
"Failed to delete page": "Échec de la suppression de la page",
|
||||
"Failed to fetch recent pages": "Échec de la récupération des pages récentes",
|
||||
"Failed to import pages": "Échec de l'importation des pages",
|
||||
"Failed to load page. An error occurred.": "Échec du chargement de la page. Une erreur s'est produite.",
|
||||
"Failed to update data": "Échec de la mise à jour des données",
|
||||
"Full access": "Accès complet",
|
||||
"Full page width": "Largeur de page complète",
|
||||
"Full width": "Largeur complète",
|
||||
"General": "Général",
|
||||
"Group": "Groupe",
|
||||
"Group description": "Description du groupe",
|
||||
"Group name": "Nom du groupe",
|
||||
"Groups": "Groupes",
|
||||
"Has full access to space settings and pages.": "A un accès complet aux paramètres de l'espace et aux pages.",
|
||||
"Home": "Accueil",
|
||||
"Import pages": "Importer des pages",
|
||||
"Import pages & space settings": "Importer des pages et paramètres de l'espace",
|
||||
"Importing pages": "Importation des pages",
|
||||
"invalid invitation link": "lien d'invitation invalide",
|
||||
"Invitation signup": "Inscription par invitation",
|
||||
"Invite by email": "Inviter par email",
|
||||
"Invite members": "Inviter des membres",
|
||||
"Invite new members": "Inviter de nouveaux membres",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Les membres invités qui n'ont pas encore accepté leur invitation apparaîtront ici.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Les membres invités auront accès aux espaces auxquels les groupes peuvent accéder",
|
||||
"Join the workspace": "Rejoindre l'espace de travail",
|
||||
"Language": "Langue",
|
||||
"Light": "Clair",
|
||||
"Link copied": "Lien copié",
|
||||
"Login": "Connexion",
|
||||
"Logout": "Déconnexion",
|
||||
"Manage Group": "Gérer le groupe",
|
||||
"Manage members": "Gérer les membres",
|
||||
"member": "membre",
|
||||
"Member": "Membre",
|
||||
"members": "membres",
|
||||
"Members": "Membres",
|
||||
"My preferences": "Mes préférences",
|
||||
"My Profile": "Mon Profil",
|
||||
"My profile": "Mon profil",
|
||||
"Name": "Nom",
|
||||
"New email": "Nouvel email",
|
||||
"New page": "Nouvelle page",
|
||||
"New password": "Nouveau mot de passe",
|
||||
"No group found": "Aucun groupe trouvé",
|
||||
"No page history saved yet.": "Aucun historique de la page enregistré pour l'instant.",
|
||||
"No pages yet": "Aucune page pour l'instant",
|
||||
"No results found...": "Aucun résultat trouvé...",
|
||||
"No user found": "Aucun utilisateur trouvé",
|
||||
"Overview": "Vue d'ensemble",
|
||||
"Owner": "Propriétaire",
|
||||
"page": "page",
|
||||
"Page deleted successfully": "Page supprimée avec succès",
|
||||
"Page history": "Historique de la page",
|
||||
"Page import is in progress. Please do not close this tab.": "L'importation de la page est en cours. Veuillez ne pas fermer cet onglet.",
|
||||
"Pages": "Pages",
|
||||
"pages": "pages",
|
||||
"Password": "Mot de passe",
|
||||
"Password changed successfully": "Mot de passe changé avec succès",
|
||||
"Pending": "En attente",
|
||||
"Please confirm your action": "Veuillez confirmer votre action",
|
||||
"Preferences": "Préférences",
|
||||
"Print PDF": "Imprimer PDF",
|
||||
"Profile": "Profil",
|
||||
"Recently updated": "Récemment mis à jour",
|
||||
"Remove": "Retirer",
|
||||
"Remove group member": "Retirer un membre du groupe",
|
||||
"Remove space member": "Retirer un membre de l'espace",
|
||||
"Restore": "Restaurer",
|
||||
"Role": "Rôle",
|
||||
"Save": "Enregistrer",
|
||||
"Search": "Rechercher",
|
||||
"Search for groups": "Rechercher des groupes",
|
||||
"Search for users": "Rechercher des utilisateurs",
|
||||
"Search for users and groups": "Rechercher des utilisateurs et des groupes",
|
||||
"Search...": "Rechercher...",
|
||||
"Select language": "Sélectionner la langue",
|
||||
"Select role": "Sélectionner un rôle",
|
||||
"Select role to assign to all invited members": "Sélectionner le rôle à attribuer à tous les membres invités",
|
||||
"Select theme": "Sélectionner le thème",
|
||||
"Send invitation": "Envoyer l'invitation",
|
||||
"Invitation sent": "Invitation envoyée",
|
||||
"Settings": "Paramètres",
|
||||
"Setup workspace": "Configurer l'espace de travail",
|
||||
"Sign In": "Se connecter",
|
||||
"Sign Up": "S'inscrire",
|
||||
"Slug": "Slug",
|
||||
"Space": "Espace",
|
||||
"Space description": "Description de l'espace",
|
||||
"Space menu": "Menu de l'espace",
|
||||
"Space name": "Nom de l'espace",
|
||||
"Space settings": "Paramètres de l'espace",
|
||||
"Space slug": "Slug de l'espace",
|
||||
"Spaces": "Espaces",
|
||||
"Spaces you belong to": "Espaces auxquels vous appartenez",
|
||||
"No space found": "Aucun espace trouvé",
|
||||
"Search for spaces": "Rechercher des espaces",
|
||||
"Start typing to search...": "Commencez à taper pour rechercher...",
|
||||
"Status": "Statut",
|
||||
"Successfully imported": "Importé avec succès",
|
||||
"Successfully restored": "Restauré avec succès",
|
||||
"System settings": "Paramètres système",
|
||||
"Theme": "Thème",
|
||||
"To change your email, you have to enter your password and new email.": "Pour changer votre email, vous devez entrer votre mot de passe et votre nouvel email.",
|
||||
"Toggle full page width": "Basculer sur la largeur complète de la page",
|
||||
"Unable to import pages. Please try again.": "Impossible d'importer les pages. Veuillez réessayer.",
|
||||
"untitled": "sans titre",
|
||||
"Untitled": "Sans titre",
|
||||
"Updated successfully": "Mis à jour avec succès",
|
||||
"User": "Utilisateur",
|
||||
"Workspace": "Espace de travail",
|
||||
"Workspace Name": "Nom de l'espace de travail",
|
||||
"Workspace settings": "Paramètres de l'espace de travail",
|
||||
"You can change your password here.": "Vous pouvez changer votre mot de passe ici.",
|
||||
"Your Email": "Votre Email",
|
||||
"Your import is complete.": "Votre importation est terminée.",
|
||||
"Your name": "Votre nom",
|
||||
"Your Name": "Votre Nom",
|
||||
"Your password": "Votre mot de passe",
|
||||
"Your password must be a minimum of 8 characters.": "Votre mot de passe doit contenir au moins 8 caractères.",
|
||||
"Sidebar toggle": "Bascule de la barre latérale",
|
||||
"Comments": "Commentaires",
|
||||
"404 page not found": "404 page non trouvée",
|
||||
"Sorry, we can't find the page you are looking for.": "Désolé, nous ne pouvons pas trouver la page que vous cherchez.",
|
||||
"Take me back to homepage": "Ramenez-moi à la page d'accueil",
|
||||
"Forgot password": "Mot de passe oublié",
|
||||
"Forgot your password?": "Mot de passe oublié?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Un lien de réinitialisation de mot de passe a été envoyé à votre e-mail. Veuillez vérifier votre boîte de réception.",
|
||||
"Send reset link": "Envoyer le lien de réinitialisation",
|
||||
"Password reset": "Réinitialisation du mot de passe",
|
||||
"Your new password": "Votre nouveau mot de passe",
|
||||
"Set password": "Définir le mot de passe",
|
||||
"Write a comment": "Écrire un commentaire",
|
||||
"Reply...": "Répondre...",
|
||||
"Error loading comments.": "Erreur lors du chargement des commentaires.",
|
||||
"No comments yet.": "Pas de commentaires pour l'instant.",
|
||||
"Edit comment": "Modifier le commentaire",
|
||||
"Delete comment": "Supprimer le commentaire",
|
||||
"Are you sure you want to delete this comment?": "Êtes-vous sûr de vouloir supprimer ce commentaire ?",
|
||||
"Comment created successfully": "Commentaire créé avec succès",
|
||||
"Error creating comment": "Erreur lors de la création du commentaire",
|
||||
"Comment updated successfully": "Commentaire mis à jour avec succès",
|
||||
"Failed to update comment": "Échec de la mise à jour du commentaire",
|
||||
"Comment deleted successfully": "Commentaire supprimé avec succès",
|
||||
"Failed to delete comment": "Échec de la suppression du commentaire",
|
||||
"Comment resolved successfully": "Commentaire résolu avec succès",
|
||||
"Failed to resolve comment": "Échec de la résolution du commentaire",
|
||||
"Revoke invitation": "Révoquer l'invitation",
|
||||
"Revoke": "Révoquer",
|
||||
"Don't": "Ne pas",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Êtes-vous sûr de vouloir révoquer cette invitation ? L'utilisateur ne pourra pas rejoindre l'espace de travail.",
|
||||
"Resend invitation": "Renvoyer l'invitation",
|
||||
"Anyone with this link can join this workspace.": "Toute personne ayant ce lien peut rejoindre cet espace de travail.",
|
||||
"Invite link": "Lien d'invitation",
|
||||
"Copy": "Copier",
|
||||
"Copied": "Copié",
|
||||
"Select a user": "Sélectionner un utilisateur",
|
||||
"Select a group": "Sélectionner un groupe",
|
||||
"Export all pages and attachments in this space.": "Exporter toutes les pages et pièces jointes dans cet espace.",
|
||||
"Delete space": "Supprimer l'espace",
|
||||
"Are you sure you want to delete this space?": "Êtes-vous sûr de vouloir supprimer cet espace ?",
|
||||
"Delete this space with all its pages and data.": "Supprimer cet espace avec toutes ses pages et données.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Toutes les pages, commentaires, pièces jointes et autorisations dans cet espace seront supprimés irréversiblement.",
|
||||
"Confirm space name": "Confirmer le nom de l'espace",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Tapez le nom de l'espace <b>{{spaceName}}</b> pour confirmer votre action.",
|
||||
"Format": "Format",
|
||||
"Include subpages": "Inclure les sous-pages",
|
||||
"Include attachments": "Inclure les pièces jointes",
|
||||
"Select export format": "Sélectionner le format d'exportation",
|
||||
"Export failed:": "Échec de l'exportation :",
|
||||
"export error": "exporter l'erreur",
|
||||
"Export page": "Exporter la page",
|
||||
"Export space": "Exporter l'espace",
|
||||
"Export {{type}}": "Exporter {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Le fichier dépasse la limite de {{limit}} pièces jointes",
|
||||
"Align left": "Aligner à gauche",
|
||||
"Align right": "Aligner à droite",
|
||||
"Align center": "Aligner au centre",
|
||||
"Justify": "Justifier",
|
||||
"Merge cells": "Fusionner les cellules",
|
||||
"Split cell": "Diviser la cellule",
|
||||
"Delete column": "Supprimer la colonne",
|
||||
"Delete row": "Supprimer la ligne",
|
||||
"Add left column": "Ajouter colonne à gauche",
|
||||
"Add right column": "Ajouter colonne à droite",
|
||||
"Add row above": "Ajouter une ligne au-dessus",
|
||||
"Add row below": "Ajouter une ligne en dessous",
|
||||
"Delete table": "Supprimer le tableau",
|
||||
"Info": "Info",
|
||||
"Success": "Succès",
|
||||
"Warning": "Avertissement",
|
||||
"Danger": "Danger",
|
||||
"Mermaid diagram error:": "Erreur de diagramme Mermaid :",
|
||||
"Invalid Mermaid diagram": "Diagramme Mermaid invalide",
|
||||
"Double-click to edit Draw.io diagram": "Double-cliquez pour modifier le diagramme Draw.io",
|
||||
"Exit": "Quitter",
|
||||
"Save & Exit": "Enregistrer & Quitter",
|
||||
"Double-click to edit Excalidraw diagram": "Double-cliquez pour modifier le diagramme Excalidraw",
|
||||
"Paste link": "Coller le lien",
|
||||
"Edit link": "Modifier le lien",
|
||||
"Remove link": "Supprimer le lien",
|
||||
"Add link": "Ajouter un lien",
|
||||
"Please enter a valid url": "Veuillez entrer une URL valide",
|
||||
"Empty equation": "Équation vide",
|
||||
"Invalid equation": "Équation invalide",
|
||||
"Color": "Couleur",
|
||||
"Text color": "Couleur du texte",
|
||||
"Default": "Par défaut",
|
||||
"Blue": "Bleu",
|
||||
"Green": "Vert",
|
||||
"Purple": "Violet",
|
||||
"Red": "Rouge",
|
||||
"Yellow": "Jaune",
|
||||
"Orange": "Orange",
|
||||
"Pink": "Rose",
|
||||
"Gray": "Gris",
|
||||
"Embed link": "Intégrer un lien",
|
||||
"Invalid {{provider}} embed link": "Lien d'intégration {{provider}} non valide",
|
||||
"Embed {{provider}}": "Intégrer {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Entrez le lien {{provider}} à intégrer",
|
||||
"Bold": "Gras",
|
||||
"Italic": "Italique",
|
||||
"Underline": "Souligner",
|
||||
"Strike": "Barrer",
|
||||
"Code": "Code",
|
||||
"Comment": "Commentaire",
|
||||
"Text": "Texte",
|
||||
"Heading 1": "Titre 1",
|
||||
"Heading 2": "Titre 2",
|
||||
"Heading 3": "Titre 3",
|
||||
"To-do List": "Liste de tâches",
|
||||
"Bullet List": "Liste à puces",
|
||||
"Numbered List": "Liste numérotée",
|
||||
"Blockquote": "Bloc de citation",
|
||||
"Just start typing with plain text.": "Commencez simplement à taper avec du texte brut.",
|
||||
"Track tasks with a to-do list.": "Suivez les tâches avec une liste de tâches.",
|
||||
"Big section heading.": "Grand titre de section.",
|
||||
"Medium section heading.": "Titre de section moyen.",
|
||||
"Small section heading.": "Petit titre de section.",
|
||||
"Create a simple bullet list.": "Créez une simple liste à puces.",
|
||||
"Create a list with numbering.": "Créez une liste numérotée.",
|
||||
"Create block quote.": "Créez un bloc de citation.",
|
||||
"Insert code snippet.": "Insérez un extrait de code.",
|
||||
"Insert horizontal rule divider": "Insérer un séparateur de règle horizontale",
|
||||
"Upload any image from your device.": "Téléchargez n'importe quelle image depuis votre appareil.",
|
||||
"Upload any video from your device.": "Téléchargez n'importe quelle vidéo depuis votre appareil.",
|
||||
"Upload any file from your device.": "Téléchargez n'importe quel fichier depuis votre appareil.",
|
||||
"Table": "Tableau",
|
||||
"Insert a table.": "Insérez un tableau.",
|
||||
"Insert collapsible block.": "Insérer un bloc repliable.",
|
||||
"Video": "Vidéo",
|
||||
"Divider": "Diviseur",
|
||||
"Quote": "Citation",
|
||||
"Image": "Image",
|
||||
"File attachment": "Pièce jointe",
|
||||
"Toggle block": "Basculer le bloc",
|
||||
"Callout": "Appel",
|
||||
"Insert callout notice.": "Insérer un avis d'appel.",
|
||||
"Math inline": "Mathématiques en ligne",
|
||||
"Insert inline math equation.": "Insérez une équation mathématique en ligne.",
|
||||
"Math block": "Bloc mathématiques",
|
||||
"Insert math equation": "Insérer une équation mathématique",
|
||||
"Mermaid diagram": "Diagramme Mermaid",
|
||||
"Insert mermaid diagram": "Insérer un diagramme Mermaid",
|
||||
"Insert and design Drawio diagrams": "Insérer et concevoir des diagrammes Drawio",
|
||||
"Insert current date": "Insérer la date actuelle",
|
||||
"Draw and sketch excalidraw diagrams": "Dessiner et esquisser des diagrammes Excalidraw",
|
||||
"Multiple": "Multiple",
|
||||
"Heading {{level}}": "Titre {{level}}",
|
||||
"Toggle title": "Basculer le titre",
|
||||
"Write anything. Enter \"/\" for commands": "Écrivez n'importe quoi. Entrez \"/\" pour les commandes",
|
||||
"Names do not match": "Les noms ne correspondent pas",
|
||||
"Today, {{time}}": "Aujourd'hui, {{time}}",
|
||||
"Yesterday, {{time}}": "Hier, {{time}}",
|
||||
"Space created successfully": "Espace créé avec succès",
|
||||
"Space updated successfully": "Espace mis à jour avec succès",
|
||||
"Space deleted successfully": "Espace supprimé avec succès",
|
||||
"Members added successfully": "Membres ajoutés avec succès",
|
||||
"Member removed successfully": "Membre supprimé avec succès",
|
||||
"Member role updated successfully": "Rôle du membre mis à jour avec succès",
|
||||
"Created by: <b>{{creatorName}}</b>": "Créé par : <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Créé à : {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Modifié par {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Nombre de mots : {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Nombre de caractères : {{characterCount}}"
|
||||
}
|
355
apps/client/public/locales/it-IT/translation.json
Normal file
355
apps/client/public/locales/it-IT/translation.json
Normal file
@ -0,0 +1,355 @@
|
||||
{
|
||||
"Account": "Account",
|
||||
"Active": "Attivo",
|
||||
"Add": "Aggiungi",
|
||||
"Add group members": "Aggiungi membri al gruppo",
|
||||
"Add groups": "Aggiungi gruppi",
|
||||
"Add members": "Aggiungi membri",
|
||||
"Add to groups": "Aggiungi ai gruppi",
|
||||
"Add space members": "Aggiungi membri allo spazio",
|
||||
"Admin": "Amministratore",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Sei sicuro di voler eliminare questo gruppo? I membri perderanno l'accesso alle risorse accessibili da questo gruppo.",
|
||||
"Are you sure you want to delete this page?": "Sei sicuro di voler eliminare questa pagina?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Sei sicuro di voler rimuovere questo utente dal gruppo? L'utente perderà l'accesso alle risorse accessibili da questo gruppo.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Sei sicuro di voler rimuovere questo utente dallo spazio? L'utente perderà tutti gli accessi a questo spazio.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Sei sicuro di voler ripristinare questa versione? Qualsiasi modifica non versionata verrà persa.",
|
||||
"Can become members of groups and spaces in workspace": "Può diventare membro di gruppi e spazi nell'area di lavoro",
|
||||
"Can create and edit pages in space.": "Può creare e modificare le pagine nello spazio.",
|
||||
"Can edit": "Può modificare",
|
||||
"Can manage workspace": "Può gestire l'area di lavoro",
|
||||
"Can manage workspace but cannot delete it": "Può gestire lo spazio di lavoro ma non può eliminarlo",
|
||||
"Can view": "Può visualizzare",
|
||||
"Can view pages in space but not edit.": "Può visualizzare le pagine nello spazio ma non può modificarle.",
|
||||
"Cancel": "Annulla",
|
||||
"Change email": "Cambia email",
|
||||
"Change password": "Cambia password",
|
||||
"Change photo": "Cambia foto",
|
||||
"Choose a role": "Scegli un ruolo",
|
||||
"Choose your preferred color scheme.": "Scegli il tema che preferisci.",
|
||||
"Choose your preferred interface language.": "Scegli la lingua da utilizzare per l'interfaccia.",
|
||||
"Choose your preferred page width.": "Scegli la larghezza della pagina che preferisci.",
|
||||
"Confirm": "Conferma",
|
||||
"Copy link": "Copia link",
|
||||
"Create": "Crea",
|
||||
"Create group": "Crea gruppo",
|
||||
"Create page": "Crea pagina",
|
||||
"Create space": "Crea spazio",
|
||||
"Create workspace": "Crea area di lavoro",
|
||||
"Current password": "Password attuale",
|
||||
"Dark": "Scuro",
|
||||
"Date": "Data",
|
||||
"Delete": "Elimina",
|
||||
"Delete group": "Elimina gruppo",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Sei sicuro di voler eliminare questa pagina? Verranno cancellate anche le sue sottopagine e la cronologia. Questa azione è irreversibile.",
|
||||
"Description": "Descrizione",
|
||||
"Details": "Dettagli",
|
||||
"e.g ACME": "es. ACME",
|
||||
"e.g ACME Inc": "es. ACME Inc",
|
||||
"e.g Developers": "es. Sviluppatori",
|
||||
"e.g Group for developers": "es. Gruppo per gli sviluppatori",
|
||||
"e.g product": "es. prodotto",
|
||||
"e.g Product Team": "es. Team di Prodotto",
|
||||
"e.g Sales": "es. Vendite",
|
||||
"e.g Space for product team": "es. Spazio per il team di prodotto",
|
||||
"e.g Space for sales team to collaborate": "es. Spazio per la collaborazione del team di vendita",
|
||||
"Edit": "Modifica",
|
||||
"Edit group": "Modifica gruppo",
|
||||
"Email": "Email",
|
||||
"Enter a strong password": "Inserisci una password sicura",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Inserisci degli indirizzi email validi separati da virgola o spazio [max: 50]",
|
||||
"enter valid emails addresses": "inserisci degli indirizzi email validi",
|
||||
"Enter your current password": "Inserisci la tua password attuale",
|
||||
"enter your full name": "inserisci il tuo nome completo",
|
||||
"Enter your new password": "Inserisci la tua nuova password",
|
||||
"Enter your new preferred email": "Inserisci la tua nuova email preferita",
|
||||
"Enter your password": "Inserisci la tua password",
|
||||
"Error fetching page data.": "Si è verificato un errore durante il recupero dei dati della pagina.",
|
||||
"Error loading page history.": "Si è verificato un errore durante il caricamento della cronologia della pagina.",
|
||||
"Export": "Esporta",
|
||||
"Failed to create page": "Impossibile creare la pagina",
|
||||
"Failed to delete page": "Impossibile eliminare la pagina",
|
||||
"Failed to fetch recent pages": "Impossibile recuperare le pagine recenti",
|
||||
"Failed to import pages": "Impossibile importare le pagine",
|
||||
"Failed to load page. An error occurred.": "Il caricamento della pagina è fallito. Si è verificato un errore.",
|
||||
"Failed to update data": "Impossibile aggiornare i dati",
|
||||
"Full access": "Accesso completo",
|
||||
"Full page width": "Pagina a larghezza intera",
|
||||
"Full width": "Larghezza intera",
|
||||
"General": "Generale",
|
||||
"Group": "Gruppo",
|
||||
"Group description": "Descrizione del gruppo",
|
||||
"Group name": "Nome del gruppo",
|
||||
"Groups": "Gruppi",
|
||||
"Has full access to space settings and pages.": "Ha pieno accesso alle impostazioni dello spazio e alle sue pagine.",
|
||||
"Home": "Casa",
|
||||
"Import pages": "Importa pagine",
|
||||
"Import pages & space settings": "Importa pagine e impostazioni dello spazio",
|
||||
"Importing pages": "Importazione pagine",
|
||||
"invalid invitation link": "link di invito non valido",
|
||||
"Invitation signup": "Iscrizione invito",
|
||||
"Invite by email": "Invita tramite email",
|
||||
"Invite members": "Invita membri",
|
||||
"Invite new members": "Invita nuovi membri",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "I membri invitati che non hanno ancora accettato il loro invito appariranno qui.",
|
||||
"Invited members will be granted access to spaces the groups can access": "I membri invitati avranno accesso agli spazi a cui i gruppi possono accedere",
|
||||
"Join the workspace": "Unisciti all'area di lavoro",
|
||||
"Language": "Lingua",
|
||||
"Light": "Chiaro",
|
||||
"Link copied": "Link copiato",
|
||||
"Login": "Login",
|
||||
"Logout": "Esci",
|
||||
"Manage Group": "Gestisci Gruppo",
|
||||
"Manage members": "Gestisci membri",
|
||||
"member": "membro",
|
||||
"Member": "Membro",
|
||||
"members": "membri",
|
||||
"Members": "Membri",
|
||||
"My preferences": "Le mie preferenze",
|
||||
"My Profile": "Il Mio Profilo",
|
||||
"My profile": "Il mio profilo",
|
||||
"Name": "Nome",
|
||||
"New email": "Nuova email",
|
||||
"New page": "Nuova pagina",
|
||||
"New password": "Nuova password",
|
||||
"No group found": "Nessun gruppo trovato",
|
||||
"No page history saved yet.": "La pagina non ha una cronologia per ora.",
|
||||
"No pages yet": "Nessuna pagina per ora",
|
||||
"No results found...": "Nessun risultato trovato...",
|
||||
"No user found": "Nessun utente trovato",
|
||||
"Overview": "Panoramica",
|
||||
"Owner": "Proprietario",
|
||||
"page": "pagina",
|
||||
"Page deleted successfully": "Pagina eliminata con successo",
|
||||
"Page history": "Cronologia della pagina",
|
||||
"Page import is in progress. Please do not close this tab.": "L'importazione della pagina è in corso. Si prega di non chiudere questa scheda.",
|
||||
"Pages": "Pagine",
|
||||
"pages": "pagine",
|
||||
"Password": "Password",
|
||||
"Password changed successfully": "Password cambiata con successo",
|
||||
"Pending": "In sospeso",
|
||||
"Please confirm your action": "Si prega di confermare la propria azione",
|
||||
"Preferences": "Preferenze",
|
||||
"Print PDF": "Stampa PDF",
|
||||
"Profile": "Profilo",
|
||||
"Recently updated": "Aggiornato di recente",
|
||||
"Remove": "Rimuovi",
|
||||
"Remove group member": "Rimuovi membro dal gruppo",
|
||||
"Remove space member": "Rimuovi membro dallo spazio",
|
||||
"Restore": "Ripristina",
|
||||
"Role": "Ruolo",
|
||||
"Save": "Salva",
|
||||
"Search": "Cerca",
|
||||
"Search for groups": "Cerca un gruppo",
|
||||
"Search for users": "Cerca un utente",
|
||||
"Search for users and groups": "Cerca un utente o un gruppo",
|
||||
"Search...": "Cerca...",
|
||||
"Select language": "Seleziona una lingua",
|
||||
"Select role": "Seleziona un ruolo",
|
||||
"Select role to assign to all invited members": "Seleziona il ruolo da assegnare a tutti i membri invitati",
|
||||
"Select theme": "Seleziona un tema",
|
||||
"Send invitation": "Invia invito",
|
||||
"Invitation sent": "Invito inviato",
|
||||
"Settings": "Impostazioni",
|
||||
"Setup workspace": "Configura l'area di lavoro",
|
||||
"Sign In": "Accedi",
|
||||
"Sign Up": "Registrati",
|
||||
"Slug": "Slug",
|
||||
"Space": "Spazio",
|
||||
"Space description": "Descrizione dello spazio",
|
||||
"Space menu": "Menu spazio",
|
||||
"Space name": "Nome dello spazio",
|
||||
"Space settings": "Impostazioni dello spazio",
|
||||
"Space slug": "Slug dello spazio",
|
||||
"Spaces": "Spazi",
|
||||
"Spaces you belong to": "Spazi a cui appartieni",
|
||||
"No space found": "Nessuno spazio trovato",
|
||||
"Search for spaces": "Cerca uno spazio",
|
||||
"Start typing to search...": "Inizia a digitare per cercare...",
|
||||
"Status": "Stato",
|
||||
"Successfully imported": "Importato con successo",
|
||||
"Successfully restored": "Ripristinato con successo",
|
||||
"System settings": "Impostazioni di sistema",
|
||||
"Theme": "Tema",
|
||||
"To change your email, you have to enter your password and new email.": "Per cambiare la tua email, devi inserire la tua password e la nuova email.",
|
||||
"Toggle full page width": "Attiva/disattiva pagina a larghezza intera",
|
||||
"Unable to import pages. Please try again.": "Impossibile importare le pagine. Riprova.",
|
||||
"untitled": "senza titolo",
|
||||
"Untitled": "Senza titolo",
|
||||
"Updated successfully": "Aggiornato con successo",
|
||||
"User": "Utente",
|
||||
"Workspace": "Area di lavoro",
|
||||
"Workspace Name": "Nome dell'area di lavoro",
|
||||
"Workspace settings": "Impostazioni dell'area di lavoro",
|
||||
"You can change your password here.": "Qui puoi cambiare la tua password.",
|
||||
"Your Email": "La tua email",
|
||||
"Your import is complete.": "La tua importazione è completata.",
|
||||
"Your name": "Il tuo nome",
|
||||
"Your Name": "Il Tuo Nome",
|
||||
"Your password": "La tua password",
|
||||
"Your password must be a minimum of 8 characters.": "La tua password deve contenere almeno 8 caratteri.",
|
||||
"Sidebar toggle": "Attiva/disattiva barra laterale",
|
||||
"Comments": "Commenti",
|
||||
"404 page not found": "404 pagina non trovata",
|
||||
"Sorry, we can't find the page you are looking for.": "Siamo spiacenti, non riusciamo a trovare la pagina che stai cercando.",
|
||||
"Take me back to homepage": "Torna all'homepage",
|
||||
"Forgot password": "Password dimenticata",
|
||||
"Forgot your password?": "Hai dimenticato la password?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Un link per il reset della password è stato inviato al tuo indirizzo email. Per favore, controlla la tua casella di posta.",
|
||||
"Send reset link": "Invia link per il ripristino della password",
|
||||
"Password reset": "Reimposta password",
|
||||
"Your new password": "La tua nuova password",
|
||||
"Set password": "Imposta password",
|
||||
"Write a comment": "Scrivi un commento",
|
||||
"Reply...": "Rispondi...",
|
||||
"Error loading comments.": "Si è verificato un errore durante il caricamento dei commenti.",
|
||||
"No comments yet.": "Nessun commento per ora.",
|
||||
"Edit comment": "Modifica commento",
|
||||
"Delete comment": "Elimina commento",
|
||||
"Are you sure you want to delete this comment?": "Sei sicuro di voler eliminare questo commento?",
|
||||
"Comment created successfully": "Commento creato con successo",
|
||||
"Error creating comment": "Si è verificato un errore durante la creazione del commento",
|
||||
"Comment updated successfully": "Commento aggiornato con successo",
|
||||
"Failed to update comment": "Impossibile aggiornare il commento",
|
||||
"Comment deleted successfully": "Commento eliminato con successo",
|
||||
"Failed to delete comment": "Impossibile eliminare il commento",
|
||||
"Comment resolved successfully": "Commento risolto con successo",
|
||||
"Failed to resolve comment": "Impossibile risolvere il commento",
|
||||
"Revoke invitation": "Revoca invito",
|
||||
"Revoke": "Revoca",
|
||||
"Don't": "Non",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Sei sicuro di voler revocare questo invito? L'utente non potrà unirsi all'area di lavoro.",
|
||||
"Resend invitation": "Rispedisci invito",
|
||||
"Anyone with this link can join this workspace.": "Chiunque con questo link può unirsi a questa area di lavoro.",
|
||||
"Invite link": "Link d'invito",
|
||||
"Copy": "Copia",
|
||||
"Copied": "Copiato",
|
||||
"Select a user": "Seleziona un utente",
|
||||
"Select a group": "Seleziona un gruppo",
|
||||
"Export all pages and attachments in this space.": "Esporta tutte le pagine e gli allegati di questo spazio.",
|
||||
"Delete space": "Elimina spazio",
|
||||
"Are you sure you want to delete this space?": "Sei sicuro di voler eliminare questo spazio?",
|
||||
"Delete this space with all its pages and data.": "Elimina questo spazio con tutte le sue pagine e i suoi dati.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Tutte le pagine, i commenti, gli allegati e i permessi di questo spazio verranno eliminati irreversibilmente.",
|
||||
"Confirm space name": "Conferma nome spazio",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Digita il nome dello spazio <b>{{spaceName}}</b> per confermare la tua azione.",
|
||||
"Format": "Formato",
|
||||
"Include subpages": "Includi sottopagine",
|
||||
"Include attachments": "Includi allegati",
|
||||
"Select export format": "Seleziona formato di esportazione",
|
||||
"Export failed:": "Esportazione fallita:",
|
||||
"export error": "errore di esportazione",
|
||||
"Export page": "Esporta pagina",
|
||||
"Export space": "Esporta spazio",
|
||||
"Export {{type}}": "Esporta {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Il file supera il limite per gli allegati di {{limit}}",
|
||||
"Align left": "Allinea a sinistra",
|
||||
"Align right": "Allinea a destra",
|
||||
"Align center": "Allinea al centro",
|
||||
"Justify": "Giustifica",
|
||||
"Merge cells": "Unisci celle",
|
||||
"Split cell": "Dividi cella",
|
||||
"Delete column": "Elimina colonna",
|
||||
"Delete row": "Elimina riga",
|
||||
"Add left column": "Aggiungi colonna a sinistra",
|
||||
"Add right column": "Aggiungi colonna a destra",
|
||||
"Add row above": "Aggiungi riga sopra",
|
||||
"Add row below": "Aggiungi riga sotto",
|
||||
"Delete table": "Elimina tabella",
|
||||
"Info": "Informazioni",
|
||||
"Success": "Successo",
|
||||
"Warning": "Avviso",
|
||||
"Danger": "Pericolo",
|
||||
"Mermaid diagram error:": "Errore nel diagramma di Mermaid:",
|
||||
"Invalid Mermaid diagram": "Diagramma di Mermaid non valido",
|
||||
"Double-click to edit Draw.io diagram": "Fai doppio clic per modificare il diagramma di Draw.io",
|
||||
"Exit": "Esci",
|
||||
"Save & Exit": "Salva ed esci",
|
||||
"Double-click to edit Excalidraw diagram": "Fai doppio clic per modificare il diagramma di Excalidraw",
|
||||
"Paste link": "Incolla link",
|
||||
"Edit link": "Modifica link",
|
||||
"Remove link": "Rimuovi link",
|
||||
"Add link": "Aggiungi link",
|
||||
"Please enter a valid url": "Per favore inserisci un URL valido",
|
||||
"Empty equation": "Equazione vuota",
|
||||
"Invalid equation": "Equazione non valida",
|
||||
"Color": "Colore",
|
||||
"Text color": "Colore del testo",
|
||||
"Default": "Predefinito",
|
||||
"Blue": "Blu",
|
||||
"Green": "Verde",
|
||||
"Purple": "Viola",
|
||||
"Red": "Rosso",
|
||||
"Yellow": "Giallo",
|
||||
"Orange": "Arancione",
|
||||
"Pink": "Rosa",
|
||||
"Gray": "Grigio",
|
||||
"Embed link": "Incorpora collegamento",
|
||||
"Invalid {{provider}} embed link": "Link di incorporamento {{provider}} non valido",
|
||||
"Embed {{provider}}": "Incorpora {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Inserisci il link {{provider}} per incorporare",
|
||||
"Bold": "Grassetto",
|
||||
"Italic": "Corsivo",
|
||||
"Underline": "Sottolineato",
|
||||
"Strike": "Barrato",
|
||||
"Code": "Codice",
|
||||
"Comment": "Commento",
|
||||
"Text": "Testo",
|
||||
"Heading 1": "Intestazione 1",
|
||||
"Heading 2": "Intestazione 2",
|
||||
"Heading 3": "Intestazione 3",
|
||||
"To-do List": "Lista delle cose da fare",
|
||||
"Bullet List": "Elenco Puntato",
|
||||
"Numbered List": "Elenco Numerato",
|
||||
"Blockquote": "Citazione",
|
||||
"Just start typing with plain text.": "Inizia a digitare con testo semplice.",
|
||||
"Track tasks with a to-do list.": "Tieni traccia delle attività con una lista di cose da fare.",
|
||||
"Big section heading.": "Intestazione di una grande sezione.",
|
||||
"Medium section heading.": "Intestazione di sezione media.",
|
||||
"Small section heading.": "Piccolo titolo di sezione.",
|
||||
"Create a simple bullet list.": "Crea un semplice elenco puntato.",
|
||||
"Create a list with numbering.": "Crea un elenco numerato.",
|
||||
"Create block quote.": "Crea blocco citazione.",
|
||||
"Insert code snippet.": "Inserisci frammento di codice.",
|
||||
"Insert horizontal rule divider": "Inserisci divisore di regola orizzontale",
|
||||
"Upload any image from your device.": "Carica un'immagine dal tuo dispositivo.",
|
||||
"Upload any video from your device.": "Carica qualsiasi video dal tuo dispositivo.",
|
||||
"Upload any file from your device.": "Carica qualsiasi file dal tuo dispositivo.",
|
||||
"Table": "Tabella",
|
||||
"Insert a table.": "Inserisci una tabella.",
|
||||
"Insert collapsible block.": "Inserisci blocco comprimibile.",
|
||||
"Video": "Video",
|
||||
"Divider": "Divisore",
|
||||
"Quote": "Preventivo",
|
||||
"Image": "Immagine",
|
||||
"File attachment": "Allegato file",
|
||||
"Toggle block": "Attiva blocco",
|
||||
"Callout": "Avviso",
|
||||
"Insert callout notice.": "Inserisci avviso di richiamo.",
|
||||
"Math inline": "Matematica in linea",
|
||||
"Insert inline math equation.": "Inserisci equazione matematica in linea.",
|
||||
"Math block": "Blocco matematico",
|
||||
"Insert math equation": "Inserisci equazione matematica",
|
||||
"Mermaid diagram": "Diagramma di Mermaid",
|
||||
"Insert mermaid diagram": "Inserisci un diagramma di Mermaid",
|
||||
"Insert and design Drawio diagrams": "Inserisci e progetta diagrammi Drawio",
|
||||
"Insert current date": "Inserisci la data corrente",
|
||||
"Draw and sketch excalidraw diagrams": "Disegna e schizza diagrammi excalidraw",
|
||||
"Multiple": "Multiplo",
|
||||
"Heading {{level}}": "Intestazione {{level}}",
|
||||
"Toggle title": "Attiva/disattiva titolo",
|
||||
"Write anything. Enter \"/\" for commands": "Scrivi qualcosa. Digita \"/\" per i comandi",
|
||||
"Names do not match": "I nomi non corrispondono",
|
||||
"Today, {{time}}": "Oggi, {{time}}",
|
||||
"Yesterday, {{time}}": "Ieri, {{time}}",
|
||||
"Space created successfully": "Spazio creato con successo",
|
||||
"Space updated successfully": "Spazio aggiornato con successo",
|
||||
"Space deleted successfully": "Spazio eliminato con successo",
|
||||
"Members added successfully": "Membri aggiunti con successo",
|
||||
"Member removed successfully": "Membro rimosso con successo",
|
||||
"Member role updated successfully": "Ruolo del membro aggiornato con successo",
|
||||
"Created by: <b>{{creatorName}}</b>": "Created by: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Created at: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Character count: {{characterCount}}"
|
||||
}
|
355
apps/client/public/locales/ja-JP/translation.json
Normal file
355
apps/client/public/locales/ja-JP/translation.json
Normal file
@ -0,0 +1,355 @@
|
||||
{
|
||||
"Account": "アカウント",
|
||||
"Active": "アクティブ",
|
||||
"Add": "追加",
|
||||
"Add group members": "グループメンバーを追加",
|
||||
"Add groups": "グループを追加",
|
||||
"Add members": "メンバーを追加",
|
||||
"Add to groups": "グループに追加",
|
||||
"Add space members": "スペースメンバーを追加",
|
||||
"Admin": "管理者",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "このグループを削除してもよろしいですか? メンバーはこのグループがアクセス権を持つリソースにアクセスできなくなります。",
|
||||
"Are you sure you want to delete this page?": "このページを削除してもよろしいですか?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "このユーザをグループから削除してもよろしいですか? ユーザはこのグループがアクセス権を持つリソースにアクセスできなくなります。",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "このユーザをスペースから削除してもよろしいですか? ユーザはこのスペースへのアクセス権をすべて失います。",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "このバージョンを復元してもよろしいですか? バージョン管理されていない変更は失われます。",
|
||||
"Can become members of groups and spaces in workspace": "ワークスペース内のグループやスペースのメンバーになることができます",
|
||||
"Can create and edit pages in space.": "スペース内のページを作成および編集できます。",
|
||||
"Can edit": "編集可能",
|
||||
"Can manage workspace": "ワークスペースを管理できます",
|
||||
"Can manage workspace but cannot delete it": "ワークスペースを管理できますが、削除はできません",
|
||||
"Can view": "閲覧可能",
|
||||
"Can view pages in space but not edit.": "スペース内のページを閲覧できますが、編集はできません。",
|
||||
"Cancel": "キャンセル",
|
||||
"Change email": "メールアドレスの変更",
|
||||
"Change password": "パスワードの変更",
|
||||
"Change photo": "画像の変更",
|
||||
"Choose a role": "ロールを選んでください",
|
||||
"Choose your preferred color scheme.": "お好みのカラースキームを選択してください。",
|
||||
"Choose your preferred interface language.": "お好みのインターフェース言語を選択してください。",
|
||||
"Choose your preferred page width.": "左右の余白を縮小する場合はオンにしてください。",
|
||||
"Confirm": "確認",
|
||||
"Copy link": "リンクをコピー",
|
||||
"Create": "新規作成",
|
||||
"Create group": "グループを作成",
|
||||
"Create page": "新規ページ",
|
||||
"Create space": "新規スペース",
|
||||
"Create workspace": "ワークスペースを作成",
|
||||
"Current password": "現在のパスワード",
|
||||
"Dark": "ダーク",
|
||||
"Date": "日付",
|
||||
"Delete": "削除",
|
||||
"Delete group": "グループを削除",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "このページを削除してもよろしいですか?この操作により、子ページおよびページ履歴が削除されます。この操作は元に戻せません。",
|
||||
"Description": "説明",
|
||||
"Details": "詳細",
|
||||
"e.g ACME": "例: 山田太郎",
|
||||
"e.g ACME Inc": "例: 株式会社サンプル",
|
||||
"e.g Developers": "例: エンジニア",
|
||||
"e.g Group for developers": "例: エンジニアグループ",
|
||||
"e.g product": "例: product",
|
||||
"e.g Product Team": "例: 製品チーム",
|
||||
"e.g Sales": "例: 営業",
|
||||
"e.g Space for product team": "例: 製品チームのスペース",
|
||||
"e.g Space for sales team to collaborate": "例: 営業チーム連携用スペース",
|
||||
"Edit": "編集",
|
||||
"Edit group": "グループを編集",
|
||||
"Email": "メールアドレス",
|
||||
"Enter a strong password": "強力なパスワードを入力してください",
|
||||
"Enter valid email addresses separated by comma or space max_50": "有効なメールアドレスをカンマまたはスペースで区切って入力してください(最大 50 個)",
|
||||
"enter valid emails addresses": "有効なメールアドレスを入力してください",
|
||||
"Enter your current password": "現在のパスワードを入力してください",
|
||||
"enter your full name": "氏名を入力してください",
|
||||
"Enter your new password": "新しいパスワードを入力してください",
|
||||
"Enter your new preferred email": "新しいメールアドレスを入力してください",
|
||||
"Enter your password": "パスワードを入力してください",
|
||||
"Error fetching page data.": "ページデータ取得中にエラーが発生しました。",
|
||||
"Error loading page history.": "ページ履歴の読み込み中にエラーが発生しました。",
|
||||
"Export": "エクスポート",
|
||||
"Failed to create page": "ページの作成に失敗しました",
|
||||
"Failed to delete page": "ページの削除に失敗しました",
|
||||
"Failed to fetch recent pages": "最近のページを取得できませんでした",
|
||||
"Failed to import pages": "ページのインポートに失敗しました",
|
||||
"Failed to load page. An error occurred.": "ページの読み込みに失敗しました。エラーが発生しました。",
|
||||
"Failed to update data": "データの更新に失敗しました",
|
||||
"Full access": "フルアクセス",
|
||||
"Full page width": "フルページ幅で表示",
|
||||
"Full width": "左右の余白を縮小",
|
||||
"General": "一般",
|
||||
"Group": "グループ",
|
||||
"Group description": "グループ説明",
|
||||
"Group name": "グループ名",
|
||||
"Groups": "グループ",
|
||||
"Has full access to space settings and pages.": "スペース設定とページにフルアクセスできます。",
|
||||
"Home": "ホーム",
|
||||
"Import pages": "ページをインポート",
|
||||
"Import pages & space settings": "ページとスペース設定をインポート",
|
||||
"Importing pages": "ページをインポートしています",
|
||||
"invalid invitation link": "招待リンクが間違っています",
|
||||
"Invitation signup": "招待登録",
|
||||
"Invite by email": "メールアドレスで招待する",
|
||||
"Invite members": "メンバーを招待する",
|
||||
"Invite new members": "新しいメンバーを招待する",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "招待をまだ承諾していないメンバーはここに表示されます。",
|
||||
"Invited members will be granted access to spaces the groups can access": "招待されたメンバーは、グループがアクセスできるスペースにアクセス権が付与されます",
|
||||
"Join the workspace": "ワークスペースに参加",
|
||||
"Language": "言語",
|
||||
"Light": "ライト",
|
||||
"Link copied": "リンクをコピーしました",
|
||||
"Login": "ログイン",
|
||||
"Logout": "ログアウト",
|
||||
"Manage Group": "グループを管理",
|
||||
"Manage members": "メンバーを管理",
|
||||
"member": "メンバー",
|
||||
"Member": "メンバー",
|
||||
"members": "メンバー",
|
||||
"Members": "メンバー",
|
||||
"My preferences": "個人設定",
|
||||
"My Profile": "プロフィール",
|
||||
"My profile": "プロフィール",
|
||||
"Name": "名前",
|
||||
"New email": "新しいメールアドレス",
|
||||
"New page": "新規ページ",
|
||||
"New password": "新しいパスワード",
|
||||
"No group found": "グループが見つかりません",
|
||||
"No page history saved yet.": "まだページの履歴が保存されていません。",
|
||||
"No pages yet": "ページがありません",
|
||||
"No results found...": "結果が見つかりませんでした...",
|
||||
"No user found": "ユーザがいません",
|
||||
"Overview": "概要",
|
||||
"Owner": "所有者",
|
||||
"page": "ページ",
|
||||
"Page deleted successfully": "ページが正常に削除されました",
|
||||
"Page history": "ページの履歴",
|
||||
"Page import is in progress. Please do not close this tab.": "ページのインポートが進行中です。このタブを閉じないでください。",
|
||||
"Pages": "ページ",
|
||||
"pages": "ページ",
|
||||
"Password": "パスワード",
|
||||
"Password changed successfully": "パスワードが正常に変更されました",
|
||||
"Pending": "保留中",
|
||||
"Please confirm your action": "アクションを確認してください",
|
||||
"Preferences": "設定",
|
||||
"Print PDF": "PDFを印刷",
|
||||
"Profile": "プロフィール",
|
||||
"Recently updated": "最近の更新",
|
||||
"Remove": "削除",
|
||||
"Remove group member": "グループメンバーを削除",
|
||||
"Remove space member": "スペースメンバーを削除",
|
||||
"Restore": "復元",
|
||||
"Role": "役割",
|
||||
"Save": "保存",
|
||||
"Search": "検索",
|
||||
"Search for groups": "グループを検索",
|
||||
"Search for users": "ユーザーを検索",
|
||||
"Search for users and groups": "ユーザーとグループを検索",
|
||||
"Search...": "検索する語句を入力",
|
||||
"Select language": "言語を選択",
|
||||
"Select role": "ロールを選択",
|
||||
"Select role to assign to all invited members": "招待されたすべてのメンバーに割り当てるロールを選択してください",
|
||||
"Select theme": "テーマを選択",
|
||||
"Send invitation": "招待を送る",
|
||||
"Invitation sent": "招待が送信されました",
|
||||
"Settings": "設定",
|
||||
"Setup workspace": "ワークスペースを設定する",
|
||||
"Sign In": "サインイン",
|
||||
"Sign Up": "アカウント登録",
|
||||
"Slug": "Slug (URL用文字列)",
|
||||
"Space": "スペース",
|
||||
"Space description": "スペース説明",
|
||||
"Space menu": "スペースメニュー",
|
||||
"Space name": "スペース名",
|
||||
"Space settings": "スペース設定",
|
||||
"Space slug": "スペースのSlug (URL用文字列)",
|
||||
"Spaces": "スペース",
|
||||
"Spaces you belong to": "所属しているスペース",
|
||||
"No space found": "スペースが見つかりません",
|
||||
"Search for spaces": "スペースを検索",
|
||||
"Start typing to search...": "検索を開始するには入力してください...",
|
||||
"Status": "ステータス",
|
||||
"Successfully imported": "インポートに成功しました",
|
||||
"Successfully restored": "正常に復元されました",
|
||||
"System settings": "システム設定",
|
||||
"Theme": "テーマ",
|
||||
"To change your email, you have to enter your password and new email.": "メールアドレスを変更するには、パスワードと新しいメールアドレスを入力する必要があります。",
|
||||
"Toggle full page width": "ページ幅を切り替える",
|
||||
"Unable to import pages. Please try again.": "ページをインポートできません。もう一度お試しください。",
|
||||
"untitled": "無題",
|
||||
"Untitled": "無題",
|
||||
"Updated successfully": "正常に更新されました",
|
||||
"User": "ユーザー",
|
||||
"Workspace": "ワークスペース",
|
||||
"Workspace Name": "ワークスペース名",
|
||||
"Workspace settings": "ワークスペース設定",
|
||||
"You can change your password here.": "パスワードを変更できます。",
|
||||
"Your Email": "メールアドレス",
|
||||
"Your import is complete.": "インポートが完了しました。",
|
||||
"Your name": "名前",
|
||||
"Your Name": "名前",
|
||||
"Your password": "パスワード",
|
||||
"Your password must be a minimum of 8 characters.": "パスワードは最低 8 文字必要です。",
|
||||
"Sidebar toggle": "サイドバー切り替え",
|
||||
"Comments": "コメント",
|
||||
"404 page not found": "404 ページが見つかりません",
|
||||
"Sorry, we can't find the page you are looking for.": "お探しのページが見つかりません。",
|
||||
"Take me back to homepage": "ホームに戻る",
|
||||
"Forgot password": "パスワードを忘れた",
|
||||
"Forgot your password?": "パスワードを忘れましたか?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "パスワードリセットリンクがあなたのメールアドレスに送信されました。受信箱を確認してください。",
|
||||
"Send reset link": "リセットリンクを送る",
|
||||
"Password reset": "パスワードリセット",
|
||||
"Your new password": "新しいパスワード",
|
||||
"Set password": "パスワードを設定",
|
||||
"Write a comment": "コメントを書く",
|
||||
"Reply...": "返信...",
|
||||
"Error loading comments.": "コメントの読み込み中にエラーが発生しました。",
|
||||
"No comments yet.": "コメントがありません。",
|
||||
"Edit comment": "コメントを編集する",
|
||||
"Delete comment": "コメントを削除する",
|
||||
"Are you sure you want to delete this comment?": "このコメントを削除してもよろしいですか?",
|
||||
"Comment created successfully": "コメントが作成されました",
|
||||
"Error creating comment": "コメントの作成中にエラーが発生しました",
|
||||
"Comment updated successfully": "コメントが更新されました",
|
||||
"Failed to update comment": "コメントの更新に失敗しました",
|
||||
"Comment deleted successfully": "コメントが削除されました",
|
||||
"Failed to delete comment": "コメントの削除に失敗しました",
|
||||
"Comment resolved successfully": "コメントが解決されました",
|
||||
"Failed to resolve comment": "コメントの解決に失敗しました",
|
||||
"Revoke invitation": "招待を取り消す",
|
||||
"Revoke": "取り消す",
|
||||
"Don't": "取り消さない",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "この招待を取り消してもよろしいですか? ユーザはワークスペースに参加できなくなります。",
|
||||
"Resend invitation": "招待を再度送る",
|
||||
"Anyone with this link can join this workspace.": "このリンクを持っている人は誰でもこのワークスペースに参加できます。",
|
||||
"Invite link": "招待リンク",
|
||||
"Copy": "コピー",
|
||||
"Copied": "コピーしました",
|
||||
"Select a user": "ユーザを選択",
|
||||
"Select a group": "グループを選択",
|
||||
"Export all pages and attachments in this space.": "このスペースのすべてのページと添付ファイルをエクスポートします。",
|
||||
"Delete space": "スペースを削除",
|
||||
"Are you sure you want to delete this space?": "このスペースを削除してもよろしいですか?",
|
||||
"Delete this space with all its pages and data.": "このスペースおよびスペース内のすべてのページとデータを削除します。",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "このスペース内のすべてのページ、コメント、添付ファイル、および権限は完全に削除されます。",
|
||||
"Confirm space name": "スペース名を確認する",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "アクションを確認するためにスペース名 <b>{{spaceName}}</b> を入力してください。",
|
||||
"Format": "フォーマット",
|
||||
"Include subpages": "サブページを含める",
|
||||
"Include attachments": "添付ファイルを含める",
|
||||
"Select export format": "エクスポート形式を選択",
|
||||
"Export failed:": "エクスポートに失敗しました:",
|
||||
"export error": "エクスポートエラー",
|
||||
"Export page": "エクスポートページ",
|
||||
"Export space": "エクスポートスペース",
|
||||
"Export {{type}}": "{{type}}をエクスポート",
|
||||
"File exceeds the {{limit}} attachment limit": "ファイルが{{limit}}の添付制限を超えています",
|
||||
"Align left": "左揃え",
|
||||
"Align right": "右揃え",
|
||||
"Align center": "中央揃え",
|
||||
"Justify": "両端揃え",
|
||||
"Merge cells": "セルを結合",
|
||||
"Split cell": "セルを分割",
|
||||
"Delete column": "列を削除",
|
||||
"Delete row": "行を削除",
|
||||
"Add left column": "左側に列を追加",
|
||||
"Add right column": "右側の列を追加",
|
||||
"Add row above": "上に行を追加",
|
||||
"Add row below": "下に行を追加",
|
||||
"Delete table": "テーブルを削除",
|
||||
"Info": "情報",
|
||||
"Success": "成功",
|
||||
"Warning": "警告",
|
||||
"Danger": "危険",
|
||||
"Mermaid diagram error:": "Mermaid コードエラー",
|
||||
"Invalid Mermaid diagram": "無効な Mermaid コードです",
|
||||
"Double-click to edit Draw.io diagram": "ダブルクリックしてDraw.ioの図を編集",
|
||||
"Exit": "終了",
|
||||
"Save & Exit": "保存して終了",
|
||||
"Double-click to edit Excalidraw diagram": "ダブルクリックしてExcalidraw図を編集",
|
||||
"Paste link": "リンクを貼り付け",
|
||||
"Edit link": "リンクを編集",
|
||||
"Remove link": "リンクを削除",
|
||||
"Add link": "リンクを追加",
|
||||
"Please enter a valid url": "有効なURLを入力してください",
|
||||
"Empty equation": "空の数式です",
|
||||
"Invalid equation": "不正な数式です",
|
||||
"Color": "カラー",
|
||||
"Text color": "テキストカラー",
|
||||
"Default": "デフォルト",
|
||||
"Blue": "青色",
|
||||
"Green": "緑色",
|
||||
"Purple": "紫色",
|
||||
"Red": "赤色",
|
||||
"Yellow": "黄色",
|
||||
"Orange": "オレンジ色",
|
||||
"Pink": "ピンク色",
|
||||
"Gray": "灰色",
|
||||
"Embed link": "リンクを埋め込む",
|
||||
"Invalid {{provider}} embed link": "埋め込まれた {{provider}} のリンクは無効です",
|
||||
"Embed {{provider}}": "埋め込まれた {{provider}}",
|
||||
"Enter {{provider}} link to embed": "埋め込みたい {{provider}} のリンクを入力してください",
|
||||
"Bold": "太字",
|
||||
"Italic": "斜線",
|
||||
"Underline": "下線",
|
||||
"Strike": "打ち消し線",
|
||||
"Code": "コードブロック",
|
||||
"Comment": "コメント",
|
||||
"Text": "テキスト",
|
||||
"Heading 1": "見出し 1",
|
||||
"Heading 2": "見出し 2",
|
||||
"Heading 3": "見出し 3",
|
||||
"To-do List": "To-doリスト",
|
||||
"Bullet List": "箇条書きリスト",
|
||||
"Numbered List": "番号付きリスト",
|
||||
"Blockquote": "引用",
|
||||
"Just start typing with plain text.": "すぐに文章を書き始められます。",
|
||||
"Track tasks with a to-do list.": "Todoリストでタスクを追跡します。",
|
||||
"Big section heading.": "大きいフォントのセクション見出しです。",
|
||||
"Medium section heading.": "中くらいのフォントのセクション見出しです。",
|
||||
"Small section heading.": "小さいフォントのセクション見出しです。",
|
||||
"Create a simple bullet list.": "シンプルな箇条書きのリストを作成します。",
|
||||
"Create a list with numbering.": "番号付きのリストを作成します。",
|
||||
"Create block quote.": "引用文を作成します。",
|
||||
"Insert code snippet.": "コードスニペットを入力します。",
|
||||
"Insert horizontal rule divider": "水平線を挿入します。",
|
||||
"Upload any image from your device.": "画像をアップロードします。",
|
||||
"Upload any video from your device.": "動画をアップロードします。",
|
||||
"Upload any file from your device.": "ファイルをアップロードします。",
|
||||
"Table": "テーブル",
|
||||
"Insert a table.": "表を挿入します。",
|
||||
"Insert collapsible block.": "折りたたみ可能なブロックを挿入します。",
|
||||
"Video": "動画",
|
||||
"Divider": "区切り線",
|
||||
"Quote": "引用",
|
||||
"Image": "画像",
|
||||
"File attachment": "ファイル添付",
|
||||
"Toggle block": "ブロックを切り替える",
|
||||
"Callout": "コールアウト",
|
||||
"Insert callout notice.": "コールアウトブロックを挿入します。",
|
||||
"Math inline": "インライン数式",
|
||||
"Insert inline math equation.": "インライン数式を挿入します。",
|
||||
"Math block": "数式ブロック",
|
||||
"Insert math equation": "数式を挿入します",
|
||||
"Mermaid diagram": "Mermaidコード",
|
||||
"Insert mermaid diagram": "Mermaidコードを記述して図を挿入します",
|
||||
"Insert and design Drawio diagrams": "Drawioの図を挿入してデザインします",
|
||||
"Insert current date": "今日の日付を挿入します",
|
||||
"Draw and sketch excalidraw diagrams": "Excalidrawの図を埋め込みます",
|
||||
"Multiple": "複数",
|
||||
"Heading {{level}}": "見出し {{level}}",
|
||||
"Toggle title": "タイトルの表示/非表示を切り替える",
|
||||
"Write anything. Enter \"/\" for commands": "文字を入力するか、「/」でコマンドを呼び出します",
|
||||
"Names do not match": "名前が一致しません",
|
||||
"Today, {{time}}": "今日、{{time}}",
|
||||
"Yesterday, {{time}}": "昨日、{{time}}",
|
||||
"Space created successfully": "スペースを作成しました",
|
||||
"Space updated successfully": "スペースを更新しました",
|
||||
"Space deleted successfully": "スペースが削除されました",
|
||||
"Members added successfully": "メンバーを追加しました",
|
||||
"Member removed successfully": "メンバーが削除されました",
|
||||
"Member role updated successfully": "メンバーのロールを更新しました",
|
||||
"Created by: <b>{{creatorName}}</b>": "Created by: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Created at: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Character count: {{characterCount}}"
|
||||
}
|
355
apps/client/public/locales/ko-KR/translation.json
Normal file
355
apps/client/public/locales/ko-KR/translation.json
Normal file
@ -0,0 +1,355 @@
|
||||
{
|
||||
"Account": "계정",
|
||||
"Active": "활성",
|
||||
"Add": "추가",
|
||||
"Add group members": "팀에 사용자 추가",
|
||||
"Add groups": "팀 생성",
|
||||
"Add members": "사용자 추가",
|
||||
"Add to groups": "팀에 추가",
|
||||
"Add space members": "Space에 사용자 추가",
|
||||
"Admin": "관리자",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "이 팀을 삭제하시겠습니까? 해당 팀에 속한 사용자들은 이 팀이 가진 모든 권한을 잃게 됩니다.",
|
||||
"Are you sure you want to delete this page?": "이 페이지를 삭제하시겠습니까?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "이 사용자를 팀에서 제거하시겠습니까? 사용자는 이 팀이 가진 모든 권한을 잃게 됩니다.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "이 사용자를 Space에서 제거하시겠습니까? 사용자는 이 Space에 대한 모든 접근 권한을 잃게 됩니다.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "이 버전으로 복원하시겠습니까? 저장되지 않은 모든 변경사항이 손실됩니다.",
|
||||
"Can become members of groups and spaces in workspace": "Workspace 내 팀 및 Space의 사용자가 될 수 있습니다.",
|
||||
"Can create and edit pages in space.": "Space에 페이지를 생성하고 편집할 수 있습니다.",
|
||||
"Can edit": "편집할 수 있음",
|
||||
"Can manage workspace": "Workspace를 관리할 수 있음",
|
||||
"Can manage workspace but cannot delete it": "Workspace를 관리할 수 있지만, 삭제는 불가능.",
|
||||
"Can view": "볼 수 있음",
|
||||
"Can view pages in space but not edit.": "Space의 페이지를 볼 수 있지만, 편집은 불가능.",
|
||||
"Cancel": "취소",
|
||||
"Change email": "이메일 변경",
|
||||
"Change password": "비밀번호 변경",
|
||||
"Change photo": "사진 변경",
|
||||
"Choose a role": "역할 선택",
|
||||
"Choose your preferred color scheme.": "선호하는 배경 색을 선택하세요.",
|
||||
"Choose your preferred interface language.": "선호하는 인터페이스 언어를 선택하세요.",
|
||||
"Choose your preferred page width.": "선호하는 페이지 너비를 선택하세요.",
|
||||
"Confirm": "확인",
|
||||
"Copy link": "링크 복사",
|
||||
"Create": "생성",
|
||||
"Create group": "팀 생성",
|
||||
"Create page": "페이지 생성",
|
||||
"Create space": "Space 생성",
|
||||
"Create workspace": "Workspace 생성",
|
||||
"Current password": "현재 비밀번호",
|
||||
"Dark": "어두운",
|
||||
"Date": "날짜",
|
||||
"Delete": "삭제",
|
||||
"Delete group": "팀 삭제",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "이 페이지를 삭제하시겠습니까? 하위 페이지와 페이지 기록이 모두 삭제됩니다. 이 작업은 되돌릴 수 없습니다.",
|
||||
"Description": "설명",
|
||||
"Details": "세부사항",
|
||||
"e.g ACME": "예: ACME",
|
||||
"e.g ACME Inc": "예: ACME Inc",
|
||||
"e.g Developers": "예: 개발자",
|
||||
"e.g Group for developers": "예: 개발자를 위한 팀",
|
||||
"e.g product": "예: 제품",
|
||||
"e.g Product Team": "예: 제품 팀",
|
||||
"e.g Sales": "예: 영업",
|
||||
"e.g Space for product team": "예: 제품 팀을 위한 Space",
|
||||
"e.g Space for sales team to collaborate": "예: 영업 팀의 Space",
|
||||
"Edit": "편집",
|
||||
"Edit group": "팀 편집",
|
||||
"Email": "이메일",
|
||||
"Enter a strong password": "강력한 비밀번호를 입력하세요",
|
||||
"Enter valid email addresses separated by comma or space max_50": "유효한 이메일 주소를 쉼표나 공백으로 구분하여 입력하세요 [최대: 50]",
|
||||
"enter valid emails addresses": "유효한 이메일 주소를 입력하세요",
|
||||
"Enter your current password": "현재 비밀번호를 입력하세요",
|
||||
"enter your full name": "전체 이름을 입력하세요",
|
||||
"Enter your new password": "새 비밀번호를 입력하세요",
|
||||
"Enter your new preferred email": "새로운 이메일을 입력하세요",
|
||||
"Enter your password": "비밀번호를 입력하세요",
|
||||
"Error fetching page data.": "페이지 데이터 불러오기 오류.",
|
||||
"Error loading page history.": "페이지 기록 불러오기 오류.",
|
||||
"Export": "내보내기",
|
||||
"Failed to create page": "페이지 생성 실패",
|
||||
"Failed to delete page": "페이지 삭제 실패",
|
||||
"Failed to fetch recent pages": "최근 페이지 불러오기 실패",
|
||||
"Failed to import pages": "페이지 가져오기 실패",
|
||||
"Failed to load page. An error occurred.": "페이지 불러오기 실패. 오류가 발생했습니다.",
|
||||
"Failed to update data": "데이터 갱신 실패",
|
||||
"Full access": "전체 권한",
|
||||
"Full page width": "전체 페이지 너비",
|
||||
"Full width": "전체 너비",
|
||||
"General": "일반",
|
||||
"Group": "팀",
|
||||
"Group description": "팀 설명",
|
||||
"Group name": "팀 이름",
|
||||
"Groups": "팀",
|
||||
"Has full access to space settings and pages.": "Space 설정과 페이지에 대한 전체 접근 권한이 있습니다.",
|
||||
"Home": "홈",
|
||||
"Import pages": "페이지 가져오기",
|
||||
"Import pages & space settings": "페이지 및 Space 설정 가져오기",
|
||||
"Importing pages": "페이지 가져오는 중",
|
||||
"invalid invitation link": "유효하지 않은 초대 링크",
|
||||
"Invitation signup": "초대 가입",
|
||||
"Invite by email": "이메일로 초대",
|
||||
"Invite members": "사용자 초대",
|
||||
"Invite new members": "새 사용자 초대",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "초대를 아직 수락하지 않은 초대된 사용자가 여기에 표시됩니다.",
|
||||
"Invited members will be granted access to spaces the groups can access": "초대된 사용자는 팀이 접근할 수 있는 Space에 대한 접근 권한을 받게 됩니다",
|
||||
"Join the workspace": "Workspace 참여",
|
||||
"Language": "언어",
|
||||
"Light": "밝은",
|
||||
"Link copied": "링크 복사됨",
|
||||
"Login": "로그인",
|
||||
"Logout": "로그아웃",
|
||||
"Manage Group": "팀 관리",
|
||||
"Manage members": "사용자 관리",
|
||||
"member": "사용자",
|
||||
"Member": "사용자",
|
||||
"members": "사용자들",
|
||||
"Members": "사용자들",
|
||||
"My preferences": "내 설정",
|
||||
"My Profile": "내 프로필",
|
||||
"My profile": "내 프로필",
|
||||
"Name": "이름",
|
||||
"New email": "새 이메일",
|
||||
"New page": "새 페이지",
|
||||
"New password": "새 비밀번호",
|
||||
"No group found": "팀을 찾을 수 없음",
|
||||
"No page history saved yet.": "아직 저장된 페이지 기록이 없습니다.",
|
||||
"No pages yet": "아직 페이지가 없습니다",
|
||||
"No results found...": "결과를 찾을 수 없습니다...",
|
||||
"No user found": "사용자를 찾을 수 없음",
|
||||
"Overview": "개요",
|
||||
"Owner": "소유자",
|
||||
"page": "페이지",
|
||||
"Page deleted successfully": "페이지 삭제 완료",
|
||||
"Page history": "페이지 기록",
|
||||
"Page import is in progress. Please do not close this tab.": "페이지 가져오기가 진행 중입니다. 이 탭을 닫지 마세요.",
|
||||
"Pages": "페이지",
|
||||
"pages": "페이지",
|
||||
"Password": "비밀번호",
|
||||
"Password changed successfully": "비밀번호 변경 완료",
|
||||
"Pending": "대기 중",
|
||||
"Please confirm your action": "작업을 확인해 주세요",
|
||||
"Preferences": "설정",
|
||||
"Print PDF": "PDF로 인쇄",
|
||||
"Profile": "프로필",
|
||||
"Recently updated": "최근 업데이트",
|
||||
"Remove": "제거",
|
||||
"Remove group member": "팀에서 사용자 제거",
|
||||
"Remove space member": "Space에서 사용자 제거",
|
||||
"Restore": "복원",
|
||||
"Role": "역할",
|
||||
"Save": "저장",
|
||||
"Search": "검색",
|
||||
"Search for groups": "팀 검색",
|
||||
"Search for users": "사용자 검색",
|
||||
"Search for users and groups": "사용자 및 팀 검색",
|
||||
"Search...": "검색...",
|
||||
"Select language": "언어 선택",
|
||||
"Select role": "역할 선택",
|
||||
"Select role to assign to all invited members": "초대된 모든 사용자에게 할당할 역할 선택",
|
||||
"Select theme": "배경 선택",
|
||||
"Send invitation": "초대 보내기",
|
||||
"Invitation sent": "Invitation sent",
|
||||
"Settings": "설정",
|
||||
"Setup workspace": "Workspace 설정",
|
||||
"Sign In": "로그인",
|
||||
"Sign Up": "회원 가입",
|
||||
"Slug": "고유 경로",
|
||||
"Space": "Space",
|
||||
"Space description": "Space 설명",
|
||||
"Space menu": "Space 메뉴",
|
||||
"Space name": "Space 이름",
|
||||
"Space settings": "Space 설정",
|
||||
"Space slug": "Space의 고유 경로",
|
||||
"Spaces": "Space",
|
||||
"Spaces you belong to": "소속된 Space",
|
||||
"No space found": "Space을 찾을 수 없음",
|
||||
"Search for spaces": "Space 검색",
|
||||
"Start typing to search...": "검색하려면 입력을 시작하세요...",
|
||||
"Status": "상태",
|
||||
"Successfully imported": "가져오기 완료",
|
||||
"Successfully restored": "복원 완료",
|
||||
"System settings": "시스템 설정",
|
||||
"Theme": "배경",
|
||||
"To change your email, you have to enter your password and new email.": "이메일을 변경하려면 현재 비밀번호와 새 이메일을 입력해야 합니다.",
|
||||
"Toggle full page width": "전체 페이지 너비 전환",
|
||||
"Unable to import pages. Please try again.": "페이지를 가져올 수 없습니다. 다시 시도해주세요.",
|
||||
"untitled": "제목 없음",
|
||||
"Untitled": "제목 없음",
|
||||
"Updated successfully": "업데이트 완료",
|
||||
"User": "사용자",
|
||||
"Workspace": "Workspace",
|
||||
"Workspace Name": "Workspce 이름",
|
||||
"Workspace settings": "Workspace 설정",
|
||||
"You can change your password here.": "여기서 비밀번호를 변경할 수 있습니다.",
|
||||
"Your Email": "이메일",
|
||||
"Your import is complete.": "가져오기가 완료되었습니다.",
|
||||
"Your name": "이름",
|
||||
"Your Name": "이름",
|
||||
"Your password": "비밀번호",
|
||||
"Your password must be a minimum of 8 characters.": "비밀번호는 최소 8자 이상이어야 합니다.",
|
||||
"Sidebar toggle": "사이드바 전환",
|
||||
"Comments": "댓글",
|
||||
"404 page not found": "404 페이지를 찾을 수 없음",
|
||||
"Sorry, we can't find the page you are looking for.": "죄송합니다. 페이지를 찾을 수 없습니다.",
|
||||
"Take me back to homepage": "홈페이지로 돌아가기",
|
||||
"Forgot password": "비밀번호 찾기",
|
||||
"Forgot your password?": "비밀번호를 잊으셨나요?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "비밀번호 재설정 링크가 이메일로 전송되었습니다. 받은 편지함을 확인해주세요.",
|
||||
"Send reset link": "재설정 링크 보내기",
|
||||
"Password reset": "비밀번호 재설정",
|
||||
"Your new password": "새 비밀번호",
|
||||
"Set password": "비밀번호 설정",
|
||||
"Write a comment": "댓글 작성",
|
||||
"Reply...": "답글...",
|
||||
"Error loading comments.": "댓글 불러오기 오류.",
|
||||
"No comments yet.": "아직 댓글이 없습니다.",
|
||||
"Edit comment": "댓글 수정",
|
||||
"Delete comment": "댓글 삭제",
|
||||
"Are you sure you want to delete this comment?": "이 댓글을 삭제하시겠습니까?",
|
||||
"Comment created successfully": "댓글 생성 완료",
|
||||
"Error creating comment": "댓글 생성 오류",
|
||||
"Comment updated successfully": "댓글 업데이트 완료",
|
||||
"Failed to update comment": "댓글 업데이트 실패",
|
||||
"Comment deleted successfully": "댓글 삭제 완료",
|
||||
"Failed to delete comment": "댓글 삭제 실패",
|
||||
"Comment resolved successfully": "댓글 처리 완료",
|
||||
"Failed to resolve comment": "댓글 처리 실패",
|
||||
"Revoke invitation": "초대 취소",
|
||||
"Revoke": "취소",
|
||||
"Don't": "하지 않음",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "이 초대를 취소하시겠습니까? 사용자가 Workspace에 참여할 수 없게 됩니다.",
|
||||
"Resend invitation": "초대 재전송",
|
||||
"Anyone with this link can join this workspace.": "이 링크를 가진 모든 사용자가 이 Workspace에 참여할 수 있습니다.",
|
||||
"Invite link": "초대 링크",
|
||||
"Copy": "복사",
|
||||
"Copied": "복사됨",
|
||||
"Select a user": "사용자 선택",
|
||||
"Select a group": "팀 선택",
|
||||
"Export all pages and attachments in this space.": "이 Space의 모든 페이지와 첨부파일을 내보냅니다.",
|
||||
"Delete space": "Space 삭제",
|
||||
"Are you sure you want to delete this space?": "이 Space을 삭제하시겠습니까?",
|
||||
"Delete this space with all its pages and data.": "이 Space의 모든 페이지와 데이터를 삭제합니다.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "이 Space의 모든 페이지, 댓글, 첨부파일 및 권한이 영구적으로 삭제됩니다.",
|
||||
"Confirm space name": "Space 이름 확인",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "작업을 진행하려면 Space 이름 <b>{{spaceName}}</b>을 입력하세요.",
|
||||
"Format": "형식",
|
||||
"Include subpages": "하위 페이지 포함",
|
||||
"Include attachments": "첨부파일 포함",
|
||||
"Select export format": "내보내기 형식 선택",
|
||||
"Export failed:": "내보내기 실패:",
|
||||
"export error": "내보내기 오류",
|
||||
"Export page": "페이지 내보내기",
|
||||
"Export space": "Space 내보내기",
|
||||
"Export {{type}}": "{{type}} 내보내기",
|
||||
"File exceeds the {{limit}} attachment limit": "첨부 파일 크기 제한 {{limit}}을 초과했습니다",
|
||||
"Align left": "왼쪽 정렬",
|
||||
"Align right": "오른쪽 정렬",
|
||||
"Align center": "가운데 정렬",
|
||||
"Justify": "Justify",
|
||||
"Merge cells": "셀 병합",
|
||||
"Split cell": "셀 분할",
|
||||
"Delete column": "열 삭제",
|
||||
"Delete row": "행 삭제",
|
||||
"Add left column": "왼쪽 열 추가",
|
||||
"Add right column": "오른쪽 열 추가",
|
||||
"Add row above": "위에 행 추가",
|
||||
"Add row below": "아래에 행 추가",
|
||||
"Delete table": "테이블 삭제",
|
||||
"Info": "정보",
|
||||
"Success": "완료",
|
||||
"Warning": "주의",
|
||||
"Danger": "위험",
|
||||
"Mermaid diagram error:": "Mermaid diagram 오류:",
|
||||
"Invalid Mermaid diagram": "잘못된 Mermaid diagram",
|
||||
"Double-click to edit Draw.io diagram": "Draw.io diagram을 편집하려면 더블 클릭하세요",
|
||||
"Exit": "나가기",
|
||||
"Save & Exit": "저장 후 나가기",
|
||||
"Double-click to edit Excalidraw diagram": "Excalidraw diagram을 편집하려면 더블 클릭하세요",
|
||||
"Paste link": "링크 붙여넣기",
|
||||
"Edit link": "링크 수정",
|
||||
"Remove link": "링크 제거",
|
||||
"Add link": "링크 추가",
|
||||
"Please enter a valid url": "유효한 URL을 입력하세요",
|
||||
"Empty equation": "빈 수식",
|
||||
"Invalid equation": "잘못된 수식",
|
||||
"Color": "색상",
|
||||
"Text color": "텍스트 색상",
|
||||
"Default": "기본값",
|
||||
"Blue": "파란색",
|
||||
"Green": "초록색",
|
||||
"Purple": "보라색",
|
||||
"Red": "빨간색",
|
||||
"Yellow": "노란색",
|
||||
"Orange": "주황색",
|
||||
"Pink": "분홍색",
|
||||
"Gray": "회색",
|
||||
"Embed link": "임베드 링크",
|
||||
"Invalid {{provider}} embed link": "잘못된 {{provider}} 임베드 링크",
|
||||
"Embed {{provider}}": "{{provider}} 임베드",
|
||||
"Enter {{provider}} link to embed": "임베드를 할 {{provider}} 링크 입력",
|
||||
"Bold": "굵게",
|
||||
"Italic": "기울임",
|
||||
"Underline": "밑줄",
|
||||
"Strike": "취소선",
|
||||
"Code": "코드",
|
||||
"Comment": "댓글",
|
||||
"Text": "텍스트",
|
||||
"Heading 1": "제목 1",
|
||||
"Heading 2": "제목 2",
|
||||
"Heading 3": "제목 3",
|
||||
"To-do List": "할 일 목록",
|
||||
"Bullet List": "글머리 표",
|
||||
"Numbered List": "문단 번호",
|
||||
"Blockquote": "인용구",
|
||||
"Just start typing with plain text.": "일반 텍스트로 입력을 시작하세요.",
|
||||
"Track tasks with a to-do list.": "할 일 목록으로 작업을 정리하세요.",
|
||||
"Big section heading.": "대제목.",
|
||||
"Medium section heading.": "중제목.",
|
||||
"Small section heading.": "소제목.",
|
||||
"Create a simple bullet list.": "글머리 기호 만들기.",
|
||||
"Create a list with numbering.": "숫자 목록 만들기.",
|
||||
"Create block quote.": "인용구 만들기.",
|
||||
"Insert code snippet.": "코드 블록 삽입.",
|
||||
"Insert horizontal rule divider": "가로 구분선 삽입",
|
||||
"Upload any image from your device.": "기기에서 이미지를 업로드하세요.",
|
||||
"Upload any video from your device.": "기기에서 비디오를 업로드하세요.",
|
||||
"Upload any file from your device.": "기기에서 파일을 업로드하세요.",
|
||||
"Table": "테이블",
|
||||
"Insert a table.": "테이블 삽입.",
|
||||
"Insert collapsible block.": "접을 수 있는 블록 삽입.",
|
||||
"Video": "비디오",
|
||||
"Divider": "구분선",
|
||||
"Quote": "인용",
|
||||
"Image": "이미지",
|
||||
"File attachment": "파일 첨부",
|
||||
"Toggle block": "블록 토글",
|
||||
"Callout": "경고 상자",
|
||||
"Insert callout notice.": "돋보이는 글을 작성하기.",
|
||||
"Math inline": "수식",
|
||||
"Insert inline math equation.": "수식 삽입.",
|
||||
"Math block": "수식 블록",
|
||||
"Insert math equation": "수식 삽입",
|
||||
"Mermaid diagram": "Mermaid diagram",
|
||||
"Insert mermaid diagram": "Mermaid diagram 삽입",
|
||||
"Insert and design Drawio diagrams": "Drawio diagram 삽입 및 디자인",
|
||||
"Insert current date": "현재 날짜 삽입",
|
||||
"Draw and sketch excalidraw diagrams": "Excalidraw diagram 그리기 및 스케치",
|
||||
"Multiple": "복제",
|
||||
"Heading {{level}}": "제목 {{level}}",
|
||||
"Toggle title": "제목 토글",
|
||||
"Write anything. Enter \"/\" for commands": "아무거나 입력하세요. 명령어를 사용하려면 \"/\"를 입력하세요",
|
||||
"Names do not match": "이름이 일치하지 않습니다",
|
||||
"Today, {{time}}": "오늘, {{time}}",
|
||||
"Yesterday, {{time}}": "어제, {{time}}",
|
||||
"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>": "Created by: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Created at: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Character count: {{characterCount}}"
|
||||
}
|
355
apps/client/public/locales/nl-NL/translation.json
Normal file
355
apps/client/public/locales/nl-NL/translation.json
Normal file
@ -0,0 +1,355 @@
|
||||
{
|
||||
"Account": "Account",
|
||||
"Active": "Actief",
|
||||
"Add": "Toevoegen",
|
||||
"Add group members": "Groepsleden toevoegen",
|
||||
"Add groups": "Groepen Toevoegen",
|
||||
"Add members": "Leden toevoegen",
|
||||
"Add to groups": "Toevoegen aan groepen",
|
||||
"Add space members": "Voeg leden toe ruimte",
|
||||
"Admin": "Beheerder",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Weet je zeker dat je deze groep wilt verwijderen? Leden verliezen toegang tot documenten waar deze groep toegang toe heeft.",
|
||||
"Are you sure you want to delete this page?": "Weet u zeker dat u deze pagina wil verwijderen?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Weet je zeker dat je deze groep wilt verwijderen? Leden verliezen toegang tot documenten waar deze groep toegang toe heeft.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Weet u zeker dat u deze gebruiker van de ruimte wilt verwijderen? De gebruiker zal alle toegang tot deze ruimte verliezen.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Weet u zeker dat u deze versie wilt herstellen? Wijzigingen die geen versie hebben zullen verloren gaan.",
|
||||
"Can become members of groups and spaces in workspace": "Kunnen lid worden van groepen en ruimtes in de werkruimte",
|
||||
"Can create and edit pages in space.": "Kan pagina's in de ruimte maken en bewerken.",
|
||||
"Can edit": "Kan bewerken",
|
||||
"Can manage workspace": "Kan werkruimte beheren",
|
||||
"Can manage workspace but cannot delete it": "Kan een werkruimte beheren, maar kan deze niet verwijderen",
|
||||
"Can view": "Kan bekijken",
|
||||
"Can view pages in space but not edit.": "Kan pagina's in de ruimte bekijken maar niet bewerken.",
|
||||
"Cancel": "Annuleren",
|
||||
"Change email": "Wijzig e-mailadres",
|
||||
"Change password": "Wijzig wachtwoord",
|
||||
"Change photo": "Wijzig foto",
|
||||
"Choose a role": "Kies een rol",
|
||||
"Choose your preferred color scheme.": "Kies uw gewenste kleurenschema.",
|
||||
"Choose your preferred interface language.": "Kies uw gewenste interfacetaal.",
|
||||
"Choose your preferred page width.": "Kies uw gewenste paginabreedte.",
|
||||
"Confirm": "Bevestig",
|
||||
"Copy link": "Link kopiëren",
|
||||
"Create": "Aanmaken",
|
||||
"Create group": "Groep aanmaken",
|
||||
"Create page": "Pagina aanmaken",
|
||||
"Create space": "Ruimte aanmaken",
|
||||
"Create workspace": "Wwerkruimte aanmaken",
|
||||
"Current password": "Huidig wachtwoord",
|
||||
"Dark": "Donker",
|
||||
"Date": "Datum",
|
||||
"Delete": "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.",
|
||||
"Description": "Beschrijving",
|
||||
"Details": "Details",
|
||||
"e.g ACME": "bijv. ACME",
|
||||
"e.g ACME Inc": "bijv. ACME Inc",
|
||||
"e.g Developers": "bijv. Ontwikkelaars",
|
||||
"e.g Group for developers": "bijv. Groep voor ontwikkelaars",
|
||||
"e.g product": "bijv. product",
|
||||
"e.g Product Team": "bijv. Product Team",
|
||||
"e.g Sales": "bijv. Verkopen",
|
||||
"e.g Space for product team": "bijv. Ruimte voor productteam",
|
||||
"e.g Space for sales team to collaborate": "bijv. Ruimte voor verkoopteam om samen te werken",
|
||||
"Edit": "Bewerken",
|
||||
"Edit group": "Groep bewerken",
|
||||
"Email": "E-mailadres",
|
||||
"Enter a strong password": "Voer een sterk wachtwoord in",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Voer geldige e-mailadressen in, gescheiden door komma of spatie [max: 50]",
|
||||
"enter valid emails addresses": "voer geldige e-mailadressen in",
|
||||
"Enter your current password": "Voer uw huidige wachtwoord in",
|
||||
"enter your full name": "voer uw volledige naam in",
|
||||
"Enter your new password": "Voer uw nieuwe wachtwoord in",
|
||||
"Enter your new preferred email": "Voer uw nieuwe e-mailadres in",
|
||||
"Enter your password": "Voer uw wachtwoord in",
|
||||
"Error fetching page data.": "Fout bij het ophalen van paginagegevens.",
|
||||
"Error loading page history.": "Fout bij het laden van de paginageschiedenis.",
|
||||
"Export": "Exporteer",
|
||||
"Failed to create page": "Pagina aanmaken mislukt",
|
||||
"Failed to delete page": "Verwijderen van pagina mislukt",
|
||||
"Failed to fetch recent pages": "Kan recente pagina's niet ophalen",
|
||||
"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 update data": "Bijwerken van gegevens mislukt",
|
||||
"Full access": "Volledig toegang",
|
||||
"Full page width": "Volledige pagina breedte",
|
||||
"Full width": "Volledige breedte",
|
||||
"General": "Algemeen",
|
||||
"Group": "Groep",
|
||||
"Group description": "Groepsomschrijving",
|
||||
"Group name": "Groepsnaam",
|
||||
"Groups": "Groepen",
|
||||
"Has full access to space settings and pages.": "Heeft volledige toegang tot ruimte instellingen en pagina's.",
|
||||
"Home": "Startpagina",
|
||||
"Import pages": "Importeer pagina's",
|
||||
"Import pages & space settings": "Importeer pagina en ruimte instellingen",
|
||||
"Importing pages": "Importeer pagina's",
|
||||
"invalid invitation link": "ongeldige uitnodigingslink",
|
||||
"Invitation signup": "Uitnodiging aanmelding",
|
||||
"Invite by email": "Uitnodigen via e-mail",
|
||||
"Invite members": "Leden uitnodigen",
|
||||
"Invite new members": "Nieuwe leden uitnodigen",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Uigenodigde leden die hun uitnodiging nog moeten accepteren zullen hier worden getoond.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Uitgenodigde leden wordt toegang gegeven tot ruimtes de groepen toegang toe heeft",
|
||||
"Join the workspace": "Word lid van de werkruimte",
|
||||
"Language": "Taal",
|
||||
"Light": "Licht",
|
||||
"Link copied": "Link gekopieerd",
|
||||
"Login": "Inloggen",
|
||||
"Logout": "Uitloggen",
|
||||
"Manage Group": "Groep beheren",
|
||||
"Manage members": "Leden beheren",
|
||||
"member": "lid",
|
||||
"Member": "Lid",
|
||||
"members": "leden",
|
||||
"Members": "Leden",
|
||||
"My preferences": "Mijn voorkeuren",
|
||||
"My Profile": "Mijn profiel",
|
||||
"My profile": "Mijn profiel",
|
||||
"Name": "Naam",
|
||||
"New email": "Nieuw e-mail",
|
||||
"New page": "Nieuwe pagina",
|
||||
"New password": "Nieuw wachtwoord",
|
||||
"No group found": "Geen groep gevonden",
|
||||
"No page history saved yet.": "Er is nog geen pagina geschiedenis opgeslagen.",
|
||||
"No pages yet": "Nog geen pagina's",
|
||||
"No results found...": "Geen resultaten gevonden...",
|
||||
"No user found": "Geen gebruiker gevonden",
|
||||
"Overview": "Overzicht",
|
||||
"Owner": "Eigenaar",
|
||||
"page": "pagina",
|
||||
"Page deleted successfully": "Pagina succesvol verwijderd",
|
||||
"Page history": "Pagina geschiedenis",
|
||||
"Page import is in progress. Please do not close this tab.": "Importeren van pagina's is bezig. Sluit dit tabblad niet.",
|
||||
"Pages": "Pagina's",
|
||||
"pages": "pagina's",
|
||||
"Password": "Wachtwoord",
|
||||
"Password changed successfully": "Wachtwoord met succes gewijzigd",
|
||||
"Pending": "Wachtende",
|
||||
"Please confirm your action": "Bevestig alstublieft uw actie",
|
||||
"Preferences": "Voorkeuren",
|
||||
"Print PDF": "PDF afdrukken",
|
||||
"Profile": "Profiel",
|
||||
"Recently updated": "Recent bijgewerkt",
|
||||
"Remove": "Verwijderen",
|
||||
"Remove group member": "Lid uit groep verwijderd",
|
||||
"Remove space member": "Lid uit ruimte verwijderd",
|
||||
"Restore": "Herstellen",
|
||||
"Role": "Rol",
|
||||
"Save": "Opslaan",
|
||||
"Search": "Zoeken",
|
||||
"Search for groups": "Zoek naar groepen",
|
||||
"Search for users": "Zoek naar gebruikers",
|
||||
"Search for users and groups": "Zoek naar gebruikers en groepen",
|
||||
"Search...": "Zoeken...",
|
||||
"Select language": "Selecteer taal",
|
||||
"Select role": "Selecteer rol",
|
||||
"Select role to assign to all invited members": "Selecteer rol en wijs toe aan alle uitgenodigde leden",
|
||||
"Select theme": "Selecteer thema",
|
||||
"Send invitation": "Uitnodiging versturen",
|
||||
"Invitation sent": "Uitnodiging verzonden",
|
||||
"Settings": "Instellingen",
|
||||
"Setup workspace": "Werkruimte instellen",
|
||||
"Sign In": "Inloggen",
|
||||
"Sign Up": "Aanmelden",
|
||||
"Slug": "Afkorting",
|
||||
"Space": "Ruimte",
|
||||
"Space description": "Omschrijving van de ruimte",
|
||||
"Space menu": "Ruimte menu",
|
||||
"Space name": "Naam ruimte",
|
||||
"Space settings": "Ruimte instellingen",
|
||||
"Space slug": "Ruimte afkorting",
|
||||
"Spaces": "Ruimtes",
|
||||
"Spaces you belong to": "Ruimtes waar je bij hoort",
|
||||
"No space found": "Geen ruimte gevonden",
|
||||
"Search for spaces": "Zoek naar ruimtes",
|
||||
"Start typing to search...": "Begin met typen om te zoeken...",
|
||||
"Status": "Status",
|
||||
"Successfully imported": "Succesvol geïmporteerd",
|
||||
"Successfully restored": "Succesvol hersteld",
|
||||
"System settings": "Systeem instellingen",
|
||||
"Theme": "Thema",
|
||||
"To change your email, you have to enter your password and new email.": "Om uw e-mailadres te wijzigen, moet u uw wachtwoord en nieuwe e-mail invullen.",
|
||||
"Toggle full page width": "Schakel volledige pagina breedte in",
|
||||
"Unable to import pages. Please try again.": "Pagina's importeren is niet gelukt. Probeer het opnieuw.",
|
||||
"untitled": "naamloos",
|
||||
"Untitled": "Naamloos",
|
||||
"Updated successfully": "Succesvol bijgewerkt",
|
||||
"User": "Gebruiker",
|
||||
"Workspace": "Werkruimte",
|
||||
"Workspace Name": "Naam werkruimte",
|
||||
"Workspace settings": "Instellingen werkruimte",
|
||||
"You can change your password here.": "U kunt hier uw wachtwoord wijzigen.",
|
||||
"Your Email": "Uw e-mailadres",
|
||||
"Your import is complete.": "Uw import is voltooid.",
|
||||
"Your name": "Uw naam",
|
||||
"Your Name": "Uw Naam",
|
||||
"Your password": "Uw wachtwoord",
|
||||
"Your password must be a minimum of 8 characters.": "Uw wachtwoord moet minimaal 8 tekens bevatten.",
|
||||
"Sidebar toggle": "Zijbalk toggelen",
|
||||
"Comments": "Opmerkingen",
|
||||
"404 page not found": "404 pagina niet gevonden",
|
||||
"Sorry, we can't find the page you are looking for.": "Sorry, we kunnen de pagina die u zoekt niet vinden.",
|
||||
"Take me back to homepage": "Ga terug naar de homepage",
|
||||
"Forgot password": "Wachtwoord vergeten",
|
||||
"Forgot your password?": "Wachtwoord vergeten?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Een link om uw wachtwoord te resetten is verstuurd naar uw e-mail. Controleer uw inbox.",
|
||||
"Send reset link": "Verstuur een link om uw wachtwoord te herstellen",
|
||||
"Password reset": "Wachtwoord opnieuw instellen",
|
||||
"Your new password": "Uw nieuwe wachtwoord",
|
||||
"Set password": "Voer wachtwoord in",
|
||||
"Write a comment": "Schrijf een reactie",
|
||||
"Reply...": "Antwoord...",
|
||||
"Error loading comments.": "Fout bij het laden van reacties.",
|
||||
"No comments yet.": "Nog geen reacties.",
|
||||
"Edit comment": "Bewerk reactie",
|
||||
"Delete comment": "Verwijder reactie",
|
||||
"Are you sure you want to delete this comment?": "Weet je zeker dat je deze reactie wilt verwijderen?",
|
||||
"Comment created successfully": "Reactie succesvol aangemaakt",
|
||||
"Error creating comment": "Fout bij het aanmaken van reactie",
|
||||
"Comment updated successfully": "Opmerking succesvol bijgewerkt",
|
||||
"Failed to update comment": "Bijwerken van reactie mislukt",
|
||||
"Comment deleted successfully": "Reactie met succes verwijderd",
|
||||
"Failed to delete comment": "Verwijderen van reactie mislukt",
|
||||
"Comment resolved successfully": "Reactie succesvol opgelost",
|
||||
"Failed to resolve comment": "Reactie oplossen mislukt",
|
||||
"Revoke invitation": "Uitnodiging intrekken",
|
||||
"Revoke": "Intrekken",
|
||||
"Don't": "Niet doen",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Weet u zeker dat u deze uitnodiging wilt intrekken? De gebruiker kan niet deelnemen aan de werkruimte.",
|
||||
"Resend invitation": "Uitnodiging opnieuw verzenden",
|
||||
"Anyone with this link can join this workspace.": "Iedereen met deze link kan zich aansluiten bij deze werkruimte.",
|
||||
"Invite link": "Uitnodigingslink",
|
||||
"Copy": "Kopieer",
|
||||
"Copied": "Gekopieerd",
|
||||
"Select a user": "Selecteer een gebruiker",
|
||||
"Select a group": "Selecteer een groep",
|
||||
"Export all pages and attachments in this space.": "Exporteer alle pagina's en bijlagen in deze ruimte.",
|
||||
"Delete space": "Verwijder ruimte",
|
||||
"Are you sure you want to delete this space?": "Weet u zeker dat u deze ruimte wil verwijderen?",
|
||||
"Delete this space with all its pages and data.": "Verwijder deze ruimte met alle pagina's en gegevens.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Alle pagina's, opmerkingen, bijlagen en permissies in deze ruimte zullen onherroepelijk worden verwijderd.",
|
||||
"Confirm space name": "Bevestig naam van ruimte",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Typ de ruimtenaam <b>{{spaceName}}</b> om uw actie te bevestigen.",
|
||||
"Format": "Formaat",
|
||||
"Include subpages": "Inclusief onderliggend pagina's",
|
||||
"Include attachments": "Inclusief bijlages",
|
||||
"Select export format": "Selecteer export formaat",
|
||||
"Export failed:": "Exporteren mislukt:",
|
||||
"export error": "Exporteer fout",
|
||||
"Export page": "Exporteer pagina",
|
||||
"Export space": "Exporteer ruimte",
|
||||
"Export {{type}}": "Exporteer {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Bestand overschrijdt de bijlagelimiet van {{limit}}",
|
||||
"Align left": "Links uitlijnen",
|
||||
"Align right": "Rechts uitlijnen",
|
||||
"Align center": "Centreren",
|
||||
"Justify": "Uitvullen",
|
||||
"Merge cells": "Cellen samenvoegen",
|
||||
"Split cell": "Cel splitsen",
|
||||
"Delete column": "Kolom verwijderen",
|
||||
"Delete row": "Rij verwijderen",
|
||||
"Add left column": "Linker kolom toevoegen",
|
||||
"Add right column": "Rechter kolom toevoegen",
|
||||
"Add row above": "Rij hierboven toevoegen",
|
||||
"Add row below": "Rij hieronder toevoegen",
|
||||
"Delete table": "Verwijder tabel",
|
||||
"Info": "Info",
|
||||
"Success": "Geslaagd",
|
||||
"Warning": "Waarschuwing",
|
||||
"Danger": "Gevaar",
|
||||
"Mermaid diagram error:": "Mermaid diagram fout:",
|
||||
"Invalid Mermaid diagram": "Ongeldig Mermaid diagram",
|
||||
"Double-click to edit Draw.io diagram": "Dubbelklik om Draw.io diagram te bewerken",
|
||||
"Exit": "Afsluiten",
|
||||
"Save & Exit": "Opslaan & Afsluiten",
|
||||
"Double-click to edit Excalidraw diagram": "Dubbelklik om Excalidraw diagram te bewerken",
|
||||
"Paste link": "Link plakken",
|
||||
"Edit link": "Link bewerken",
|
||||
"Remove link": "Link verwijderen",
|
||||
"Add link": "Link toevoegen",
|
||||
"Please enter a valid url": "Voer een geldige URL in",
|
||||
"Empty equation": "Lege vergelijking",
|
||||
"Invalid equation": "Ongeldige vergelijking",
|
||||
"Color": "Kleur",
|
||||
"Text color": "Tekstkleur",
|
||||
"Default": "Standaard",
|
||||
"Blue": "Blauw",
|
||||
"Green": "Groen",
|
||||
"Purple": "Paars",
|
||||
"Red": "Rood",
|
||||
"Yellow": "Geel",
|
||||
"Orange": "Oranje",
|
||||
"Pink": "Roze",
|
||||
"Gray": "Grijs",
|
||||
"Embed link": "Link insluiten",
|
||||
"Invalid {{provider}} embed link": "Ongeldige {{provider}} insluitingslink",
|
||||
"Embed {{provider}}": "Insluiten {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Voer {{provider}} link in om in te voegen",
|
||||
"Bold": "Dikgedrukt",
|
||||
"Italic": "Schuingedrukt",
|
||||
"Underline": "Onderstrepen",
|
||||
"Strike": "Doorhalen",
|
||||
"Code": "Code",
|
||||
"Comment": "Reactie",
|
||||
"Text": "Tekst",
|
||||
"Heading 1": "Kop 1",
|
||||
"Heading 2": "Kop 2",
|
||||
"Heading 3": "Kop 3",
|
||||
"To-do List": "Takenlijst",
|
||||
"Bullet List": "Opsommingslijst",
|
||||
"Numbered List": "Genummerde lijst",
|
||||
"Blockquote": "Blockquote",
|
||||
"Just start typing with plain text.": "Begin met typen.",
|
||||
"Track tasks with a to-do list.": "Houd taken bij met een takenlijst.",
|
||||
"Big section heading.": "Grote sectie kop.",
|
||||
"Medium section heading.": "Middelgrote sectie kop.",
|
||||
"Small section heading.": "Kleine sectie kop.",
|
||||
"Create a simple bullet list.": "Maak een eenvoudige opsommingslijst aan.",
|
||||
"Create a list with numbering.": "Maak een lijst met nummering.",
|
||||
"Create block quote.": "Maak een block quote.",
|
||||
"Insert code snippet.": "Codefragment invoegen.",
|
||||
"Insert horizontal rule divider": "Horizontale lijn invoegen",
|
||||
"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 file from your device.": "Upload een bestand vanaf uw apparaat.",
|
||||
"Table": "Tabel",
|
||||
"Insert a table.": "Voeg een tabel in.",
|
||||
"Insert collapsible block.": "Inklapbaar blok invoegen.",
|
||||
"Video": "Video",
|
||||
"Divider": "Scheidingslijn",
|
||||
"Quote": "Quote",
|
||||
"Image": "Afbeelding",
|
||||
"File attachment": "Bestand bijlage",
|
||||
"Toggle block": "Schakel blok in/uit",
|
||||
"Callout": "Opmerking",
|
||||
"Insert callout notice.": "Invoegen opmerking.",
|
||||
"Math inline": "Wiskundige inline",
|
||||
"Insert inline math equation.": "Wiskundige inline vergelijking invoegen.",
|
||||
"Math block": "Wiskunde blok",
|
||||
"Insert math equation": "Wiskundige inline vergelijking invoegen",
|
||||
"Mermaid diagram": "Mermaid diagram",
|
||||
"Insert mermaid diagram": "Voeg mermaid diagram in",
|
||||
"Insert and design Drawio diagrams": "Drawio diagrammen invoegen en ontwerpen",
|
||||
"Insert current date": "Huidige datum invoeren",
|
||||
"Draw and sketch excalidraw diagrams": "Teken en schets excalidraw diagrammen",
|
||||
"Multiple": "Meerdere",
|
||||
"Heading {{level}}": "Kop {{level}}",
|
||||
"Toggle title": "Schakel titel in/uit",
|
||||
"Write anything. Enter \"/\" for commands": "Schrijf iets. Voer \"/\" in voor commando's",
|
||||
"Names do not match": "Namen komen niet overeen",
|
||||
"Today, {{time}}": "Vandaag, {{time}}",
|
||||
"Yesterday, {{time}}": "Gisteren, {{time}}",
|
||||
"Space created successfully": "Ruimte succesvol aangemaakt",
|
||||
"Space updated successfully": "Ruimte succesvol bijgewerkt",
|
||||
"Space deleted successfully": "Ruimte succesvol verwijderd",
|
||||
"Members added successfully": "Leden succesvol toegevoegd",
|
||||
"Member removed successfully": "Lid succesvol verwijderd",
|
||||
"Member role updated successfully": "Lidrol succesvol bijgewerkt",
|
||||
"Created by: <b>{{creatorName}}</b>": "Gemaakt door: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Aangemaakt op: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Bewerkt door {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Aantal woorden: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Aantal tekens: {{characterCount}}"
|
||||
}
|
355
apps/client/public/locales/pt-BR/translation.json
Normal file
355
apps/client/public/locales/pt-BR/translation.json
Normal file
@ -0,0 +1,355 @@
|
||||
{
|
||||
"Account": "Conta",
|
||||
"Active": "Ativo",
|
||||
"Add": "Adicionar",
|
||||
"Add group members": "Adicionar membros ao grupo",
|
||||
"Add groups": "Adicionar grupos",
|
||||
"Add members": "Adicionar membros",
|
||||
"Add to groups": "Adicionar aos grupos",
|
||||
"Add space members": "Adicionar membros do espaço",
|
||||
"Admin": "Administrador",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Tem certeza de que deseja excluir este grupo? Os membros perderão acesso aos recursos que este grupo possui.",
|
||||
"Are you sure you want to delete this page?": "Tem certeza de que deseja excluir esta página?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Tem certeza de que deseja remover este usuário do grupo? O usuário perderá acesso aos recursos que este grupo possui.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Tem certeza de que deseja remover este usuário do espaço? O usuário perderá todo acesso a este espaço.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Tem certeza de que deseja restaurar esta versão? Quaisquer alterações não versionadas serão perdidas.",
|
||||
"Can become members of groups and spaces in workspace": "Pode se tornar membro de grupos e espaços no workspace",
|
||||
"Can create and edit pages in space.": "Pode criar e editar páginas no espaço.",
|
||||
"Can edit": "Pode editar",
|
||||
"Can manage workspace": "Pode gerenciar o workspace",
|
||||
"Can manage workspace but cannot delete it": "Pode gerenciar o workspace, mas não pode excluí-lo",
|
||||
"Can view": "Pode visualizar",
|
||||
"Can view pages in space but not edit.": "Pode visualizar páginas no espaço, mas não editar.",
|
||||
"Cancel": "Cancelar",
|
||||
"Change email": "Alterar email",
|
||||
"Change password": "Alterar senha",
|
||||
"Change photo": "Alterar foto",
|
||||
"Choose a role": "Escolha um papel",
|
||||
"Choose your preferred color scheme.": "Escolha seu esquema de cores preferido.",
|
||||
"Choose your preferred interface language.": "Escolha o idioma da interface.",
|
||||
"Choose your preferred page width.": "Escolha a largura preferida da página.",
|
||||
"Confirm": "Confirmar",
|
||||
"Copy link": "Copiar link",
|
||||
"Create": "Criar",
|
||||
"Create group": "Criar grupo",
|
||||
"Create page": "Criar página",
|
||||
"Create space": "Criar espaço",
|
||||
"Create workspace": "Criar workspace",
|
||||
"Current password": "Senha atual",
|
||||
"Dark": "Escuro",
|
||||
"Date": "Data",
|
||||
"Delete": "Excluir",
|
||||
"Delete group": "Excluir grupo",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Você tem certeza que quer deletar essa página? Isso irá deletar todas as páginas filhas e to o histórico. Esta ação é irreversível.",
|
||||
"Description": "Descrição",
|
||||
"Details": "Detalhes",
|
||||
"e.g ACME": "ex.: ACME",
|
||||
"e.g ACME Inc": "ex.: ACME Inc",
|
||||
"e.g Developers": "ex.: Desenvolvedores",
|
||||
"e.g Group for developers": "ex.: Grupo para desenvolvedores",
|
||||
"e.g product": "ex.: produto",
|
||||
"e.g Product Team": "ex.: Equipe de Produto",
|
||||
"e.g Sales": "ex.: Vendas",
|
||||
"e.g Space for product team": "ex.: Espaço para a equipe de produto",
|
||||
"e.g Space for sales team to collaborate": "ex.: Espaço para a equipe de vendas colaborar",
|
||||
"Edit": "Editar",
|
||||
"Edit group": "Editar grupo",
|
||||
"Email": "Email",
|
||||
"Enter a strong password": "Insira uma senha forte",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Insira endereços de email válidos separados por vírgula ou espaço [máx: 50]",
|
||||
"enter valid emails addresses": "insira endereços de email válidos",
|
||||
"Enter your current password": "Insira sua senha atual",
|
||||
"enter your full name": "insira seu nome completo",
|
||||
"Enter your new password": "Insira sua nova senha",
|
||||
"Enter your new preferred email": "Insira seu novo email preferido",
|
||||
"Enter your password": "Insira sua senha",
|
||||
"Error fetching page data.": "Erro ao buscar dados da página.",
|
||||
"Error loading page history.": "Erro ao carregar o histórico da página.",
|
||||
"Export": "Exportar",
|
||||
"Failed to create page": "Falha ao criar página",
|
||||
"Failed to delete page": "Falha ao excluir página",
|
||||
"Failed to fetch recent pages": "Falha ao buscar páginas recentes",
|
||||
"Failed to import pages": "Falha ao importar páginas",
|
||||
"Failed to load page. An error occurred.": "Falha ao carregar página. Ocorreu um erro.",
|
||||
"Failed to update data": "Falha ao atualizar dados",
|
||||
"Full access": "Acesso total",
|
||||
"Full page width": "Usar largura total da página",
|
||||
"Full width": "Largura total",
|
||||
"General": "Geral",
|
||||
"Group": "Grupo",
|
||||
"Group description": "Descrição do grupo",
|
||||
"Group name": "Nome do grupo",
|
||||
"Groups": "Grupos",
|
||||
"Has full access to space settings and pages.": "Tem acesso total às configurações do espaço e às páginas.",
|
||||
"Home": "Início",
|
||||
"Import pages": "Importar páginas",
|
||||
"Import pages & space settings": "Importar páginas e configurações de espaço",
|
||||
"Importing pages": "Importando páginas",
|
||||
"invalid invitation link": "link de convite inválido",
|
||||
"Invitation signup": "Cadastro por convite",
|
||||
"Invite by email": "Convidar por email",
|
||||
"Invite members": "Convidar membros",
|
||||
"Invite new members": "Convidar novos membros",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Membros convidados que ainda não aceitaram o convite aparecerão aqui.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Os membros convidados terão acesso aos espaços que os grupos podem acessar",
|
||||
"Join the workspace": "Entrar no workspace",
|
||||
"Language": "Idioma",
|
||||
"Light": "Claro",
|
||||
"Link copied": "Link copiado",
|
||||
"Login": "Entrar",
|
||||
"Logout": "Sair",
|
||||
"Manage Group": "Gerenciar Grupo",
|
||||
"Manage members": "Gerenciar membros",
|
||||
"member": "membro",
|
||||
"Member": "Membro",
|
||||
"members": "membros",
|
||||
"Members": "Membros",
|
||||
"My preferences": "Minhas preferências",
|
||||
"My Profile": "Meu Perfil",
|
||||
"My profile": "Meu perfil",
|
||||
"Name": "Nome",
|
||||
"New email": "Novo email",
|
||||
"New page": "Nova página",
|
||||
"New password": "Nova senha",
|
||||
"No group found": "Nenhum grupo encontrado",
|
||||
"No page history saved yet.": "Nenhum histórico de página salvo ainda.",
|
||||
"No pages yet": "Nenhuma página ainda",
|
||||
"No results found...": "Nenhum resultado encontrado...",
|
||||
"No user found": "Nenhum usuário encontrado",
|
||||
"Overview": "Visão geral",
|
||||
"Owner": "Proprietário",
|
||||
"page": "página",
|
||||
"Page deleted successfully": "Página excluída com sucesso",
|
||||
"Page history": "Histórico da página",
|
||||
"Page import is in progress. Please do not close this tab.": "A importação da página está em andamento. Por favor, não feche esta aba.",
|
||||
"Pages": "Páginas",
|
||||
"pages": "páginas",
|
||||
"Password": "Senha",
|
||||
"Password changed successfully": "Senha alterada com sucesso",
|
||||
"Pending": "Pendente",
|
||||
"Please confirm your action": "Por favor, confirme sua ação",
|
||||
"Preferences": "Preferências",
|
||||
"Print PDF": "Imprimir PDF",
|
||||
"Profile": "Perfil",
|
||||
"Recently updated": "Atualizado recentemente",
|
||||
"Remove": "Remover",
|
||||
"Remove group member": "Remover membro do grupo",
|
||||
"Remove space member": "Remover membro do espaço",
|
||||
"Restore": "Restaurar",
|
||||
"Role": "Função",
|
||||
"Save": "Salvar",
|
||||
"Search": "Buscar",
|
||||
"Search for groups": "Buscar grupos",
|
||||
"Search for users": "Buscar usuários",
|
||||
"Search for users and groups": "Buscar usuários e grupos",
|
||||
"Search...": "Buscar...",
|
||||
"Select language": "Selecionar idioma",
|
||||
"Select role": "Selecionar função",
|
||||
"Select role to assign to all invited members": "Selecione a função para atribuir a todos os membros convidados",
|
||||
"Select theme": "Selecionar tema",
|
||||
"Send invitation": "Enviar convite",
|
||||
"Invitation sent": "Invitation sent",
|
||||
"Settings": "Configurações",
|
||||
"Setup workspace": "Configurar workspace",
|
||||
"Sign In": "Entrar",
|
||||
"Sign Up": "Registrar-se",
|
||||
"Slug": "Slug",
|
||||
"Space": "Espaço",
|
||||
"Space description": "Descrição do espaço",
|
||||
"Space menu": "Menu do espaço",
|
||||
"Space name": "Nome do espaço",
|
||||
"Space settings": "Configurações do espaço",
|
||||
"Space slug": "Slug do espaço",
|
||||
"Spaces": "Espaços",
|
||||
"Spaces you belong to": "Espaços aos quais você pertence",
|
||||
"No space found": "Nenhum espaço encontrado",
|
||||
"Search for spaces": "Pesquisar espaços",
|
||||
"Start typing to search...": "Comece a digitar para buscar...",
|
||||
"Status": "Estado",
|
||||
"Successfully imported": "Importado com sucesso",
|
||||
"Successfully restored": "Restaurado com sucesso",
|
||||
"System settings": "Configurações do sistema",
|
||||
"Theme": "Tema",
|
||||
"To change your email, you have to enter your password and new email.": "Para alterar seu email, você precisa inserir sua senha e o novo email.",
|
||||
"Toggle full page width": "Alternar para largura total da página",
|
||||
"Unable to import pages. Please try again.": "Não foi possível importar as páginas. Por favor, tente novamente.",
|
||||
"untitled": "sem título",
|
||||
"Untitled": "Sem título",
|
||||
"Updated successfully": "Atualizado com sucesso",
|
||||
"User": "Usuário",
|
||||
"Workspace": "Espaço de Trabalho",
|
||||
"Workspace Name": "Nome do Workspace",
|
||||
"Workspace settings": "Configurações do workspace",
|
||||
"You can change your password here.": "Você pode alterar sua senha aqui.",
|
||||
"Your Email": "Seu email",
|
||||
"Your import is complete.": "Sua importação está concluída.",
|
||||
"Your name": "Seu nome",
|
||||
"Your Name": "Seu Nome",
|
||||
"Your password": "Sua senha",
|
||||
"Your password must be a minimum of 8 characters.": "Sua senha deve ter no mínimo 8 caracteres.",
|
||||
"Sidebar toggle": "Interruptor do painel lateral",
|
||||
"Comments": "Comentários",
|
||||
"404 page not found": "Erro 404: Página não encontrada",
|
||||
"Sorry, we can't find the page you are looking for.": "Desculpe, não conseguimos encontrar a página que você está procurando.",
|
||||
"Take me back to homepage": "Leve-me de volta para a página inicial",
|
||||
"Forgot password": "Esqueci a senha",
|
||||
"Forgot your password?": "Esqueceu sua senha?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Um link de redefinição de senha foi enviado para o seu email. Por favor, verifique sua caixa de entrada.",
|
||||
"Send reset link": "Enviar link de recuperação",
|
||||
"Password reset": "Resetar a senha",
|
||||
"Your new password": "Sua nova senha",
|
||||
"Set password": "Definir a senha",
|
||||
"Write a comment": "Escreva um comentário",
|
||||
"Reply...": "Responder...",
|
||||
"Error loading comments.": "Erro ao carregar comentários.",
|
||||
"No comments yet.": "Ainda sem comentários.",
|
||||
"Edit comment": "Editar comentário",
|
||||
"Delete comment": "Excluir comentário",
|
||||
"Are you sure you want to delete this comment?": "Você tem certeza de que deseja excluir este comentário?",
|
||||
"Comment created successfully": "Comentário criado com sucesso",
|
||||
"Error creating comment": "Erro ao criar comentário",
|
||||
"Comment updated successfully": "Comentário atualizado com sucesso",
|
||||
"Failed to update comment": "Falha ao atualizar comentário",
|
||||
"Comment deleted successfully": "Comentário excluído com sucesso",
|
||||
"Failed to delete comment": "Falha ao excluir comentário",
|
||||
"Comment resolved successfully": "Comentário resolvido com sucesso",
|
||||
"Failed to resolve comment": "Falha ao resolver comentário",
|
||||
"Revoke invitation": "Cancelar o convite",
|
||||
"Revoke": "Anular",
|
||||
"Don't": "Não",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Tem certeza de que deseja revogar este convite? O usuário não poderá participar do espaço de trabalho.",
|
||||
"Resend invitation": "Reenviar convite",
|
||||
"Anyone with this link can join this workspace.": "Qualquer um com este link pode participar deste espaço de trabalho.",
|
||||
"Invite link": "Link do convite",
|
||||
"Copy": "Copiar",
|
||||
"Copied": "Copiado",
|
||||
"Select a user": "Selecione um usuário",
|
||||
"Select a group": "Selecione um grupo",
|
||||
"Export all pages and attachments in this space.": "Exportar todas as páginas e anexos deste espaço.",
|
||||
"Delete space": "Excluir Espaço",
|
||||
"Are you sure you want to delete this space?": "Tem certeza de que deseja excluir este espaço?",
|
||||
"Delete this space with all its pages and data.": "Excluir este espaço com todas as suas páginas e dados.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Todas as páginas, comentários, anexos e permissões neste espaço serão excluídos de forma irreversível.",
|
||||
"Confirm space name": "Confirme o nome do espaço",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Digite o nome do espaço <b>{{spaceName}}</b> para confirmar sua ação.",
|
||||
"Format": "Formato",
|
||||
"Include subpages": "Incluir subpáginas",
|
||||
"Include attachments": "Incluir anexos",
|
||||
"Select export format": "Selecionado o formato para exportação",
|
||||
"Export failed:": "Falha ao exportar:",
|
||||
"export error": "erro de exportação",
|
||||
"Export page": "Exportar página",
|
||||
"Export space": "Exportar espaço",
|
||||
"Export {{type}}": "Exportar para {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "O arquivo excede o limite de anexos {{limit}}",
|
||||
"Align left": "Alinhar à esquerda",
|
||||
"Align right": "Alinhar à direita",
|
||||
"Align center": "Alinhar ao centro",
|
||||
"Justify": "Justify",
|
||||
"Merge cells": "Mesclar células",
|
||||
"Split cell": "Dividir célula",
|
||||
"Delete column": "Excluir coluna",
|
||||
"Delete row": "Excluir linha",
|
||||
"Add left column": "Adicionar coluna à esquerda",
|
||||
"Add right column": "Adicionar coluna à direita",
|
||||
"Add row above": "Adicionar linha acima",
|
||||
"Add row below": "Adicionar linha abaixo",
|
||||
"Delete table": "Excluir tabela",
|
||||
"Info": "Informação",
|
||||
"Success": "Sucesso",
|
||||
"Warning": "Aviso",
|
||||
"Danger": "Perigo",
|
||||
"Mermaid diagram error:": "Erro no diagrama Mermaid:",
|
||||
"Invalid Mermaid diagram": "Diagrama Mermaid inválido",
|
||||
"Double-click to edit Draw.io diagram": "Clique duas vezes para editar o diagrama Draw.io",
|
||||
"Exit": "Sair",
|
||||
"Save & Exit": "Salvar e Sair",
|
||||
"Double-click to edit Excalidraw diagram": "Clique duas vezes para editar o diagrama Excalidraw",
|
||||
"Paste link": "Colar link",
|
||||
"Edit link": "Editar link",
|
||||
"Remove link": "Remover link",
|
||||
"Add link": "Adicionar link",
|
||||
"Please enter a valid url": "Por favor, insira uma URL válida",
|
||||
"Empty equation": "Equação vazia",
|
||||
"Invalid equation": "Equação inválida",
|
||||
"Color": "Cor",
|
||||
"Text color": "Cor do texto",
|
||||
"Default": "Padrão",
|
||||
"Blue": "Azul",
|
||||
"Green": "Verde",
|
||||
"Purple": "Violeta",
|
||||
"Red": "Vermelho",
|
||||
"Yellow": "Amarelo",
|
||||
"Orange": "Laranja",
|
||||
"Pink": "Rosa",
|
||||
"Gray": "Cinza",
|
||||
"Embed link": "Link embutido",
|
||||
"Invalid {{provider}} embed link": "Link de incorporação {{provider}} inválido",
|
||||
"Embed {{provider}}": "Incorporar {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Digite o link do {{provider}} para incorporar",
|
||||
"Bold": "Negrito",
|
||||
"Italic": "Itálico",
|
||||
"Underline": "Sublinhado",
|
||||
"Strike": "Tracejado",
|
||||
"Code": "Código",
|
||||
"Comment": "Comentário",
|
||||
"Text": "Texto",
|
||||
"Heading 1": "Título 1",
|
||||
"Heading 2": "Título 2",
|
||||
"Heading 3": "Título 3",
|
||||
"To-do List": "Lista de Tarefas",
|
||||
"Bullet List": "Lista de Pontos",
|
||||
"Numbered List": "Lista Numerada",
|
||||
"Blockquote": "Bloco de Citação",
|
||||
"Just start typing with plain text.": "Basta começar a digita.",
|
||||
"Track tasks with a to-do list.": "Acompanhe tarefas com uma lista de tarefas.",
|
||||
"Big section heading.": "Título de seção grande.",
|
||||
"Medium section heading.": "Título de seção média.",
|
||||
"Small section heading.": "Título de seção pequena.",
|
||||
"Create a simple bullet list.": "Crie uma lista simples com marcadores.",
|
||||
"Create a list with numbering.": "Crie uma lista com numeração.",
|
||||
"Create block quote.": "Crie uma citação em bloco.",
|
||||
"Insert code snippet.": "Insira um trecho de código.",
|
||||
"Insert horizontal rule divider": "Insira um divisor horizontal",
|
||||
"Upload any image from your device.": "Envie qualquer imagem do seu dispositivo.",
|
||||
"Upload any video from your device.": "Envie qualquer vídeo do seu dispositivo.",
|
||||
"Upload any file from your device.": "Envie qualquer arquivo do seu dispositivo.",
|
||||
"Table": "Tabela",
|
||||
"Insert a table.": "Insira uma tabela.",
|
||||
"Insert collapsible block.": "Insira um bloco colapsável.",
|
||||
"Video": "Vídeo",
|
||||
"Divider": "Divisor",
|
||||
"Quote": "Citação",
|
||||
"Image": "Imagem",
|
||||
"File attachment": "Anexo de arquivo",
|
||||
"Toggle block": "Bloco colapsável",
|
||||
"Callout": "Aviso",
|
||||
"Insert callout notice.": "Insira um aviso.",
|
||||
"Math inline": "Matemática inline",
|
||||
"Insert inline math equation.": "Insira uma equação matemática inline.",
|
||||
"Math block": "Bloco de matemática",
|
||||
"Insert math equation": "Insira uma equação matemática",
|
||||
"Mermaid diagram": "Diagrama Mermaid",
|
||||
"Insert mermaid diagram": "Insira um diagrama Mermaid",
|
||||
"Insert and design Drawio diagrams": "Insira e projete diagramas Drawio",
|
||||
"Insert current date": "Insira a data atual",
|
||||
"Draw and sketch excalidraw diagrams": "Desenhe e esboce diagramas Excalidraw",
|
||||
"Multiple": "Múltiplo",
|
||||
"Heading {{level}}": "Título {{level}}",
|
||||
"Toggle title": "Alternar título",
|
||||
"Write anything. Enter \"/\" for commands": "Escreva qualquer coisa. Digite \"/\" para comandos",
|
||||
"Names do not match": "Os nomes não coincidem",
|
||||
"Today, {{time}}": "Hoje, {{time}}",
|
||||
"Yesterday, {{time}}": "Ontem, {{time}}",
|
||||
"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>": "Created by: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Created at: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Character count: {{characterCount}}"
|
||||
}
|
355
apps/client/public/locales/ru-RU/translation.json
Normal file
355
apps/client/public/locales/ru-RU/translation.json
Normal file
@ -0,0 +1,355 @@
|
||||
{
|
||||
"Account": "Аккаунт",
|
||||
"Active": "Активный",
|
||||
"Add": "Добавить",
|
||||
"Add group members": "Добавить участников группы",
|
||||
"Add groups": "Добавить группы",
|
||||
"Add members": "Добавить участников",
|
||||
"Add to groups": "Добавить в группы",
|
||||
"Add space members": "Добавить участников пространства",
|
||||
"Admin": "Администратор",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "Вы уверены, что хотите удалить эту группу? Участники потеряют доступ к материалам, к которым у этой группы есть доступ.",
|
||||
"Are you sure you want to delete this page?": "Вы уверены, что хотите удалить эту страницу?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "Вы уверены, что хотите удалить этого пользователя из группы? Пользователь потеряет доступ к материалам, к которым у этой группы есть доступ.",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "Вы уверены, что хотите удалить этого пользователя из пространства? Пользователь потеряет весь доступ к этому пространству.",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "Вы уверены, что хотите восстановить эту версию? Все не зафиксированные изменения будут потеряны.",
|
||||
"Can become members of groups and spaces in workspace": "Могут становиться участниками групп и пространств в рабочем пространстве",
|
||||
"Can create and edit pages in space.": "Может создавать и редактировать страницы в пространстве.",
|
||||
"Can edit": "Может изменять",
|
||||
"Can manage workspace": "Может управлять рабочим пространством",
|
||||
"Can manage workspace but cannot delete it": "Может управлять рабочим пространством, но не может его удалить",
|
||||
"Can view": "Может просматривать",
|
||||
"Can view pages in space but not edit.": "Может просматривать страницы в пространстве, но не может их редактировать.",
|
||||
"Cancel": "Отменить",
|
||||
"Change email": "Изменить электронную почту",
|
||||
"Change password": "Изменить пароль",
|
||||
"Change photo": "Изменить фото",
|
||||
"Choose a role": "Выберите роль",
|
||||
"Choose your preferred color scheme.": "Выберите предпочитаемую цветовую схему.",
|
||||
"Choose your preferred interface language.": "Выберите предпочитаемый язык интерфейса.",
|
||||
"Choose your preferred page width.": "Выберите предпочитаемую ширину страницы.",
|
||||
"Confirm": "Подтвердить",
|
||||
"Copy link": "Копировать ссылку",
|
||||
"Create": "Создать",
|
||||
"Create group": "Создать группу",
|
||||
"Create page": "Создать страницу",
|
||||
"Create space": "Создать пространство",
|
||||
"Create workspace": "Создать рабочее пространство",
|
||||
"Current password": "Текущий пароль",
|
||||
"Dark": "Темная",
|
||||
"Date": "Дата",
|
||||
"Delete": "Удалить",
|
||||
"Delete group": "Удалить группу",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "Вы уверены, что хотите удалить эту страницу? Это удалит её дочерние страницы, а также историю страницы. Это действие необратимо.",
|
||||
"Description": "Описание",
|
||||
"Details": "Подробности",
|
||||
"e.g ACME": "например, ACME",
|
||||
"e.g ACME Inc": "например, ACME Inc",
|
||||
"e.g Developers": "например, Разработчики",
|
||||
"e.g Group for developers": "например, Группа для разработчиков",
|
||||
"e.g product": "например, продукт",
|
||||
"e.g Product Team": "например, Продуктовая команда",
|
||||
"e.g Sales": "например, Продажи",
|
||||
"e.g Space for product team": "например, Пространство для продуктовой команды",
|
||||
"e.g Space for sales team to collaborate": "например, Пространство для совместной работы команды продаж",
|
||||
"Edit": "Редактировать",
|
||||
"Edit group": "Редактировать группу",
|
||||
"Email": "Электронная почта",
|
||||
"Enter a strong password": "Введите надёжный пароль",
|
||||
"Enter valid email addresses separated by comma or space max_50": "Введите действительные адреса электронной почты, разделенные запятой или пробелом [макс: 50]",
|
||||
"enter valid emails addresses": "введите действительные адреса электронной почты",
|
||||
"Enter your current password": "Введите ваш текущий пароль",
|
||||
"enter your full name": "введите ваше полное имя",
|
||||
"Enter your new password": "Введите ваш новый пароль",
|
||||
"Enter your new preferred email": "Введите ваш новый предпочитаемый адрес электронной почты",
|
||||
"Enter your password": "Введите ваш пароль",
|
||||
"Error fetching page data.": "Ошибка при загрузке данных страницы.",
|
||||
"Error loading page history.": "Ошибка при загрузке истории страницы.",
|
||||
"Export": "Экспорт",
|
||||
"Failed to create page": "Не удалось создать страницу",
|
||||
"Failed to delete page": "Не удалось удалить страницу",
|
||||
"Failed to fetch recent pages": "Не удалось получить недавние страницы",
|
||||
"Failed to import pages": "Не удалось импортировать страницы",
|
||||
"Failed to load page. An error occurred.": "Не удалось загрузить страницу. Произошла ошибка.",
|
||||
"Failed to update data": "Не удалось обновить данные",
|
||||
"Full access": "Полный доступ",
|
||||
"Full page width": "Ширина на всю страницу",
|
||||
"Full width": "Во всю ширину",
|
||||
"General": "Основные",
|
||||
"Group": "Группа",
|
||||
"Group description": "Описание группы",
|
||||
"Group name": "Название группы",
|
||||
"Groups": "Группы",
|
||||
"Has full access to space settings and pages.": "Имеет полный доступ к настройкам пространства и страницам.",
|
||||
"Home": "Главная",
|
||||
"Import pages": "Импорт страниц",
|
||||
"Import pages & space settings": "Импорт страниц и настройки пространства",
|
||||
"Importing pages": "Импортирование страниц",
|
||||
"invalid invitation link": "ссылка на приглашение недействительна",
|
||||
"Invitation signup": "Регистрация по приглашению",
|
||||
"Invite by email": "Пригласить по электронной почте",
|
||||
"Invite members": "Пригласить участников",
|
||||
"Invite new members": "Пригласить новых участников",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "Приглашённые участники, которые ещё не приняли приглашение, появятся здесь.",
|
||||
"Invited members will be granted access to spaces the groups can access": "Приглашённые участники получат доступ к пространствам, доступ к которым есть у группы",
|
||||
"Join the workspace": "Присоединиться к рабочему пространству",
|
||||
"Language": "Язык",
|
||||
"Light": "Светлая",
|
||||
"Link copied": "Ссылка скопирована",
|
||||
"Login": "Войти",
|
||||
"Logout": "Выйти",
|
||||
"Manage Group": "Управление группой",
|
||||
"Manage members": "Управление участниками",
|
||||
"member": "участник",
|
||||
"Member": "Участник",
|
||||
"members": "участники",
|
||||
"Members": "Участники",
|
||||
"My preferences": "Мои настройки",
|
||||
"My Profile": "Мой Профиль",
|
||||
"My profile": "Мой профиль",
|
||||
"Name": "Имя",
|
||||
"New email": "Новый электронный адрес",
|
||||
"New page": "Новая страница",
|
||||
"New password": "Новый пароль",
|
||||
"No group found": "Группа не найдена",
|
||||
"No page history saved yet.": "История страниц ещё не сохранена.",
|
||||
"No pages yet": "Страниц пока нет",
|
||||
"No results found...": "Результаты не найдены...",
|
||||
"No user found": "Пользователь не найден",
|
||||
"Overview": "Обзор",
|
||||
"Owner": "Владелец",
|
||||
"page": "страница",
|
||||
"Page deleted successfully": "Страница успешно удалена",
|
||||
"Page history": "История страницы",
|
||||
"Page import is in progress. Please do not close this tab.": "Импорт страницы в процессе. Пожалуйста, не закрывайте эту вкладку.",
|
||||
"Pages": "Страницы",
|
||||
"pages": "страницы",
|
||||
"Password": "Пароль",
|
||||
"Password changed successfully": "Пароль успешно изменён",
|
||||
"Pending": "В ожидании",
|
||||
"Please confirm your action": "Пожалуйста, подтвердите ваше действие",
|
||||
"Preferences": "Настройки",
|
||||
"Print PDF": "Печать PDF",
|
||||
"Profile": "Профиль",
|
||||
"Recently updated": "Обновлено недавно",
|
||||
"Remove": "Удалить",
|
||||
"Remove group member": "Удалить участника группы",
|
||||
"Remove space member": "Удалить участника пространства",
|
||||
"Restore": "Восстановить",
|
||||
"Role": "Роль",
|
||||
"Save": "Сохранить",
|
||||
"Search": "Поиск",
|
||||
"Search for groups": "Поиск групп",
|
||||
"Search for users": "Поиск пользователей",
|
||||
"Search for users and groups": "Поиск пользователей и групп",
|
||||
"Search...": "Поиск...",
|
||||
"Select language": "Выберите язык",
|
||||
"Select role": "Выберите роль",
|
||||
"Select role to assign to all invited members": "Выберите роль для всех приглашённых участников",
|
||||
"Select theme": "Выберите тему",
|
||||
"Send invitation": "Отправить приглашение",
|
||||
"Invitation sent": "Invitation sent",
|
||||
"Settings": "Настройки",
|
||||
"Setup workspace": "Настроить рабочее пространство",
|
||||
"Sign In": "Вход",
|
||||
"Sign Up": "Регистрация",
|
||||
"Slug": "Slug",
|
||||
"Space": "Пространство",
|
||||
"Space description": "Описание пространства",
|
||||
"Space menu": "Меню пространства",
|
||||
"Space name": "Название пространства",
|
||||
"Space settings": "Настройки пространства",
|
||||
"Space slug": "Slug пространства",
|
||||
"Spaces": "Пространства",
|
||||
"Spaces you belong to": "Пространства, к которым вы принадлежите",
|
||||
"No space found": "Пространства не найдены",
|
||||
"Search for spaces": "Поиск пространств",
|
||||
"Start typing to search...": "Начните вводить для поиска...",
|
||||
"Status": "Статус",
|
||||
"Successfully imported": "Успешно импортировано",
|
||||
"Successfully restored": "Успешно восстановлено",
|
||||
"System settings": "Системные настройки",
|
||||
"Theme": "Тема",
|
||||
"To change your email, you have to enter your password and new email.": "Чтобы изменить электронную почту, вам нужно ввести пароль и новый адрес.",
|
||||
"Toggle full page width": "Переключить ширину на всю страницу",
|
||||
"Unable to import pages. Please try again.": "Не удалось импортировать страницы. Пожалуйста, попробуйте ещё раз.",
|
||||
"untitled": "без названия",
|
||||
"Untitled": "Без названия",
|
||||
"Updated successfully": "Обновлено успешно",
|
||||
"User": "Пользователь",
|
||||
"Workspace": "Рабочее пространство",
|
||||
"Workspace Name": "Имя рабочего пространства",
|
||||
"Workspace settings": "Настройки рабочего пространства",
|
||||
"You can change your password here.": "Вы можете изменить свой пароль здесь.",
|
||||
"Your Email": "Ваш адрес электронной почты",
|
||||
"Your import is complete.": "Ваш импорт завершен.",
|
||||
"Your name": "Ваше имя",
|
||||
"Your Name": "Ваше Имя",
|
||||
"Your password": "Ваш пароль",
|
||||
"Your password must be a minimum of 8 characters.": "Ваш пароль должен содержать минимум 8 символов.",
|
||||
"Sidebar toggle": "Переключить боковую панель",
|
||||
"Comments": "Комментарии",
|
||||
"404 page not found": "404 страница не найдена",
|
||||
"Sorry, we can't find the page you are looking for.": "К сожалению, мы не можем найти страницу, которую вы ищете.",
|
||||
"Take me back to homepage": "Вернуться на главную страницу",
|
||||
"Forgot password": "Забыли пароль",
|
||||
"Forgot your password?": "Забыли пароль?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "Ссылка для сброса пароля была отправлена на ваш электронный адрес. Пожалуйста, проверьте входящие сообщения.",
|
||||
"Send reset link": "Отправить ссылку для сброса",
|
||||
"Password reset": "Сброс пароля",
|
||||
"Your new password": "Ваш новый пароль",
|
||||
"Set password": "Установить пароль",
|
||||
"Write a comment": "Написать комментарий",
|
||||
"Reply...": "Ответить...",
|
||||
"Error loading comments.": "Ошибка при загрузке комментариев.",
|
||||
"No comments yet.": "Комментариев пока нет.",
|
||||
"Edit comment": "Редактировать комментарий",
|
||||
"Delete comment": "Удалить комментарий",
|
||||
"Are you sure you want to delete this comment?": "Вы уверены, что хотите удалить этот комментарий?",
|
||||
"Comment created successfully": "Комментарий успешно создан",
|
||||
"Error creating comment": "Ошибка при создании комментария",
|
||||
"Comment updated successfully": "Комментарий успешно обновлён",
|
||||
"Failed to update comment": "Не удалось обновить комментарий",
|
||||
"Comment deleted successfully": "Комментарий успешно удалён",
|
||||
"Failed to delete comment": "Не удалось удалить комментарий",
|
||||
"Comment resolved successfully": "Комментарий успешно разрешён",
|
||||
"Failed to resolve comment": "Не удалось разрешить комментарий",
|
||||
"Revoke invitation": "Отозвать приглашение",
|
||||
"Revoke": "Отозвать",
|
||||
"Don't": "Нет",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "Вы уверены, что хотите отозвать это приглашение? Пользователь не сможет присоединиться к рабочему пространству.",
|
||||
"Resend invitation": "Отправить приглашение повторно",
|
||||
"Anyone with this link can join this workspace.": "Любой, у кого есть эта ссылка, может присоединиться к этому рабочему пространству.",
|
||||
"Invite link": "Ссылка для приглашения",
|
||||
"Copy": "Копировать",
|
||||
"Copied": "Скопировано",
|
||||
"Select a user": "Выберите пользователя",
|
||||
"Select a group": "Выберите группу",
|
||||
"Export all pages and attachments in this space.": "Экспортировать все страницы и вложения в этом пространстве.",
|
||||
"Delete space": "Удалить пространство",
|
||||
"Are you sure you want to delete this space?": "Вы уверены, что хотите удалить это пространство?",
|
||||
"Delete this space with all its pages and data.": "Удалить это пространство со всеми его страницами и данными.",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "Все страницы, комментарии, вложения и разрешения в этом пространстве будут удалены безвозвратно.",
|
||||
"Confirm space name": "Подтвердите название пространства",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "Введите название пространства <b>{{spaceName}}</b>, чтобы подтвердить ваше действие.",
|
||||
"Format": "Формат",
|
||||
"Include subpages": "Включить вложенные страницы",
|
||||
"Include attachments": "Включить вложения",
|
||||
"Select export format": "Выберите формат экспорта",
|
||||
"Export failed:": "Экспортирование не удалось:",
|
||||
"export error": "ошибка экспорта",
|
||||
"Export page": "Экспорт страницы",
|
||||
"Export space": "Экспорт пространства",
|
||||
"Export {{type}}": "Экспорт {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "Файл превышает лимит вложений {{limit}}",
|
||||
"Align left": "По левому краю",
|
||||
"Align right": "По правому краю",
|
||||
"Align center": "По центру",
|
||||
"Justify": "Justify",
|
||||
"Merge cells": "Объединить ячейки",
|
||||
"Split cell": "Разделить ячейку",
|
||||
"Delete column": "Удалить столбец",
|
||||
"Delete row": "Удалить строку",
|
||||
"Add left column": "Добавить столбец слева",
|
||||
"Add right column": "Добавить столбец справа",
|
||||
"Add row above": "Добавить строку выше",
|
||||
"Add row below": "Добавить строку ниже",
|
||||
"Delete table": "Удалить таблицу",
|
||||
"Info": "Информация",
|
||||
"Success": "Успешно",
|
||||
"Warning": "Предупреждение",
|
||||
"Danger": "Важно",
|
||||
"Mermaid diagram error:": "Ошибка диаграммы Mermaid:",
|
||||
"Invalid Mermaid diagram": "Недопустимая диаграмма Mermaid",
|
||||
"Double-click to edit Draw.io diagram": "Кликните дважды для редактирования диаграммы Draw.io",
|
||||
"Exit": "Выйти",
|
||||
"Save & Exit": "Сохранить и выйти",
|
||||
"Double-click to edit Excalidraw diagram": "Кликните дважды для редактирования диаграммы Excalidraw",
|
||||
"Paste link": "Вставить ссылку",
|
||||
"Edit link": "Редактировать ссылку",
|
||||
"Remove link": "Удалить ссылку",
|
||||
"Add link": "Добавить ссылку",
|
||||
"Please enter a valid url": "Пожалуйста, введите корректный url",
|
||||
"Empty equation": "Пустое выражение",
|
||||
"Invalid equation": "Недопустимое уравнение",
|
||||
"Color": "Цвет",
|
||||
"Text color": "Цвет текста",
|
||||
"Default": "По умолчанию",
|
||||
"Blue": "Синий",
|
||||
"Green": "Зелёный",
|
||||
"Purple": "Фиолетовый",
|
||||
"Red": "Красный",
|
||||
"Yellow": "Жёлтый",
|
||||
"Orange": "Оранжевый",
|
||||
"Pink": "Розовый",
|
||||
"Gray": "Серый",
|
||||
"Embed link": "Встроенная ссылка",
|
||||
"Invalid {{provider}} embed link": "Неверная ссылка для встраивания {{provider}}",
|
||||
"Embed {{provider}}": "Встроить {{provider}}",
|
||||
"Enter {{provider}} link to embed": "Введите ссылку для встраивания {{provider}}",
|
||||
"Bold": "Жирный",
|
||||
"Italic": "Курсив",
|
||||
"Underline": "Подчёркнутый",
|
||||
"Strike": "Перечёркнутый",
|
||||
"Code": "Код",
|
||||
"Comment": "Комментарий",
|
||||
"Text": "Текст",
|
||||
"Heading 1": "Заголовок 1",
|
||||
"Heading 2": "Заголовок 2",
|
||||
"Heading 3": "Заголовок 3",
|
||||
"To-do List": "Список дел",
|
||||
"Bullet List": "Маркированный список",
|
||||
"Numbered List": "Нумерованный список",
|
||||
"Blockquote": "Блок цитирования",
|
||||
"Just start typing with plain text.": "Просто начните печатать обычный текст.",
|
||||
"Track tasks with a to-do list.": "Отследить задачи с помощью списка дел.",
|
||||
"Big section heading.": "Большой заголовок раздела.",
|
||||
"Medium section heading.": "Средний заголовок раздела.",
|
||||
"Small section heading.": "Маленький заголовок раздела.",
|
||||
"Create a simple bullet list.": "Создать простой маркированный список.",
|
||||
"Create a list with numbering.": "Создать нумерованный список.",
|
||||
"Create block quote.": "Создать блок цитирования.",
|
||||
"Insert code snippet.": "Вставить фрагмент кода.",
|
||||
"Insert horizontal rule divider": "Вставить горизонтальный разделитель",
|
||||
"Upload any image from your device.": "Загрузить любое изображение с вашего устройства.",
|
||||
"Upload any video from your device.": "Загрузить любое видео с вашего устройства.",
|
||||
"Upload any file from your device.": "Загрузить любой файл с вашего устройства.",
|
||||
"Table": "Таблица",
|
||||
"Insert a table.": "Вставить таблицу.",
|
||||
"Insert collapsible block.": "Вставить сворачиваемый блок.",
|
||||
"Video": "Видео",
|
||||
"Divider": "Разделитель",
|
||||
"Quote": "Цитата",
|
||||
"Image": "Изображение",
|
||||
"File attachment": "Прикрепленный файл",
|
||||
"Toggle block": "Сворачиваемый блок",
|
||||
"Callout": "Выноска",
|
||||
"Insert callout notice.": "Вставить выноску с сообщением.",
|
||||
"Math inline": "Формула",
|
||||
"Insert inline math equation.": "Вставить математическое выражение в строку.",
|
||||
"Math block": "Блок формул",
|
||||
"Insert math equation": "Вставить математическое выражение",
|
||||
"Mermaid diagram": "Диаграмма Mermaid",
|
||||
"Insert mermaid diagram": "Вставить диаграмму Mermaid",
|
||||
"Insert and design Drawio diagrams": "Вставьте и редактируйте диаграммы Draw.io",
|
||||
"Insert current date": "Вставить текущую дату",
|
||||
"Draw and sketch excalidraw diagrams": "Создайте и рисуйте диаграммы Excalidraw",
|
||||
"Multiple": "Несколько",
|
||||
"Heading {{level}}": "Заголовок {{level}}",
|
||||
"Toggle title": "Переключить заголовок",
|
||||
"Write anything. Enter \"/\" for commands": "Пишите что угодно. Введите \"/\" для выбора команд",
|
||||
"Names do not match": "Названия не совпадают",
|
||||
"Today, {{time}}": "Сегодня, {{time}}",
|
||||
"Yesterday, {{time}}": "Вчера, {{time}}",
|
||||
"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>": "Created by: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Created at: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Character count: {{characterCount}}"
|
||||
}
|
355
apps/client/public/locales/zh-CN/translation.json
Normal file
355
apps/client/public/locales/zh-CN/translation.json
Normal file
@ -0,0 +1,355 @@
|
||||
{
|
||||
"Account": "账户",
|
||||
"Active": "活跃",
|
||||
"Add": "添加",
|
||||
"Add group members": "添加群组成员",
|
||||
"Add groups": "添加群组",
|
||||
"Add members": "添加成员",
|
||||
"Add to groups": "添加到群组",
|
||||
"Add space members": "添加空间成员",
|
||||
"Admin": "管理员",
|
||||
"Are you sure you want to delete this group? Members will lose access to resources this group has access to.": "您确定要删除这个群组吗?成员将失去对该群组可访问资源的访问权限。",
|
||||
"Are you sure you want to delete this page?": "您确定要删除这个页面吗?",
|
||||
"Are you sure you want to remove this user from the group? The user will lose access to resources this group has access to.": "您确定要从群组中移除这个用户吗?该用户将失去对该群组可访问资源的访问权限。",
|
||||
"Are you sure you want to remove this user from the space? The user will lose all access to this space.": "您确定要从空间中移除这个用户吗?该用户将失去对这个空间的所有访问权限。",
|
||||
"Are you sure you want to restore this version? Any changes not versioned will be lost.": "您确定要恢复此版本吗?任何未版本化的更改将会丢失。",
|
||||
"Can become members of groups and spaces in workspace": "可以成为工作区中群组和空间的成员",
|
||||
"Can create and edit pages in space.": "能够在空间中创建和编辑页面",
|
||||
"Can edit": "可以编辑",
|
||||
"Can manage workspace": "可以管理工作区",
|
||||
"Can manage workspace but cannot delete it": "可以管理工作区但不能删除它",
|
||||
"Can view": "可以查看",
|
||||
"Can view pages in space but not edit.": "能够在空间中读取页面,但不允许编辑",
|
||||
"Cancel": "取消",
|
||||
"Change email": "更改电子邮箱",
|
||||
"Change password": "更改密码",
|
||||
"Change photo": "更改照片",
|
||||
"Choose a role": "选择一个角色",
|
||||
"Choose your preferred color scheme.": "选择您喜欢的配色方案。",
|
||||
"Choose your preferred interface language.": "选择您喜欢的界面语言。",
|
||||
"Choose your preferred page width.": "选择您喜欢的页面宽度。",
|
||||
"Confirm": "确认",
|
||||
"Copy link": "复制链接",
|
||||
"Create": "创建",
|
||||
"Create group": "创建群组",
|
||||
"Create page": "创建页面",
|
||||
"Create space": "创建空间",
|
||||
"Create workspace": "创建工作空间",
|
||||
"Current password": "当前密码",
|
||||
"Dark": "深色",
|
||||
"Date": "日期",
|
||||
"Delete": "删除",
|
||||
"Delete group": "删除群组",
|
||||
"Are you sure you want to delete this page? This will delete its children and page history. This action is irreversible.": "您确定要删除这个页面吗?这将删除其子页面和页面历史记录。此操作不可逆。",
|
||||
"Description": "描述",
|
||||
"Details": "详情",
|
||||
"e.g ACME": "例如:ACME",
|
||||
"e.g ACME Inc": "例如:ACME Inc",
|
||||
"e.g Developers": "例如:开发人员",
|
||||
"e.g Group for developers": "例如:开发人员群组",
|
||||
"e.g product": "例如:product",
|
||||
"e.g Product Team": "例如:产品团队",
|
||||
"e.g Sales": "例如:销售",
|
||||
"e.g Space for product team": "例如:产品团队的空间",
|
||||
"e.g Space for sales team to collaborate": "例如:销售团队协作的空间",
|
||||
"Edit": "编辑",
|
||||
"Edit group": "编辑群组",
|
||||
"Email": "电子邮箱",
|
||||
"Enter a strong password": "输入一个强密码",
|
||||
"Enter valid email addresses separated by comma or space max_50": "输入有效的电子邮箱地址,用逗号或空格分隔 [最多:50个]",
|
||||
"enter valid emails addresses": "输入有效的电子邮箱地址",
|
||||
"Enter your current password": "输入您的当前密码",
|
||||
"enter your full name": "输入您的全名",
|
||||
"Enter your new password": "输入您的新密码",
|
||||
"Enter your new preferred email": "输入您新的首选电子邮箱",
|
||||
"Enter your password": "输入您的密码",
|
||||
"Error fetching page data.": "获取页面数据时出错。",
|
||||
"Error loading page history.": "加载页面历史时出错。",
|
||||
"Export": "导出",
|
||||
"Failed to create page": "创建页面失败",
|
||||
"Failed to delete page": "删除页面失败",
|
||||
"Failed to fetch recent pages": "获取最近页面失败",
|
||||
"Failed to import pages": "导入页面失败",
|
||||
"Failed to load page. An error occurred.": "页面加载失败。发生了一个错误。",
|
||||
"Failed to update data": "数据更新失败",
|
||||
"Full access": "完全访问",
|
||||
"Full page width": "全页宽度",
|
||||
"Full width": "全宽",
|
||||
"General": "常规",
|
||||
"Group": "群组",
|
||||
"Group description": "群组描述",
|
||||
"Group name": "群组名称",
|
||||
"Groups": "群组",
|
||||
"Has full access to space settings and pages.": "能够更改全部空间设置和页面",
|
||||
"Home": "首页",
|
||||
"Import pages": "导入页面",
|
||||
"Import pages & space settings": "导入页面和空间设置",
|
||||
"Importing pages": "正在导入页面",
|
||||
"invalid invitation link": "无效的邀请链接",
|
||||
"Invitation signup": "邀请注册",
|
||||
"Invite by email": "通过电子邮箱邀请",
|
||||
"Invite members": "邀请成员",
|
||||
"Invite new members": "邀请新成员",
|
||||
"Invited members who are yet to accept their invitation will appear here.": "尚未接受邀请的成员将显示在这里。",
|
||||
"Invited members will be granted access to spaces the groups can access": "被邀请的成员将被授予访问群组可以访问的空间的权限",
|
||||
"Join the workspace": "加入工作空间",
|
||||
"Language": "语言",
|
||||
"Light": "浅色",
|
||||
"Link copied": "链接已复制",
|
||||
"Login": "登录",
|
||||
"Logout": "退出登录",
|
||||
"Manage Group": "管理群组",
|
||||
"Manage members": "管理成员",
|
||||
"member": "成员",
|
||||
"Member": "成员",
|
||||
"members": "成员",
|
||||
"Members": "成员",
|
||||
"My preferences": "我的偏好设置",
|
||||
"My Profile": "我的个人资料",
|
||||
"My profile": "我的个人资料",
|
||||
"Name": "名称",
|
||||
"New email": "新电子邮箱",
|
||||
"New page": "新建页面",
|
||||
"New password": "新密码",
|
||||
"No group found": "未找到群组",
|
||||
"No page history saved yet.": "尚未保存页面历史。",
|
||||
"No pages yet": "暂无页面",
|
||||
"No results found...": "未找到结果...",
|
||||
"No user found": "未找到用户",
|
||||
"Overview": "概览",
|
||||
"Owner": "所有者",
|
||||
"page": "个页面",
|
||||
"Page deleted successfully": "页面已成功删除",
|
||||
"Page history": "页面历史",
|
||||
"Page import is in progress. Please do not close this tab.": "页面导入正在进行中。请不要关闭此标签页。",
|
||||
"Pages": "页面",
|
||||
"pages": "个页面",
|
||||
"Password": "密码",
|
||||
"Password changed successfully": "密码更改成功",
|
||||
"Pending": "待定",
|
||||
"Please confirm your action": "请确认您的操作",
|
||||
"Preferences": "偏好设置",
|
||||
"Print PDF": "打印 PDF",
|
||||
"Profile": "个人资料",
|
||||
"Recently updated": "最近更新",
|
||||
"Remove": "移除",
|
||||
"Remove group member": "移除群组成员",
|
||||
"Remove space member": "移除空间成员",
|
||||
"Restore": "恢复",
|
||||
"Role": "角色",
|
||||
"Save": "保存",
|
||||
"Search": "搜索",
|
||||
"Search for groups": "搜索群组",
|
||||
"Search for users": "搜索用户",
|
||||
"Search for users and groups": "搜索用户和群组",
|
||||
"Search...": "搜索...",
|
||||
"Select language": "选择语言",
|
||||
"Select role": "选择角色",
|
||||
"Select role to assign to all invited members": "选择要分配给所有被邀请成员的角色",
|
||||
"Select theme": "选择主题",
|
||||
"Send invitation": "发送邀请",
|
||||
"Invitation sent": "Invitation sent",
|
||||
"Settings": "设置",
|
||||
"Setup workspace": "设置工作空间",
|
||||
"Sign In": "登录",
|
||||
"Sign Up": "注册",
|
||||
"Slug": "短链接",
|
||||
"Space": "空间",
|
||||
"Space description": "空间描述",
|
||||
"Space menu": "空间菜单",
|
||||
"Space name": "空间名称",
|
||||
"Space settings": "空间设置",
|
||||
"Space slug": "空间短链接",
|
||||
"Spaces": "空间",
|
||||
"Spaces you belong to": "您所属的空间",
|
||||
"No space found": "未找到空间",
|
||||
"Search for spaces": "搜索空间",
|
||||
"Start typing to search...": "开始输入以搜索...",
|
||||
"Status": "状态",
|
||||
"Successfully imported": "成功导入",
|
||||
"Successfully restored": "恢复成功",
|
||||
"System settings": "系统设置",
|
||||
"Theme": "主题",
|
||||
"To change your email, you have to enter your password and new email.": "要更改您的电子邮箱,您需要输入密码和新的电子邮箱地址。",
|
||||
"Toggle full page width": "切换全页宽度",
|
||||
"Unable to import pages. Please try again.": "无法导入页面。请重试。",
|
||||
"untitled": "无标题",
|
||||
"Untitled": "无标题",
|
||||
"Updated successfully": "更新成功",
|
||||
"User": "用户",
|
||||
"Workspace": "工作区",
|
||||
"Workspace Name": "工作空间名称",
|
||||
"Workspace settings": "工作区设置",
|
||||
"You can change your password here.": "您可以在这里更改密码。",
|
||||
"Your Email": "您的电子邮箱",
|
||||
"Your import is complete.": "导入已完成。",
|
||||
"Your name": "您的姓名",
|
||||
"Your Name": "您的姓名",
|
||||
"Your password": "您的密码",
|
||||
"Your password must be a minimum of 8 characters.": "您的密码必须至少包含8个字符。",
|
||||
"Sidebar toggle": "切换侧边栏",
|
||||
"Comments": "评论",
|
||||
"404 page not found": "404 页面未找到",
|
||||
"Sorry, we can't find the page you are looking for.": "抱歉,我们无法找到你所需要的页面",
|
||||
"Take me back to homepage": "回到主页",
|
||||
"Forgot password": "忘记密码",
|
||||
"Forgot your password?": "忘记密码了吗?",
|
||||
"A password reset link has been sent to your email. Please check your inbox.": "密码重置链接已经发送到您的邮箱,请检查收件箱",
|
||||
"Send reset link": "发送重置链接",
|
||||
"Password reset": "重置密码",
|
||||
"Your new password": "您的新密码",
|
||||
"Set password": "设置密码",
|
||||
"Write a comment": "编写评论",
|
||||
"Reply...": "回复...",
|
||||
"Error loading comments.": "加载评论时出错",
|
||||
"No comments yet.": "目前还没有评论",
|
||||
"Edit comment": "编辑评论",
|
||||
"Delete comment": "删除评论",
|
||||
"Are you sure you want to delete this comment?": "你确定要删除这条评论吗?",
|
||||
"Comment created successfully": "成功创建评论",
|
||||
"Error creating comment": "创建评论时出错",
|
||||
"Comment updated successfully": "评论更新成功",
|
||||
"Failed to update comment": "更新评论失败",
|
||||
"Comment deleted successfully": "成功删除评论",
|
||||
"Failed to delete comment": "删除评论失败",
|
||||
"Comment resolved successfully": "成功标记评论为解决",
|
||||
"Failed to resolve comment": "标记评论为解决失败",
|
||||
"Revoke invitation": "撤回邀请",
|
||||
"Revoke": "撤销",
|
||||
"Don't": "不要",
|
||||
"Are you sure you want to revoke this invitation? The user will not be able to join the workspace.": "你确定要撤回这个邀请吗?此用户将不能再加入此工作空间。",
|
||||
"Resend invitation": "重新发送邀请",
|
||||
"Anyone with this link can join this workspace.": "任何拥有此连接的人都可以加入此工作区",
|
||||
"Invite link": "邀请链接",
|
||||
"Copy": "复制",
|
||||
"Copied": "已复制",
|
||||
"Select a user": "选择一个用户",
|
||||
"Select a group": "选择一个组",
|
||||
"Export all pages and attachments in this space.": "导出当前空间的所有页面和附件",
|
||||
"Delete space": "删除空间",
|
||||
"Are you sure you want to delete this space?": "您确定要删除这个空间吗?",
|
||||
"Delete this space with all its pages and data.": "删除空间及其所有页面和数据",
|
||||
"All pages, comments, attachments and permissions in this space will be deleted irreversibly.": "此空间中的所有页面、评论、附件和权限将被不可逆转地删除。",
|
||||
"Confirm space name": "确认空间名称",
|
||||
"Type the space name <b>{{spaceName}}</b> to confirm your action.": "输入空间名称<b>{{spaceName}}</b>以确认您的操作。",
|
||||
"Format": "格式",
|
||||
"Include subpages": "包括子页面",
|
||||
"Include attachments": "包括附件",
|
||||
"Select export format": "选择导出格式",
|
||||
"Export failed:": "导出失败:",
|
||||
"export error": "导出出错",
|
||||
"Export page": "导出页面",
|
||||
"Export space": "导出空间",
|
||||
"Export {{type}}": "导出为 {{type}}",
|
||||
"File exceeds the {{limit}} attachment limit": "文件超出了 {{limit}} 类型附件限制",
|
||||
"Align left": "靠左对齐",
|
||||
"Align right": "靠右对齐",
|
||||
"Align center": "居中对齐",
|
||||
"Justify": "Justify",
|
||||
"Merge cells": "合并单元格",
|
||||
"Split cell": "分割单元格",
|
||||
"Delete column": "删除整列",
|
||||
"Delete row": "删除整行",
|
||||
"Add left column": "在左侧添加列",
|
||||
"Add right column": "在右侧添加列",
|
||||
"Add row above": "在上方添加行",
|
||||
"Add row below": "在下方插入行",
|
||||
"Delete table": "删除表格",
|
||||
"Info": "信息",
|
||||
"Success": "成功",
|
||||
"Warning": "警告",
|
||||
"Danger": "危险",
|
||||
"Mermaid diagram error:": "Mermaid 图表错误:",
|
||||
"Invalid Mermaid diagram": "无效的 Mermaid 图表",
|
||||
"Double-click to edit Draw.io diagram": "双击以编辑 Draw.io 图表",
|
||||
"Exit": "退出",
|
||||
"Save & Exit": "保存并退出",
|
||||
"Double-click to edit Excalidraw diagram": "双击以编辑 Excalidraw 图表",
|
||||
"Paste link": "粘贴链接",
|
||||
"Edit link": "编辑链接",
|
||||
"Remove link": "移除链接",
|
||||
"Add link": "添加链接",
|
||||
"Please enter a valid url": "请输入一个合法的 URL",
|
||||
"Empty equation": "空白公式",
|
||||
"Invalid equation": "无效的公式",
|
||||
"Color": "颜色",
|
||||
"Text color": "文字颜色",
|
||||
"Default": "默认",
|
||||
"Blue": "蓝色",
|
||||
"Green": "绿色",
|
||||
"Purple": "紫色",
|
||||
"Red": "红色",
|
||||
"Yellow": "黄色",
|
||||
"Orange": "橙色",
|
||||
"Pink": "粉色",
|
||||
"Gray": "灰色",
|
||||
"Embed link": "嵌入链接",
|
||||
"Invalid {{provider}} embed link": "无效的 {{provider}} 嵌入链接",
|
||||
"Embed {{provider}}": "嵌入 {{provider}}",
|
||||
"Enter {{provider}} link to embed": "输入 {{provider}} 链接来嵌入",
|
||||
"Bold": "粗体",
|
||||
"Italic": "斜体",
|
||||
"Underline": "下划线",
|
||||
"Strike": "删除线",
|
||||
"Code": "代码",
|
||||
"Comment": "评论",
|
||||
"Text": "文字",
|
||||
"Heading 1": "1 级标题",
|
||||
"Heading 2": "2 级标题",
|
||||
"Heading 3": "3 级标题",
|
||||
"To-do List": "代办列表",
|
||||
"Bullet List": "无需列表",
|
||||
"Numbered List": "有序列表",
|
||||
"Blockquote": "引用块",
|
||||
"Just start typing with plain text.": "只需开始键入纯文本",
|
||||
"Track tasks with a to-do list.": "使用代办列表跟踪任务",
|
||||
"Big section heading.": "大标题",
|
||||
"Medium section heading.": "中标题",
|
||||
"Small section heading.": "小标题",
|
||||
"Create a simple bullet list.": "创建一个简单的无序列表",
|
||||
"Create a list with numbering.": "创建一个有序列表",
|
||||
"Create block quote.": "创建引用块",
|
||||
"Insert code snippet.": "插入代码片段",
|
||||
"Insert horizontal rule divider": "插入水平分割线",
|
||||
"Upload any image from your device.": "从设备上传任何图像",
|
||||
"Upload any video from your device.": "从设备上传任何视频",
|
||||
"Upload any file from your device.": "从设备上传任何文件",
|
||||
"Table": "表格",
|
||||
"Insert a table.": "插入一个表格",
|
||||
"Insert collapsible block.": "插入一个折叠块",
|
||||
"Video": "视频",
|
||||
"Divider": "分割线",
|
||||
"Quote": "引用",
|
||||
"Image": "图像",
|
||||
"File attachment": "文件附件",
|
||||
"Toggle block": "切换块",
|
||||
"Callout": "标注块",
|
||||
"Insert callout notice.": "插入标注提示块",
|
||||
"Math inline": "行内公式",
|
||||
"Insert inline math equation.": "插入行内公式",
|
||||
"Math block": "公式块",
|
||||
"Insert math equation": "插入数学公式",
|
||||
"Mermaid diagram": "Mermaid 图表",
|
||||
"Insert mermaid diagram": "插入 Mermaid 图表",
|
||||
"Insert and design Drawio diagrams": "插入并设计 Draw.io 图表",
|
||||
"Insert current date": "插入当前日期",
|
||||
"Draw and sketch excalidraw diagrams": "绘制 Excalidraw 图表",
|
||||
"Multiple": "多个",
|
||||
"Heading {{level}}": "{{level}} 级标题",
|
||||
"Toggle title": "切换标题",
|
||||
"Write anything. Enter \"/\" for commands": "开始编写内容,输入 \"/\" 以使用指令",
|
||||
"Names do not match": "名称不匹配",
|
||||
"Today, {{time}}": "今天,{{time}}",
|
||||
"Yesterday, {{time}}": "昨天,{{time}}",
|
||||
"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>": "Created by: <b>{{creatorName}}</b>",
|
||||
"Created at: {{time}}": "Created at: {{time}}",
|
||||
"Edited by {{name}} {{time}}": "Edited by {{name}} {{time}}",
|
||||
"Word count: {{wordCount}}": "Word count: {{wordCount}}",
|
||||
"Character count: {{characterCount}}": "Character count: {{characterCount}}"
|
||||
}
|
@ -10,14 +10,6 @@ import Groups from "@/pages/settings/group/groups";
|
||||
import GroupInfo from "./pages/settings/group/group-info";
|
||||
import Spaces from "@/pages/settings/space/spaces.tsx";
|
||||
import { Error404 } from "@/components/ui/error-404.tsx";
|
||||
import { useQuerySubscription } from "@/features/websocket/use-query-subscription.ts";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { socketAtom } from "@/features/websocket/atoms/socket-atom.ts";
|
||||
import { useTreeSocket } from "@/features/websocket/use-tree-socket.ts";
|
||||
import { useEffect } from "react";
|
||||
import { io } from "socket.io-client";
|
||||
import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom.ts";
|
||||
import { SOCKET_URL } from "@/features/websocket/types";
|
||||
import AccountPreferences from "@/pages/settings/account/account-preferences.tsx";
|
||||
import SpaceHome from "@/pages/space/space-home.tsx";
|
||||
import PageRedirect from "@/pages/page/page-redirect.tsx";
|
||||
@ -26,37 +18,18 @@ import { ErrorBoundary } from "react-error-boundary";
|
||||
import InviteSignup from "@/pages/auth/invite-signup.tsx";
|
||||
import ForgotPassword from "@/pages/auth/forgot-password.tsx";
|
||||
import PasswordReset from "./pages/auth/password-reset";
|
||||
import 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 { 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";
|
||||
|
||||
export default function App() {
|
||||
const [, setSocket] = useAtom(socketAtom);
|
||||
const authToken = useAtomValue(authTokensAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authToken?.accessToken) {
|
||||
return;
|
||||
}
|
||||
const newSocket = io(SOCKET_URL, {
|
||||
transports: ["websocket"],
|
||||
auth: {
|
||||
token: authToken.accessToken,
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
setSocket(newSocket);
|
||||
|
||||
newSocket.on("connect", () => {
|
||||
console.log("ws connected");
|
||||
});
|
||||
|
||||
return () => {
|
||||
console.log("ws disconnected");
|
||||
newSocket.disconnect();
|
||||
};
|
||||
}, [authToken?.accessToken]);
|
||||
|
||||
useQuerySubscription();
|
||||
useTreeSocket();
|
||||
const { t } = useTranslation();
|
||||
useRedirectToCloudSelect();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -64,21 +37,30 @@ export default function App() {
|
||||
<Route index element={<Navigate to="/home" />} />
|
||||
<Route path={"/login"} element={<LoginPage />} />
|
||||
<Route path={"/invites/:invitationId"} element={<InviteSignup />} />
|
||||
<Route path={"/setup/register"} element={<SetupWorkspace />} />
|
||||
<Route path={"/forgot-password"} element={<ForgotPassword />} />
|
||||
<Route path={"/password-reset"} element={<PasswordReset />} />
|
||||
|
||||
{!isCloud() && (
|
||||
<Route path={"/setup/register"} element={<SetupWorkspace />} />
|
||||
)}
|
||||
|
||||
{isCloud() && (
|
||||
<>
|
||||
<Route path={"/create"} element={<CreateWorkspace />} />
|
||||
<Route path={"/select"} element={<CloudLogin />} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Route path={"/p/:pageSlug"} element={<PageRedirect />} />
|
||||
|
||||
<Route element={<Layout />}>
|
||||
<Route path={"/home"} element={<Home />} />
|
||||
|
||||
<Route path={"/s/:spaceSlug"} element={<SpaceHome />} />
|
||||
<Route
|
||||
path={"/s/:spaceSlug/p/:pageSlug"}
|
||||
element={
|
||||
<ErrorBoundary
|
||||
fallback={<>Failed to load page. An error occurred.</>}
|
||||
fallback={<>{t("Failed to load page. An error occurred.")}</>}
|
||||
>
|
||||
<Page />
|
||||
</ErrorBoundary>
|
||||
@ -96,6 +78,9 @@ export default function App() {
|
||||
<Route path={"groups"} element={<Groups />} />
|
||||
<Route path={"groups/:groupId"} element={<GroupInfo />} />
|
||||
<Route path={"spaces"} element={<Spaces />} />
|
||||
<Route path={"security"} element={<Security />} />
|
||||
{!isCloud() && <Route path={"license"} element={<License />} />}
|
||||
{isCloud() && <Route path={"billing"} element={<Billing />} />}
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
31
apps/client/src/components/common/copy.tsx
Normal file
31
apps/client/src/components/common/copy.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { ActionIcon, CopyButton, Tooltip } from "@mantine/core";
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface CopyProps {
|
||||
text: string;
|
||||
}
|
||||
export default function CopyTextButton({ text }: CopyProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<CopyButton value={text} timeout={2000}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip
|
||||
label={copied ? t("Copied") : t("Copy")}
|
||||
withArrow
|
||||
position="right"
|
||||
>
|
||||
<ActionIcon
|
||||
color={copied ? "teal" : "gray"}
|
||||
variant="subtle"
|
||||
onClick={copy}
|
||||
>
|
||||
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</CopyButton>
|
||||
);
|
||||
}
|
@ -12,6 +12,7 @@ import { useState } from "react";
|
||||
import { ExportFormat } from "@/features/page/types/page.types.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { exportSpace } from "@/features/space/services/space-service";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ExportModalProps {
|
||||
id: string;
|
||||
@ -29,6 +30,7 @@ export default function ExportModal({
|
||||
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
|
||||
const [includeChildren, setIncludeChildren] = useState<boolean>(false);
|
||||
const [includeAttachments, setIncludeAttachments] = useState<boolean>(true);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
@ -73,7 +75,7 @@ export default function ExportModal({
|
||||
<Modal.Body>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<div>
|
||||
<Text size="md">Format</Text>
|
||||
<Text size="md">{t("Format")}</Text>
|
||||
</div>
|
||||
<ExportFormatSelection format={format} onChange={handleChange} />
|
||||
</Group>
|
||||
@ -84,7 +86,7 @@ export default function ExportModal({
|
||||
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<div>
|
||||
<Text size="md">Include subpages</Text>
|
||||
<Text size="md">{t("Include subpages")}</Text>
|
||||
</div>
|
||||
<Switch
|
||||
onChange={(event) =>
|
||||
@ -102,7 +104,7 @@ export default function ExportModal({
|
||||
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<div>
|
||||
<Text size="md">Include attachments</Text>
|
||||
<Text size="md">{t("Include attachments")}</Text>
|
||||
</div>
|
||||
<Switch
|
||||
onChange={(event) =>
|
||||
@ -116,9 +118,9 @@ export default function ExportModal({
|
||||
|
||||
<Group justify="center" mt="md">
|
||||
<Button onClick={onClose} variant="default">
|
||||
Cancel
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleExport}>Export</Button>
|
||||
<Button onClick={handleExport}>{t("Export")}</Button>
|
||||
</Group>
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
@ -131,6 +133,8 @@ interface ExportFormatSelection {
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Select
|
||||
data={[
|
||||
@ -143,7 +147,7 @@ function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
|
||||
comboboxProps={{ width: "120" }}
|
||||
allowDeselect={false}
|
||||
withCheckIcon={false}
|
||||
aria-label="Select export format"
|
||||
aria-label={t("Select export format")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
19
apps/client/src/components/common/no-table-results.tsx
Normal file
19
apps/client/src/components/common/no-table-results.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Table, Text } from "@mantine/core";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface NoTableResultsProps {
|
||||
colSpan: number;
|
||||
}
|
||||
export default function NoTableResults({ colSpan }: NoTableResultsProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={colSpan}>
|
||||
<Text fw={500} c="dimmed" ta="center">
|
||||
{t("No results found...")}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
}
|
44
apps/client/src/components/common/paginate.tsx
Normal file
44
apps/client/src/components/common/paginate.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { Button, Group } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface PagePaginationProps {
|
||||
currentPage: number;
|
||||
hasPrevPage: boolean;
|
||||
hasNextPage: boolean;
|
||||
onPageChange: (newPage: number) => void;
|
||||
}
|
||||
|
||||
export default function Paginate({
|
||||
currentPage,
|
||||
hasPrevPage,
|
||||
hasNextPage,
|
||||
onPageChange,
|
||||
}: PagePaginationProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!hasPrevPage && !hasNextPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Group mt="md">
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-sm"
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
disabled={!hasPrevPage}
|
||||
>
|
||||
{t("Prev")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
size="compact-sm"
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
disabled={!hasNextPage}
|
||||
>
|
||||
{t("Next")}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
@ -8,17 +8,19 @@ import {
|
||||
} from '@mantine/core';
|
||||
import {Link} from 'react-router-dom';
|
||||
import PageListSkeleton from '@/components/ui/page-list-skeleton.tsx';
|
||||
import {buildPageUrl} from '@/features/page/page.utils.ts';
|
||||
import {formattedDate} from '@/lib/time.ts';
|
||||
import {useRecentChangesQuery} from '@/features/page/queries/page-query.ts';
|
||||
import {IconFileDescription} from '@tabler/icons-react';
|
||||
import {getSpaceUrl} from '@/lib/config.ts';
|
||||
import { buildPageUrl } from '@/features/page/page.utils.ts';
|
||||
import { formattedDate } from '@/lib/time.ts';
|
||||
import { useRecentChangesQuery } from '@/features/page/queries/page-query.ts';
|
||||
import { IconFileDescription } from '@tabler/icons-react';
|
||||
import { getSpaceUrl } from '@/lib/config.ts';
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
spaceId?: string;
|
||||
}
|
||||
|
||||
export default function RecentChanges({spaceId}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const {data: pages, isLoading, isError} = useRecentChangesQuery(spaceId);
|
||||
|
||||
if (isLoading) {
|
||||
@ -26,7 +28,7 @@ export default function RecentChanges({spaceId}: Props) {
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <Text>Failed to fetch recent pages</Text>;
|
||||
return <Text>{t("Failed to fetch recent pages")}</Text>;
|
||||
}
|
||||
|
||||
return pages && pages.items.length > 0 ? (
|
||||
@ -48,7 +50,7 @@ export default function RecentChanges({spaceId}: Props) {
|
||||
)}
|
||||
|
||||
<Text fw={500} size="md" lineClamp={1}>
|
||||
{page.title || 'Untitled'}
|
||||
{page.title || t("Untitled")}
|
||||
</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
@ -78,7 +80,7 @@ export default function RecentChanges({spaceId}: Props) {
|
||||
</Table.ScrollContainer>
|
||||
) : (
|
||||
<Text size="md" ta="center">
|
||||
No pages yet
|
||||
{t("No pages yet")}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
37
apps/client/src/components/common/search-input.tsx
Normal file
37
apps/client/src/components/common/search-input.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { TextInput, Group } from "@mantine/core";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface SearchInputProps {
|
||||
placeholder?: string;
|
||||
debounceDelay?: number;
|
||||
onSearch: (value: string) => void;
|
||||
}
|
||||
|
||||
export function SearchInput({
|
||||
placeholder,
|
||||
debounceDelay = 500,
|
||||
onSearch,
|
||||
}: SearchInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState("");
|
||||
const [debouncedValue] = useDebouncedValue(value, debounceDelay);
|
||||
|
||||
useEffect(() => {
|
||||
onSearch(debouncedValue);
|
||||
}, [debouncedValue, onSearch]);
|
||||
|
||||
return (
|
||||
<Group mb="sm">
|
||||
<TextInput
|
||||
size="sm"
|
||||
placeholder={placeholder || t("Search...")}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
}
|
33
apps/client/src/components/icons/google-icon.tsx
Normal file
33
apps/client/src/components/icons/google-icon.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { rem } from "@mantine/core";
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function GoogleIcon({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
viewBox="0 0 256 262"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782"
|
||||
/>
|
||||
<path
|
||||
fill="#EB4335"
|
||||
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
23
apps/client/src/components/icons/google-sheets-icon.tsx
Normal file
23
apps/client/src/components/icons/google-sheets-icon.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { rem } from '@mantine/core';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function GoogleSheetsIcon({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 48 48"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path fill="#43a047" d="M37,45H11c-1.657,0-3-1.343-3-3V6c0-1.657,1.343-3,3-3h19l10,10v29C40,43.657,38.657,45,37,45z"/>
|
||||
<path fill="#c8e6c9" d="M40 13L30 13 30 3z"/>
|
||||
<path fill="#2e7d32" d="M30 13L40 23 40 13z"/>
|
||||
<path
|
||||
fill="#e8f5e9"
|
||||
d="M31,23H17h-2v2v2v2v2v2v2v2h18v-2v-2v-2v-2v-2v-2v-2H31z M17,25h4v2h-4V25z M17,29h4v2h-4V29z M17,33h4v2h-4V33z M31,35h-8v-2h8V35z M31,31h-8v-2h8V31z M31,27h-8v-2h8V27z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -4,6 +4,7 @@ export { TypeformIcon } from "./typeform-icon.tsx";
|
||||
export { VimeoIcon } from "./vimeo-icon.tsx";
|
||||
export { MiroIcon } from "./miro-icon.tsx";
|
||||
export { GoogleDriveIcon } from "./google-drive-icon.tsx";
|
||||
export { GoogleSheetsIcon } from "./google-sheets-icon.tsx";
|
||||
export { FramerIcon } from "./framer-icon.tsx";
|
||||
export { LoomIcon } from "./loom-icon.tsx";
|
||||
export { YoutubeIcon } from "./youtube-icon.tsx";
|
||||
|
20
apps/client/src/components/icons/openid-icon.tsx
Normal file
20
apps/client/src/components/icons/openid-icon.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { rem } from "@mantine/core";
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
}
|
||||
|
||||
export function OpenIdIcon({ size }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
style={{ width: rem(size), height: rem(size) }}
|
||||
>
|
||||
<path d="M14.54.889l-3.63 1.773v18.17c-4.15-.52-7.27-2.78-7.27-5.5 0-2.58 2.8-4.75 6.63-5.41v-2.31C4.42 8.322 0 11.502 0 15.332c0 3.96 4.74 7.24 10.91 7.78l3.63-1.71V.888m.64 6.724v2.31c1.43.25 2.71.7 3.76 1.31l-1.97 1.11 7.03 1.53-.5-5.21-1.87 1.06c-1.74-1.06-3.96-1.81-6.45-2.11z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -1,31 +1,36 @@
|
||||
import {Group, Text, Tooltip} from "@mantine/core";
|
||||
import { Badge, Group, Text, Tooltip } from "@mantine/core";
|
||||
import classes from "./app-header.module.css";
|
||||
import React from "react";
|
||||
import TopMenu from "@/components/layouts/global/top-menu.tsx";
|
||||
import {Link} from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import {useAtom} from "jotai/index";
|
||||
import { useAtom } from "jotai";
|
||||
import {
|
||||
desktopSidebarAtom,
|
||||
mobileSidebarAtom,
|
||||
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import {useToggleSidebar} from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
|
||||
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
|
||||
import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useTrial from "@/ee/hooks/use-trial.tsx";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
|
||||
const links = [{link: APP_ROUTE.HOME, label: "Home"}];
|
||||
const links = [{ link: APP_ROUTE.HOME, label: "Home" }];
|
||||
|
||||
export function AppHeader() {
|
||||
const { t } = useTranslation();
|
||||
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
||||
const toggleMobile = useToggleSidebar(mobileSidebarAtom);
|
||||
|
||||
const [desktopOpened] = useAtom(desktopSidebarAtom);
|
||||
const toggleDesktop = useToggleSidebar(desktopSidebarAtom);
|
||||
const { isTrial, trialDaysLeft } = useTrial();
|
||||
|
||||
const isHomeRoute = location.pathname.startsWith("/home");
|
||||
|
||||
const items = links.map((link) => (
|
||||
<Link key={link.label} to={link.link} className={classes.link}>
|
||||
{link.label}
|
||||
{t(link.label)}
|
||||
</Link>
|
||||
));
|
||||
|
||||
@ -35,10 +40,9 @@ export function AppHeader() {
|
||||
<Group wrap="nowrap">
|
||||
{!isHomeRoute && (
|
||||
<>
|
||||
<Tooltip label="Sidebar toggle">
|
||||
|
||||
<Tooltip label={t("Sidebar toggle")}>
|
||||
<SidebarToggle
|
||||
aria-label="Sidebar toggle"
|
||||
aria-label={t("Sidebar toggle")}
|
||||
opened={mobileOpened}
|
||||
onClick={toggleMobile}
|
||||
hiddenFrom="sm"
|
||||
@ -46,9 +50,9 @@ export function AppHeader() {
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Sidebar toggle">
|
||||
<Tooltip label={t("Sidebar toggle")}>
|
||||
<SidebarToggle
|
||||
aria-label="Sidebar toggle"
|
||||
aria-label={t("Sidebar toggle")}
|
||||
opened={desktopOpened}
|
||||
onClick={toggleDesktop}
|
||||
visibleFrom="sm"
|
||||
@ -61,7 +65,7 @@ export function AppHeader() {
|
||||
<Text
|
||||
size="lg"
|
||||
fw={600}
|
||||
style={{cursor: "pointer", userSelect: "none"}}
|
||||
style={{ cursor: "pointer", userSelect: "none" }}
|
||||
component={Link}
|
||||
to="/home"
|
||||
>
|
||||
@ -73,8 +77,21 @@ export function AppHeader() {
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Group px={"xl"}>
|
||||
<TopMenu/>
|
||||
<Group px={"xl"} wrap="nowrap">
|
||||
{isCloud() && isTrial && trialDaysLeft !== 0 && (
|
||||
<Badge
|
||||
variant="light"
|
||||
style={{ cursor: "pointer" }}
|
||||
component={Link}
|
||||
to={APP_ROUTE.SETTINGS.WORKSPACE.BILLING}
|
||||
visibleFrom="xs"
|
||||
>
|
||||
{trialDaysLeft === 1
|
||||
? "1 day left"
|
||||
: `${trialDaysLeft} days left`}
|
||||
</Badge>
|
||||
)}
|
||||
<TopMenu />
|
||||
</Group>
|
||||
</Group>
|
||||
</>
|
||||
|
@ -3,9 +3,11 @@ import CommentList from "@/features/comment/components/comment-list.tsx";
|
||||
import { useAtom } from "jotai";
|
||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import React, { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Aside() {
|
||||
const [{ tab }] = useAtom(asideStateAtom);
|
||||
const { t } = useTranslation();
|
||||
|
||||
let title: string;
|
||||
let component: ReactNode;
|
||||
@ -25,7 +27,7 @@ export default function Aside() {
|
||||
{component && (
|
||||
<>
|
||||
<Text mb="md" fw={500}>
|
||||
{title}
|
||||
{t(title)}
|
||||
</Text>
|
||||
|
||||
<ScrollArea
|
||||
|
@ -1,23 +1,26 @@
|
||||
import { AppShell, Container } from "@mantine/core";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import SettingsSidebar from "@/components/settings/settings-sidebar.tsx";
|
||||
import { useAtom } from "jotai";
|
||||
import {
|
||||
asideStateAtom,
|
||||
desktopSidebarAtom,
|
||||
mobileSidebarAtom, sidebarWidthAtom,
|
||||
mobileSidebarAtom,
|
||||
sidebarWidthAtom,
|
||||
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx";
|
||||
import { AppHeader } from "@/components/layouts/global/app-header.tsx";
|
||||
import Aside from "@/components/layouts/global/aside.tsx";
|
||||
import classes from "./app-shell.module.css";
|
||||
import { useTrialEndAction } from "@/ee/hooks/use-trial-end-action.tsx";
|
||||
|
||||
export default function GlobalAppShell({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
useTrialEndAction();
|
||||
const [mobileOpened] = useAtom(mobileSidebarAtom);
|
||||
const [desktopOpened] = useAtom(desktopSidebarAtom);
|
||||
const [{ isAsideOpen }] = useAtom(asideStateAtom);
|
||||
@ -37,7 +40,9 @@ export default function GlobalAppShell({
|
||||
const resize = React.useCallback(
|
||||
(mouseMoveEvent) => {
|
||||
if (isResizing) {
|
||||
const newWidth = mouseMoveEvent.clientX - sidebarRef.current.getBoundingClientRect().left;
|
||||
const newWidth =
|
||||
mouseMoveEvent.clientX -
|
||||
sidebarRef.current.getBoundingClientRect().left;
|
||||
if (newWidth < 220) {
|
||||
setSidebarWidth(220);
|
||||
return;
|
||||
@ -49,7 +54,7 @@ export default function GlobalAppShell({
|
||||
setSidebarWidth(newWidth);
|
||||
}
|
||||
},
|
||||
[isResizing]
|
||||
[isResizing],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -94,7 +99,11 @@ export default function GlobalAppShell({
|
||||
<AppHeader />
|
||||
</AppShell.Header>
|
||||
{!isHomeRoute && (
|
||||
<AppShell.Navbar className={classes.navbar} withBorder={false} ref={sidebarRef}>
|
||||
<AppShell.Navbar
|
||||
className={classes.navbar}
|
||||
withBorder={false}
|
||||
ref={sidebarRef}
|
||||
>
|
||||
<div className={classes.resizeHandle} onMouseDown={startResizing} />
|
||||
{isSpaceRoute && <SpaceSidebar />}
|
||||
{isSettingsRoute && <SettingsSidebar />}
|
||||
|
@ -13,8 +13,10 @@ import { Link } from "react-router-dom";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import useAuth from "@/features/auth/hooks/use-auth.ts";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function TopMenu() {
|
||||
const { t } = useTranslation();
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
const { logout } = useAuth();
|
||||
|
||||
@ -31,27 +33,27 @@ export default function TopMenu() {
|
||||
<UnstyledButton>
|
||||
<Group gap={7} wrap={"nowrap"}>
|
||||
<CustomAvatar
|
||||
avatarUrl={workspace.logo}
|
||||
name={workspace.name}
|
||||
avatarUrl={workspace?.logo}
|
||||
name={workspace?.name}
|
||||
variant="filled"
|
||||
size="sm"
|
||||
/>
|
||||
<Text fw={500} size="sm" lh={1} mr={3}>
|
||||
{workspace.name}
|
||||
<Text fw={500} size="sm" lh={1} mr={3} lineClamp={1}>
|
||||
{workspace?.name}
|
||||
</Text>
|
||||
<IconChevronDown size={16} />
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>Workspace</Menu.Label>
|
||||
<Menu.Label>{t("Workspace")}</Menu.Label>
|
||||
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
to={APP_ROUTE.SETTINGS.WORKSPACE.GENERAL}
|
||||
leftSection={<IconSettings size={16} />}
|
||||
>
|
||||
Workspace settings
|
||||
{t("Workspace settings")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
@ -59,12 +61,12 @@ export default function TopMenu() {
|
||||
to={APP_ROUTE.SETTINGS.WORKSPACE.MEMBERS}
|
||||
leftSection={<IconUsers size={16} />}
|
||||
>
|
||||
Manage members
|
||||
{t("Manage members")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Label>Account</Menu.Label>
|
||||
<Menu.Label>{t("Account")}</Menu.Label>
|
||||
<Menu.Item component={Link} to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}>
|
||||
<Group wrap={"nowrap"}>
|
||||
<CustomAvatar
|
||||
@ -88,7 +90,7 @@ export default function TopMenu() {
|
||||
to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}
|
||||
leftSection={<IconUserCircle size={16} />}
|
||||
>
|
||||
My profile
|
||||
{t("My profile")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
@ -96,13 +98,13 @@ export default function TopMenu() {
|
||||
to={APP_ROUTE.SETTINGS.ACCOUNT.PREFERENCES}
|
||||
leftSection={<IconBrush size={16} />}
|
||||
>
|
||||
My preferences
|
||||
{t("My preferences")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item onClick={logout} leftSection={<IconLogout size={16} />}>
|
||||
Logout
|
||||
{t("Logout")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
53
apps/client/src/components/settings/app-version.tsx
Normal file
53
apps/client/src/components/settings/app-version.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { useAppVersion } from "@/features/workspace/queries/workspace-query.ts";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
import classes from "@/components/settings/settings.module.css";
|
||||
import { Indicator, Text, Tooltip } from "@mantine/core";
|
||||
import React from "react";
|
||||
import semverGt from "semver/functions/gt";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function AppVersion() {
|
||||
const { t } = useTranslation();
|
||||
const { data: appVersion } = useAppVersion(!isCloud());
|
||||
let hasUpdate = false;
|
||||
try {
|
||||
hasUpdate =
|
||||
appVersion &&
|
||||
parseFloat(appVersion.latestVersion) > 0 &&
|
||||
semverGt(appVersion.latestVersion, appVersion.currentVersion);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.text}>
|
||||
<Tooltip
|
||||
label={t("{{latestVersion}} is available", {
|
||||
latestVersion: `v${appVersion?.latestVersion}`,
|
||||
})}
|
||||
disabled={!hasUpdate}
|
||||
>
|
||||
<Indicator
|
||||
label={t("New update")}
|
||||
color="gray"
|
||||
inline
|
||||
size={16}
|
||||
position="middle-end"
|
||||
style={{ cursor: "pointer" }}
|
||||
disabled={!hasUpdate}
|
||||
>
|
||||
<Text
|
||||
size="sm"
|
||||
c="dimmed"
|
||||
component="a"
|
||||
mr={45}
|
||||
href="https://github.com/docmost/docmost/releases"
|
||||
target="_blank"
|
||||
>
|
||||
v{APP_VERSION}
|
||||
</Text>
|
||||
</Indicator>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
59
apps/client/src/components/settings/settings-queries.tsx
Normal file
59
apps/client/src/components/settings/settings-queries.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { queryClient } from "@/main.tsx";
|
||||
import {
|
||||
getBilling,
|
||||
getBillingPlans,
|
||||
} from "@/ee/billing/services/billing-service.ts";
|
||||
import { getSpaces } from "@/features/space/services/space-service.ts";
|
||||
import { getGroups } from "@/features/group/services/group-service.ts";
|
||||
import { QueryParams } from "@/lib/types.ts";
|
||||
import { getWorkspaceMembers } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { getLicenseInfo } from "@/ee/licence/services/license-service.ts";
|
||||
import { getSsoProviders } from '@/ee/security/services/security-service.ts';
|
||||
|
||||
export const prefetchWorkspaceMembers = () => {
|
||||
const params = { limit: 100, page: 1, query: "" } as QueryParams;
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["workspaceMembers", params],
|
||||
queryFn: () => getWorkspaceMembers(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchSpaces = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["spaces", { page: 1 }],
|
||||
queryFn: () => getSpaces({ page: 1 }),
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchGroups = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["groups", { page: 1 }],
|
||||
queryFn: () => getGroups({ page: 1 }),
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchBilling = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["billing"],
|
||||
queryFn: () => getBilling(),
|
||||
});
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["billing-plans"],
|
||||
queryFn: () => getBillingPlans(),
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchLicense = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["license"],
|
||||
queryFn: () => getLicenseInfo(),
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchSsoProviders = () => {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["sso-providers"],
|
||||
queryFn: () => getSsoProviders(),
|
||||
});
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Group, Text, ScrollArea, ActionIcon, rem } from "@mantine/core";
|
||||
import { Group, Text, ScrollArea, ActionIcon } from "@mantine/core";
|
||||
import {
|
||||
IconUser,
|
||||
IconSettings,
|
||||
@ -8,14 +8,35 @@ import {
|
||||
IconUsersGroup,
|
||||
IconSpaces,
|
||||
IconBrush,
|
||||
IconCoin,
|
||||
IconLock,
|
||||
IconKey,
|
||||
} from "@tabler/icons-react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import classes from "./settings.module.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { useAtom } from "jotai/index";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import {
|
||||
prefetchBilling,
|
||||
prefetchGroups,
|
||||
prefetchLicense,
|
||||
prefetchSpaces,
|
||||
prefetchSsoProviders,
|
||||
prefetchWorkspaceMembers,
|
||||
} from "@/components/settings/settings-queries.tsx";
|
||||
import AppVersion from "@/components/settings/app-version.tsx";
|
||||
|
||||
interface DataItem {
|
||||
label: string;
|
||||
icon: React.ElementType;
|
||||
path: string;
|
||||
isCloud?: boolean;
|
||||
isEnterprise?: boolean;
|
||||
isAdmin?: boolean;
|
||||
isSelfhosted?: boolean;
|
||||
}
|
||||
|
||||
interface DataGroup {
|
||||
@ -44,39 +65,131 @@ const groupedData: DataGroup[] = [
|
||||
icon: IconUsers,
|
||||
path: "/settings/members",
|
||||
},
|
||||
{
|
||||
label: "Billing",
|
||||
icon: IconCoin,
|
||||
path: "/settings/billing",
|
||||
isCloud: true,
|
||||
isAdmin: true,
|
||||
},
|
||||
{
|
||||
label: "Security & SSO",
|
||||
icon: IconLock,
|
||||
path: "/settings/security",
|
||||
isCloud: true,
|
||||
isEnterprise: true,
|
||||
isAdmin: true,
|
||||
},
|
||||
{ label: "Groups", icon: IconUsersGroup, path: "/settings/groups" },
|
||||
{ label: "Spaces", icon: IconSpaces, path: "/settings/spaces" },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: "System",
|
||||
items: [
|
||||
{
|
||||
label: "License & Edition",
|
||||
icon: IconKey,
|
||||
path: "/settings/license",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function SettingsSidebar() {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const [active, setActive] = useState(location.pathname);
|
||||
const navigate = useNavigate();
|
||||
const { isAdmin } = useUserRole();
|
||||
const [workspace] = useAtom(workspaceAtom);
|
||||
|
||||
useEffect(() => {
|
||||
setActive(location.pathname);
|
||||
}, [location.pathname]);
|
||||
|
||||
const menuItems = groupedData.map((group) => (
|
||||
<div key={group.heading}>
|
||||
<Text c="dimmed" className={classes.linkHeader}>
|
||||
{group.heading}
|
||||
</Text>
|
||||
{group.items.map((item) => (
|
||||
<Link
|
||||
className={classes.link}
|
||||
data-active={active.startsWith(item.path) || undefined}
|
||||
key={item.label}
|
||||
to={item.path}
|
||||
>
|
||||
<item.icon className={classes.linkIcon} stroke={2} />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
const canShowItem = (item: DataItem) => {
|
||||
if (item.isCloud && item.isEnterprise) {
|
||||
if (!(isCloud() || workspace?.hasLicenseKey)) return false;
|
||||
return item.isAdmin ? isAdmin : true;
|
||||
}
|
||||
|
||||
if (item.isCloud) {
|
||||
return isCloud() ? (item.isAdmin ? isAdmin : true) : false;
|
||||
}
|
||||
|
||||
if (item.isSelfhosted) {
|
||||
return !isCloud() ? (item.isAdmin ? isAdmin : true) : false;
|
||||
}
|
||||
|
||||
if (item.isEnterprise) {
|
||||
return workspace?.hasLicenseKey ? (item.isAdmin ? isAdmin : true) : false;
|
||||
}
|
||||
|
||||
if (item.isAdmin) {
|
||||
return isAdmin;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const menuItems = groupedData.map((group) => {
|
||||
if (group.heading === "System" && (!isAdmin || isCloud())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={group.heading}>
|
||||
<Text c="dimmed" className={classes.linkHeader}>
|
||||
{t(group.heading)}
|
||||
</Text>
|
||||
{group.items.map((item) => {
|
||||
if (!canShowItem(item)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let prefetchHandler: any;
|
||||
switch (item.label) {
|
||||
case "Members":
|
||||
prefetchHandler = prefetchWorkspaceMembers;
|
||||
break;
|
||||
case "Spaces":
|
||||
prefetchHandler = prefetchSpaces;
|
||||
break;
|
||||
case "Groups":
|
||||
prefetchHandler = prefetchGroups;
|
||||
break;
|
||||
case "Billing":
|
||||
prefetchHandler = prefetchBilling;
|
||||
break;
|
||||
case "License & Edition":
|
||||
if (workspace?.hasLicenseKey) {
|
||||
prefetchHandler = prefetchLicense;
|
||||
}
|
||||
break;
|
||||
case "Security & SSO":
|
||||
prefetchHandler = prefetchSsoProviders;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
onMouseEnter={prefetchHandler}
|
||||
className={classes.link}
|
||||
data-active={active.startsWith(item.path) || undefined}
|
||||
key={item.label}
|
||||
to={item.path}
|
||||
>
|
||||
<item.icon className={classes.linkIcon} stroke={2} />
|
||||
<span>{t(item.label)}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classes.navbar}>
|
||||
@ -89,22 +202,25 @@ export default function SettingsSidebar() {
|
||||
>
|
||||
<IconArrowLeft stroke={2} />
|
||||
</ActionIcon>
|
||||
<Text fw={500}>Settings</Text>
|
||||
<Text fw={500}>{t("Settings")}</Text>
|
||||
</Group>
|
||||
|
||||
<ScrollArea w="100%">{menuItems}</ScrollArea>
|
||||
<div className={classes.version}>
|
||||
<Text
|
||||
className={classes.version}
|
||||
size="sm"
|
||||
c="dimmed"
|
||||
component="a"
|
||||
href="https://github.com/docmost/docmost/releases"
|
||||
target="_blank"
|
||||
>
|
||||
v{APP_VERSION}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{!isCloud() && <AppVersion />}
|
||||
|
||||
{isCloud() && (
|
||||
<div className={classes.text}>
|
||||
<Text
|
||||
size="sm"
|
||||
c="dimmed"
|
||||
component="a"
|
||||
href="mailto:help@docmost.com"
|
||||
>
|
||||
help@docmost.com
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -58,7 +58,7 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.version {
|
||||
.text {
|
||||
padding-left: var(--mantine-spacing-xs) ;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactNode, useState } from "react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Popover,
|
||||
Button,
|
||||
useMantineColorScheme,
|
||||
} from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
const Picker = React.lazy(() => import('@emoji-mart/react'));
|
||||
} from "@mantine/core";
|
||||
import { useClickOutside, useDisclosure, useWindowEvent } from "@mantine/hooks";
|
||||
import { Suspense } from "react";
|
||||
const Picker = React.lazy(() => import("@emoji-mart/react"));
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface EmojiPickerInterface {
|
||||
onEmojiSelect: (emoji: any) => void;
|
||||
@ -23,8 +23,26 @@ function EmojiPicker({
|
||||
removeEmojiAction,
|
||||
readOnly,
|
||||
}: EmojiPickerInterface) {
|
||||
const { t } = useTranslation();
|
||||
const [opened, handlers] = useDisclosure(false);
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const [target, setTarget] = useState<HTMLElement | null>(null);
|
||||
const [dropdown, setDropdown] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
useClickOutside(
|
||||
() => handlers.close(),
|
||||
["mousedown", "touchstart"],
|
||||
[dropdown, target],
|
||||
);
|
||||
|
||||
// We need this because the default Mantine popover closeOnEscape does not work
|
||||
useWindowEvent("keydown", (event) => {
|
||||
if (opened && event.key === "Escape") {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
handlers.close();
|
||||
}
|
||||
});
|
||||
|
||||
const handleEmojiSelect = (emoji) => {
|
||||
onEmojiSelect(emoji);
|
||||
@ -43,37 +61,38 @@ function EmojiPicker({
|
||||
width={332}
|
||||
position="bottom"
|
||||
disabled={readOnly}
|
||||
closeOnEscape={true}
|
||||
>
|
||||
<Popover.Target>
|
||||
<Popover.Target ref={setTarget}>
|
||||
<ActionIcon c="gray" variant="transparent" onClick={handlers.toggle}>
|
||||
{icon}
|
||||
</ActionIcon>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown bg="000" style={{ border: 'none' }}>
|
||||
<Suspense fallback={null}>
|
||||
<Suspense fallback={null}>
|
||||
<Popover.Dropdown bg="000" style={{ border: "none" }} ref={setDropdown}>
|
||||
<Picker
|
||||
data={async () => (await import('@emoji-mart/data')).default}
|
||||
data={async () => (await import("@emoji-mart/data")).default}
|
||||
onEmojiSelect={handleEmojiSelect}
|
||||
perLine={8}
|
||||
skinTonePosition="search"
|
||||
theme={colorScheme}
|
||||
/>
|
||||
</Suspense>
|
||||
<Button
|
||||
variant="default"
|
||||
c="gray"
|
||||
size="xs"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
zIndex: 2,
|
||||
bottom: '1rem',
|
||||
right: '1rem',
|
||||
}}
|
||||
onClick={handleRemoveEmoji}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</Popover.Dropdown>
|
||||
<Button
|
||||
variant="default"
|
||||
c="gray"
|
||||
size="xs"
|
||||
style={{
|
||||
position: "absolute",
|
||||
zIndex: 2,
|
||||
bottom: "1rem",
|
||||
right: "1rem",
|
||||
}}
|
||||
onClick={handleRemoveEmoji}
|
||||
>
|
||||
{t("Remove")}
|
||||
</Button>
|
||||
</Popover.Dropdown>
|
||||
</Suspense>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
@ -2,21 +2,24 @@ import { Title, Text, Button, Container, Group } from "@mantine/core";
|
||||
import classes from "./error-404.module.css";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function Error404() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>404 page not found - Docmost</title>
|
||||
<title>{t("404 page not found")} - Docmost</title>
|
||||
</Helmet>
|
||||
<Container className={classes.root}>
|
||||
<Title className={classes.title}>404 Page Not Found</Title>
|
||||
<Title className={classes.title}>{t("404 page not found")}</Title>
|
||||
<Text c="dimmed" size="lg" ta="center" className={classes.description}>
|
||||
Sorry, we can't find the page you are looking for.
|
||||
{t("Sorry, we can't find the page you are looking for.")}
|
||||
</Text>
|
||||
<Group justify="center">
|
||||
<Button component={Link} to={"/home"} variant="subtle" size="md">
|
||||
Take me back to homepage
|
||||
{t("Take me back to homepage")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Container>
|
||||
|
@ -2,6 +2,7 @@ import React, { forwardRef } from "react";
|
||||
import { IconCheck, IconChevronDown } from "@tabler/icons-react";
|
||||
import { Group, Text, Menu, Button } from "@mantine/core";
|
||||
import { IRoleData } from "@/lib/types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface RoleButtonProps extends React.ComponentPropsWithoutRef<"button"> {
|
||||
name: string;
|
||||
@ -36,10 +37,12 @@ export default function RoleSelectMenu({
|
||||
onChange,
|
||||
disabled,
|
||||
}: RoleMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Menu withArrow>
|
||||
<Menu.Target>
|
||||
<RoleButton name={roleName} disabled={disabled} />
|
||||
<RoleButton name={t(roleName)} disabled={disabled} />
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
@ -50,9 +53,9 @@ export default function RoleSelectMenu({
|
||||
>
|
||||
<Group flex="1" gap="xs">
|
||||
<div>
|
||||
<Text size="sm">{item.label}</Text>
|
||||
<Text size="sm">{t(item.label)}</Text>
|
||||
<Text size="xs" opacity={0.65}>
|
||||
{item.description}
|
||||
{t(item.description)}
|
||||
</Text>
|
||||
</div>
|
||||
{item.label === roleName && <IconCheck size={20} />}
|
||||
|
1
apps/client/src/ee/LICENSE
Normal file
1
apps/client/src/ee/LICENSE
Normal file
@ -0,0 +1 @@
|
||||
Files in this directory are subject to the Docmost Enterprise Edition license.
|
130
apps/client/src/ee/billing/components/billing-details.tsx
Normal file
130
apps/client/src/ee/billing/components/billing-details.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
import {
|
||||
useBillingPlans,
|
||||
useBillingQuery,
|
||||
} from "@/ee/billing/queries/billing-query.ts";
|
||||
import { Group, Text, SimpleGrid, Paper } from "@mantine/core";
|
||||
import classes from "./billing.module.css";
|
||||
import { format } from "date-fns";
|
||||
import { formatInterval } from "@/ee/billing/utils.ts";
|
||||
|
||||
export default function BillingDetails() {
|
||||
const { data: billing } = useBillingQuery();
|
||||
const { data: plans } = useBillingPlans();
|
||||
|
||||
if (!billing || !plans) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<SimpleGrid cols={{ base: 1, xs: 2, sm: 3 }}>
|
||||
<Paper p="md" radius="md">
|
||||
<Group justify="apart">
|
||||
<div>
|
||||
<Text
|
||||
c="dimmed"
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
fz="xs"
|
||||
className={classes.label}
|
||||
>
|
||||
Plan
|
||||
</Text>
|
||||
<Text fw={700} fz="lg">
|
||||
{
|
||||
plans.find(
|
||||
(plan) => plan.productId === billing.stripeProductId,
|
||||
)?.name
|
||||
}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Paper p="md" radius="md">
|
||||
<Group justify="apart">
|
||||
<div>
|
||||
<Text
|
||||
c="dimmed"
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
fz="xs"
|
||||
className={classes.label}
|
||||
>
|
||||
Billing Period
|
||||
</Text>
|
||||
<Text fw={700} fz="lg" tt="capitalize">
|
||||
{formatInterval(billing.interval)}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Paper p="md" radius="md">
|
||||
<Group justify="apart">
|
||||
<div>
|
||||
<Text
|
||||
c="dimmed"
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
fz="xs"
|
||||
className={classes.label}
|
||||
>
|
||||
{billing.cancelAtPeriodEnd
|
||||
? "Cancellation date"
|
||||
: "Renewal date"}
|
||||
</Text>
|
||||
<Text fw={700} fz="lg">
|
||||
{format(billing.periodEndAt, "dd MMM, yyyy")}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, xs: 2, sm: 3 }}>
|
||||
<Paper p="md" radius="md">
|
||||
<Group justify="apart">
|
||||
<div>
|
||||
<Text
|
||||
c="dimmed"
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
fz="xs"
|
||||
className={classes.label}
|
||||
>
|
||||
Seat count
|
||||
</Text>
|
||||
<Text fw={700} fz="lg">
|
||||
{billing.quantity}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Paper p="md" radius="md">
|
||||
<Group justify="apart">
|
||||
<div>
|
||||
<Text
|
||||
c="dimmed"
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
fz="xs"
|
||||
className={classes.label}
|
||||
>
|
||||
Total
|
||||
</Text>
|
||||
<Text fw={700} fz="lg">
|
||||
{(billing.amount / 100) * billing.quantity}{" "}
|
||||
{billing.currency.toUpperCase()}
|
||||
</Text>
|
||||
<Text c="dimmed" fz="sm">
|
||||
${billing.amount / 100} /user/{billing.interval}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
);
|
||||
}
|
13
apps/client/src/ee/billing/components/billing-incomplete.tsx
Normal file
13
apps/client/src/ee/billing/components/billing-incomplete.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { Alert } from "@mantine/core";
|
||||
import React from "react";
|
||||
|
||||
export default function BillingIncomplete() {
|
||||
return (
|
||||
<>
|
||||
<Alert variant="light" color="blue">
|
||||
Your subscription is in an incomplete state. Please refresh this page if
|
||||
you recently made your payment.
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
}
|
115
apps/client/src/ee/billing/components/billing-plans.tsx
Normal file
115
apps/client/src/ee/billing/components/billing-plans.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
List,
|
||||
SegmentedControl,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
Text,
|
||||
Group,
|
||||
} from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
import { useBillingPlans } from "@/ee/billing/queries/billing-query.ts";
|
||||
import { getCheckoutLink } from "@/ee/billing/services/billing-service.ts";
|
||||
|
||||
export default function BillingPlans() {
|
||||
const { data: plans } = useBillingPlans();
|
||||
const [interval, setInterval] = useState("yearly");
|
||||
|
||||
if (!plans) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleCheckout = async (priceId: string) => {
|
||||
try {
|
||||
const checkoutLink = await getCheckoutLink({
|
||||
priceId: priceId,
|
||||
});
|
||||
window.location.href = checkoutLink.url;
|
||||
} catch (err) {
|
||||
console.error("Failed to get checkout link", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Group justify="center" p="xl">
|
||||
{plans.map((plan) => {
|
||||
const price =
|
||||
interval === "monthly" ? plan.price.monthly : plan.price.yearly;
|
||||
const priceId = interval === "monthly" ? plan.monthlyId : plan.yearlyId;
|
||||
const yearlyMonthPrice = parseInt(plan.price.yearly) / 12;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={plan.name}
|
||||
withBorder
|
||||
radius="md"
|
||||
shadow="sm"
|
||||
p="xl"
|
||||
w={300}
|
||||
>
|
||||
<SegmentedControl
|
||||
value={interval}
|
||||
onChange={setInterval}
|
||||
fullWidth
|
||||
data={[
|
||||
{ label: "Monthly", value: "monthly" },
|
||||
{ label: "Yearly (25% OFF)", value: "yearly" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Title order={3} ta="center" mt="sm" mb="xs">
|
||||
{plan.name}
|
||||
</Title>
|
||||
<Text ta="center" size="lg" fw={700}>
|
||||
{interval === "monthly" && (
|
||||
<>
|
||||
${price}{" "}
|
||||
<Text span size="sm" fw={500} c="dimmed">
|
||||
/user/month
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{interval === "yearly" && (
|
||||
<>
|
||||
${yearlyMonthPrice}{" "}
|
||||
<Text span size="sm" fw={500} c="dimmed">
|
||||
/user/month
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
<br/>
|
||||
<Text span ta="center" size="md" fw={500} c="dimmed">
|
||||
billed {interval}
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
<Card.Section mt="lg">
|
||||
<Button onClick={() => handleCheckout(priceId)} fullWidth>
|
||||
Subscribe
|
||||
</Button>
|
||||
</Card.Section>
|
||||
|
||||
<Card.Section mt="md">
|
||||
<List
|
||||
spacing="xs"
|
||||
size="sm"
|
||||
center
|
||||
icon={
|
||||
<ThemeIcon variant="light" size={24} radius="xl">
|
||||
<IconCheck size={16} />
|
||||
</ThemeIcon>
|
||||
}
|
||||
>
|
||||
{plan.features.map((feature, index) => (
|
||||
<List.Item key={index}>{feature}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
);
|
||||
}
|
31
apps/client/src/ee/billing/components/billing-trial.tsx
Normal file
31
apps/client/src/ee/billing/components/billing-trial.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { Alert } from "@mantine/core";
|
||||
import { useBillingQuery } from "@/ee/billing/queries/billing-query.ts";
|
||||
import useTrial from "@/ee/hooks/use-trial.tsx";
|
||||
|
||||
export default function BillingTrial() {
|
||||
const { data: billing, isLoading } = useBillingQuery();
|
||||
const { trialDaysLeft } = useTrial();
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{trialDaysLeft > 0 && !billing && (
|
||||
<Alert title="Your Trial is Active 🎉" color="blue" radius="md">
|
||||
You have {trialDaysLeft} {trialDaysLeft === 1 ? "day" : "days"} left
|
||||
in your 7-day trial. Please subscribe to a plan before your trial
|
||||
ends.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{trialDaysLeft === 0 && (
|
||||
<Alert title="Your Trial has ended" color="red" radius="md">
|
||||
Your 7-day trial has come to an end. Please subscribe to a plan to
|
||||
continue using this service.
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
10
apps/client/src/ee/billing/components/billing.module.css
Normal file
10
apps/client/src/ee/billing/components/billing.module.css
Normal file
@ -0,0 +1,10 @@
|
||||
.root {
|
||||
padding-top: var(--mantine-spacing-xs);
|
||||
padding-bottom: var(--mantine-spacing-xs);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-family:
|
||||
Greycliff CF,
|
||||
var(--mantine-font-family);
|
||||
}
|
34
apps/client/src/ee/billing/components/manage-billing.tsx
Normal file
34
apps/client/src/ee/billing/components/manage-billing.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Button, Group, Text } from "@mantine/core";
|
||||
import React from "react";
|
||||
import { getBillingPortalLink } from "@/ee/billing/services/billing-service.ts";
|
||||
|
||||
export default function ManageBilling() {
|
||||
const handleBillingPortal = async () => {
|
||||
try {
|
||||
const portalLink = await getBillingPortalLink();
|
||||
window.location.href = portalLink.url;
|
||||
} catch (err) {
|
||||
console.error("Failed to get billing portal link", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group justify="space-between" wrap="wrap" gap="xl">
|
||||
<div style={{ flex: 1, minWidth: "200px" }}>
|
||||
<Text size="md" fw={500}>
|
||||
Manage subscription
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Manage your your subscription, invoices, update payment details, and
|
||||
more.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Button style={{ flexShrink: 0 }} onClick={handleBillingPortal}>
|
||||
Manage
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
);
|
||||
}
|
41
apps/client/src/ee/billing/pages/billing.tsx
Normal file
41
apps/client/src/ee/billing/pages/billing.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { getAppName } from "@/lib/config.ts";
|
||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||
import BillingPlans from "@/ee/billing/components/billing-plans.tsx";
|
||||
import BillingTrial from "@/ee/billing/components/billing-trial.tsx";
|
||||
import ManageBilling from "@/ee/billing/components/manage-billing.tsx";
|
||||
import { Divider } from "@mantine/core";
|
||||
import React from "react";
|
||||
import BillingDetails from "@/ee/billing/components/billing-details.tsx";
|
||||
import { useBillingQuery } from "@/ee/billing/queries/billing-query.ts";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
|
||||
export default function Billing() {
|
||||
const { data: billing, isError: isBillingError } = useBillingQuery();
|
||||
const { isAdmin } = useUserRole();
|
||||
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Billing - {getAppName()}</title>
|
||||
</Helmet>
|
||||
<SettingsTitle title="Billing" />
|
||||
|
||||
<BillingTrial />
|
||||
<BillingDetails />
|
||||
|
||||
{isBillingError && <BillingPlans />}
|
||||
|
||||
{billing && (
|
||||
<>
|
||||
<Divider my="lg" />
|
||||
<ManageBilling />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
20
apps/client/src/ee/billing/queries/billing-query.ts
Normal file
20
apps/client/src/ee/billing/queries/billing-query.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { useQuery, UseQueryResult } from "@tanstack/react-query";
|
||||
import {
|
||||
getBilling,
|
||||
getBillingPlans,
|
||||
} from "@/ee/billing/services/billing-service.ts";
|
||||
import { IBilling, IBillingPlan } from "@/ee/billing/types/billing.types.ts";
|
||||
|
||||
export function useBillingQuery(): UseQueryResult<IBilling, Error> {
|
||||
return useQuery({
|
||||
queryKey: ["billing"],
|
||||
queryFn: () => getBilling(),
|
||||
});
|
||||
}
|
||||
|
||||
export function useBillingPlans(): UseQueryResult<IBillingPlan[], Error> {
|
||||
return useQuery({
|
||||
queryKey: ["billing-plans"],
|
||||
queryFn: () => getBillingPlans(),
|
||||
});
|
||||
}
|
29
apps/client/src/ee/billing/services/billing-service.ts
Normal file
29
apps/client/src/ee/billing/services/billing-service.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import api from "@/lib/api-client.ts";
|
||||
import {
|
||||
IBilling,
|
||||
IBillingPlan,
|
||||
IBillingPortal,
|
||||
ICheckoutLink,
|
||||
} from "@/ee/billing/types/billing.types.ts";
|
||||
|
||||
export async function getBilling(): Promise<IBilling> {
|
||||
const req = await api.post<IBilling>("/billing/info");
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function getBillingPlans(): Promise<IBillingPlan[]> {
|
||||
const req = await api.post<IBillingPlan[]>("/billing/plans");
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function getCheckoutLink(data: {
|
||||
priceId: string;
|
||||
}): Promise<ICheckoutLink> {
|
||||
const req = await api.post<ICheckoutLink>("/billing/checkout", data);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function getBillingPortalLink(): Promise<IBillingPortal> {
|
||||
const req = await api.post<IBillingPortal>("/billing/portal");
|
||||
return req.data;
|
||||
}
|
49
apps/client/src/ee/billing/types/billing.types.ts
Normal file
49
apps/client/src/ee/billing/types/billing.types.ts
Normal file
@ -0,0 +1,49 @@
|
||||
export enum BillingPlan {
|
||||
STANDARD = "standard",
|
||||
}
|
||||
|
||||
export interface IBilling {
|
||||
id: string;
|
||||
stripeSubscriptionId: string;
|
||||
stripeCustomerId: string;
|
||||
status: string;
|
||||
quantity: number;
|
||||
amount: number;
|
||||
interval: string;
|
||||
currency: string;
|
||||
metadata: Record<string, any>;
|
||||
stripePriceId: string;
|
||||
stripeItemId: string;
|
||||
stripeProductId: string;
|
||||
periodStartAt: Date;
|
||||
periodEndAt: Date;
|
||||
cancelAtPeriodEnd: boolean;
|
||||
cancelAt: Date;
|
||||
canceledAt: Date;
|
||||
workspaceId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deletedAt: Date;
|
||||
}
|
||||
|
||||
export interface ICheckoutLink {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface IBillingPortal {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface IBillingPlan {
|
||||
name: string;
|
||||
description: string;
|
||||
productId: string;
|
||||
monthlyId: string;
|
||||
yearlyId: string;
|
||||
currency: string;
|
||||
price: {
|
||||
monthly: string;
|
||||
yearly: string;
|
||||
};
|
||||
features: string[];
|
||||
}
|
17
apps/client/src/ee/billing/utils.ts
Normal file
17
apps/client/src/ee/billing/utils.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { differenceInCalendarDays } from "date-fns";
|
||||
|
||||
export function formatInterval(interval: string): string {
|
||||
if (interval === "month") {
|
||||
return "monthly";
|
||||
}
|
||||
if (interval === "year") {
|
||||
return "yearly";
|
||||
}
|
||||
}
|
||||
|
||||
export function getTrialDaysLeft(trialEndAt: Date) {
|
||||
if (!trialEndAt) return null;
|
||||
|
||||
const daysLeft = differenceInCalendarDays(trialEndAt, new Date());
|
||||
return daysLeft > 0 ? daysLeft : 0;
|
||||
}
|
13
apps/client/src/ee/cloud/query/cloud-query.ts
Normal file
13
apps/client/src/ee/cloud/query/cloud-query.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { useQuery, UseQueryResult } from "@tanstack/react-query";
|
||||
import { IWorkspace } from "@/features/workspace/types/workspace.types.ts";
|
||||
import { getJoinedWorkspaces } from "@/ee/cloud/service/cloud-service.ts";
|
||||
|
||||
export function useJoinedWorkspacesQuery(): UseQueryResult<
|
||||
Partial<IWorkspace[]>,
|
||||
Error
|
||||
> {
|
||||
return useQuery({
|
||||
queryKey: ["joined-workspaces"],
|
||||
queryFn: () => getJoinedWorkspaces(),
|
||||
});
|
||||
}
|
7
apps/client/src/ee/cloud/service/cloud-service.ts
Normal file
7
apps/client/src/ee/cloud/service/cloud-service.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { IWorkspace } from "@/features/workspace/types/workspace.types.ts";
|
||||
import api from "@/lib/api-client.ts";
|
||||
|
||||
export async function getJoinedWorkspaces(): Promise<Partial<IWorkspace[]>> {
|
||||
const req = await api.post<Partial<IWorkspace[]>>("/workspace/joined");
|
||||
return req.data;
|
||||
}
|
96
apps/client/src/ee/components/cloud-login-form.tsx
Normal file
96
apps/client/src/ee/components/cloud-login-form.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import * as z from "zod";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import {
|
||||
Container,
|
||||
Title,
|
||||
TextInput,
|
||||
Button,
|
||||
Box,
|
||||
Text,
|
||||
Anchor,
|
||||
Divider,
|
||||
} from "@mantine/core";
|
||||
import classes from "../../features/auth/components/auth.module.css";
|
||||
import { getCheckHostname } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { useState } from "react";
|
||||
import { getSubdomainHost } from "@/lib/config.ts";
|
||||
import { Link } from "react-router-dom";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import JoinedWorkspaces from "@/ee/components/joined-workspaces.tsx";
|
||||
import { useJoinedWorkspacesQuery } from "@/ee/cloud/query/cloud-query.ts";
|
||||
|
||||
const formSchema = z.object({
|
||||
hostname: z.string().min(1, { message: "subdomain is required" }),
|
||||
});
|
||||
|
||||
export function CloudLoginForm() {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const { data: joinedWorkspaces } = useJoinedWorkspacesQuery();
|
||||
|
||||
const form = useForm<any>({
|
||||
validate: zodResolver(formSchema),
|
||||
initialValues: {
|
||||
hostname: "",
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(data: { hostname: string }) {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const checkHostname = await getCheckHostname(data.hostname);
|
||||
window.location.href = checkHostname.hostname;
|
||||
} catch (err) {
|
||||
if (err?.status === 404) {
|
||||
form.setFieldError("hostname", "We could not find this workspace");
|
||||
} else {
|
||||
form.setFieldError("hostname", "An error occurred");
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Container size={420} className={classes.container}>
|
||||
<Box p="xl" className={classes.containerBox}>
|
||||
<Title order={2} ta="center" fw={500} mb="md">
|
||||
{t("Login")}
|
||||
</Title>
|
||||
|
||||
<JoinedWorkspaces />
|
||||
|
||||
{joinedWorkspaces?.length > 0 && (
|
||||
<Divider my="xs" label="OR" labelPosition="center" />
|
||||
)}
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<TextInput
|
||||
type="text"
|
||||
placeholder="my-team"
|
||||
description="Enter your workspace hostname"
|
||||
label="Workspace hostname"
|
||||
rightSection={<Text fw={500}>.{getSubdomainHost()}</Text>}
|
||||
rightSectionWidth={150}
|
||||
withErrorStyles={false}
|
||||
{...form.getInputProps("hostname")}
|
||||
/>
|
||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||
{t("Continue")}
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
<Text ta="center">
|
||||
{t("Don't have a workspace?")}{" "}
|
||||
<Anchor component={Link} to={APP_ROUTE.AUTH.CREATE_WORKSPACE} fw={500}>
|
||||
{t("Create new workspace")}
|
||||
</Anchor>
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
13
apps/client/src/ee/components/joined-workspaces.module.css
Normal file
13
apps/client/src/ee/components/joined-workspaces.module.css
Normal file
@ -0,0 +1,13 @@
|
||||
.workspace {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-xs);
|
||||
margin-bottom: var(--mantine-spacing-xs);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
border-radius: var(--mantine-spacing-xs);
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
|
||||
}
|
||||
}
|
51
apps/client/src/ee/components/joined-workspaces.tsx
Normal file
51
apps/client/src/ee/components/joined-workspaces.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { Group, Text, UnstyledButton } from "@mantine/core";
|
||||
import { useJoinedWorkspacesQuery } from "../cloud/query/cloud-query";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import classes from "./joined-workspaces.module.css";
|
||||
import { IconChevronRight } from "@tabler/icons-react";
|
||||
import { getHostnameUrl } from "@/ee/utils.ts";
|
||||
import { Link } from "react-router-dom";
|
||||
import { IWorkspace } from "@/features/workspace/types/workspace.types.ts";
|
||||
|
||||
export default function JoinedWorkspaces() {
|
||||
const { data, isLoading } = useJoinedWorkspacesQuery();
|
||||
if (isLoading || !data || data?.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{data
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((workspace: Partial<IWorkspace>, index) => (
|
||||
<UnstyledButton
|
||||
key={index}
|
||||
component={Link}
|
||||
to={getHostnameUrl(workspace?.hostname) + "/home"}
|
||||
className={classes.workspace}
|
||||
>
|
||||
<Group wrap="nowrap">
|
||||
<CustomAvatar
|
||||
avatarUrl={workspace?.logo}
|
||||
name={workspace?.name}
|
||||
variant="filled"
|
||||
size="md"
|
||||
/>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="sm" fw={500} lineClamp={1}>
|
||||
{workspace?.name}
|
||||
</Text>
|
||||
|
||||
<Text c="dimmed" size="sm">
|
||||
{getHostnameUrl(workspace?.hostname)?.split("//")[1]}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<IconChevronRight size={16} />
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
119
apps/client/src/ee/components/manage-hostname.tsx
Normal file
119
apps/client/src/ee/components/manage-hostname.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import { Button, Group, Text, Modal, TextInput } from "@mantine/core";
|
||||
import * as z from "zod";
|
||||
import { useState } from "react";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import * as React from "react";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getSubdomainHost } from "@/lib/config.ts";
|
||||
import { IWorkspace } from "@/features/workspace/types/workspace.types.ts";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { getHostnameUrl } from "@/ee/utils.ts";
|
||||
import { useAtom } from "jotai/index";
|
||||
import {
|
||||
currentUserAtom,
|
||||
workspaceAtom,
|
||||
} from "@/features/user/atoms/current-user-atom.ts";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { RESET } from "jotai/utils";
|
||||
|
||||
export default function ManageHostname() {
|
||||
const { t } = useTranslation();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [workspace] = useAtom(workspaceAtom);
|
||||
const { isAdmin } = useUserRole();
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">{t("Hostname")}</Text>
|
||||
<Text size="sm" c="dimmed" fw={500}>
|
||||
{workspace?.hostname}.{getSubdomainHost()}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{isAdmin && (
|
||||
<Button onClick={open} variant="default">
|
||||
{t("Change hostname")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={t("Change hostname")}
|
||||
centered
|
||||
>
|
||||
<ChangeHostnameForm onClose={close} />
|
||||
</Modal>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
hostname: z.string().min(4),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
interface ChangeHostnameFormProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
function ChangeHostnameForm({ onClose }: ChangeHostnameFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentUser, setCurrentUser] = useAtom(currentUserAtom);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
validate: zodResolver(formSchema),
|
||||
initialValues: {
|
||||
hostname: currentUser?.workspace?.hostname,
|
||||
},
|
||||
});
|
||||
|
||||
async function handleSubmit(data: Partial<IWorkspace>) {
|
||||
setIsLoading(true);
|
||||
|
||||
if (data.hostname === currentUser?.workspace?.hostname) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateWorkspace({
|
||||
hostname: data.hostname,
|
||||
});
|
||||
setCurrentUser(RESET);
|
||||
window.location.href = getHostnameUrl(data.hostname.toLowerCase());
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
message: err?.response?.data?.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<TextInput
|
||||
type="text"
|
||||
placeholder="e.g my-team"
|
||||
label="Hostname"
|
||||
variant="filled"
|
||||
rightSection={<Text fw={500}>.{getSubdomainHost()}</Text>}
|
||||
rightSectionWidth={150}
|
||||
withErrorStyles={false}
|
||||
width={200}
|
||||
{...form.getInputProps("hostname")}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button type="submit" disabled={isLoading} loading={isLoading}>
|
||||
{t("Change hostname")}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
);
|
||||
}
|
25
apps/client/src/ee/components/sso-cloud-signup.tsx
Normal file
25
apps/client/src/ee/components/sso-cloud-signup.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { Button, Divider, Stack } from "@mantine/core";
|
||||
import { getGoogleSignupUrl } from "@/ee/security/sso.utils.ts";
|
||||
import { GoogleIcon } from "@/components/icons/google-icon.tsx";
|
||||
|
||||
export default function SsoCloudSignup() {
|
||||
const handleSsoLogin = () => {
|
||||
window.location.href = getGoogleSignupUrl();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack align="stretch" justify="center" gap="sm">
|
||||
<Button
|
||||
onClick={handleSsoLogin}
|
||||
leftSection={<GoogleIcon size={16} />}
|
||||
variant="default"
|
||||
fullWidth
|
||||
>
|
||||
Signup with Google
|
||||
</Button>
|
||||
</Stack>
|
||||
<Divider my="xs" label="OR" labelPosition="center" />
|
||||
</>
|
||||
);
|
||||
}
|
57
apps/client/src/ee/components/sso-login.tsx
Normal file
57
apps/client/src/ee/components/sso-login.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { useWorkspacePublicDataQuery } from "@/features/workspace/queries/workspace-query.ts";
|
||||
import { Button, Divider, Stack } from "@mantine/core";
|
||||
import { IconLock } from "@tabler/icons-react";
|
||||
import { IAuthProvider } from "@/ee/security/types/security.types.ts";
|
||||
import { buildSsoLoginUrl } from "@/ee/security/sso.utils.ts";
|
||||
import { SSO_PROVIDER } from "@/ee/security/contants.ts";
|
||||
import { GoogleIcon } from "@/components/icons/google-icon.tsx";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
|
||||
export default function SsoLogin() {
|
||||
const { data, isLoading } = useWorkspacePublicDataQuery();
|
||||
|
||||
if (!data?.authProviders || data?.authProviders?.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSsoLogin = (provider: IAuthProvider) => {
|
||||
window.location.href = buildSsoLoginUrl({
|
||||
providerId: provider.id,
|
||||
type: provider.type,
|
||||
workspaceId: data.id,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isCloud() || data.hasLicenseKey) && (
|
||||
<>
|
||||
<Stack align="stretch" justify="center" gap="sm">
|
||||
{data.authProviders.map((provider) => (
|
||||
<div key={provider.id}>
|
||||
<Button
|
||||
onClick={() => handleSsoLogin(provider)}
|
||||
leftSection={
|
||||
provider.type === SSO_PROVIDER.GOOGLE ? (
|
||||
<GoogleIcon size={16} />
|
||||
) : (
|
||||
<IconLock size={16} />
|
||||
)
|
||||
}
|
||||
variant="default"
|
||||
fullWidth
|
||||
>
|
||||
{provider.name}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{!data.enforceSso && (
|
||||
<Divider my="xs" label="OR" labelPosition="center" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
9
apps/client/src/ee/hooks/use-license.tsx
Normal file
9
apps/client/src/ee/hooks/use-license.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
|
||||
export const useLicense = () => {
|
||||
const [currentUser] = useAtom(currentUserAtom);
|
||||
return { hasLicenseKey: currentUser?.workspace?.hasLicenseKey };
|
||||
};
|
||||
|
||||
export default useLicense;
|
15
apps/client/src/ee/hooks/use-plan.tsx
Normal file
15
apps/client/src/ee/hooks/use-plan.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { BillingPlan } from "@/ee/billing/types/billing.types.ts";
|
||||
|
||||
export const usePlan = () => {
|
||||
const [workspace] = useAtom(workspaceAtom);
|
||||
|
||||
const isStandard =
|
||||
typeof workspace?.plan === "string" &&
|
||||
workspace?.plan.toLowerCase() === BillingPlan.STANDARD.toLowerCase();
|
||||
|
||||
return { isStandard };
|
||||
};
|
||||
|
||||
export default usePlan;
|
20
apps/client/src/ee/hooks/use-redirect-to-cloud-select.tsx
Normal file
20
apps/client/src/ee/hooks/use-redirect-to-cloud-select.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { useEffect } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { getAppUrl, getServerAppUrl, isCloud } from "@/lib/config.ts";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
|
||||
export const useRedirectToCloudSelect = () => {
|
||||
const navigate = useNavigate();
|
||||
const pathname = useLocation().pathname;
|
||||
|
||||
useEffect(() => {
|
||||
const pathsToRedirect = ["/login", "/home"];
|
||||
if (isCloud() && pathsToRedirect.includes(pathname)) {
|
||||
const frontendUrl = getAppUrl();
|
||||
const serverUrl = getServerAppUrl();
|
||||
if (frontendUrl === serverUrl) {
|
||||
navigate(APP_ROUTE.AUTH.SELECT_WORKSPACE);
|
||||
}
|
||||
}
|
||||
}, [navigate]);
|
||||
};
|
36
apps/client/src/ee/hooks/use-trial-end-action.tsx
Normal file
36
apps/client/src/ee/hooks/use-trial-end-action.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { useEffect } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import useTrial from "@/ee/hooks/use-trial.tsx";
|
||||
|
||||
export const useTrialEndAction = () => {
|
||||
const navigate = useNavigate();
|
||||
const pathname = useLocation().pathname;
|
||||
const { isAdmin } = useUserRole();
|
||||
const { trialDaysLeft } = useTrial();
|
||||
|
||||
useEffect(() => {
|
||||
if (isCloud() && trialDaysLeft === 0) {
|
||||
if (!pathname.startsWith("/settings")) {
|
||||
notifications.show({
|
||||
position: "top-right",
|
||||
color: "red",
|
||||
title: "Your 7-day trial has ended",
|
||||
message:
|
||||
"Please upgrade to a paid plan or contact your workspace admin.",
|
||||
autoClose: false,
|
||||
});
|
||||
|
||||
// only admins can access the billing page
|
||||
if (isAdmin) {
|
||||
navigate(APP_ROUTE.SETTINGS.WORKSPACE.BILLING);
|
||||
} else {
|
||||
navigate(APP_ROUTE.SETTINGS.ACCOUNT.PROFILE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [navigate]);
|
||||
};
|
16
apps/client/src/ee/hooks/use-trial.tsx
Normal file
16
apps/client/src/ee/hooks/use-trial.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import { getTrialDaysLeft } from "@/ee/billing/utils.ts";
|
||||
import { ICurrentUser } from "@/features/user/types/user.types.ts";
|
||||
|
||||
export const useTrial = () => {
|
||||
const [currentUser] = useAtom<ICurrentUser>(currentUserAtom);
|
||||
const workspace = currentUser?.workspace;
|
||||
|
||||
const trialDaysLeft = getTrialDaysLeft(workspace?.trialEndAt);
|
||||
const isTrial = !!workspace?.trialEndAt && trialDaysLeft !== null;
|
||||
|
||||
return { isTrial: isTrial, trialDaysLeft: trialDaysLeft };
|
||||
};
|
||||
|
||||
export default useTrial;
|
@ -0,0 +1,89 @@
|
||||
import * as z from "zod";
|
||||
import React from "react";
|
||||
import { Button, Group, Modal, Textarea } from "@mantine/core";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useActivateMutation } from "@/ee/licence/queries/license-query.ts";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import RemoveLicense from "@/ee/licence/components/remove-license.tsx";
|
||||
|
||||
export default function ActivateLicense() {
|
||||
const { t } = useTranslation();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [workspace] = useAtom(workspaceAtom);
|
||||
|
||||
return (
|
||||
<Group justify="flex-end" wrap="nowrap" mb="sm">
|
||||
<Button onClick={open}>
|
||||
{workspace?.hasLicenseKey ? t("Update license") : t("Add license")}
|
||||
</Button>
|
||||
|
||||
{workspace?.hasLicenseKey && <RemoveLicense />}
|
||||
|
||||
<Modal
|
||||
size="550"
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={t("Enterprise license")}
|
||||
centered
|
||||
>
|
||||
<ActivateLicenseForm onClose={close} />
|
||||
</Modal>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
licenseKey: z.string().min(1),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
interface ActivateLicenseFormProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
export function ActivateLicenseForm({ onClose }: ActivateLicenseFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const activateLicenseMutation = useActivateMutation();
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
validate: zodResolver(formSchema),
|
||||
initialValues: {
|
||||
licenseKey: "",
|
||||
},
|
||||
});
|
||||
|
||||
async function handleSubmit(data: { licenseKey: string }) {
|
||||
await activateLicenseMutation.mutateAsync(data.licenseKey);
|
||||
form.reset();
|
||||
onClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Textarea
|
||||
label={t("License key")}
|
||||
description="Enter a valid enterprise license key. Contact sales@docmost.com to purchase one."
|
||||
placeholder={t("e.g eyJhb.....")}
|
||||
variant="filled"
|
||||
autosize
|
||||
minRows={3}
|
||||
maxRows={5}
|
||||
data-autofocus
|
||||
{...form.getInputProps("licenseKey")}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={activateLicenseMutation.isPending}
|
||||
loading={activateLicenseMutation.isPending}
|
||||
>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
);
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import React from "react";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import classes from "@/ee/billing/components/billing.module.css";
|
||||
import {
|
||||
Group,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import CopyTextButton from "@/components/common/copy.tsx";
|
||||
|
||||
export default function InstallationDetails() {
|
||||
const { isAdmin } = useUserRole();
|
||||
const [workspace] = useAtom(workspaceAtom);
|
||||
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SimpleGrid cols={{ base: 1, xs: 2, sm: 2 }}>
|
||||
<Paper p="sm" radius="md" withBorder={true}>
|
||||
<Group justify="apart" grow>
|
||||
<div>
|
||||
<Text
|
||||
c="dimmed"
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
fz="xs"
|
||||
className={classes.label}
|
||||
>
|
||||
Workspace ID
|
||||
</Text>
|
||||
<TextInput
|
||||
style={{ fontWeight: 700 }}
|
||||
variant="unstyled"
|
||||
readOnly
|
||||
value={workspace?.id}
|
||||
pointer
|
||||
rightSection={<CopyTextButton text={workspace?.id} />}
|
||||
/>
|
||||
</div>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Paper p="md" radius="md" withBorder={true}>
|
||||
<Group justify="apart">
|
||||
<div>
|
||||
<Text
|
||||
c="dimmed"
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
fz="xs"
|
||||
className={classes.label}
|
||||
>
|
||||
Member count
|
||||
</Text>
|
||||
<Text fw={700} fz="lg" tt="capitalize">
|
||||
{workspace?.memberCount}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
</>
|
||||
);
|
||||
}
|
81
apps/client/src/ee/licence/components/license-details.tsx
Normal file
81
apps/client/src/ee/licence/components/license-details.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { Badge, Table } from "@mantine/core";
|
||||
import { format } from "date-fns";
|
||||
import { useLicenseInfo } from "@/ee/licence/queries/license-query.ts";
|
||||
import { isLicenseExpired } from "@/ee/licence/license.utils.ts";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
|
||||
export default function LicenseDetails() {
|
||||
const { data: license, isError } = useLicenseInfo();
|
||||
const [workspace] = useAtom(workspaceAtom);
|
||||
|
||||
if (!license) {
|
||||
return null;
|
||||
}
|
||||
if (isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table.ScrollContainer minWidth={500} py="md">
|
||||
<Table
|
||||
variant="vertical"
|
||||
verticalSpacing="sm"
|
||||
layout="fixed"
|
||||
withTableBorder
|
||||
>
|
||||
<Table.Caption>
|
||||
Contact sales@docmost.com for support and enquiries.
|
||||
</Table.Caption>
|
||||
<Table.Tbody>
|
||||
<Table.Tr>
|
||||
<Table.Th w={160}>Edition</Table.Th>
|
||||
<Table.Td>
|
||||
Enterprise {license.trial && <Badge color="green">Trial</Badge>}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
<Table.Tr>
|
||||
<Table.Th>Licensed to</Table.Th>
|
||||
<Table.Td>{license.customerName}</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
<Table.Tr>
|
||||
<Table.Th>Seat count</Table.Th>
|
||||
<Table.Td>
|
||||
{license.seatCount} ({workspace?.memberCount} used)
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
<Table.Tr>
|
||||
<Table.Th>Issued at</Table.Th>
|
||||
<Table.Td>{format(license.issuedAt, "dd MMMM, yyyy")}</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
<Table.Tr>
|
||||
<Table.Th>Expires at</Table.Th>
|
||||
<Table.Td>{format(license.expiresAt, "dd MMMM, yyyy")}</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Th>License ID</Table.Th>
|
||||
<Table.Td>{license.id}</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Td>
|
||||
{isLicenseExpired(license) ? (
|
||||
<Badge color="red" variant="light">
|
||||
Expired
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge color="blue" variant="light">
|
||||
Valid
|
||||
</Badge>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
);
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export default function LicenseMessage() {
|
||||
return <>To unlock enterprise features, please contact sales@docmost.com to purchase a license.</>;
|
||||
}
|
39
apps/client/src/ee/licence/components/oss-details.tsx
Normal file
39
apps/client/src/ee/licence/components/oss-details.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { Group, Table, ThemeIcon } from "@mantine/core";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
|
||||
export default function OssDetails() {
|
||||
return (
|
||||
<Table.ScrollContainer minWidth={500} py="md">
|
||||
<Table
|
||||
variant="vertical"
|
||||
verticalSpacing="sm"
|
||||
layout="fixed"
|
||||
withTableBorder
|
||||
>
|
||||
<Table.Caption>
|
||||
To unlock enterprise features like SSO, contact sales@docmost.com.
|
||||
</Table.Caption>
|
||||
<Table.Tbody>
|
||||
<Table.Tr>
|
||||
<Table.Th w={160}>Edition</Table.Th>
|
||||
<Table.Td>
|
||||
<Group wrap="nowrap">
|
||||
Open Source
|
||||
<div>
|
||||
<ThemeIcon
|
||||
color="green"
|
||||
variant="light"
|
||||
size={24}
|
||||
radius="xl"
|
||||
>
|
||||
<IconCheck size={16} />
|
||||
</ThemeIcon>
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
);
|
||||
}
|
33
apps/client/src/ee/licence/components/remove-license.tsx
Normal file
33
apps/client/src/ee/licence/components/remove-license.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRemoveLicenseMutation } from "@/ee/licence/queries/license-query.ts";
|
||||
import { Button, Group, Text } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import React from "react";
|
||||
|
||||
export default function RemoveLicense() {
|
||||
const { t } = useTranslation();
|
||||
const removeLicenseMutation = useRemoveLicenseMutation();
|
||||
|
||||
const openDeleteModal = () =>
|
||||
modals.openConfirmModal({
|
||||
title: t("Remove license key"),
|
||||
centered: true,
|
||||
children: (
|
||||
<Text size="sm">
|
||||
{t(
|
||||
"Are you sure you want to remove your license key? Your workspace will be downgraded to the non-enterprise version.",
|
||||
)}
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: t("Remove"), cancel: t("Don't") },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => removeLicenseMutation.mutate(),
|
||||
});
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Button variant="light" color="red" onClick={openDeleteModal}>Remove license</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
26
apps/client/src/ee/licence/license.utils.ts
Normal file
26
apps/client/src/ee/licence/license.utils.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { ILicenseInfo } from "@/ee/licence/types/license.types.ts";
|
||||
import { differenceInDays, isAfter } from "date-fns";
|
||||
|
||||
export const GRACE_PERIOD_DAYS = 10;
|
||||
|
||||
export function isLicenseExpired(license: ILicenseInfo): boolean {
|
||||
return isAfter(new Date(), license.expiresAt);
|
||||
}
|
||||
|
||||
export function daysToExpire(license: ILicenseInfo): number {
|
||||
const days = differenceInDays(license.expiresAt, new Date());
|
||||
return days > 0 ? days : 0;
|
||||
}
|
||||
|
||||
export function isTrial(license: ILicenseInfo): boolean {
|
||||
return license.trial;
|
||||
}
|
||||
|
||||
export function isValid(license: ILicenseInfo): boolean {
|
||||
return !isLicenseExpired(license);
|
||||
}
|
||||
|
||||
export function hasExpiredGracePeriod(license: ILicenseInfo): boolean {
|
||||
if (!isLicenseExpired(license)) return false;
|
||||
return differenceInDays(new Date(), license.expiresAt) > GRACE_PERIOD_DAYS;
|
||||
}
|
35
apps/client/src/ee/licence/pages/license.tsx
Normal file
35
apps/client/src/ee/licence/pages/license.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { getAppName } from "@/lib/config.ts";
|
||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||
import React from "react";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import LicenseDetails from "@/ee/licence/components/license-details.tsx";
|
||||
import ActivateLicenseForm from "@/ee/licence/components/activate-license-modal.tsx";
|
||||
import InstallationDetails from "@/ee/licence/components/installation-details.tsx";
|
||||
import OssDetails from "@/ee/licence/components/oss-details.tsx";
|
||||
import { useAtom } from "jotai/index";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
|
||||
export default function License() {
|
||||
const [workspace] = useAtom(workspaceAtom);
|
||||
const { isAdmin } = useUserRole();
|
||||
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>License - {getAppName()}</title>
|
||||
</Helmet>
|
||||
<SettingsTitle title="License" />
|
||||
|
||||
<ActivateLicenseForm />
|
||||
|
||||
<InstallationDetails />
|
||||
|
||||
{workspace?.hasLicenseKey ? <LicenseDetails /> : <OssDetails />}
|
||||
</>
|
||||
);
|
||||
}
|
52
apps/client/src/ee/licence/queries/license-query.ts
Normal file
52
apps/client/src/ee/licence/queries/license-query.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
UseQueryResult,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
activateLicense,
|
||||
removeLicense,
|
||||
getLicenseInfo,
|
||||
} from "@/ee/licence/services/license-service.ts";
|
||||
import { ILicenseInfo } from "@/ee/licence/types/license.types.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
export function useLicenseInfo(): UseQueryResult<ILicenseInfo, Error> {
|
||||
return useQuery({
|
||||
queryKey: ["license"],
|
||||
queryFn: () => getLicenseInfo(),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useActivateMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<ILicenseInfo, Error, string>({
|
||||
mutationFn: (licenseKey) => activateLicense(licenseKey),
|
||||
onSuccess: () => {
|
||||
notifications.show({ message: "License activated successfully" });
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ["license"],
|
||||
});
|
||||
queryClient.refetchQueries({ queryKey: ["currentUser"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error["response"]?.data?.message;
|
||||
notifications.show({ message: errorMessage, color: "red" });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRemoveLicenseMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => removeLicense(),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["license"] });
|
||||
queryClient.refetchQueries({ queryKey: ["currentUser"] });
|
||||
},
|
||||
});
|
||||
}
|
18
apps/client/src/ee/licence/services/license-service.ts
Normal file
18
apps/client/src/ee/licence/services/license-service.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import api from "@/lib/api-client.ts";
|
||||
import { ILicenseInfo } from "@/ee/licence/types/license.types.ts";
|
||||
|
||||
export async function getLicenseInfo(): Promise<ILicenseInfo> {
|
||||
const req = await api.post<ILicenseInfo>("/license/info");
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function activateLicense(
|
||||
licenseKey: string,
|
||||
): Promise<ILicenseInfo> {
|
||||
const req = await api.post<ILicenseInfo>("/license/activate", { licenseKey });
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function removeLicense(): Promise<void> {
|
||||
await api.post<void>("/license/remove");
|
||||
}
|
8
apps/client/src/ee/licence/types/license.types.ts
Normal file
8
apps/client/src/ee/licence/types/license.types.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface ILicenseInfo {
|
||||
id: string;
|
||||
customerName: string;
|
||||
seatCount: number;
|
||||
issuedAt: Date;
|
||||
expiresAt: Date;
|
||||
trial: boolean;
|
||||
}
|
20
apps/client/src/ee/pages/cloud-login.tsx
Normal file
20
apps/client/src/ee/pages/cloud-login.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { getAppName } from "@/lib/config.ts";
|
||||
import { CloudLoginForm } from "@/ee/components/cloud-login-form.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function CloudLogin() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>
|
||||
{t("Login")} - {getAppName()}
|
||||
</title>
|
||||
</Helmet>
|
||||
|
||||
<CloudLoginForm />
|
||||
</>
|
||||
);
|
||||
}
|
15
apps/client/src/ee/pages/create-workspace.tsx
Normal file
15
apps/client/src/ee/pages/create-workspace.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { SetupWorkspaceForm } from "@/features/auth/components/setup-workspace-form.tsx";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import React from "react";
|
||||
import { getAppName } from "@/lib/config.ts";
|
||||
|
||||
export default function CreateWorkspace() {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Create Workspace - {getAppName()}</title>
|
||||
</Helmet>
|
||||
<SetupWorkspaceForm />
|
||||
</>
|
||||
);
|
||||
}
|
88
apps/client/src/ee/security/components/allowed-domains.tsx
Normal file
88
apps/client/src/ee/security/components/allowed-domains.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { useAtom } from "jotai";
|
||||
import * as z from "zod";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import React, { useState } from "react";
|
||||
import { Button, Text, TagsInput } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { IWorkspace } from "@/features/workspace/types/workspace.types.ts";
|
||||
|
||||
const formSchema = z.object({
|
||||
emailDomains: z.array(z.string()),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
export default function AllowedDomains() {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const [, setDomains] = useState<string[]>([]);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
validate: zodResolver(formSchema),
|
||||
initialValues: {
|
||||
emailDomains: workspace?.emailDomains || [],
|
||||
},
|
||||
});
|
||||
|
||||
async function handleSubmit(data: Partial<IWorkspace>) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const updatedWorkspace = await updateWorkspace({
|
||||
emailDomains: data.emailDomains,
|
||||
});
|
||||
setWorkspace(updatedWorkspace);
|
||||
|
||||
notifications.show({
|
||||
message: t("Updated successfully"),
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
notifications.show({
|
||||
message: err.response.data.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
|
||||
form.resetDirty();
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Text size="md">Allowed email domains</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Only users with email addresses from these domains can signup via SSO.
|
||||
</Text>
|
||||
</div>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<TagsInput
|
||||
mt="sm"
|
||||
description={t(
|
||||
"Enter valid domain names separated by comma or space",
|
||||
)}
|
||||
placeholder={t("e.g acme.com")}
|
||||
variant="filled"
|
||||
splitChars={[",", " "]}
|
||||
maxDropdownHeight={0}
|
||||
maxTags={20}
|
||||
onChange={setDomains}
|
||||
{...form.getInputProps("emailDomains")}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
mt="sm"
|
||||
disabled={!form.isDirty()}
|
||||
loading={isLoading}
|
||||
>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import React, { useState } from "react";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { Button, Menu, Group } from "@mantine/core";
|
||||
import { IconChevronDown, IconLock } from "@tabler/icons-react";
|
||||
import { useCreateSsoProviderMutation } from "@/ee/security/queries/security-query.ts";
|
||||
import { SSO_PROVIDER } from "@/ee/security/contants.ts";
|
||||
import { IAuthProvider } from "@/ee/security/types/security.types.ts";
|
||||
import SsoProviderModal from "@/ee/security/components/sso-provider-modal.tsx";
|
||||
import { OpenIdIcon } from "@/components/icons/openid-icon.tsx";
|
||||
|
||||
export default function CreateSsoProvider() {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [provider, setProvider] = useState<IAuthProvider | null>(null);
|
||||
|
||||
const createSsoProviderMutation = useCreateSsoProviderMutation();
|
||||
|
||||
const handleCreateSAML = async () => {
|
||||
try {
|
||||
const newProvider = await createSsoProviderMutation.mutateAsync({
|
||||
type: SSO_PROVIDER.SAML,
|
||||
name: "SAML",
|
||||
});
|
||||
setProvider(newProvider);
|
||||
open();
|
||||
} catch (error) {
|
||||
console.error("Failed to create SAML provider", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateOIDC = async () => {
|
||||
try {
|
||||
const newProvider = await createSsoProviderMutation.mutateAsync({
|
||||
type: SSO_PROVIDER.OIDC,
|
||||
name: "OIDC",
|
||||
});
|
||||
setProvider(newProvider);
|
||||
open();
|
||||
} catch (error) {
|
||||
console.error("Failed to create OIDC provider", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SsoProviderModal opened={opened} onClose={close} provider={provider} />
|
||||
|
||||
<Group justify="flex-end">
|
||||
<Menu
|
||||
transitionProps={{ transition: "pop-top-right" }}
|
||||
position="bottom"
|
||||
width={220}
|
||||
withinPortal
|
||||
>
|
||||
<Menu.Target>
|
||||
<Button rightSection={<IconChevronDown size={16} />} pr={12}>
|
||||
Create SSO
|
||||
</Button>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={handleCreateSAML}
|
||||
leftSection={<IconLock size={16} />}
|
||||
>
|
||||
SAML
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
onClick={handleCreateOIDC}
|
||||
leftSection={<OpenIdIcon size={16} />}
|
||||
>
|
||||
OpenID (OIDC)
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</>
|
||||
);
|
||||
}
|
61
apps/client/src/ee/security/components/enforce-sso.tsx
Normal file
61
apps/client/src/ee/security/components/enforce-sso.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { Group, Text, Switch, MantineSize } from "@mantine/core";
|
||||
import { useAtom } from "jotai";
|
||||
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { updateWorkspace } from "@/features/workspace/services/workspace-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
export default function EnforceSso() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap" gap="xl">
|
||||
<div>
|
||||
<Text size="md">{t("Enforce SSO")}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t(
|
||||
"Once enforced, members will not able able to login with email and password.",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<EnforceSsoToggle />
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
interface EnforceSsoToggleProps {
|
||||
size?: MantineSize;
|
||||
label?: string;
|
||||
}
|
||||
export function EnforceSsoToggle({ size, label }: EnforceSsoToggleProps) {
|
||||
const { t } = useTranslation();
|
||||
const [workspace, setWorkspace] = useAtom(workspaceAtom);
|
||||
const [checked, setChecked] = useState(workspace?.enforceSso);
|
||||
|
||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.checked;
|
||||
try {
|
||||
const updatedWorkspace = await updateWorkspace({ enforceSso: value });
|
||||
setChecked(value);
|
||||
setWorkspace(updatedWorkspace);
|
||||
} catch (err) {
|
||||
notifications.show({
|
||||
message: err?.response?.data?.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Switch
|
||||
size={size}
|
||||
label={label}
|
||||
labelPosition="left"
|
||||
defaultChecked={checked}
|
||||
onChange={handleChange}
|
||||
aria-label={t("Toggle sso enforcement")}
|
||||
/>
|
||||
);
|
||||
}
|
91
apps/client/src/ee/security/components/sso-google-form.tsx
Normal file
91
apps/client/src/ee/security/components/sso-google-form.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import React from "react";
|
||||
import { z } from "zod";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { Box, Button, Group, Stack, Switch, TextInput } from "@mantine/core";
|
||||
import classes from "@/ee/security/components/sso.module.css";
|
||||
import { IAuthProvider } from "@/ee/security/types/security.types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useUpdateSsoProviderMutation } from "@/ee/security/queries/security-query.ts";
|
||||
|
||||
const ssoSchema = z.object({
|
||||
name: z.string().min(1, "Provider name is required"),
|
||||
isEnabled: z.boolean(),
|
||||
allowSignup: z.boolean(),
|
||||
});
|
||||
|
||||
type SSOFormValues = z.infer<typeof ssoSchema>;
|
||||
|
||||
interface SsoFormProps {
|
||||
provider: IAuthProvider;
|
||||
onClose?: () => void;
|
||||
}
|
||||
export function SsoGoogleForm({ provider, onClose }: SsoFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const updateSsoProviderMutation = useUpdateSsoProviderMutation();
|
||||
|
||||
const form = useForm<SSOFormValues>({
|
||||
initialValues: {
|
||||
name: provider.name || "",
|
||||
isEnabled: provider.isEnabled,
|
||||
allowSignup: provider.allowSignup,
|
||||
},
|
||||
validate: zodResolver(ssoSchema),
|
||||
});
|
||||
|
||||
const handleSubmit = async (values: SSOFormValues) => {
|
||||
const ssoData: Partial<IAuthProvider> = {
|
||||
providerId: provider.id,
|
||||
};
|
||||
if (form.isDirty("name")) {
|
||||
ssoData.name = values.name;
|
||||
}
|
||||
if (form.isDirty("isEnabled")) {
|
||||
ssoData.isEnabled = values.isEnabled;
|
||||
}
|
||||
if (form.isDirty("allowSignup")) {
|
||||
ssoData.allowSignup = values.allowSignup;
|
||||
}
|
||||
|
||||
await updateSsoProviderMutation.mutateAsync(ssoData);
|
||||
form.resetDirty();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box maw={600} mx="auto">
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label="Display name"
|
||||
placeholder="e.g Okta SSO"
|
||||
readOnly
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<Group justify="space-between">
|
||||
<div>{t("Allow signup")}</div>
|
||||
<Switch
|
||||
className={classes.switch}
|
||||
checked={form.values.allowSignup}
|
||||
{...form.getInputProps("allowSignup")}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group justify="space-between">
|
||||
<div>{t("Enabled")}</div>
|
||||
<Switch
|
||||
className={classes.switch}
|
||||
checked={form.values.isEnabled}
|
||||
{...form.getInputProps("isEnabled")}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group mt="md" justify="flex-end">
|
||||
<Button type="submit" disabled={!form.isDirty()}>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
}
|
140
apps/client/src/ee/security/components/sso-oidc-form.tsx
Normal file
140
apps/client/src/ee/security/components/sso-oidc-form.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import React from "react";
|
||||
import { z } from "zod";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { Box, Button, Group, Stack, Switch, TextInput } from "@mantine/core";
|
||||
import { buildCallbackUrl } from "@/ee/security/sso.utils.ts";
|
||||
import classes from "@/ee/security/components/sso.module.css";
|
||||
import { IAuthProvider } from "@/ee/security/types/security.types.ts";
|
||||
import CopyTextButton from "@/components/common/copy.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useUpdateSsoProviderMutation } from "@/ee/security/queries/security-query.ts";
|
||||
|
||||
const ssoSchema = z.object({
|
||||
name: z.string().min(1, "Display name is required"),
|
||||
oidcIssuer: z.string().url(),
|
||||
oidcClientId: z.string().min(1, "Client id is required"),
|
||||
oidcClientSecret: z.string().min(1, "Client secret is required"),
|
||||
isEnabled: z.boolean(),
|
||||
allowSignup: z.boolean(),
|
||||
});
|
||||
|
||||
type SSOFormValues = z.infer<typeof ssoSchema>;
|
||||
|
||||
interface SsoFormProps {
|
||||
provider: IAuthProvider;
|
||||
onClose?: () => void;
|
||||
}
|
||||
export function SsoOIDCForm({ provider, onClose }: SsoFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const updateSsoProviderMutation = useUpdateSsoProviderMutation();
|
||||
|
||||
const form = useForm<SSOFormValues>({
|
||||
initialValues: {
|
||||
name: provider.name || "",
|
||||
oidcIssuer: provider.oidcIssuer || "",
|
||||
oidcClientId: provider.oidcClientId || "",
|
||||
oidcClientSecret: provider.oidcClientSecret || "",
|
||||
isEnabled: provider.isEnabled,
|
||||
allowSignup: provider.allowSignup,
|
||||
},
|
||||
validate: zodResolver(ssoSchema),
|
||||
});
|
||||
|
||||
const callbackUrl = buildCallbackUrl({
|
||||
providerId: provider.id,
|
||||
type: provider.type,
|
||||
});
|
||||
|
||||
const handleSubmit = async (values: SSOFormValues) => {
|
||||
const ssoData: Partial<IAuthProvider> = {
|
||||
providerId: provider.id,
|
||||
};
|
||||
if (form.isDirty("name")) {
|
||||
ssoData.name = values.name;
|
||||
}
|
||||
if (form.isDirty("oidcIssuer")) {
|
||||
ssoData.oidcIssuer = values.oidcIssuer;
|
||||
}
|
||||
if (form.isDirty("oidcClientId")) {
|
||||
ssoData.oidcClientId = values.oidcClientId;
|
||||
}
|
||||
if (form.isDirty("oidcClientSecret")) {
|
||||
ssoData.oidcClientSecret = values.oidcClientSecret;
|
||||
}
|
||||
if (form.isDirty("isEnabled")) {
|
||||
ssoData.isEnabled = values.isEnabled;
|
||||
}
|
||||
if (form.isDirty("allowSignup")) {
|
||||
ssoData.allowSignup = values.allowSignup;
|
||||
}
|
||||
|
||||
await updateSsoProviderMutation.mutateAsync(ssoData);
|
||||
form.resetDirty();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box maw={600} mx="auto">
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label="Display name"
|
||||
placeholder="e.g Google SSO"
|
||||
data-autofocus
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput
|
||||
label="Callback URL"
|
||||
variant="filled"
|
||||
value={callbackUrl}
|
||||
pointer
|
||||
readOnly
|
||||
rightSection={<CopyTextButton text={callbackUrl} />}
|
||||
/>
|
||||
<TextInput
|
||||
label="Issuer URL"
|
||||
description="Enter your OIDC issuer URL"
|
||||
placeholder="e.g https://accounts.google.com/"
|
||||
{...form.getInputProps("oidcIssuer")}
|
||||
/>
|
||||
<TextInput
|
||||
label="Client ID"
|
||||
description="Enter your OIDC ClientId"
|
||||
placeholder="e.g 292085223830.apps.googleusercontent.com"
|
||||
{...form.getInputProps("oidcClientId")}
|
||||
/>
|
||||
<TextInput
|
||||
label="Client Secret"
|
||||
description="Enter your OIDC Client Secret"
|
||||
placeholder="e.g OCSPX-zVCkotEPGRnJA1XKUrbgjlf7PQQ-"
|
||||
{...form.getInputProps("oidcClientSecret")}
|
||||
/>
|
||||
|
||||
<Group justify="space-between">
|
||||
<div>{t("Allow signup")}</div>
|
||||
<Switch
|
||||
className={classes.switch}
|
||||
checked={form.values.allowSignup}
|
||||
{...form.getInputProps("allowSignup")}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group justify="space-between">
|
||||
<div>{t("Enabled")}</div>
|
||||
<Switch
|
||||
className={classes.switch}
|
||||
checked={form.values.isEnabled}
|
||||
{...form.getInputProps("isEnabled")}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group mt="md" justify="flex-end">
|
||||
<Button type="submit" disabled={!form.isDirty()}>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
}
|
186
apps/client/src/ee/security/components/sso-provider-list.tsx
Normal file
186
apps/client/src/ee/security/components/sso-provider-list.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
useDeleteSsoProviderMutation,
|
||||
useGetSsoProviders,
|
||||
} from "@/ee/security/queries/security-query.ts";
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Card,
|
||||
Group,
|
||||
Menu,
|
||||
Table,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconCheck,
|
||||
IconDots,
|
||||
IconLock,
|
||||
IconPencil,
|
||||
IconTrash,
|
||||
IconX,
|
||||
} from "@tabler/icons-react";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { IAuthProvider } from "@/ee/security/types/security.types.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SsoProviderModal from "@/ee/security/components/sso-provider-modal.tsx";
|
||||
import { SSO_PROVIDER } from "@/ee/security/contants.ts";
|
||||
import { GoogleIcon } from "@/components/icons/google-icon.tsx";
|
||||
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
|
||||
import RoleSelectMenu from "@/components/ui/role-select-menu.tsx";
|
||||
import { getUserRoleLabel } from "@/features/workspace/types/user-role-data.ts";
|
||||
|
||||
export default function SsoProviderList() {
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading } = useGetSsoProviders();
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const deleteSsoProviderMutation = useDeleteSsoProviderMutation();
|
||||
const [editProvider, setEditProvider] = useState<IAuthProvider | null>(null);
|
||||
|
||||
if (isLoading || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data?.length === 0) {
|
||||
return <Text c="dimmed">{t("No SSO providers found.")}</Text>;
|
||||
}
|
||||
|
||||
const handleEdit = (provider: IAuthProvider) => {
|
||||
setEditProvider(provider);
|
||||
open();
|
||||
};
|
||||
|
||||
const openDeleteModal = (providerId: string) =>
|
||||
modals.openConfirmModal({
|
||||
title: t("Delete SSO provider"),
|
||||
centered: true,
|
||||
children: (
|
||||
<Text size="sm">
|
||||
{t("Are you sure you want to delete this SSO provider?")}
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: t("Delete"), cancel: t("Don't") },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => deleteSsoProviderMutation.mutateAsync(providerId),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card shadow="sm" radius="sm">
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t("Name")}</Table.Th>
|
||||
<Table.Th>{t("Type")}</Table.Th>
|
||||
<Table.Th>{t("Status")}</Table.Th>
|
||||
<Table.Th>{t("Allow signup")}</Table.Th>
|
||||
<Table.Th>{t("Action")}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{data
|
||||
.sort((a, b) => {
|
||||
const enabledDiff = Number(b.isEnabled) - Number(a.isEnabled);
|
||||
if (enabledDiff !== 0) return enabledDiff;
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
.map((provider: IAuthProvider, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
{provider.type === SSO_PROVIDER.GOOGLE ? (
|
||||
<GoogleIcon size={16} />
|
||||
) : (
|
||||
<IconLock size={16} />
|
||||
)}
|
||||
<div>
|
||||
<Text fz="sm" fw={500}>
|
||||
{provider.name}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={"gray"} variant="light">
|
||||
{provider.type.toUpperCase()}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
color={provider.isEnabled ? "blue" : "gray"}
|
||||
variant="light"
|
||||
>
|
||||
{provider.isEnabled ? "Active" : "InActive"}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{provider.allowSignup ? (
|
||||
<ThemeIcon variant="light" size={24} radius="xl">
|
||||
<IconCheck size={16} />
|
||||
</ThemeIcon>
|
||||
) : (
|
||||
<ThemeIcon
|
||||
variant="light"
|
||||
color="red"
|
||||
size={24}
|
||||
radius="xl"
|
||||
>
|
||||
<IconX size={16} />
|
||||
</ThemeIcon>
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={() => handleEdit(provider)}
|
||||
>
|
||||
<IconPencil size={16} />
|
||||
</ActionIcon>
|
||||
<Menu
|
||||
transitionProps={{ transition: "pop" }}
|
||||
withArrow
|
||||
position="bottom-end"
|
||||
withinPortal
|
||||
>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" color="gray">
|
||||
<IconDots size={16} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={() => handleEdit(provider)}
|
||||
leftSection={<IconPencil size={16} />}
|
||||
>
|
||||
{t("Edit")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => openDeleteModal(provider.id)}
|
||||
leftSection={<IconTrash size={16} />}
|
||||
color="red"
|
||||
disabled={provider.type === SSO_PROVIDER.GOOGLE}
|
||||
>
|
||||
{t("Delete")}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
</Card>
|
||||
|
||||
<SsoProviderModal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
provider={editProvider}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
import { Modal } from "@mantine/core";
|
||||
import { IAuthProvider } from "@/ee/security/types/security.types.ts";
|
||||
import { SsoSamlForm } from "@/ee/security/components/sso-saml-form.tsx";
|
||||
import { SSO_PROVIDER } from "@/ee/security/contants.ts";
|
||||
import { SsoOIDCForm } from "@/ee/security/components/sso-oidc-form.tsx";
|
||||
import { SsoGoogleForm } from "@/ee/security/components/sso-google-form.tsx";
|
||||
|
||||
interface SsoModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
provider: IAuthProvider | null;
|
||||
}
|
||||
|
||||
export default function SsoProviderModal({
|
||||
opened,
|
||||
onClose,
|
||||
provider,
|
||||
}: SsoModalProps) {
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
title={`${provider.type.toUpperCase()} Configuration`}
|
||||
onClose={onClose}
|
||||
>
|
||||
{provider.type === SSO_PROVIDER.SAML && (
|
||||
<SsoSamlForm provider={provider} onClose={onClose} />
|
||||
)}
|
||||
|
||||
{provider.type === SSO_PROVIDER.OIDC && (
|
||||
<SsoOIDCForm provider={provider} onClose={onClose} />
|
||||
)}
|
||||
|
||||
{provider.type === SSO_PROVIDER.GOOGLE && (
|
||||
<SsoGoogleForm provider={provider} onClose={onClose} />
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
153
apps/client/src/ee/security/components/sso-saml-form.tsx
Normal file
153
apps/client/src/ee/security/components/sso-saml-form.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import React from "react";
|
||||
import { z } from "zod";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Stack,
|
||||
Switch,
|
||||
Textarea,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
buildCallbackUrl,
|
||||
buildSamlEntityId,
|
||||
} from "@/ee/security/sso.utils.ts";
|
||||
import classes from "@/ee/security/components/sso.module.css";
|
||||
import { IAuthProvider } from "@/ee/security/types/security.types.ts";
|
||||
import CopyTextButton from "@/components/common/copy.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useUpdateSsoProviderMutation } from "@/ee/security/queries/security-query.ts";
|
||||
|
||||
const ssoSchema = z.object({
|
||||
name: z.string().min(1, "Display name is required"),
|
||||
samlUrl: z.string().url(),
|
||||
samlCertificate: z.string().min(1, "SAML Idp Certificate is required"),
|
||||
isEnabled: z.boolean(),
|
||||
allowSignup: z.boolean(),
|
||||
});
|
||||
|
||||
type SSOFormValues = z.infer<typeof ssoSchema>;
|
||||
|
||||
interface SsoFormProps {
|
||||
provider: IAuthProvider;
|
||||
onClose?: () => void;
|
||||
}
|
||||
export function SsoSamlForm({ provider, onClose }: SsoFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const updateSsoProviderMutation = useUpdateSsoProviderMutation();
|
||||
|
||||
const form = useForm<SSOFormValues>({
|
||||
initialValues: {
|
||||
name: provider.name || "",
|
||||
samlUrl: provider.samlUrl || "",
|
||||
samlCertificate: provider.samlCertificate || "",
|
||||
isEnabled: provider.isEnabled,
|
||||
allowSignup: provider.allowSignup,
|
||||
},
|
||||
validate: zodResolver(ssoSchema),
|
||||
});
|
||||
|
||||
const callbackUrl = buildCallbackUrl({
|
||||
providerId: provider.id,
|
||||
type: provider.type,
|
||||
});
|
||||
|
||||
const samlEntityId = buildSamlEntityId(provider.id);
|
||||
|
||||
const handleSubmit = async (values: SSOFormValues) => {
|
||||
const ssoData: Partial<IAuthProvider> = {
|
||||
providerId: provider.id,
|
||||
};
|
||||
if (form.isDirty("name")) {
|
||||
ssoData.name = values.name;
|
||||
}
|
||||
if (form.isDirty("samlUrl")) {
|
||||
ssoData.samlUrl = values.samlUrl;
|
||||
}
|
||||
if (form.isDirty("samlCertificate")) {
|
||||
ssoData.samlCertificate = values.samlCertificate;
|
||||
}
|
||||
if (form.isDirty("isEnabled")) {
|
||||
ssoData.isEnabled = values.isEnabled;
|
||||
}
|
||||
if (form.isDirty("allowSignup")) {
|
||||
ssoData.allowSignup = values.allowSignup;
|
||||
}
|
||||
|
||||
await updateSsoProviderMutation.mutateAsync(ssoData);
|
||||
form.resetDirty();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box maw={600} mx="auto">
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label="Display name"
|
||||
placeholder="e.g Azure Entra"
|
||||
data-autofocus
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput
|
||||
label="Entity ID"
|
||||
variant="filled"
|
||||
value={buildSamlEntityId(provider.id)}
|
||||
rightSection={<CopyTextButton text={samlEntityId} />}
|
||||
pointer
|
||||
readOnly
|
||||
/>
|
||||
<TextInput
|
||||
label="Callback URL (ACS)"
|
||||
variant="filled"
|
||||
value={callbackUrl}
|
||||
pointer
|
||||
readOnly
|
||||
rightSection={<CopyTextButton text={callbackUrl} />}
|
||||
/>
|
||||
<TextInput
|
||||
label="IDP Login URL"
|
||||
description="Enter your IDP login URL"
|
||||
placeholder="e.g https://login.microsoftonline.com/7d6246d1-273b-4981-ad1e-e7bb27b86569/saml2"
|
||||
{...form.getInputProps("samlUrl")}
|
||||
/>
|
||||
<Textarea
|
||||
label="IDP Certificate"
|
||||
description="Enter your IDP certificate"
|
||||
placeholder="-----BEGIN CERTIFICATE-----"
|
||||
autosize
|
||||
minRows={3}
|
||||
maxRows={5}
|
||||
{...form.getInputProps("samlCertificate")}
|
||||
/>
|
||||
|
||||
<Group justify="space-between">
|
||||
<div>{t("Allow signup")}</div>
|
||||
<Switch
|
||||
className={classes.switch}
|
||||
checked={form.values.allowSignup}
|
||||
{...form.getInputProps("allowSignup")}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group justify="space-between">
|
||||
<div>{t("Enabled")}</div>
|
||||
<Switch
|
||||
className={classes.switch}
|
||||
checked={form.values.isEnabled}
|
||||
{...form.getInputProps("isEnabled")}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group mt="md" justify="flex-end">
|
||||
<Button type="submit" disabled={!form.isDirty()}>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
}
|
14
apps/client/src/ee/security/components/sso.module.css
Normal file
14
apps/client/src/ee/security/components/sso.module.css
Normal file
@ -0,0 +1,14 @@
|
||||
.item {
|
||||
& + & {
|
||||
padding-top: var(--mantine-spacing-sm);
|
||||
margin-top: var(--mantine-spacing-sm);
|
||||
border-top: 1px solid
|
||||
light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
|
||||
}
|
||||
}
|
||||
|
||||
.switch {
|
||||
& * {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
5
apps/client/src/ee/security/contants.ts
Normal file
5
apps/client/src/ee/security/contants.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum SSO_PROVIDER {
|
||||
OIDC = 'oidc',
|
||||
SAML = 'saml',
|
||||
GOOGLE = 'google',
|
||||
}
|
51
apps/client/src/ee/security/pages/security.tsx
Normal file
51
apps/client/src/ee/security/pages/security.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { getAppName, isCloud } from "@/lib/config.ts";
|
||||
import SettingsTitle from "@/components/settings/settings-title.tsx";
|
||||
import { Divider, Title } from "@mantine/core";
|
||||
import React from "react";
|
||||
import useUserRole from "@/hooks/use-user-role.tsx";
|
||||
import SsoProviderList from "@/ee/security/components/sso-provider-list.tsx";
|
||||
import CreateSsoProvider from "@/ee/security/components/create-sso-provider.tsx";
|
||||
import EnforceSso from "@/ee/security/components/enforce-sso.tsx";
|
||||
import AllowedDomains from "@/ee/security/components/allowed-domains.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useLicense from "@/ee/hooks/use-license.tsx";
|
||||
|
||||
export default function Security() {
|
||||
const { t } = useTranslation();
|
||||
const { isAdmin } = useUserRole();
|
||||
const { hasLicenseKey } = useLicense();
|
||||
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Security - {getAppName()}</title>
|
||||
</Helmet>
|
||||
<SettingsTitle title={t("Security")} />
|
||||
|
||||
<AllowedDomains />
|
||||
|
||||
<Divider my="lg" />
|
||||
|
||||
<Title order={4} my="lg">
|
||||
Single sign-on (SSO)
|
||||
</Title>
|
||||
|
||||
{/*TODO: revisit when we add a second plan */}
|
||||
{!isCloud() && hasLicenseKey ? (
|
||||
<>
|
||||
<EnforceSso />
|
||||
<Divider my="lg" />
|
||||
<CreateSsoProvider />
|
||||
<Divider size={0} my="lg" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<SsoProviderList />
|
||||
</>
|
||||
);
|
||||
}
|
88
apps/client/src/ee/security/queries/security-query.ts
Normal file
88
apps/client/src/ee/security/queries/security-query.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
UseQueryResult,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
createSsoProvider,
|
||||
deleteSsoProvider,
|
||||
getSsoProviderById,
|
||||
getSsoProviders,
|
||||
updateSsoProvider,
|
||||
} from "@/ee/security/services/security-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { IAuthProvider } from "@/ee/security/types/security.types.ts";
|
||||
|
||||
export function useGetSsoProviders(): UseQueryResult<IAuthProvider[], Error> {
|
||||
return useQuery({
|
||||
queryKey: ["sso-providers"],
|
||||
queryFn: () => getSsoProviders(),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSsoProvider(
|
||||
providerId: string,
|
||||
): UseQueryResult<IAuthProvider, Error> {
|
||||
return useQuery({
|
||||
queryKey: ["sso-provider", providerId],
|
||||
queryFn: () => getSsoProviderById({ providerId }),
|
||||
enabled: !!providerId,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateSsoProviderMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<any, Error, Partial<IAuthProvider>>({
|
||||
mutationFn: (data: Partial<IAuthProvider>) => createSsoProvider(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["sso-providers"],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error["response"]?.data?.message;
|
||||
notifications.show({ message: errorMessage, color: "red" });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateSsoProviderMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<any, Error, Partial<IAuthProvider>>({
|
||||
mutationFn: (data: Partial<IAuthProvider>) => updateSsoProvider(data),
|
||||
onSuccess: (data, variables) => {
|
||||
notifications.show({ message: "Updated successfully" });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["sso-providers"],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error["response"]?.data?.message;
|
||||
notifications.show({ message: errorMessage, color: "red" });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteSsoProviderMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (providerId: string) => deleteSsoProvider({ providerId }),
|
||||
onSuccess: (data, variables) => {
|
||||
notifications.show({ message: "Deleted successfully" });
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["sso-providers"],
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = error["response"]?.data?.message;
|
||||
notifications.show({ message: errorMessage, color: "red" });
|
||||
},
|
||||
});
|
||||
}
|
32
apps/client/src/ee/security/services/security-service.ts
Normal file
32
apps/client/src/ee/security/services/security-service.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import api from "@/lib/api-client.ts";
|
||||
import { IAuthProvider } from "@/ee/security/types/security.types.ts";
|
||||
|
||||
export async function getSsoProviderById(data: {
|
||||
providerId: string;
|
||||
}): Promise<any> {
|
||||
const req = await api.post<IAuthProvider>("/sso/info");
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function getSsoProviders(): Promise<IAuthProvider[]> {
|
||||
const req = await api.post<IAuthProvider[]>("/sso/providers");
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function createSsoProvider(data: any): Promise<IAuthProvider> {
|
||||
const req = await api.post<IAuthProvider>("/sso/create", data);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function deleteSsoProvider(data: {
|
||||
providerId: string;
|
||||
}): Promise<void> {
|
||||
await api.post<any>("/sso/delete", data);
|
||||
}
|
||||
|
||||
export async function updateSsoProvider(
|
||||
data: Partial<IAuthProvider>,
|
||||
): Promise<IAuthProvider> {
|
||||
const req = await api.post<IAuthProvider>("/sso/update", data);
|
||||
return req.data;
|
||||
}
|
39
apps/client/src/ee/security/sso.utils.ts
Normal file
39
apps/client/src/ee/security/sso.utils.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { SSO_PROVIDER } from "@/ee/security/contants.ts";
|
||||
import { getAppUrl, getServerAppUrl } from "@/lib/config.ts";
|
||||
|
||||
export function buildCallbackUrl(opts: {
|
||||
providerId: string;
|
||||
type: SSO_PROVIDER;
|
||||
}): string {
|
||||
const { providerId, type } = opts;
|
||||
const domain = getAppUrl();
|
||||
|
||||
if (type === SSO_PROVIDER.GOOGLE) {
|
||||
return `${domain}/api/sso/${type}/callback`;
|
||||
}
|
||||
return `${domain}/api/sso/${type}/${providerId}/callback`;
|
||||
}
|
||||
|
||||
export function buildSsoLoginUrl(opts: {
|
||||
providerId: string;
|
||||
type: SSO_PROVIDER;
|
||||
workspaceId?: string;
|
||||
}): string {
|
||||
const { providerId, type, workspaceId } = opts;
|
||||
const domain = getAppUrl();
|
||||
|
||||
if (type === SSO_PROVIDER.GOOGLE) {
|
||||
return `${getServerAppUrl()}/api/sso/${type}/login?workspaceId=${workspaceId}`;
|
||||
}
|
||||
return `${domain}/api/sso/${type}/${providerId}/login`;
|
||||
}
|
||||
|
||||
export function getGoogleSignupUrl(): string {
|
||||
// Google login is instance-wide. Use the env APP_URL instead
|
||||
return `${getServerAppUrl()}/api/sso/${SSO_PROVIDER.GOOGLE}/signup`;
|
||||
}
|
||||
|
||||
export function buildSamlEntityId(providerId: string): string {
|
||||
const domain = getAppUrl();
|
||||
return `${domain}/api/sso/${SSO_PROVIDER.SAML}/${providerId}/login`;
|
||||
}
|
20
apps/client/src/ee/security/types/security.types.ts
Normal file
20
apps/client/src/ee/security/types/security.types.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { SSO_PROVIDER } from "@/ee/security/contants.ts";
|
||||
|
||||
export interface IAuthProvider {
|
||||
id: string;
|
||||
name: string;
|
||||
type: SSO_PROVIDER;
|
||||
samlUrl: string;
|
||||
samlCertificate: string;
|
||||
oidcIssuer: string;
|
||||
oidcClientId: string;
|
||||
oidcClientSecret: string;
|
||||
allowSignup: boolean;
|
||||
isEnabled: boolean;
|
||||
creatorId: string;
|
||||
workspaceId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deletedAt: Date;
|
||||
providerId: string;
|
||||
}
|
16
apps/client/src/ee/utils.ts
Normal file
16
apps/client/src/ee/utils.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { getServerAppUrl, getSubdomainHost } from "@/lib/config.ts";
|
||||
|
||||
export function getHostnameUrl(hostname: string): string {
|
||||
const url = new URL(getServerAppUrl());
|
||||
const isHttps = url.protocol === "https:";
|
||||
|
||||
const protocol = isHttps ? "https" : "http";
|
||||
return `${protocol}://${hostname}.${getSubdomainHost()}`;
|
||||
}
|
||||
|
||||
export function exchangeTokenRedirectUrl(
|
||||
hostname: string,
|
||||
exchangeToken: string,
|
||||
) {
|
||||
return getHostnameUrl(hostname) + "/api/auth/exchange?token=" + exchangeToken;
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
import Cookies from "js-cookie";
|
||||
import { createJSONStorage, atomWithStorage } from "jotai/utils";
|
||||
import { ITokens } from "../types/auth.types";
|
||||
|
||||
const cookieStorage = createJSONStorage<ITokens>(() => {
|
||||
const cookieStorage = createJSONStorage<any>(() => {
|
||||
return {
|
||||
getItem: () => Cookies.get("authTokens"),
|
||||
setItem: (key, value) => Cookies.set(key, value, { expires: 30 }),
|
||||
@ -10,7 +9,7 @@ const cookieStorage = createJSONStorage<ITokens>(() => {
|
||||
};
|
||||
});
|
||||
|
||||
export const authTokensAtom = atomWithStorage<ITokens | null>(
|
||||
export const authTokensAtom = atomWithStorage<any | null>(
|
||||
"authTokens",
|
||||
null,
|
||||
cookieStorage,
|
||||
|
@ -2,4 +2,17 @@
|
||||
box-shadow: rgba(0, 0, 0, 0.07) 0px 2px 45px 4px;
|
||||
border-radius: 4px;
|
||||
background: light-dark(var(--mantine-color-body), rgba(0, 0, 0, 0.1));
|
||||
margin-top: 150px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
@media (max-width: $mantine-breakpoint-sm) {
|
||||
margin-top: 50px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.containerBox {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
|
||||
|
@ -6,6 +6,7 @@ import { IForgotPassword } from "@/features/auth/types/auth.types";
|
||||
import { Box, Button, Container, Text, TextInput, Title } from "@mantine/core";
|
||||
import classes from "./auth.module.css";
|
||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z
|
||||
@ -15,6 +16,7 @@ const formSchema = z.object({
|
||||
});
|
||||
|
||||
export function ForgotPasswordForm() {
|
||||
const { t } = useTranslation();
|
||||
const { forgotPassword, isLoading } = useAuth();
|
||||
const [isTokenSent, setIsTokenSent] = useState<boolean>(false);
|
||||
useRedirectIfAuthenticated();
|
||||
@ -33,10 +35,10 @@ export function ForgotPasswordForm() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container size={420} my={40} className={classes.container}>
|
||||
<Box p="xl" mt={200}>
|
||||
<Container size={420} className={classes.container}>
|
||||
<Box p="xl" className={classes.containerBox}>
|
||||
<Title order={2} ta="center" fw={500} mb="md">
|
||||
Forgot password
|
||||
{t("Forgot password")}
|
||||
</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
@ -53,14 +55,15 @@ export function ForgotPasswordForm() {
|
||||
|
||||
{isTokenSent && (
|
||||
<Text>
|
||||
A password reset link has been sent to your email. Please check
|
||||
your inbox.
|
||||
{t(
|
||||
"A password reset link has been sent to your email. Please check your inbox.",
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!isTokenSent && (
|
||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||
Send reset link
|
||||
{t("Send reset link")}
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
|
@ -17,6 +17,7 @@ import useAuth from "@/features/auth/hooks/use-auth";
|
||||
import classes from "@/features/auth/components/auth.module.css";
|
||||
import { useGetInvitationQuery } from "@/features/workspace/queries/workspace-query.ts";
|
||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().trim().min(1),
|
||||
@ -26,6 +27,7 @@ const formSchema = z.object({
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
export function InviteSignUpForm() {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@ -55,7 +57,7 @@ export function InviteSignUpForm() {
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <div>invalid invitation link</div>;
|
||||
return <div>{t("invalid invitation link")}</div>;
|
||||
}
|
||||
|
||||
if (!invitation) {
|
||||
@ -63,10 +65,10 @@ export function InviteSignUpForm() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container size={420} my={40} className={classes.container}>
|
||||
<Box p="xl" mt={200}>
|
||||
<Container size={420} className={classes.container}>
|
||||
<Box p="xl" className={classes.containerBox}>
|
||||
<Title order={2} ta="center" fw={500} mb="md">
|
||||
Join the workspace
|
||||
{t("Join the workspace")}
|
||||
</Title>
|
||||
|
||||
<Stack align="stretch" justify="center" gap="xl">
|
||||
@ -74,8 +76,8 @@ export function InviteSignUpForm() {
|
||||
<TextInput
|
||||
id="name"
|
||||
type="text"
|
||||
label="Name"
|
||||
placeholder="enter your full name"
|
||||
label={t("Name")}
|
||||
placeholder={t("enter your full name")}
|
||||
variant="filled"
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
@ -83,7 +85,7 @@ export function InviteSignUpForm() {
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
label={t("Email")}
|
||||
value={invitation.email}
|
||||
disabled
|
||||
variant="filled"
|
||||
@ -91,14 +93,14 @@ export function InviteSignUpForm() {
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Your password"
|
||||
label={t("Password")}
|
||||
placeholder={t("Your password")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||
Sign Up
|
||||
{t("Sign Up")}
|
||||
</Button>
|
||||
</form>
|
||||
</Stack>
|
||||
|
@ -9,13 +9,18 @@ import {
|
||||
Button,
|
||||
PasswordInput,
|
||||
Box,
|
||||
|
||||
Anchor,
|
||||
Group,
|
||||
} from "@mantine/core";
|
||||
import classes from "./auth.module.css";
|
||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SsoLogin from "@/ee/components/sso-login.tsx";
|
||||
import { useWorkspacePublicDataQuery } from "@/features/workspace/queries/workspace-query.ts";
|
||||
import { Error404 } from "@/components/ui/error-404.tsx";
|
||||
import React from "react";
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z
|
||||
@ -26,8 +31,15 @@ const formSchema = z.object({
|
||||
});
|
||||
|
||||
export function LoginForm() {
|
||||
const { t } = useTranslation();
|
||||
const { signIn, isLoading } = useAuth();
|
||||
useRedirectIfAuthenticated();
|
||||
const {
|
||||
data,
|
||||
isLoading: isDataLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useWorkspacePublicDataQuery();
|
||||
|
||||
const form = useForm<ILogin>({
|
||||
validate: zodResolver(formSchema),
|
||||
@ -41,44 +53,60 @@ export function LoginForm() {
|
||||
await signIn(data);
|
||||
}
|
||||
|
||||
if (isDataLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isError && error?.["response"]?.status === 404) {
|
||||
return <Error404 />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container size={420} my={40} className={classes.container}>
|
||||
<Box p="xl" mt={200}>
|
||||
<Container size={420} className={classes.container}>
|
||||
<Box p="xl" className={classes.containerBox}>
|
||||
<Title order={2} ta="center" fw={500} mb="md">
|
||||
Login
|
||||
{t("Login")}
|
||||
</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder="email@example.com"
|
||||
variant="filled"
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
<SsoLogin />
|
||||
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Your password"
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
{!data?.enforceSso && (
|
||||
<>
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
label={t("Email")}
|
||||
placeholder="email@example.com"
|
||||
variant="filled"
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
|
||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||
Sign In
|
||||
</Button>
|
||||
</form>
|
||||
<PasswordInput
|
||||
label={t("Password")}
|
||||
placeholder={t("Your password")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
|
||||
<Anchor
|
||||
to={APP_ROUTE.AUTH.FORGOT_PASSWORD}
|
||||
component={Link}
|
||||
underline="never"
|
||||
size="sm"
|
||||
>
|
||||
Forgot your password?
|
||||
</Anchor>
|
||||
<Group justify="flex-end" mt="sm">
|
||||
<Anchor
|
||||
to={APP_ROUTE.AUTH.FORGOT_PASSWORD}
|
||||
component={Link}
|
||||
underline="never"
|
||||
size="sm"
|
||||
>
|
||||
{t("Forgot your password?")}
|
||||
</Anchor>
|
||||
</Group>
|
||||
|
||||
<Button type="submit" fullWidth mt="md" loading={isLoading}>
|
||||
{t("Sign In")}
|
||||
</Button>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
|
@ -2,16 +2,10 @@ import * as z from "zod";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import useAuth from "@/features/auth/hooks/use-auth";
|
||||
import { IPasswordReset } from "@/features/auth/types/auth.types";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
PasswordInput,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { Box, Button, Container, PasswordInput, Title } from "@mantine/core";
|
||||
import classes from "./auth.module.css";
|
||||
import { useRedirectIfAuthenticated } from "@/features/auth/hooks/use-redirect-if-authenticated.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formSchema = z.object({
|
||||
newPassword: z
|
||||
@ -24,6 +18,7 @@ interface PasswordResetFormProps {
|
||||
}
|
||||
|
||||
export function PasswordResetForm({ resetToken }: PasswordResetFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const { passwordReset, isLoading } = useAuth();
|
||||
useRedirectIfAuthenticated();
|
||||
|
||||
@ -37,28 +32,28 @@ export function PasswordResetForm({ resetToken }: PasswordResetFormProps) {
|
||||
async function onSubmit(data: IPasswordReset) {
|
||||
await passwordReset({
|
||||
token: resetToken,
|
||||
newPassword: data.newPassword
|
||||
})
|
||||
newPassword: data.newPassword,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Container size={420} my={40} className={classes.container}>
|
||||
<Box p="xl" mt={200}>
|
||||
<Container size={420} className={classes.container}>
|
||||
<Box p="xl" className={classes.containerBox}>
|
||||
<Title order={2} ta="center" fw={500} mb="md">
|
||||
Password reset
|
||||
{t("Password reset")}
|
||||
</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<PasswordInput
|
||||
label="New password"
|
||||
placeholder="Your new password"
|
||||
label={t("New password")}
|
||||
placeholder={t("Your new password")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("newPassword")}
|
||||
/>
|
||||
|
||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||
Set password
|
||||
{t("Set password")}
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import * as React from "react";
|
||||
import * as z from "zod";
|
||||
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import {
|
||||
Container,
|
||||
@ -9,10 +8,17 @@ import {
|
||||
Button,
|
||||
PasswordInput,
|
||||
Box,
|
||||
Anchor,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { ISetupWorkspace } from "@/features/auth/types/auth.types";
|
||||
import useAuth from "@/features/auth/hooks/use-auth";
|
||||
import classes from "@/features/auth/components/auth.module.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SsoCloudSignup from "@/ee/components/sso-cloud-signup.tsx";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
import { Link } from "react-router-dom";
|
||||
import APP_ROUTE from "@/lib/app-route.ts";
|
||||
|
||||
const formSchema = z.object({
|
||||
workspaceName: z.string().trim().min(3).max(50),
|
||||
@ -25,6 +31,7 @@ const formSchema = z.object({
|
||||
});
|
||||
|
||||
export function SetupWorkspaceForm() {
|
||||
const { t } = useTranslation();
|
||||
const { setupWorkspace, isLoading } = useAuth();
|
||||
// useRedirectIfAuthenticated();
|
||||
|
||||
@ -43,55 +50,71 @@ export function SetupWorkspaceForm() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container size={420} my={40} className={classes.container}>
|
||||
<Box p="xl" mt={200}>
|
||||
<Title order={2} ta="center" fw={500} mb="md">
|
||||
Create workspace
|
||||
</Title>
|
||||
<div>
|
||||
<Container size={420} className={classes.container}>
|
||||
<Box p="xl" className={classes.containerBox}>
|
||||
<Title order={2} ta="center" fw={500} mb="md">
|
||||
{t("Create workspace")}
|
||||
</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<TextInput
|
||||
id="workspaceName"
|
||||
type="text"
|
||||
label="Workspace Name"
|
||||
placeholder="e.g ACME Inc"
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("workspaceName")}
|
||||
/>
|
||||
{isCloud() && <SsoCloudSignup />}
|
||||
|
||||
<TextInput
|
||||
id="name"
|
||||
type="text"
|
||||
label="Your Name"
|
||||
placeholder="enter your full name"
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<TextInput
|
||||
id="workspaceName"
|
||||
type="text"
|
||||
label={t("Workspace Name")}
|
||||
placeholder={t("e.g ACME Inc")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("workspaceName")}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
label="Your Email"
|
||||
placeholder="email@example.com"
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
<TextInput
|
||||
id="name"
|
||||
type="text"
|
||||
label={t("Your Name")}
|
||||
placeholder={t("enter your full name")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Enter a strong password"
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||
Setup workspace
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
</Container>
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
label={t("Your Email")}
|
||||
placeholder="email@example.com"
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("email")}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label={t("Password")}
|
||||
placeholder={t("Enter a strong password")}
|
||||
variant="filled"
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<Button type="submit" fullWidth mt="xl" loading={isLoading}>
|
||||
{t("Create workspace")}
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
</Container>
|
||||
{isCloud() && (
|
||||
<Text ta="center">
|
||||
{t("Already part of an existing workspace?")}{" "}
|
||||
<Anchor
|
||||
component={Link}
|
||||
to={APP_ROUTE.AUTH.SELECT_WORKSPACE}
|
||||
fw={500}
|
||||
>
|
||||
{t("Sign-in")}
|
||||
</Anchor>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user