Compare commits

...

43 Commits

Author SHA1 Message Date
Thomas Ramé e6403be62e wip 2026-03-25 19:17:36 +01:00
Thomas Ramé ca3502ee4d wip 2026-03-25 09:51:07 +01:00
Thomas Ramé 8c5352103a WARNING TO BRAINSTORM ON PROPAGATING AUTH TO IFRAME DOMAIN (separate flow with its own OIDC clientId or using current token?) 2026-03-24 15:02:19 +01:00
Thomas Ramé 3e3ee7e698 wip 2026-03-24 15:01:31 +01:00
Thomas Ramé af1c40995b wip full settings flow for onboarding and removing encryption 2026-03-09 17:45:57 +01:00
Thomas Ramé 1da0f6600e wip fix preview of audio and video files 2026-03-06 19:18:04 +01:00
Thomas Ramé a3fdb206ef wip broadcast encryption transition to adapt ui smoothly 2026-03-06 19:06:24 +01:00
Thomas Ramé da4d323144 wip provider server command endpoints 2026-03-05 12:11:26 +01:00
Thomas Ramé 3e45193a7c wip prevent some search indexing for encrypted docs 2026-03-05 10:50:00 +01:00
Thomas Ramé 7a55e31a73 wip use a context instead for global usage 2026-03-05 10:26:34 +01:00
Thomas Ramé 4baef38cae wip allow revealing all medias at once 2026-03-05 00:46:54 +01:00
Thomas Ramé 1eba8b77c0 wip ui 2026-03-05 00:46:40 +01:00
Thomas Ramé 579ff98a5a wip encryption requirements 2026-03-04 23:56:09 +01:00
Thomas Ramé fe34b93249 wip encryption issue to access doc 2026-03-04 23:38:35 +01:00
Thomas Ramé 205960106b wip improve accesses tips 2026-03-04 23:22:22 +01:00
Thomas Ramé d685b541c5 wip block placeholders to save resources when encrypted 2026-03-04 19:58:32 +01:00
Thomas Ramé 834ed4226f wip decryption with attachments 2026-03-03 18:21:31 +01:00
Thomas Ramé 3f8e105035 wip encrypt with attachments 2026-03-03 17:45:56 +01:00
Thomas Ramé 431bec3970 wip manage encrypted attachments 2026-03-03 10:09:12 +01:00
Thomas Ramé 54f2762e79 wip save ok 2026-03-02 17:55:54 +01:00
Thomas Ramé 9c438eba06 wip 2026-03-02 16:52:31 +01:00
Thomas Ramé bedb0573b8 just before trying standard server but with no in-memory y.doc 2026-02-10 17:55:30 +01:00
Thomas Ramé 9d3088d9db TO REMOVE: 2026-02-09 16:30:27 +01:00
buildwithricky 7cf42e6404 🐛(frontend) fix doc timestamp display
Implemented the logic to show 'Just now' instead
of '0 seconds ago' when the difference is under
one second.

Signed-off-by: buildwithricky <nwakezepatrick@gmail.com>
2026-02-04 09:34:21 +01:00
Manuel Raynaud 9903bd73e2 ️(actions) enable trivy scan on backend image
The trivy was disabled because protobuf library was blocking the release
process. We can now enable it again, a new release of protobuf is
available.
2026-02-03 16:45:13 +00:00
Anthony LC 44b38347c4 🐛(frontend) fix broadcast store sync
When going from one subdoc to another by example,
the broadcast store could have difficulty to resync.
This commit ensures that the broadcast store
cleans up and resets its state when rerendering.
It will stop as well triggering the action for
the current user avoiding potential unecessary
requests.
2026-02-03 14:25:35 +01:00
Thai Pangsakulyanont 709076067b 🐛(backend) add AWS_S3_SIGNATURE_VERSION environment variable support
Add support for the `AWS_S3_SIGNATURE_VERSION` environment variable to
allow configuring S3 signature version for compatibility with
S3-compatible storage services like Linode Object Storage.

Fixes #1788

Signed-off-by: dtinth on MBP M1 <dtinth@spacet.me>
2026-02-02 10:47:14 +00:00
Anthony LC db014cfc6f 🔖(minor) release 4.5.0
Added:
- (frontend) integrate configurable Waffle
-  Import of documents
- 🚨(CI) gives warning if theme not updated
- (frontend) Add stat for Crisp
- (auth) add silent login
- 🔧(project) add DJANGO_EMAIL_URL_APP environment variable

Changed:
- (frontend) improve accessibility:
  - ️(frontend) fix subdoc opening and emoji pick focus
- (backend) add field for button label in email template

Fixed:
- (e2e) fix e2e test for other browsers
- 🐛(export) fix export column NaN
- 🐛(frontend) add fallback for unsupported Blocknote
  languages
- 🐛(frontend) fix emojipicker closing in tree
- 🐛(frontend) display children in favorite
- 🐛(frontend) preserve typed text after @ on escape

Removed:
- 🔥(project) remove all code related to template

Security:
- 🔒️(trivy) fix vulnerability about jaraco.context
2026-01-29 16:13:37 +01:00
Manuel Raynaud 52cd76eb93 🔧(backend) customize cache config
We want to split the cache config between the app cache and the session
cache. In the app cache, the default one, we allow to configure a
prefix. By default this prefix is a fixed string so the cache will be
never revoked because it is changing but it allow every instance to
implement its own strategy like prefixing the keyx cache with a
timestamp.
To not impact session, the session cache is splitted in the settings.
2026-01-29 16:13:36 +01:00
Anthony LC 505b144968 🔊(CHANGELOG) change link version CHANGELOG
A "v" was introduced in the link format, we need
to update the version in the link accordingly to
match it.
We add "v" to fit with others projects.
2026-01-29 10:24:28 +01:00
lunika 009de5299f 🌐(i18n) update translated strings
Update translated files with new translations
2026-01-29 10:11:56 +01:00
Anthony LC 0fddabb354 🩺(CI) remove backend trivy guard
We need to do a release but the backend cannot
build because of a trivy issue.
So we temporarily remove the backend trivy guard
to be able to release.
2026-01-29 10:11:56 +01:00
Anthony LC cd25c3a63b 🚚(frontend) move from hook to hooks
We has 2 folders for hooks: "hook" and "hooks".
To keep consistency, we move all hooks to "hooks"
folder and delete "hook" folder.
2026-01-28 16:59:45 +01:00
Anthony LC adb216fbdf (frontend) add stat from Crisp
We want to track document views with user
authentication status using Crisp analytics.
2026-01-28 16:59:45 +01:00
Anthony LC 235c1828e6 💄(export) improve heading line height
Import heading line height mapping for PDF export
to match the one used in the doc.
2026-01-28 11:12:47 +01:00
Anthony LC 4588c71e8a 🏷️(frontend) adjust typing to fit styled-component
Recent upgrade of styled-components caused
type issues in Box and Text components.
We adjust the typing to fit the new version.
2026-01-28 11:12:47 +01:00
Anthony LC 6b7fc915dd ️(frontend) load Marianne font
Ui-kit expose now correctly the Marianne font.
We can use it now just by importing the font
from our css, no need to copy paste the font
in the public folder.
2026-01-28 11:12:46 +01:00
Anthony LC c3e83c6612 ⬆️(dependencies) let docx dependencies be upgraded
Blocknote seems to manage the last version of
docx, we do not have to pin it to a specific version
anymore.
2026-01-28 11:12:46 +01:00
Anthony LC 586089c8e4 📌(dependencies) stop upgrading react-resizable-panels
Last versions of react-resizable-panels have some
issues performance issues. We will stick to
version 3.0.6 for now.
2026-01-28 11:12:46 +01:00
renovate[bot] 1b5ce3ed10 ⬆️(dependencies) update js dependencies 2026-01-28 11:12:46 +01:00
Anthony LC 989c70ed57 🚩(project) add FRONTEND_SILENT_LOGIN_ENABLED feature flag
Not every project requires silent login.
This commit adds a new feature flag
FRONTEND_SILENT_LOGIN_ENABLED to enable or
disable silent login functionality.
2026-01-28 10:35:34 +01:00
Anthony LC c6ded3f267 (auth) add silent login
Currently users already logged in to the SSO have to click on
the login button again to be connected.
This extra step should not be necessary.

This commit uses the "silent=true" parameter to the login
endpoint to avoid the extra step.
2026-01-28 10:35:33 +01:00
Cyril 781f0815a8 🐛(frontend) preserve typed text after @ on escape
prevents losing input when closing interlink search
2026-01-27 15:14:30 +01:00
176 changed files with 9614 additions and 3843 deletions
+2 -1
View File
@@ -146,8 +146,9 @@ jobs:
notify-argocd:
needs:
- build-and-push-frontend
- build-and-push-backend
- build-and-push-frontend
- build-and-push-y-provider
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview')
steps:
+1
View File
@@ -0,0 +1 @@
22.21.1
+1
View File
@@ -0,0 +1 @@
nodejs 22.21.1
+65 -55
View File
@@ -6,11 +6,19 @@ and this project adheres to
## [Unreleased]
### Fixed
🐛(frontend) fix broadcast store sync #1846
## [v4.5.0] - 2026-01-28
### Added
- ✨(frontend) integrate configurable Waffle #1795
- ✨ Import of documents #1609
- 🚨(CI) gives warning if theme not updated #1811
- ✨(frontend) Add stat for Crisp #1824
- ✨(auth) add silent login #1690
- 🔧(project) add DJANGO_EMAIL_URL_APP environment variable #1825
### Changed
@@ -26,6 +34,7 @@ and this project adheres to
- 🐛(frontend) add fallback for unsupported Blocknote languages #1810
- 🐛(frontend) fix emojipicker closing in tree #1808
- 🐛(frontend) display children in favorite #1782
- 🐛(frontend) preserve typed text after @ on escape #1833
### Removed
@@ -35,7 +44,7 @@ and this project adheres to
- 🔒️(trivy) fix vulnerability about jaraco.context #1806
## [4.4.0] - 2026-01-13
## [v4.4.0] - 2026-01-13
### Added
@@ -63,7 +72,7 @@ and this project adheres to
- 🔒️(backend) validate more strictly url used by cors-proxy endpoint #1768
- 🔒️(frontend) fix props vulnerability in Interlinking #1792
## [4.3.0] - 2026-01-05
## [v4.3.0] - 2026-01-05
### Added
@@ -82,7 +91,7 @@ and this project adheres to
- 🐛(frontend) fix tables deletion #1739
- 🐛(frontend) fix children not display when first resize #1753
## [4.2.0] - 2025-12-17
## [v4.2.0] - 2025-12-17
### Added
@@ -106,7 +115,7 @@ and this project adheres to
- 🐛(frontend) Select text + Go back one page crash the app #1733
- 🐛(frontend) fix versioning conflict #1742
## [4.1.0] - 2025-12-09
## [v4.1.0] - 2025-12-09
### Added
@@ -125,7 +134,7 @@ and this project adheres to
- 🐛(nginx) fix / location to handle new static pages #1682
- 🐛(frontend) rerendering during resize window #1715
## [4.0.0] - 2025-12-01
## [v4.0.0] - 2025-12-01
### Added
@@ -148,7 +157,7 @@ and this project adheres to
- 🐛(frontend) preserve left panel width on window resize #1588
- 🐛(frontend) prevent duplicate as first character in title #1595
## [3.10.0] - 2025-11-18
## [v3.10.0] - 2025-11-18
### Added
@@ -182,7 +191,7 @@ and this project adheres to
- 🔥(backend) remove api managing templates
## [3.9.0] - 2025-11-10
## [v3.9.0] - 2025-11-10
### Added
@@ -208,13 +217,13 @@ and this project adheres to
- 🐛(frontend) button new doc UI fix #1557
- 🐛(frontend) interlinking UI fix #1557
## [3.8.2] - 2025-10-17
## [v3.8.2] - 2025-10-17
### Fixed
- 🐛(service-worker) fix sw registration and page reload logic #1500
## [3.8.1] - 2025-10-17
## [v3.8.1] - 2025-10-17
### Fixed
@@ -228,7 +237,7 @@ and this project adheres to
- 🔥(backend) remove treebeard form for the document admin #1470
## [3.8.0] - 2025-10-14
## [v3.8.0] - 2025-10-14
### Added
@@ -281,7 +290,7 @@ and this project adheres to
- 🔥(frontend) remove custom DividerBlock ##1375
## [3.7.0] - 2025-09-12
## [v3.7.0] - 2025-09-12
### Added
@@ -313,7 +322,7 @@ and this project adheres to
- 🐛(frontend) fix callout emoji list #1366
## [3.6.0] - 2025-09-04
## [v3.6.0] - 2025-09-04
### Added
@@ -349,7 +358,7 @@ and this project adheres to
- 🐛(frontend) fix display bug on homepage #1332
- 🐛link role update #1287
## [3.5.0] - 2025-07-31
## [v3.5.0] - 2025-07-31
### Added
@@ -377,7 +386,7 @@ and this project adheres to
- 🐛(frontend) 401 redirection overridden #1214
- 🐛(frontend) include root parent in search #1243
## [3.4.2] - 2025-07-18
## [v3.4.2] - 2025-07-18
### Changed
@@ -387,7 +396,7 @@ and this project adheres to
- 🐛(backend) improve prompt to not use code blocks delimiter #1188
## [3.4.1] - 2025-07-15
## [v3.4.1] - 2025-07-15
### Fixed
@@ -398,7 +407,7 @@ and this project adheres to
- 🐛(frontend) fix crash share modal on grid options #1174
- 🐛(frontend) fix unfold subdocs not clickable at the bottom #1179
## [3.4.0] - 2025-07-09
## [v3.4.0] - 2025-07-09
### Added
@@ -442,7 +451,7 @@ and this project adheres to
- 🔥(frontend) remove Beta from logo #1095
## [3.3.0] - 2025-05-06
## [v3.3.0] - 2025-05-06
### Added
@@ -474,14 +483,14 @@ and this project adheres to
- 🔥(back) remove footer endpoint #948
## [3.2.1] - 2025-05-06
## [v3.2.1] - 2025-05-06
## Fixed
- 🐛(frontend) fix list copy paste #943
- 📝(doc) update contributing policy (commit signatures are now mandatory) #895
## [3.2.0] - 2025-05-05
## [v3.2.0] - 2025-05-05
## Added
@@ -508,7 +517,7 @@ and this project adheres to
- 🐛(backend) race condition create doc #633
- 🐛(frontend) fix breaklines in custom blocks #908
## [3.1.0] - 2025-04-07
## [v3.1.0] - 2025-04-07
## Added
@@ -526,7 +535,7 @@ and this project adheres to
- 🐛(back) validate document content in serializer #822
- 🐛(frontend) fix selection click past end of content #840
## [3.0.0] - 2025-03-28
## [v3.0.0] - 2025-03-28
## Added
@@ -542,7 +551,7 @@ and this project adheres to
- 🐛(backend) compute ancestor_links in get_abilities if needed #725
- 🔒️(back) restrict access to document accesses #801
## [2.6.0] - 2025-03-21
## [v2.6.0] - 2025-03-21
## Added
@@ -560,7 +569,7 @@ and this project adheres to
- 🔒️(back) throttle user list endpoint #636
- 🔒️(back) remove pagination and limit to 5 for user list endpoint #636
## [2.5.0] - 2025-03-18
## [v2.5.0] - 2025-03-18
## Added
@@ -590,7 +599,7 @@ and this project adheres to
- 🚨(helm) fix helmfile lint #736
- 🚚(frontend) redirect to 401 page when 401 error #759
## [2.4.0] - 2025-03-06
## [v2.4.0] - 2025-03-06
## Added
@@ -604,7 +613,7 @@ and this project adheres to
- 🐛(frontend) fix collaboration error #684
## [2.3.0] - 2025-03-03
## [v2.3.0] - 2025-03-03
## Added
@@ -631,7 +640,7 @@ and this project adheres to
- ♻️(frontend) improve table pdf rendering
- 🐛(email) invitation emails in receivers language
## [2.2.0] - 2025-02-10
## [v2.2.0] - 2025-02-10
## Added
@@ -650,7 +659,7 @@ and this project adheres to
- 🐛(frontend) fix cursor breakline #609
- 🐛(frontend) fix style pdf export #609
## [2.1.0] - 2025-01-29
## [v2.1.0] - 2025-01-29
## Added
@@ -679,14 +688,14 @@ and this project adheres to
- 🔥(backend) remove "content" field from list serializer # 516
## [2.0.1] - 2025-01-17
## [v2.0.1] - 2025-01-17
## Fixed
-🐛(frontend) share modal is shown when you don't have the abilities #557
-🐛(frontend) title copy break app #564
## [2.0.0] - 2025-01-13
## [v2.0.0] - 2025-01-13
## Added
@@ -717,7 +726,7 @@ and this project adheres to
- 🐛(frontend) hide search and create doc button if not authenticated #555
- 🐛(backend) race condition creation issue #556
## [1.10.0] - 2024-12-17
## [v1.10.0] - 2024-12-17
## Added
@@ -738,7 +747,7 @@ and this project adheres to
- 🐛(frontend) update doc editor height #481
- 💄(frontend) add doc search #485
## [1.9.0] - 2024-12-11
## [v1.9.0] - 2024-12-11
## Added
@@ -759,19 +768,19 @@ and this project adheres to
- 🐛(frontend) Fix hidden menu on Firefox #468
- 🐛(backend) fix sanitize problem IA #490
## [1.8.2] - 2024-11-28
## [v1.8.2] - 2024-11-28
## Changed
- ♻️(SW) change strategy html caching #460
## [1.8.1] - 2024-11-27
## [v1.8.1] - 2024-11-27
## Fixed
- 🐛(frontend) link not clickable and flickering firefox #457
## [1.8.0] - 2024-11-25
## [v1.8.0] - 2024-11-25
## Added
@@ -799,7 +808,7 @@ and this project adheres to
- 🐛(frontend) users have view access when revoked #387
- 🐛(frontend) fix placeholder editable when double clicks #454
## [1.7.0] - 2024-10-24
## [v1.7.0] - 2024-10-24
## Added
@@ -826,7 +835,7 @@ and this project adheres to
- 🔥(helm) remove infra related codes #366
## [1.6.0] - 2024-10-17
## [v1.6.0] - 2024-10-17
## Added
@@ -848,13 +857,13 @@ and this project adheres to
- 🐛(backend) fix nginx docker container #340
- 🐛(frontend) fix copy paste firefox #353
## [1.5.1] - 2024-10-10
## [v1.5.1] - 2024-10-10
## Fixed
- 🐛(db) fix users duplicate #316
## [1.5.0] - 2024-10-09
## [v1.5.0] - 2024-10-09
## Added
@@ -882,7 +891,7 @@ and this project adheres to
- 🔧(backend) fix configuration to avoid different ssl warning #297
- 🐛(frontend) fix editor break line not working #302
## [1.4.0] - 2024-09-17
## [v1.4.0] - 2024-09-17
## Added
@@ -902,7 +911,7 @@ and this project adheres to
- 🐛(backend) Fix forcing ID when creating a document via API endpoint #234
- 🐛 Rebuild frontend dev container from makefile #248
## [1.3.0] - 2024-09-05
## [v1.3.0] - 2024-09-05
## Added
@@ -926,14 +935,14 @@ and this project adheres to
- 🔥(frontend) remove saving modal #213
## [1.2.1] - 2024-08-23
## [v1.2.1] - 2024-08-23
## Changed
- ♻️ Change ordering docs datagrid #195
- 🔥(helm) use scaleway email #194
## [1.2.0] - 2024-08-22
## [v1.2.0] - 2024-08-22
## Added
@@ -959,7 +968,7 @@ and this project adheres to
- 🔥(helm) remove htaccess #181
## [1.1.0] - 2024-07-15
## [v1.1.0] - 2024-07-15
## Added
@@ -974,7 +983,7 @@ and this project adheres to
- ♻️(frontend) create a doc from a modal #132
- ♻️(frontend) manage members from the share modal #140
## [1.0.0] - 2024-07-02
## [v1.0.0] - 2024-07-02
## Added
@@ -1012,14 +1021,15 @@ and this project adheres to
- 💚(CI) Remove trigger workflow on push tags on CI (#68)
- 🔥(frontend) Remove coming soon page (#121)
## [0.1.0] - 2024-05-24
## [v0.1.0] - 2024-05-24
## Added
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/suitenumerique/docs/compare/v4.4.0...main
[unreleased]: https://github.com/suitenumerique/docs/compare/v4.5.0...main
[v4.5.0]: https://github.com/suitenumerique/docs/releases/v4.5.0
[v4.4.0]: https://github.com/suitenumerique/docs/releases/v4.4.0
[v4.3.0]: https://github.com/suitenumerique/docs/releases/v4.3.0
[v4.2.0]: https://github.com/suitenumerique/docs/releases/v4.2.0
@@ -1056,12 +1066,12 @@ and this project adheres to
[v1.8.0]: https://github.com/suitenumerique/docs/releases/v1.8.0
[v1.7.0]: https://github.com/suitenumerique/docs/releases/v1.7.0
[v1.6.0]: https://github.com/suitenumerique/docs/releases/v1.6.0
[1.5.1]: https://github.com/suitenumerique/docs/releases/v1.5.1
[1.5.0]: https://github.com/suitenumerique/docs/releases/v1.5.0
[1.4.0]: https://github.com/suitenumerique/docs/releases/v1.4.0
[1.3.0]: https://github.com/suitenumerique/docs/releases/v1.3.0
[1.2.1]: https://github.com/suitenumerique/docs/releases/v1.2.1
[1.2.0]: https://github.com/suitenumerique/docs/releases/v1.2.0
[1.1.0]: https://github.com/suitenumerique/docs/releases/v1.1.0
[1.0.0]: https://github.com/suitenumerique/docs/releases/v1.0.0
[0.1.0]: https://github.com/suitenumerique/docs/releases/v0.1.0
[v1.5.1]: https://github.com/suitenumerique/docs/releases/v1.5.1
[v1.5.0]: https://github.com/suitenumerique/docs/releases/v1.5.0
[v1.4.0]: https://github.com/suitenumerique/docs/releases/v1.4.0
[v1.3.0]: https://github.com/suitenumerique/docs/releases/v1.3.0
[v1.2.1]: https://github.com/suitenumerique/docs/releases/v1.2.1
[v1.2.0]: https://github.com/suitenumerique/docs/releases/v1.2.0
[v1.1.0]: https://github.com/suitenumerique/docs/releases/v1.1.0
[v1.0.0]: https://github.com/suitenumerique/docs/releases/v1.0.0
[v0.1.0]: https://github.com/suitenumerique/docs/releases/v0.1.0
+26
View File
@@ -845,6 +845,32 @@
"offline_access",
"microprofile-jwt"
]
},
{
"clientId": "encryption",
"name": "Encryption",
"enabled": true,
"clientAuthenticatorType": "client-secret",
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"publicClient": true,
"protocol": "openid-connect",
"redirectUris": [
"http://localhost:7202/auth/callback"
],
"webOrigins": [
"http://localhost:7202"
],
"frontchannelLogout": true,
"attributes": {},
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"email"
],
"optionalClientScopes": []
}
],
"clientScopes": [
+2 -1
View File
@@ -21,9 +21,10 @@ These are the environment variables you can set for the `impress-backend` contai
| AWS_S3_ENDPOINT_URL | S3 endpoint | |
| AWS_S3_REGION_NAME | Region name for s3 endpoint | |
| AWS_S3_SECRET_ACCESS_KEY | Access key for s3 endpoint | |
| AWS_S3_SIGNATURE_VERSION | S3 signature version (`s3v4` or `s3`) | s3v4 |
| AWS_STORAGE_BUCKET_NAME | Bucket name for s3 endpoint | impress-media-storage |
| CACHES_DEFAULT_TIMEOUT | Cache default timeout | 30 |
| CACHES_KEY_PREFIX | The prefix used to every cache keys. | docs |
| CACHES_DEFAULT_KEY_PREFIX | The prefix used to every cache keys. | docs |
| COLLABORATION_API_URL | Collaboration api host | |
| COLLABORATION_SERVER_SECRET | Collaboration api secret | |
| COLLABORATION_WS_NOT_CONNECTED_READY_ONLY | Users not connected to the collaboration server cannot edit | false |
+3 -3
View File
@@ -48,11 +48,11 @@ LOGIN_REDIRECT_URL=http://localhost:3000
LOGIN_REDIRECT_URL_FAILURE=http://localhost:3000
LOGOUT_REDIRECT_URL=http://localhost:3000
OIDC_REDIRECT_ALLOWED_HOSTS=["http://localhost:8083", "http://localhost:3000"]
OIDC_REDIRECT_ALLOWED_HOSTS="localhost:8083,localhost:3000"
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
# Store OIDC tokens in the session. Needed by search/ endpoint.
# OIDC_STORE_ACCESS_TOKEN = True
# Store OIDC tokens in the session. Needed by search/ endpoint and encryption service.
OIDC_STORE_ACCESS_TOKEN = True
# OIDC_STORE_REFRESH_TOKEN = True # Store the encrypted refresh token in the session.
# Must be a valid Fernet key (32 url-safe base64-encoded bytes)
+1 -2
View File
@@ -32,7 +32,6 @@
"allowedVersions": "<6.0.0"
},
{
"groupName": "allowed celery versions",
"matchManagers": ["pep621"],
"matchPackageNames": ["celery"],
@@ -44,12 +43,12 @@
"matchManagers": ["npm"],
"matchPackageNames": [
"@next/eslint-plugin-next",
"docx",
"eslint-config-next",
"fetch-mock",
"next",
"node",
"node-fetch",
"react-resizable-panels",
"workbox-webpack-plugin"
]
}
+22 -1
View File
@@ -66,10 +66,13 @@ class ListDocumentFilter(DocumentFilter):
is_favorite = django_filters.BooleanFilter(
method="filter_is_favorite", label=_("Favorite")
)
is_encrypted = django_filters.BooleanFilter(
method="filter_is_encrypted", label=_("Encrypted")
)
class Meta:
model = models.Document
fields = ["is_creator_me", "is_favorite", "title"]
fields = ["is_creator_me", "is_favorite", "is_encrypted", "title"]
# pylint: disable=unused-argument
def filter_is_creator_me(self, queryset, name, value):
@@ -110,6 +113,24 @@ class ListDocumentFilter(DocumentFilter):
return queryset.filter(is_favorite=bool(value))
# pylint: disable=unused-argument
def filter_is_encrypted(self, queryset, name, value):
"""
Filter documents based on whether they are encrypted.
Example:
- /api/v1.0/documents/?is_encrypted=true
→ Filters documents encrypted
- /api/v1.0/documents/?is_encrypted=false
→ Filters documents not encrypted
"""
user = self.request.user
if not user.is_authenticated:
return queryset
return queryset.filter(is_encrypted=bool(value))
# pylint: disable=unused-argument
def filter_is_masked(self, queryset, name, value):
"""
+179 -5
View File
@@ -29,11 +29,12 @@ class UserSerializer(serializers.ModelSerializer):
full_name = serializers.SerializerMethodField(read_only=True)
short_name = serializers.SerializerMethodField(read_only=True)
suite_user_id = serializers.CharField(source='sub', read_only=True)
class Meta:
model = models.User
fields = ["id", "email", "full_name", "short_name", "language"]
read_only_fields = ["id", "email", "full_name", "short_name"]
fields = ["id", "email", "full_name", "short_name", "language", "suite_user_id"]
read_only_fields = ["id", "email", "full_name", "short_name", "suite_user_id"]
def get_full_name(self, instance):
"""Return the full name of the user."""
@@ -57,25 +58,33 @@ class UserLightSerializer(UserSerializer):
class Meta:
model = models.User
fields = ["full_name", "short_name"]
read_only_fields = ["full_name", "short_name"]
fields = ["id", "full_name", "short_name"]
read_only_fields = ["id", "full_name", "short_name"]
class ListDocumentSerializer(serializers.ModelSerializer):
"""Serialize documents with limited fields for display in lists."""
is_favorite = serializers.BooleanField(read_only=True)
is_encrypted = serializers.BooleanField(read_only=True)
nb_accesses_ancestors = serializers.IntegerField(read_only=True)
nb_accesses_direct = serializers.IntegerField(read_only=True)
user_role = serializers.SerializerMethodField(read_only=True)
abilities = serializers.SerializerMethodField(read_only=True)
deleted_at = serializers.SerializerMethodField(read_only=True)
accesses_user_ids = serializers.SerializerMethodField(read_only=True)
accesses_fingerprints_per_user = serializers.SerializerMethodField(read_only=True)
encrypted_document_symmetric_key_for_user = serializers.SerializerMethodField(
read_only=True
)
class Meta:
model = models.Document
fields = [
"id",
"abilities",
"accesses_fingerprints_per_user",
"accesses_user_ids",
"ancestors_link_reach",
"ancestors_link_role",
"computed_link_reach",
@@ -84,8 +93,10 @@ class ListDocumentSerializer(serializers.ModelSerializer):
"creator",
"deleted_at",
"depth",
"encrypted_document_symmetric_key_for_user",
"excerpt",
"is_favorite",
"is_encrypted",
"link_role",
"link_reach",
"nb_accesses_ancestors",
@@ -99,6 +110,7 @@ class ListDocumentSerializer(serializers.ModelSerializer):
read_only_fields = [
"id",
"abilities",
"accesses_user_ids",
"ancestors_link_reach",
"ancestors_link_role",
"computed_link_reach",
@@ -107,8 +119,10 @@ class ListDocumentSerializer(serializers.ModelSerializer):
"creator",
"deleted_at",
"depth",
"encrypted_document_symmetric_key_for_user",
"excerpt",
"is_favorite",
"is_encrypted",
"link_role",
"link_reach",
"nb_accesses_ancestors",
@@ -151,6 +165,38 @@ class ListDocumentSerializer(serializers.ModelSerializer):
"""Return the deleted_at of the current document."""
return instance.ancestors_deleted_at
def get_accesses_user_ids(self, instance):
"""Return user IDs of members with access to this document.
The frontend uses these to fetch public keys from the encryption service."""
request = self.context.get("request")
if not request or not request.user.is_authenticated:
return None
return [str(uid) for uid in instance.accesses_user_ids]
def get_accesses_fingerprints_per_user(self, instance):
"""Return fingerprints of users' public keys at share time."""
request = self.context.get("request")
if not request or not request.user.is_authenticated:
return None
if not instance.is_encrypted:
return None
return instance.accesses_fingerprints_per_user
def get_encrypted_document_symmetric_key_for_user(self, instance):
"""Return the encrypted symmetric key for the current user."""
request = self.context.get("request")
if not request or not request.user.is_authenticated:
return None
if not instance.is_encrypted:
return None
try:
access = models.DocumentAccess.objects.get(
document=instance, user=request.user
)
return access.encrypted_document_symmetric_key_for_user
except models.DocumentAccess.DoesNotExist:
return None
class DocumentLightSerializer(serializers.ModelSerializer):
"""Minial document serializer for nesting in document accesses."""
@@ -165,6 +211,7 @@ class DocumentSerializer(ListDocumentSerializer):
"""Serialize documents with all fields for display in detail views."""
content = serializers.CharField(required=False)
contentEncrypted = serializers.BooleanField(required=False, write_only=True)
websocket = serializers.BooleanField(required=False, write_only=True)
file = serializers.FileField(
required=False, write_only=True, allow_null=True, max_length=255
@@ -175,18 +222,23 @@ class DocumentSerializer(ListDocumentSerializer):
fields = [
"id",
"abilities",
"accesses_fingerprints_per_user",
"accesses_user_ids",
"ancestors_link_reach",
"ancestors_link_role",
"computed_link_reach",
"computed_link_role",
"content",
"contentEncrypted",
"created_at",
"creator",
"deleted_at",
"depth",
"excerpt",
"encrypted_document_symmetric_key_for_user",
"file",
"is_favorite",
"is_encrypted",
"link_role",
"link_reach",
"nb_accesses_ancestors",
@@ -209,7 +261,9 @@ class DocumentSerializer(ListDocumentSerializer):
"creator",
"deleted_at",
"depth",
"encrypted_document_symmetric_key_for_user",
"is_favorite",
"is_encrypted",
"link_role",
"link_reach",
"nb_accesses_ancestors",
@@ -228,6 +282,11 @@ class DocumentSerializer(ListDocumentSerializer):
if request and request.method == "POST":
fields["id"].read_only = False
# if user is not authenticated remove public keys information since he can still retrieve the document
if request and not request.user.is_authenticated:
fields.pop("accesses_user_ids", None)
fields.pop("encrypted_document_symmetric_key_for_user", None)
return fields
def validate_id(self, value):
@@ -285,7 +344,15 @@ class DocumentSerializer(ListDocumentSerializer):
"attachments" field for access control.
"""
content = self.validated_data.get("content", "")
extracted_attachments = set(utils.extract_attachments(content))
# Encrypted content cannot be parsed as a Yjs update
# TODO: for now skip attachment extraction for encrypted documents but we should have them
is_encrypted = self.validated_data.get(
"is_encrypted", self.instance and self.instance.is_encrypted
)
extracted_attachments = (
set() if is_encrypted else set(utils.extract_attachments(content))
)
existing_attachments = (
set(self.instance.attachments or []) if self.instance else set()
@@ -343,6 +410,12 @@ class DocumentAccessSerializer(serializers.ModelSerializer):
abilities = serializers.SerializerMethodField(read_only=True)
max_ancestors_role = serializers.SerializerMethodField(read_only=True)
max_role = serializers.SerializerMethodField(read_only=True)
encrypted_document_symmetric_key_for_user = serializers.CharField(
required=False, allow_blank=True, write_only=True
)
encryption_public_key_fingerprint = serializers.CharField(
required=False, allow_blank=True, max_length=16
)
class Meta:
model = models.DocumentAccess
@@ -357,6 +430,8 @@ class DocumentAccessSerializer(serializers.ModelSerializer):
"abilities",
"max_ancestors_role",
"max_role",
"encrypted_document_symmetric_key_for_user",
"encryption_public_key_fingerprint",
]
read_only_fields = [
"id",
@@ -366,6 +441,29 @@ class DocumentAccessSerializer(serializers.ModelSerializer):
"max_role",
]
def get_fields(self):
"""Dynamically control field availability and requirements based on document encryption status."""
fields = super().get_fields()
# Get the document from context (if available)
document = None
if "view" in self.context and hasattr(self.context["view"], "document"):
document = self.context["view"].document
# Get the encrypted_document_symmetric_key_for_user field
key_field = fields.get("encrypted_document_symmetric_key_for_user")
if key_field:
# If document is encrypted, make the field required
if document and getattr(document, "is_encrypted", False):
key_field.required = True
key_field.allow_blank = False
# If document is not encrypted, remove the field entirely
elif document and not getattr(document, "is_encrypted", False):
fields.pop("encrypted_document_symmetric_key_for_user", None)
return fields
def get_abilities(self, instance) -> dict:
"""Return abilities of the logged-in user on the instance."""
request = self.context.get("request")
@@ -616,6 +714,7 @@ class FileUploadSerializer(serializers.Serializer):
"""Receive file upload requests."""
file = serializers.FileField()
is_encrypted = serializers.BooleanField(default=False, required=False)
def validate_file(self, file):
"""Add file size and type constraints as defined in settings."""
@@ -626,6 +725,22 @@ class FileUploadSerializer(serializers.Serializer):
f"File size exceeds the maximum limit of {max_size:d} MB."
)
# For encrypted files, the content is ciphertext so MIME detection
# is not possible. Trust the original filename extension.
if self.initial_data.get("is_encrypted") in ("true", "True", True):
extension = (
file.name.rpartition(".")[-1] if "." in file.name else None
)
if extension is None or len(extension) > 5:
raise serializers.ValidationError(
"Could not determine file extension."
)
self.context["expected_extension"] = extension
self.context["content_type"] = "application/octet-stream"
self.context["is_unsafe"] = False
self.context["file_name"] = file.name
return file
extension = file.name.rpartition(".")[-1] if "." in file.name else None
# Read the first few bytes to determine the MIME type accurately
@@ -856,6 +971,65 @@ class MoveDocumentSerializer(serializers.Serializer):
)
class EncryptDocumentSerializer(serializers.Serializer):
"""
Serializer for encrypting a document.
Fields:
- content (CharField): The encrypted content of the document.
This field is required.
- encryptedSymmetricKeyPerUser (DictField): Mapping of user IDs to their encrypted symmetric keys.
This field is required.
Example:
Input payload for encrypting a document:
{
"content": "<encrypted_content>",
"encryptedSymmetricKeyPerUser": {
"user1_id": "encrypted_key_1",
"user2_id": "encrypted_key_2"
}
}
"""
content = serializers.CharField(required=True)
encryptedSymmetricKeyPerUser = serializers.DictField(child=serializers.CharField(), required=True)
attachmentKeyMapping = serializers.DictField(
child=serializers.CharField(),
required=False,
default=dict,
help_text="Mapping of original attachment key to new encrypted attachment key. "
"During encryption, existing attachments are uploaded encrypted under new keys. "
"This mapping tells the backend to copy each new key over the original and clean up.",
)
class RemoveEncryptionSerializer(serializers.Serializer):
"""
Serializer for removing encryption from a document.
Fields:
- content (CharField): The decrypted content of the document.
This field is required.
Example:
Input payload for removing encryption from a document:
{
"content": "<decrypted_content>"
}
"""
content = serializers.CharField(required=True)
attachmentKeyMapping = serializers.DictField(
child=serializers.CharField(),
required=False,
default=dict,
help_text="Mapping of old encrypted attachment key to new decrypted attachment key. "
"During decryption, encrypted attachments are re-uploaded decrypted under new keys. "
"This mapping tells the backend to remove the old keys and clean up.",
)
class ReactionSerializer(serializers.ModelSerializer):
"""Serialize reactions."""
+333 -6
View File
@@ -168,6 +168,10 @@ class UserViewSet(
):
"""User ViewSet"""
#
# TODO: adjust update public key
#
permission_classes = [permissions.IsSelf]
queryset = models.User.objects.filter(is_active=True)
serializer_class = serializers.UserSerializer
@@ -354,6 +358,19 @@ class DocumentViewSet(
Returns: JSON response with the translated text.
Throttled by: AIDocumentRateThrottle, AIUserRateThrottle.
12. **Encrypt**: Encrypt a document.
Example: PATCH /documents/{id}/encrypt/
Expected data:
- content (str): The encrypted content.
- encryptedSymmetricKeyPerUser (dict): Mapping of user IDs to encrypted symmetric keys.
Returns: JSON response with the updated document.
13. **Remove Encryption**: Remove encryption from a document.
Example: PATCH /documents/{id}/remove-encryption/
Expected data:
- content (str): The decrypted content.
Returns: JSON response with the updated document.
### Ordering: created_at, updated_at, is_favorite, title
Example:
@@ -365,11 +382,18 @@ class DocumentViewSet(
- `is_creator_me=false`: Returns documents created by other users.
- `is_favorite=true`: Returns documents marked as favorite by the current user
- `is_favorite=false`: Returns documents not marked as favorite by the current user
- `is_encrypted=true`: Returns documents encrypted
- `is_encrypted=false`: Returns documents not encrypted
- `title=hello`: Returns documents which title contains the "hello" string
Example:
- GET /api/v1.0/documents/?is_creator_me=true&is_favorite=true
- GET /api/v1.0/documents/?is_creator_me=false&title=hello
- GET /api/v1.0/documents/?is_creator_me=false&title=hello&is_encrypted=false
### Encryption Management:
The encryption status of documents can be managed using the dedicated endpoints:
- PATCH /documents/{id}/encrypt/ - Set is_encrypted to true
- PATCH /documents/{id}/remove-encryption/ - Set is_encrypted to false
### Annotations:
1. **is_favorite**: Indicates whether the document is marked as favorite by the current user.
@@ -613,6 +637,20 @@ class DocumentViewSet(
def perform_update(self, serializer):
"""Check rules about collaboration."""
content_encrypted = serializer.validated_data.pop("contentEncrypted", None)
if (
content_encrypted is not None
and content_encrypted != serializer.instance.is_encrypted
):
raise drf.exceptions.ValidationError(
{
"contentEncrypted": (
"Content encryption status does not match the document's "
"current state. Please refresh and try again."
)
}
)
if (
serializer.validated_data.get("websocket", False)
or not settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY
@@ -1347,6 +1385,14 @@ class DocumentViewSet(
# Check permissions first
document = self.get_object()
if document.is_encrypted:
raise drf.exceptions.ValidationError(
{
"detail": "Visibility cannot be changed for encrypted documents. "
"Encrypted documents must remain restricted.",
}
)
# Deserialize and validate the data
serializer = serializers.LinkDocumentSerializer(
document, data=request.data, partial=True
@@ -1442,18 +1488,34 @@ class DocumentViewSet(
serializer = serializers.FileUploadSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# Generate a generic yet unique filename to store the image in object storage
file_id = uuid.uuid4()
ext = serializer.validated_data["expected_extension"]
# Normally encrypted attachments would be only allowed on encrypted documents and vice-versa
# but since during encryption/decryption we upload all attachments before the switch, we cannot enforce this rule
is_file_encrypted = serializer.validated_data.get("is_encrypted", False)
# For encrypted files, set status to READY immediately since the server
# cannot inspect ciphertext for malware scanning.
initial_status = (
enums.DocumentAttachmentStatus.READY
if is_file_encrypted
else enums.DocumentAttachmentStatus.PROCESSING
)
# Prepare metadata for storage
extra_args = {
"Metadata": {
"owner": str(request.user.id),
"status": enums.DocumentAttachmentStatus.PROCESSING,
"status": initial_status,
},
"ContentType": serializer.validated_data["content_type"],
}
if is_file_encrypted:
extra_args["Metadata"]["is_encrypted"] = "true"
# Generate a generic yet unique filename to store the image in object storage
file_id = uuid.uuid4()
ext = serializer.validated_data["expected_extension"]
file_unsafe = ""
if serializer.validated_data["is_unsafe"]:
extra_args["Metadata"]["is_unsafe"] = "true"
@@ -1483,7 +1545,9 @@ class DocumentViewSet(
document.attachments.append(key)
document.save()
malware_detection.analyse_file(key, document_id=document.id)
# Only run malware scan for unencrypted files
if not is_file_encrypted:
malware_detection.analyse_file(key, document_id=document.id)
url = reverse(
"documents-media-check",
@@ -1937,6 +2001,228 @@ class DocumentViewSet(
}
)
def perform_update(self, serializer):
"""
Perform update with safety check for encryption state changes.
If contentEncrypted parameter is provided, it must match the current
is_encrypted state to prevent accidental content overrides during
encryption state transitions.
"""
document = self.get_object()
# Prevent direct changes to is_encrypted field via PATCH
# (encryption state should only be changed via /encrypt/ or /remove-encryption/ endpoints)
if 'is_encrypted' in serializer.validated_data:
raise drf.exceptions.ValidationError({
'is_encrypted':
'Cannot modify is_encrypted directly. '
'Use the /encrypt/ or /remove-encryption/ endpoints to manage encryption.'
})
# Check if contentEncrypted parameter was provided
content_encrypted = serializer.validated_data.get('contentEncrypted')
if content_encrypted is not None:
# Get the current document instance
document = self.get_object()
# Safety check: contentEncrypted must match current is_encrypted state
if content_encrypted != document.is_encrypted:
raise drf.exceptions.ValidationError({
'contentEncrypted':
f'contentEncrypted must match current encryption state. '
f'Current: is_encrypted={document.is_encrypted}, '
f'Provided: contentEncrypted={content_encrypted}'
})
# Proceed with normal update
return super().perform_update(serializer)
@transaction.atomic
@drf.decorators.action(
detail=True,
methods=["patch"],
name="Encrypt a document",
url_path="encrypt",
)
def encrypt(self, request, *args, **kwargs):
"""
PATCH /api/v1.0/documents/<resource_id>/encrypt/
with expected data:
- content: str (encrypted content)
- encryptedSymmetricKeyPerUser: dict (user_id -> encrypted_key)
Updates the document's content and marks it as encrypted.
"""
document = self.get_object()
serializer = serializers.EncryptDocumentSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
content = serializer.validated_data["content"]
encryptedSymmetricKeyPerUser = serializer.validated_data["encryptedSymmetricKeyPerUser"]
attachment_key_mapping = serializer.validated_data.get("attachmentKeyMapping", {})
# Prevent encryption if the document is not restricted (private)
if document.computed_link_reach != models.LinkReachChoices.RESTRICTED:
raise drf.exceptions.ValidationError({
'non_field_errors':
'Cannot encrypt a document that is not private. '
'Please set the document access to "Restricted" before encrypting.'
})
# Prevent encryption if there are pending invitations
if document.invitations.exists():
raise drf.exceptions.ValidationError({
'non_field_errors':
'Cannot encrypt a document with pending invitations. '
'Please resolve all invitations before encrypting.'
})
# Validate that all users with access have an encryption public key
document_accesses = models.DocumentAccess.objects.filter(
document=document, user__isnull=False
).select_related('user')
users_without_public_key = [
access.user.email or str(access.user_id)
for access in document_accesses
if not access.user.encryption_public_key
]
if users_without_public_key:
raise drf.exceptions.ValidationError({
'non_field_errors':
'Cannot encrypt a document when some members have not enabled '
'encryption: ' + ', '.join(users_without_public_key) + '. '
'All members must enable encryption in their account settings first.'
})
# Validate that we have encrypted symmetric keys for all users with access
# Keys in encryptedSymmetricKeyPerUser are keyed by the user's OIDC `sub`
users_with_access = {str(access.user.sub) for access in document_accesses}
# Check that encryptedSymmetricKeyPerUser contains all required users
provided_user_ids = set(encryptedSymmetricKeyPerUser.keys())
missing_users = users_with_access - provided_user_ids
if missing_users:
raise drf.exceptions.ValidationError({
'encryptedSymmetricKeyPerUser':
f'Missing encrypted keys for users with document access: {missing_users}. '
f'All users must have encrypted symmetric keys when encrypting a document.'
})
# Check for extra users that don't have access
extra_users = provided_user_ids - users_with_access
if extra_users:
raise drf.exceptions.ValidationError({
'encryptedSymmetricKeyPerUser':
f'Encrypted keys provided for users without document access: {extra_users}. '
f'Only users with access should have encrypted symmetric keys.'
})
# Remove old unencrypted attachment keys from the allowed list.
# The frontend uploaded encrypted copies under new keys and updated the
# Yjs content to reference them.
if attachment_key_mapping:
old_keys = set(attachment_key_mapping.keys())
document.attachments = [
k for k in (document.attachments or []) if k not in old_keys
]
# Update the document content and encryption status
document.content = content # This will be cached and saved to object storage
document.is_encrypted = True
document.save()
# Clean up old S3 objects only after the DB transaction has committed,
# so a deletion failure can never affect the encrypt operation.
if attachment_key_mapping:
def _cleanup_old_attachments():
s3_client = default_storage.connection.meta.client
bucket_name = default_storage.bucket_name
for old_key in attachment_key_mapping:
try:
s3_client.delete_object(Bucket=bucket_name, Key=old_key)
except ClientError:
logger.warning("Failed to delete old attachment %s", old_key)
transaction.on_commit(_cleanup_old_attachments)
# Store the encrypted symmetric keys in DocumentAccess for each user
# Keys are keyed by the user's OIDC `sub`, so look up by user__sub
for sub, encrypted_key in encryptedSymmetricKeyPerUser.items():
try:
# Find the DocumentAccess record for this user and document
access = models.DocumentAccess.objects.get(document=document, user__sub=sub)
access.encrypted_document_symmetric_key_for_user = encrypted_key
access.save()
except models.DocumentAccess.DoesNotExist:
# This should not happen due to our validation above, but keep as safety
pass
# Return the updated document
serializer = self.get_serializer(document)
return drf.response.Response(serializer.data, status=drf.status.HTTP_200_OK)
@transaction.atomic
@drf.decorators.action(
detail=True,
methods=["patch"],
name="Remove encryption from a document",
url_path="remove-encryption",
)
def remove_encryption(self, request, *args, **kwargs):
"""
PATCH /api/v1.0/documents/<resource_id>/remove-encryption/
with expected data:
- content: str (decrypted content)
Updates the document's content and marks it as not encrypted.
"""
document = self.get_object()
serializer = serializers.RemoveEncryptionSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
content = serializer.validated_data["content"]
attachment_key_mapping = serializer.validated_data.get("attachmentKeyMapping", {})
# Remove old encrypted attachment keys from the allowed list.
# The frontend uploaded decrypted copies under new keys and updated
# the Yjs content to reference them.
if attachment_key_mapping:
old_keys = set(attachment_key_mapping.keys())
document.attachments = [
k for k in (document.attachments or []) if k not in old_keys
]
# Update the document content and encryption status
document.content = content # This will be cached and saved to object storage
document.is_encrypted = False
document.save()
# Clean up any stored encrypted keys
models.DocumentAccess.objects.filter(document=document).update(
encrypted_document_symmetric_key_for_user=None
)
# Clean up old S3 objects only after the DB transaction has committed
if attachment_key_mapping:
def _cleanup_old_attachments():
s3_client = default_storage.connection.meta.client
bucket_name = default_storage.bucket_name
for old_key in attachment_key_mapping:
try:
s3_client.delete_object(Bucket=bucket_name, Key=old_key)
except ClientError:
logger.warning("Failed to delete old attachment %s", old_key)
transaction.on_commit(_cleanup_old_attachments)
# Return the updated document
serializer = self.get_serializer(document)
return drf.response.Response(serializer.data, status=drf.status.HTTP_200_OK)
class DocumentAccessViewSet(
ResourceAccessViewsetMixin,
@@ -2098,6 +2384,16 @@ class DocumentAccessViewSet(
"Only owners of a document can assign other users as owners."
)
# Handle encrypted_document_symmetric_key_for_user during creation
if 'encrypted_document_symmetric_key_for_user' in serializer.validated_data:
if not self.document.is_encrypted:
raise drf.exceptions.ValidationError({
'encrypted_document_symmetric_key_for_user':
'This field can only be provided when the document is encrypted.'
})
# For encrypted documents, allow the key to be provided
# The key will be stored directly in the DocumentAccess record
access = serializer.save(document_id=self.kwargs["resource_id"])
if access.user:
@@ -2112,6 +2408,14 @@ class DocumentAccessViewSet(
def perform_update(self, serializer):
"""Update an access to the document and notify the collaboration server."""
# Prevent direct modification of encrypted_document_symmetric_key_for_user
# This field should only be managed at access creation or when rotating the document key
if 'encrypted_document_symmetric_key_for_user' in serializer.validated_data:
raise drf.exceptions.ValidationError({
'encrypted_document_symmetric_key_for_user':
'This field cannot be modified directly.'
})
access = serializer.save()
access_user_id = None
@@ -2221,6 +2525,15 @@ class InvitationViewset(
def perform_create(self, serializer):
"""Save invitation to a document then send an email to the invited user."""
# Prevent invitation creation for encrypted documents
document = models.Document.objects.get(pk=self.kwargs["resource_id"])
if document.is_encrypted:
raise drf.exceptions.ValidationError({
'non_field_errors':
'Cannot create invitations for encrypted documents. '
'All invitations must be resolved before encrypting a document.'
})
invitation = serializer.save()
invitation.document.send_invitation_email(
@@ -2230,6 +2543,19 @@ class InvitationViewset(
self.request.user.language or settings.LANGUAGE_CODE,
)
def perform_update(self, serializer):
"""Update an invitation to a document."""
# Prevent invitation updates for encrypted documents
document = models.Document.objects.get(pk=self.kwargs["resource_id"])
if document.is_encrypted:
raise drf.exceptions.ValidationError({
'non_field_errors':
'Cannot update invitations for encrypted documents. '
'All invitations must be resolved before encrypting a document.'
})
return super().perform_update(serializer)
class DocumentAskForAccessViewSet(
drf.mixins.ListModelMixin,
@@ -2347,6 +2673,7 @@ class ConfigView(drf.views.APIView):
"FRONTEND_CSS_URL",
"FRONTEND_HOMEPAGE_FEATURE_ENABLED",
"FRONTEND_JS_URL",
"FRONTEND_SILENT_LOGIN_ENABLED",
"FRONTEND_THEME",
"MEDIA_BASE_URL",
"POSTHOG_KEY",
@@ -0,0 +1,28 @@
# Generated by Django 5.2.10 on 2026-02-23 10:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0028_remove_templateaccess_template_and_more'),
]
operations = [
migrations.AddField(
model_name='document',
name='is_encrypted',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='documentaccess',
name='encrypted_document_symmetric_key_for_user',
field=models.TextField(blank=True, help_text='Encrypted symmetric key for this document, specific to this user.', null=True, verbose_name='encrypted document symmetric key'),
),
migrations.AddField(
model_name='user',
name='encryption_public_key',
field=models.TextField(blank=True, help_text='Public key for end-to-end encryption.', null=True, verbose_name='encryption public key'),
),
]
@@ -0,0 +1,34 @@
"""Add encryption_public_key_fingerprint to BaseAccess (DocumentAccess).
Stores the fingerprint of the user's public key at the time of sharing,
allowing the frontend to detect key changes without relying solely on
client-side TOFU. If the user's current key fingerprint differs from
this stored value, the document access needs re-encryption.
"""
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0029_document_is_encrypted_and_more"),
]
operations = [
migrations.AddField(
model_name="documentaccess",
name="encryption_public_key_fingerprint",
field=models.CharField(
blank=True,
help_text=(
"Fingerprint of the user's public key at the time of sharing. "
"Used to detect key changes — if the user's current public key "
"fingerprint differs from this value, the access needs re-encryption."
),
max_length=16,
null=True,
verbose_name="encryption public key fingerprint",
),
),
]
@@ -0,0 +1,25 @@
"""Remove encryption_public_key from User model.
Public keys are now managed by the centralized encryption service.
Products should fetch public keys from the encryption service's API
when needed (e.g. for encrypting a document for multiple users).
The fingerprint of the public key at share time is stored on
DocumentAccess.encryption_public_key_fingerprint (added in 0030).
"""
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("core", "0030_baseaccess_encryption_public_key_fingerprint"),
]
operations = [
migrations.RemoveField(
model_name="user",
name="encryption_public_key",
),
]
+53
View File
@@ -279,6 +279,23 @@ class BaseAccess(BaseModel):
role = models.CharField(
max_length=20, choices=RoleChoices.choices, default=RoleChoices.READER
)
encrypted_document_symmetric_key_for_user = models.TextField(
_("encrypted document symmetric key"),
null=True,
blank=True,
help_text=_("Encrypted symmetric key for this document, specific to this user."),
)
encryption_public_key_fingerprint = models.CharField(
_("encryption public key fingerprint"),
max_length=16,
null=True,
blank=True,
help_text=_(
"Fingerprint of the user's public key at the time of sharing. "
"Used to detect key changes — if the user's current public key "
"fingerprint differs from this value, the access needs re-encryption."
),
)
class Meta:
abstract = True
@@ -361,6 +378,7 @@ class Document(MP_Node, BaseModel):
title = models.CharField(_("title"), max_length=255, null=True, blank=True)
excerpt = models.TextField(_("excerpt"), max_length=300, null=True, blank=True)
is_encrypted = models.BooleanField(default=False)
link_reach = models.CharField(
max_length=20,
choices=LinkReachChoices.choices,
@@ -718,6 +736,39 @@ class Document(MP_Node, BaseModel):
"""Actual link role on the document."""
return self.computed_link_definition["link_role"]
@property
def accesses_user_ids(self):
"""
Return the list of user IDs with access to this document.
The frontend uses these IDs to fetch public keys from the
centralized encryption service.
"""
return list(
DocumentAccess.objects
.filter(document=self, user__isnull=False)
.values_list('user__sub', flat=True)
.distinct()
)
@property
def accesses_fingerprints_per_user(self):
"""
Return the fingerprint of each user's public key at the time of sharing.
This allows the frontend to detect key changes by comparing the
fingerprint stored at share time with the current public key fingerprint.
"""
accesses = (
DocumentAccess.objects
.filter(document=self, user__isnull=False, encryption_public_key_fingerprint__isnull=False)
.values_list('user__sub', 'encryption_public_key_fingerprint')
)
return {
str(sub): fingerprint
for sub, fingerprint in accesses
if fingerprint
}
def get_abilities(self, user):
"""
Compute and return abilities for a given user on the document.
@@ -797,12 +848,14 @@ class Document(MP_Node, BaseModel):
"descendants": can_get,
"destroy": can_destroy,
"duplicate": can_get and user.is_authenticated,
"encrypt": is_owner_or_admin,
"favorite": can_get and user.is_authenticated,
"link_configuration": is_owner_or_admin,
"invite_owner": is_owner and not is_deleted,
"mask": can_get and user.is_authenticated,
"move": is_owner_or_admin and not is_deleted,
"partial_update": can_update,
"remove_encryption": is_owner_or_admin,
"restore": is_owner,
"retrieve": retrieve,
"media_auth": can_get,
+6 -1
View File
@@ -244,7 +244,12 @@ class SearchIndexer(BaseDocumentIndexer):
"""
doc_path = document.path
doc_content = document.content
text_content = utils.base64_yjs_to_text(doc_content) if doc_content else ""
# Encrypted content is ciphertext and it should never be indexed for search
if document.is_encrypted:
text_content = ""
else:
text_content = utils.base64_yjs_to_text(doc_content) if doc_content else ""
return {
"id": str(document.id),
@@ -351,6 +351,7 @@ def test_api_documents_all_format():
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": document.link_reach,
"link_role": document.link_role,
"nb_accesses_ancestors": 1,
@@ -46,6 +46,7 @@ def test_api_documents_children_list_anonymous_public_standalone(
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_encrypted": child1.is_encrypted,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -69,6 +70,7 @@ def test_api_documents_children_list_anonymous_public_standalone(
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_encrypted": child2.is_encrypted,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -122,6 +124,7 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_encrypted": child1.is_encrypted,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -145,6 +148,7 @@ def test_api_documents_children_list_anonymous_public_parent(django_assert_num_q
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_encrypted": child2.is_encrypted,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -217,6 +221,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_encrypted": child1.is_encrypted,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -240,6 +245,7 @@ def test_api_documents_children_list_authenticated_unrelated_public_or_authentic
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_encrypted": child2.is_encrypted,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -298,6 +304,7 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_encrypted": child1.is_encrypted,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -321,6 +328,7 @@ def test_api_documents_children_list_authenticated_public_or_authenticated_paren
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_encrypted": child2.is_encrypted,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -406,6 +414,7 @@ def test_api_documents_children_list_authenticated_related_direct(
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_encrypted": child1.is_encrypted,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -429,6 +438,7 @@ def test_api_documents_children_list_authenticated_related_direct(
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_encrypted": child2.is_encrypted,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -490,6 +500,7 @@ def test_api_documents_children_list_authenticated_related_parent(
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_encrypted": child1.is_encrypted,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -513,6 +524,7 @@ def test_api_documents_children_list_authenticated_related_parent(
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_encrypted": child2.is_encrypted,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -626,6 +638,7 @@ def test_api_documents_children_list_authenticated_related_team_members(
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_encrypted": child1.is_encrypted,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 0,
@@ -649,6 +662,7 @@ def test_api_documents_children_list_authenticated_related_team_members(
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_encrypted": child2.is_encrypted,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -43,6 +43,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_encrypted": child1.is_encrypted,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 1,
@@ -68,6 +69,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
"is_favorite": False,
"is_encrypted": grand_child.is_encrypted,
"link_reach": grand_child.link_reach,
"link_role": grand_child.link_role,
"numchild": 0,
@@ -91,6 +93,7 @@ def test_api_documents_descendants_list_anonymous_public_standalone():
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_encrypted": child2.is_encrypted,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -143,6 +146,7 @@ def test_api_documents_descendants_list_anonymous_public_parent():
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_encrypted": child1.is_encrypted,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 1,
@@ -166,6 +170,7 @@ def test_api_documents_descendants_list_anonymous_public_parent():
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
"is_favorite": False,
"is_encrypted": grand_child.is_encrypted,
"link_reach": grand_child.link_reach,
"link_role": grand_child.link_role,
"numchild": 0,
@@ -189,6 +194,7 @@ def test_api_documents_descendants_list_anonymous_public_parent():
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_encrypted": child2.is_encrypted,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -262,6 +268,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_encrypted": child1.is_encrypted,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 1,
@@ -285,6 +292,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
"is_favorite": False,
"is_encrypted": grand_child.is_encrypted,
"link_reach": grand_child.link_reach,
"link_role": grand_child.link_role,
"numchild": 0,
@@ -308,6 +316,7 @@ def test_api_documents_descendants_list_authenticated_unrelated_public_or_authen
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_encrypted": child2.is_encrypted,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -366,6 +375,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_encrypted": child1.is_encrypted,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 1,
@@ -389,6 +399,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
"is_favorite": False,
"is_encrypted": grand_child.is_encrypted,
"link_reach": grand_child.link_reach,
"link_role": grand_child.link_role,
"numchild": 0,
@@ -412,6 +423,7 @@ def test_api_documents_descendants_list_authenticated_public_or_authenticated_pa
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_encrypted": child2.is_encrypted,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -491,6 +503,7 @@ def test_api_documents_descendants_list_authenticated_related_direct():
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_encrypted": child1.is_encrypted,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 1,
@@ -514,6 +527,7 @@ def test_api_documents_descendants_list_authenticated_related_direct():
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
"is_favorite": False,
"is_encrypted": grand_child.is_encrypted,
"link_reach": grand_child.link_reach,
"link_role": grand_child.link_role,
"numchild": 0,
@@ -537,6 +551,7 @@ def test_api_documents_descendants_list_authenticated_related_direct():
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_encrypted": child2.is_encrypted,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -596,6 +611,7 @@ def test_api_documents_descendants_list_authenticated_related_parent():
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_encrypted": child1.is_encrypted,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 1,
@@ -619,6 +635,7 @@ def test_api_documents_descendants_list_authenticated_related_parent():
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
"is_favorite": False,
"is_encrypted": grand_child.is_encrypted,
"link_reach": grand_child.link_reach,
"link_role": grand_child.link_role,
"numchild": 0,
@@ -642,6 +659,7 @@ def test_api_documents_descendants_list_authenticated_related_parent():
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_encrypted": child2.is_encrypted,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -747,6 +765,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
"excerpt": child1.excerpt,
"id": str(child1.id),
"is_favorite": False,
"is_encrypted": child1.is_encrypted,
"link_reach": child1.link_reach,
"link_role": child1.link_role,
"numchild": 1,
@@ -770,6 +789,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
"excerpt": grand_child.excerpt,
"id": str(grand_child.id),
"is_favorite": False,
"is_encrypted": grand_child.is_encrypted,
"link_reach": grand_child.link_reach,
"link_role": grand_child.link_role,
"numchild": 0,
@@ -793,6 +813,7 @@ def test_api_documents_descendants_list_authenticated_related_team_members(
"excerpt": child2.excerpt,
"id": str(child2.id),
"is_favorite": False,
"is_encrypted": child2.is_encrypted,
"link_reach": child2.link_reach,
"link_role": child2.link_role,
"numchild": 0,
@@ -71,6 +71,7 @@ def test_api_document_favorite_list_authenticated_with_favorite():
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": True,
"is_encrypted": document.is_encrypted,
"link_reach": document.link_reach,
"link_role": document.link_role,
"nb_accesses_ancestors": 1,
@@ -73,6 +73,7 @@ def test_api_documents_list_format():
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": True,
"is_encrypted": document.is_encrypted,
"link_reach": document.link_reach,
"link_role": document.link_role,
"nb_accesses_ancestors": 3,
@@ -312,6 +312,69 @@ def test_api_documents_list_filter_is_favorite_invalid():
assert len(results) == 5
# Filters: is_encrypted
def test_api_documents_list_filter_is_encrypted_true():
"""
Authenticated users should be able to filter encrypted documents.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
factories.DocumentFactory.create_batch(3, users=[user])
factories.DocumentFactory.create_batch(2, users=[user])
response = client.get("/api/v1.0/documents/?is_encrypted=true")
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 3
# Ensure all results are encrypted
for result in results:
assert result["is_encrypted"] is True
def test_api_documents_list_filter_is_encrypted_false():
"""
Authenticated users should be able to filter documents not encrypted.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
factories.DocumentFactory.create_batch(3, users=[user])
factories.DocumentFactory.create_batch(2, users=[user])
response = client.get("/api/v1.0/documents/?is_encrypted=false")
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 2
# Ensure all results are not encrypted
for result in results:
assert result["is_encrypted"] is False
def test_api_documents_list_filter_is_encrypted_invalid():
"""Filtering with an invalid `is_encrypted` value should do nothing."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
factories.DocumentFactory.create_batch(3, users=[user])
factories.DocumentFactory.create_batch(2, users=[user])
response = client.get("/api/v1.0/documents/?is_encrypted=invalid")
assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 5
# Filters: is_masked
@@ -75,6 +75,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": "public",
"link_role": document.link_role,
"nb_accesses_ancestors": 0,
@@ -151,6 +152,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
"depth": 3,
"excerpt": document.excerpt,
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": document.link_reach,
"link_role": document.link_role,
"nb_accesses_ancestors": 0,
@@ -260,6 +262,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"deleted_at": None,
"excerpt": document.excerpt,
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": reach,
"link_role": document.link_role,
"nb_accesses_ancestors": 0,
@@ -343,6 +346,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"deleted_at": None,
"excerpt": document.excerpt,
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": document.link_reach,
"link_role": document.link_role,
"nb_accesses_ancestors": 0,
@@ -458,6 +462,7 @@ def test_api_documents_retrieve_authenticated_related_direct():
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": document.link_reach,
"link_role": document.link_role,
"nb_accesses_ancestors": 2,
@@ -541,6 +546,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
"deleted_at": None,
"excerpt": document.excerpt,
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": "restricted",
"link_role": document.link_role,
"nb_accesses_ancestors": 2,
@@ -698,6 +704,7 @@ def test_api_documents_retrieve_authenticated_related_team_members(
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": "restricted",
"link_role": document.link_role,
"nb_accesses_ancestors": 5,
@@ -765,6 +772,7 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": "restricted",
"link_role": document.link_role,
"nb_accesses_ancestors": 5,
@@ -832,6 +840,7 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
"depth": 1,
"excerpt": document.excerpt,
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": "restricted",
"link_role": document.link_role,
"nb_accesses_ancestors": 5,
@@ -54,6 +54,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
"is_encrypted": child.is_encrypted,
"link_reach": child.link_reach,
"link_role": child.link_role,
"numchild": 0,
@@ -78,6 +79,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": document.link_reach,
"link_role": document.link_role,
"numchild": 1,
@@ -102,6 +104,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
"excerpt": sibling1.excerpt,
"id": str(sibling1.id),
"is_favorite": False,
"is_encrypted": sibling1.is_encrypted,
"link_reach": sibling1.link_reach,
"link_role": sibling1.link_role,
"numchild": 0,
@@ -126,6 +129,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
"excerpt": sibling2.excerpt,
"id": str(sibling2.id),
"is_favorite": False,
"is_encrypted": sibling2.is_encrypted,
"link_reach": sibling2.link_reach,
"link_role": sibling2.link_role,
"numchild": 0,
@@ -146,6 +150,7 @@ def test_api_documents_tree_list_anonymous_public_standalone(django_assert_num_q
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
"is_encrypted": parent.is_encrypted,
"link_reach": parent.link_reach,
"link_role": parent.link_role,
"numchild": 3,
@@ -219,6 +224,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
"is_encrypted": child.is_encrypted,
"link_reach": child.link_reach,
"link_role": child.link_role,
"numchild": 0,
@@ -243,6 +249,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": document.link_reach,
"link_role": document.link_role,
"numchild": 1,
@@ -271,6 +278,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
"excerpt": document_sibling.excerpt,
"id": str(document_sibling.id),
"is_favorite": False,
"is_encrypted": document_sibling.is_encrypted,
"link_reach": document_sibling.link_reach,
"link_role": document_sibling.link_role,
"numchild": 0,
@@ -293,6 +301,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
"is_encrypted": parent.is_encrypted,
"link_reach": parent.link_reach,
"link_role": parent.link_role,
"numchild": 2,
@@ -319,6 +328,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
"excerpt": parent_sibling.excerpt,
"id": str(parent_sibling.id),
"is_favorite": False,
"is_encrypted": parent_sibling.is_encrypted,
"link_reach": parent_sibling.link_reach,
"link_role": parent_sibling.link_role,
"numchild": 0,
@@ -341,6 +351,7 @@ def test_api_documents_tree_list_anonymous_public_parent():
"excerpt": grand_parent.excerpt,
"id": str(grand_parent.id),
"is_favorite": False,
"is_encrypted": grand_parent.is_encrypted,
"link_reach": grand_parent.link_reach,
"link_role": grand_parent.link_role,
"numchild": 2,
@@ -421,6 +432,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
"is_encrypted": child.is_encrypted,
"link_reach": child.link_reach,
"link_role": child.link_role,
"numchild": 0,
@@ -443,6 +455,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": document.link_reach,
"link_role": document.link_role,
"numchild": 1,
@@ -467,6 +480,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
"excerpt": sibling.excerpt,
"id": str(sibling.id),
"is_favorite": False,
"is_encrypted": sibling.is_encrypted,
"link_reach": sibling.link_reach,
"link_role": sibling.link_role,
"numchild": 0,
@@ -487,6 +501,7 @@ def test_api_documents_tree_list_authenticated_unrelated_public_or_authenticated
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
"is_encrypted": parent.is_encrypted,
"link_reach": parent.link_reach,
"link_role": parent.link_role,
"numchild": 2,
@@ -565,6 +580,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
"is_encrypted": child.is_encrypted,
"link_reach": child.link_reach,
"link_role": child.link_role,
"numchild": 0,
@@ -589,6 +605,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": document.link_reach,
"link_role": document.link_role,
"numchild": 1,
@@ -617,6 +634,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
"excerpt": document_sibling.excerpt,
"id": str(document_sibling.id),
"is_favorite": False,
"is_encrypted": document_sibling.is_encrypted,
"link_reach": document_sibling.link_reach,
"link_role": document_sibling.link_role,
"numchild": 0,
@@ -639,6 +657,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
"is_encrypted": parent.is_encrypted,
"link_reach": parent.link_reach,
"link_role": parent.link_role,
"numchild": 2,
@@ -665,6 +684,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
"excerpt": parent_sibling.excerpt,
"id": str(parent_sibling.id),
"is_favorite": False,
"is_encrypted": parent_sibling.is_encrypted,
"link_reach": parent_sibling.link_reach,
"link_role": parent_sibling.link_role,
"numchild": 0,
@@ -687,6 +707,7 @@ def test_api_documents_tree_list_authenticated_public_or_authenticated_parent(
"excerpt": grand_parent.excerpt,
"id": str(grand_parent.id),
"is_favorite": False,
"is_encrypted": grand_parent.is_encrypted,
"link_reach": grand_parent.link_reach,
"link_role": grand_parent.link_role,
"numchild": 2,
@@ -769,6 +790,7 @@ def test_api_documents_tree_list_authenticated_related_direct():
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
"is_encrypted": child.is_encrypted,
"link_reach": child.link_reach,
"link_role": child.link_role,
"numchild": 0,
@@ -791,6 +813,7 @@ def test_api_documents_tree_list_authenticated_related_direct():
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": document.link_reach,
"link_role": document.link_role,
"numchild": 1,
@@ -815,6 +838,7 @@ def test_api_documents_tree_list_authenticated_related_direct():
"excerpt": sibling.excerpt,
"id": str(sibling.id),
"is_favorite": False,
"is_encrypted": sibling.is_encrypted,
"link_reach": sibling.link_reach,
"link_role": sibling.link_role,
"numchild": 0,
@@ -835,6 +859,7 @@ def test_api_documents_tree_list_authenticated_related_direct():
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
"is_encrypted": parent.is_encrypted,
"link_reach": parent.link_reach,
"link_role": parent.link_role,
"numchild": 2,
@@ -917,6 +942,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
"is_encrypted": child.is_encrypted,
"link_reach": child.link_reach,
"link_role": child.link_role,
"numchild": 0,
@@ -941,6 +967,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": document.link_reach,
"link_role": document.link_role,
"numchild": 1,
@@ -969,6 +996,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
"excerpt": document_sibling.excerpt,
"id": str(document_sibling.id),
"is_favorite": False,
"is_encrypted": document_sibling.is_encrypted,
"link_reach": document_sibling.link_reach,
"link_role": document_sibling.link_role,
"numchild": 0,
@@ -991,6 +1019,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
"is_encrypted": parent.is_encrypted,
"link_reach": parent.link_reach,
"link_role": parent.link_role,
"numchild": 2,
@@ -1017,6 +1046,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
"excerpt": parent_sibling.excerpt,
"id": str(parent_sibling.id),
"is_favorite": False,
"is_encrypted": parent_sibling.is_encrypted,
"link_reach": parent_sibling.link_reach,
"link_role": parent_sibling.link_role,
"numchild": 0,
@@ -1039,6 +1069,7 @@ def test_api_documents_tree_list_authenticated_related_parent():
"excerpt": grand_parent.excerpt,
"id": str(grand_parent.id),
"is_favorite": False,
"is_encrypted": grand_parent.is_encrypted,
"link_reach": grand_parent.link_reach,
"link_role": grand_parent.link_role,
"numchild": 2,
@@ -1129,6 +1160,7 @@ def test_api_documents_tree_list_authenticated_related_team_members(
"excerpt": child.excerpt,
"id": str(child.id),
"is_favorite": False,
"is_encrypted": child.is_encrypted,
"link_reach": child.link_reach,
"link_role": child.link_role,
"numchild": 0,
@@ -1151,6 +1183,7 @@ def test_api_documents_tree_list_authenticated_related_team_members(
"excerpt": document.excerpt,
"id": str(document.id),
"is_favorite": False,
"is_encrypted": document.is_encrypted,
"link_reach": document.link_reach,
"link_role": document.link_role,
"numchild": 1,
@@ -1175,6 +1208,7 @@ def test_api_documents_tree_list_authenticated_related_team_members(
"excerpt": sibling.excerpt,
"id": str(sibling.id),
"is_favorite": False,
"is_encrypted": sibling.is_encrypted,
"link_reach": sibling.link_reach,
"link_role": sibling.link_role,
"numchild": 0,
@@ -1195,6 +1229,7 @@ def test_api_documents_tree_list_authenticated_related_team_members(
"excerpt": parent.excerpt,
"id": str(parent.id),
"is_favorite": False,
"is_encrypted": parent.is_encrypted,
"link_reach": parent.link_reach,
"link_role": parent.link_role,
"numchild": 2,
@@ -53,6 +53,7 @@ def test_api_config(is_authenticated):
"FRONTEND_CSS_URL": "http://testcss/",
"FRONTEND_HOMEPAGE_FEATURE_ENABLED": True,
"FRONTEND_JS_URL": "http://testjs/",
"FRONTEND_SILENT_LOGIN_ENABLED": False,
"FRONTEND_THEME": "test-theme",
"LANGUAGES": [
["en-us", "English"],
@@ -239,6 +239,18 @@ def test_services_search_indexers_serialize_document_empty():
assert result["title"] == ""
@pytest.mark.usefixtures("indexer_settings")
def test_services_search_indexers_serialize_document_encrypted():
"""Encrypted documents should have empty content to avoid indexing ciphertext."""
document = factories.DocumentFactory(is_encrypted=True)
indexer = SearchIndexer()
result = indexer.serialize_document(document, {})
assert result["content"] == ""
assert result["size"] == 0
@responses.activate
def test_services_search_indexers_index_errors(indexer_settings):
"""
+38 -3
View File
@@ -169,6 +169,11 @@ class Base(Configuration):
environ_name="AWS_STORAGE_BUCKET_NAME",
environ_prefix=None,
)
AWS_S3_SIGNATURE_VERSION = values.Value(
"s3v4",
environ_name="AWS_S3_SIGNATURE_VERSION",
environ_prefix=None,
)
# Document images
DOCUMENT_IMAGE_MAX_SIZE = values.IntegerValue(
@@ -507,7 +512,9 @@ class Base(Configuration):
FRONTEND_JS_URL = values.Value(
None, environ_name="FRONTEND_JS_URL", environ_prefix=None
)
FRONTEND_SILENT_LOGIN_ENABLED = values.BooleanValue(
default=False, environ_name="FRONTEND_SILENT_LOGIN_ENABLED", environ_prefix=None
)
THEME_CUSTOMIZATION_FILE_PATH = values.Value(
os.path.join(BASE_DIR, "impress/configuration/theme/default.json"),
environ_name="THEME_CUSTOMIZATION_FILE_PATH",
@@ -549,6 +556,16 @@ class Base(Configuration):
SESSION_COOKIE_NAME = "docs_sessionid"
# OIDC - Authorization Code Flow
OIDC_AUTHENTICATE_CLASS = values.Value(
"lasuite.oidc_login.views.OIDCAuthenticationRequestView",
environ_name="OIDC_AUTHENTICATE_CLASS",
environ_prefix=None,
)
OIDC_CALLBACK_CLASS = values.Value(
"lasuite.oidc_login.views.OIDCAuthenticationCallbackView",
environ_name="OIDC_CALLBACK_CLASS",
environ_prefix=None,
)
OIDC_CREATE_USER = values.BooleanValue(
default=True,
environ_name="OIDC_CREATE_USER",
@@ -1071,6 +1088,7 @@ class Production(Base):
# Modern browsers require to have the `secure` attribute on cookies with `Samesite=none`
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SESSION_CACHE_ALIAS = "session"
# Privacy
SECURE_REFERRER_POLICY = "same-origin"
@@ -1078,11 +1096,12 @@ class Production(Base):
# Conversion API: Always verify SSL in production
CONVERSION_API_SECURE = True
# Cache
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": values.Value(
"redis://redis:6379/1",
"redis://redis:6379/0",
environ_name="REDIS_URL",
environ_prefix=None,
),
@@ -1096,10 +1115,26 @@ class Production(Base):
},
"KEY_PREFIX": values.Value(
"docs",
environ_name="CACHES_KEY_PREFIX",
environ_name="CACHES_DEFAULT_KEY_PREFIX",
environ_prefix=None,
),
},
"session": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": values.Value(
"redis://redis:6379/0",
environ_name="REDIS_URL",
environ_prefix=None,
),
"TIMEOUT": values.IntegerValue(
30, # timeout in seconds
environ_name="CACHES_SESSION_TIMEOUT",
environ_prefix=None,
),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
},
}
+80 -139
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:28 core/admin.py:28
msgid "Personal info"
msgstr "Titouroù personel"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
msgid "Permissions"
msgstr "Aotreoù"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:53 core/admin.py:53
msgid "Important dates"
msgstr "Deiziadoù a-bouez"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:131 core/admin.py:131
msgid "Tree structure"
msgstr "Gwezennadur"
@@ -50,36 +50,24 @@ msgstr "Kuzhet"
msgid "Favorite"
msgstr "Sinedoù"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
msgid "A new document was created on your behalf!"
msgstr "Ur restr nevez a zo bet krouet ganeoc'h!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
msgid "You have been granted ownership of a new document:"
msgstr "C'hwi zo bet disklaeriet perc'henn ur restr nevez:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
msgid "This field is required."
msgstr "Ar vaezienn-mañ a zo rekis."
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
msgid "Body"
msgstr "Korf"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
msgid "Body type"
msgstr "Doare korf"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
msgid "Format"
msgstr "Stumm"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
@@ -147,301 +135,259 @@ msgstr "Kleiz"
msgid "Right"
msgstr "Dehoù"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "id"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:82 core/models.py:82
msgid "primary key for the record as UUID"
msgstr "alc'hwez kentañ evit an enrollañ evel UIID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "created on"
msgstr "krouet d'ar/al"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:89 core/models.py:89
msgid "date and time at which a record was created"
msgstr "deiziad hag eurvezh krouidigezh an enrolladenn"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:94 core/models.py:94
msgid "updated on"
msgstr "hizivaet d'ar/al"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:95 core/models.py:95
msgid "date and time at which a record was last updated"
msgstr "deiziad hag eurvezh m'eo bet hizivaet an enrolladenn"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:131 core/models.py:131
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "N'hon eus kavet implijer ebet gant an isstrollad-mañ met ar postel a zo liammet ouzh un implijer enrollet."
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "sub"
msgstr "isstrollad"
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:143 core/models.py:143
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:151 core/models.py:151
msgid "full name"
msgstr "anv klok"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:153 core/models.py:153
msgid "short name"
msgstr "anv berr"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:156 core/models.py:156
msgid "identity email address"
msgstr "postel identelezh"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:161 core/models.py:161
msgid "admin email address"
msgstr "postel ar merour"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:168 core/models.py:168
msgid "language"
msgstr "yezh"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:169 core/models.py:169
msgid "The language in which the user wants to see the interface."
msgstr "Ar yezh a vo implijet evit etrefas an implijer."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:177 core/models.py:177
msgid "The timezone in which the user wants to see times."
msgstr "Ar gwerzhid-eur a vo implijet evit etrefas an implijer."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:180 core/models.py:180
msgid "device"
msgstr "trevnad"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:182 core/models.py:182
msgid "Whether the user is a device or a real user."
msgstr "Pe vefe an implijer un aparailh pe un implijer gwirion."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:185 core/models.py:185
msgid "staff status"
msgstr "statud ar skipailh"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:187 core/models.py:187
msgid "Whether the user can log into this admin site."
msgstr "Ma c'hall an implijer kevreañ ouzh al lec'hienn verañ-mañ."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:190 core/models.py:190
msgid "active"
msgstr "oberiant"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Ma rank bezañ tretet an implijer-mañ evel oberiant. Diziuzit an dra-mañ e-plas dilemel kontoù."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:205 core/models.py:205
msgid "user"
msgstr "implijer"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:206 core/models.py:206
msgid "users"
msgstr "implijerien"
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
#: core/models.py:361 core/models.py:1434
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
msgstr "titl"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
msgstr "bomm"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
msgstr "Restr"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:413 core/models.py:413
msgid "Documents"
msgstr "Restroù"
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
#: core/models.py:827
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
msgid "Untitled Document"
msgstr "Restr hep titl"
#: build/lib/core/models.py:862 core/models.py:862
#: build/lib/core/models.py:829 core/models.py:829
msgid "Open"
msgstr "Digeriñ"
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} en deus rannet ur restr ganeoc'h!"
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:868 core/models.py:868
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} en deus pedet ac'hanoc'h gant ar rol \"{role}\" war ar restr da-heul:"
#: build/lib/core/models.py:872 core/models.py:872
#: build/lib/core/models.py:874 core/models.py:874
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} en deus rannet ur restr ganeoc'h: {title}"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:975 core/models.py:975
msgid "Document/user link trace"
msgstr "Roud liamm ar restr/an implijer"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:976 core/models.py:976
msgid "Document/user link traces"
msgstr "Roudoù liamm ar restr/an implijer"
#: build/lib/core/models.py:980 core/models.py:980
#: build/lib/core/models.py:982 core/models.py:982
msgid "A link trace already exists for this document/user."
msgstr "Ur roud liamm a zo dija evit an restr/an implijer."
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1005 core/models.py:1005
msgid "Document favorite"
msgstr "Restr muiañ-karet"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "Document favorites"
msgstr "Restroù muiañ-karet"
#: build/lib/core/models.py:1010 core/models.py:1010
#: build/lib/core/models.py:1012 core/models.py:1012
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ar restr-mañ a zo ur restr muiañ karet gant an implijer-mañ."
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1034 core/models.py:1034
msgid "Document/user relation"
msgstr "Liamm restr/implijer"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "Document/user relations"
msgstr "Liammoù restr/implijer"
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This user is already in this document."
msgstr "An implijer-mañ a zo dija er restr-mañ."
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "This team is already in this document."
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
#: core/models.py:1051 core/models.py:1520
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "Either user or team must be set, not both."
msgstr "An implijer pe ar skipailh a rank bezañ termenet, ket an daou avat."
#: build/lib/core/models.py:1202 core/models.py:1202
#: build/lib/core/models.py:1204 core/models.py:1204
msgid "Document ask for access"
msgstr "Goulenn tizhout ar restr"
#: build/lib/core/models.py:1203 core/models.py:1203
#: build/lib/core/models.py:1205 core/models.py:1205
msgid "Document ask for accesses"
msgstr "Goulennoù tizhout ar restr"
#: build/lib/core/models.py:1209 core/models.py:1209
#: build/lib/core/models.py:1211 core/models.py:1211
msgid "This user has already asked for access to this document."
msgstr "An implijer en deus goulennet tizhout ar restr-mañ."
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1268 core/models.py:1268
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} en defe c'hoant da dizhout ar restr-mañ!"
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1272 core/models.py:1272
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} en defe c'hoant da dizhout ar restr da-heul:"
#: build/lib/core/models.py:1276 core/models.py:1276
#: build/lib/core/models.py:1278 core/models.py:1278
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} en defe c'hoant da dizhout ar restr: {title}"
#: build/lib/core/models.py:1318 core/models.py:1318
#: build/lib/core/models.py:1320 core/models.py:1320
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1319 core/models.py:1319
#: build/lib/core/models.py:1321 core/models.py:1321
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
#: core/models.py:1322 core/models.py:1374
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1369 core/models.py:1369
#: build/lib/core/models.py:1371 core/models.py:1371
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1370 core/models.py:1370
#: build/lib/core/models.py:1372 core/models.py:1372
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
#: build/lib/core/models.py:1421 core/models.py:1421
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1423 core/models.py:1423
#: build/lib/core/models.py:1425 core/models.py:1425
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1424 core/models.py:1424
#: build/lib/core/models.py:1426 core/models.py:1426
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1435 core/models.py:1435
msgid "description"
msgstr "deskrivadur"
#: build/lib/core/models.py:1436 core/models.py:1436
msgid "code"
msgstr "kod"
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "public"
msgstr "publik"
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "Whether this template is public for anyone to use."
msgstr "M'eo foran ar patrom-mañ hag implijus gant n'eus forzh piv."
#: build/lib/core/models.py:1447 core/models.py:1447
msgid "Template"
msgstr "Patrom"
#: build/lib/core/models.py:1448 core/models.py:1448
msgid "Templates"
msgstr "Patromoù"
#: build/lib/core/models.py:1501 core/models.py:1501
msgid "Template/user relation"
msgstr "Liamm patrom/implijer"
#: build/lib/core/models.py:1502 core/models.py:1502
msgid "Template/user relations"
msgstr "Liammoù patrom/implijer"
#: build/lib/core/models.py:1508 core/models.py:1508
msgid "This user is already in this template."
msgstr "An implijer-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1514 core/models.py:1514
msgid "This team is already in this template."
msgstr "Ar skipailh-mañ a zo dija er patrom-mañ."
#: build/lib/core/models.py:1591 core/models.py:1591
msgid "email address"
msgstr "postel"
#: build/lib/core/models.py:1610 core/models.py:1610
#: build/lib/core/models.py:1455 core/models.py:1455
msgid "Document invitation"
msgstr "Pedadenn d'ur restr"
#: build/lib/core/models.py:1611 core/models.py:1611
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitations"
msgstr "Pedadennoù d'ur restr"
#: build/lib/core/models.py:1631 core/models.py:1631
#: build/lib/core/models.py:1476 core/models.py:1476
msgid "This email is already associated to a registered user."
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
@@ -450,17 +396,12 @@ msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
msgid "Logo email"
msgstr "Logo ar postel"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Digeriñ"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:219
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, hoc'h ostilh nevez ret-holl evit aozañ, rannañ ha kenlabourat war ar restr e skipailh. "
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+80 -139
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:28 core/admin.py:28
msgid "Personal info"
msgstr "Persönliche Daten"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
msgid "Permissions"
msgstr "Berechtigungen"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:53 core/admin.py:53
msgid "Important dates"
msgstr "Wichtige Daten"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:131 core/admin.py:131
msgid "Tree structure"
msgstr "Baumstruktur"
@@ -50,36 +50,24 @@ msgstr ""
msgid "Favorite"
msgstr "Favorit"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
msgid "A new document was created on your behalf!"
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
msgid "You have been granted ownership of a new document:"
msgstr "Sie sind Besitzer eines neuen Dokuments:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
msgid "Body"
msgstr "Inhalt"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
msgid "Body type"
msgstr "Typ"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
@@ -147,301 +135,259 @@ msgstr "Links"
msgid "Right"
msgstr "Rechts"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "id"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:82 core/models.py:82
msgid "primary key for the record as UUID"
msgstr "primärer Schlüssel für den Datensatz als UUID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "created on"
msgstr "Erstellt"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:89 core/models.py:89
msgid "date and time at which a record was created"
msgstr "Datum und Uhrzeit, an dem ein Datensatz erstellt wurde"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:94 core/models.py:94
msgid "updated on"
msgstr "Aktualisiert"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:95 core/models.py:95
msgid "date and time at which a record was last updated"
msgstr "Datum und Uhrzeit, an dem zuletzt aktualisiert wurde"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:131 core/models.py:131
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Wir konnten keinen Benutzer mit diesem Abo finden, aber die E-Mail-Adresse ist bereits einem registrierten Benutzer zugeordnet."
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "sub"
msgstr "unter"
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:143 core/models.py:143
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:151 core/models.py:151
msgid "full name"
msgstr "Name"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:153 core/models.py:153
msgid "short name"
msgstr "Kurzbezeichnung"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:156 core/models.py:156
msgid "identity email address"
msgstr "Identitäts-E-Mail-Adresse"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:161 core/models.py:161
msgid "admin email address"
msgstr "Admin E-Mail-Adresse"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:168 core/models.py:168
msgid "language"
msgstr "Sprache"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:169 core/models.py:169
msgid "The language in which the user wants to see the interface."
msgstr "Die Sprache, in der der Benutzer die Benutzeroberfläche sehen möchte."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:177 core/models.py:177
msgid "The timezone in which the user wants to see times."
msgstr "Die Zeitzone, in der der Nutzer Zeiten sehen möchte."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:180 core/models.py:180
msgid "device"
msgstr "Gerät"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:182 core/models.py:182
msgid "Whether the user is a device or a real user."
msgstr "Ob der Benutzer ein Gerät oder ein echter Benutzer ist."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:185 core/models.py:185
msgid "staff status"
msgstr "Status des Teammitgliedes"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:187 core/models.py:187
msgid "Whether the user can log into this admin site."
msgstr "Gibt an, ob der Benutzer sich in diese Admin-Seite einloggen kann."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:190 core/models.py:190
msgid "active"
msgstr "aktiviert"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Ob dieser Benutzer als aktiviert behandelt werden soll. Deaktivieren Sie diese Option, anstatt Konten zu löschen."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:205 core/models.py:205
msgid "user"
msgstr "Benutzer"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:206 core/models.py:206
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
#: core/models.py:361 core/models.py:1434
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
msgstr "Titel"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
msgstr "Auszug"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:413 core/models.py:413
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
#: core/models.py:827
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:862 core/models.py:862
#: build/lib/core/models.py:829 core/models.py:829
msgid "Open"
msgstr "Öffnen"
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:868 core/models.py:868
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} hat Sie mit der Rolle \"{role}\" zu folgendem Dokument eingeladen:"
#: build/lib/core/models.py:872 core/models.py:872
#: build/lib/core/models.py:874 core/models.py:874
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} hat ein Dokument mit Ihnen geteilt: {title}"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:975 core/models.py:975
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:976 core/models.py:976
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:980 core/models.py:980
#: build/lib/core/models.py:982 core/models.py:982
msgid "A link trace already exists for this document/user."
msgstr "Für dieses Dokument/ diesen Benutzer ist bereits eine Linkverfolgung vorhanden."
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1005 core/models.py:1005
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:1010 core/models.py:1010
#: build/lib/core/models.py:1012 core/models.py:1012
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Dieses Dokument ist bereits durch den gleichen Benutzer favorisiert worden."
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1034 core/models.py:1034
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This user is already in this document."
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
#: core/models.py:1051 core/models.py:1520
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "Either user or team must be set, not both."
msgstr "Benutzer oder Team müssen gesetzt werden, nicht beides."
#: build/lib/core/models.py:1202 core/models.py:1202
#: build/lib/core/models.py:1204 core/models.py:1204
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1203 core/models.py:1203
#: build/lib/core/models.py:1205 core/models.py:1205
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1209 core/models.py:1209
#: build/lib/core/models.py:1211 core/models.py:1211
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1268 core/models.py:1268
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1272 core/models.py:1272
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1276 core/models.py:1276
#: build/lib/core/models.py:1278 core/models.py:1278
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1318 core/models.py:1318
#: build/lib/core/models.py:1320 core/models.py:1320
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1319 core/models.py:1319
#: build/lib/core/models.py:1321 core/models.py:1321
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
#: core/models.py:1322 core/models.py:1374
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1369 core/models.py:1369
#: build/lib/core/models.py:1371 core/models.py:1371
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1370 core/models.py:1370
#: build/lib/core/models.py:1372 core/models.py:1372
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
#: build/lib/core/models.py:1421 core/models.py:1421
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1423 core/models.py:1423
#: build/lib/core/models.py:1425 core/models.py:1425
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1424 core/models.py:1424
#: build/lib/core/models.py:1426 core/models.py:1426
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1435 core/models.py:1435
msgid "description"
msgstr "Beschreibung"
#: build/lib/core/models.py:1436 core/models.py:1436
msgid "code"
msgstr "Code"
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "public"
msgstr "öffentlich"
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "Whether this template is public for anyone to use."
msgstr "Ob diese Vorlage für jedermann öffentlich ist."
#: build/lib/core/models.py:1447 core/models.py:1447
msgid "Template"
msgstr "Vorlage"
#: build/lib/core/models.py:1448 core/models.py:1448
msgid "Templates"
msgstr "Vorlagen"
#: build/lib/core/models.py:1501 core/models.py:1501
msgid "Template/user relation"
msgstr "Vorlage/Benutzer-Beziehung"
#: build/lib/core/models.py:1502 core/models.py:1502
msgid "Template/user relations"
msgstr "Vorlage/Benutzerbeziehungen"
#: build/lib/core/models.py:1508 core/models.py:1508
msgid "This user is already in this template."
msgstr "Dieser Benutzer ist bereits in dieser Vorlage."
#: build/lib/core/models.py:1514 core/models.py:1514
msgid "This team is already in this template."
msgstr "Dieses Team ist bereits in diesem Template."
#: build/lib/core/models.py:1591 core/models.py:1591
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1610 core/models.py:1610
#: build/lib/core/models.py:1455 core/models.py:1455
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1611 core/models.py:1611
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1631 core/models.py:1631
#: build/lib/core/models.py:1476 core/models.py:1476
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
@@ -450,17 +396,12 @@ msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
msgid "Logo email"
msgstr "Logo-E-Mail"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Öffnen"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:219
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, Ihr neues unentbehrliches Werkzeug für die Organisation, den Austausch und die Zusammenarbeit in Ihren Dokumenten als Team. "
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+80 -139
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:28 core/admin.py:28
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
msgid "Permissions"
msgstr ""
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:53 core/admin.py:53
msgid "Important dates"
msgstr ""
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:131 core/admin.py:131
msgid "Tree structure"
msgstr ""
@@ -50,36 +50,24 @@ msgstr ""
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -147,301 +135,259 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "id"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:82 core/models.py:82
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "created on"
msgstr ""
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:89 core/models.py:89
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:94 core/models.py:94
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:95 core/models.py:95
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:131 core/models.py:131
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "sub"
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:143 core/models.py:143
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:151 core/models.py:151
msgid "full name"
msgstr ""
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:153 core/models.py:153
msgid "short name"
msgstr ""
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:156 core/models.py:156
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:161 core/models.py:161
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:168 core/models.py:168
msgid "language"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:169 core/models.py:169
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:177 core/models.py:177
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:180 core/models.py:180
msgid "device"
msgstr ""
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:182 core/models.py:182
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:185 core/models.py:185
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:187 core/models.py:187
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:190 core/models.py:190
msgid "active"
msgstr ""
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:205 core/models.py:205
msgid "user"
msgstr ""
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:206 core/models.py:206
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
#: core/models.py:361 core/models.py:1434
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
msgstr ""
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
msgstr ""
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:413 core/models.py:413
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
#: core/models.py:827
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:862 core/models.py:862
#: build/lib/core/models.py:829 core/models.py:829
msgid "Open"
msgstr ""
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:868 core/models.py:868
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:872 core/models.py:872
#: build/lib/core/models.py:874 core/models.py:874
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:975 core/models.py:975
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:976 core/models.py:976
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:980 core/models.py:980
#: build/lib/core/models.py:982 core/models.py:982
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1005 core/models.py:1005
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1010 core/models.py:1010
#: build/lib/core/models.py:1012 core/models.py:1012
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1034 core/models.py:1034
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
#: core/models.py:1051 core/models.py:1520
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1202 core/models.py:1202
#: build/lib/core/models.py:1204 core/models.py:1204
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1203 core/models.py:1203
#: build/lib/core/models.py:1205 core/models.py:1205
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1209 core/models.py:1209
#: build/lib/core/models.py:1211 core/models.py:1211
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1268 core/models.py:1268
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1272 core/models.py:1272
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1276 core/models.py:1276
#: build/lib/core/models.py:1278 core/models.py:1278
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1318 core/models.py:1318
#: build/lib/core/models.py:1320 core/models.py:1320
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1319 core/models.py:1319
#: build/lib/core/models.py:1321 core/models.py:1321
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
#: core/models.py:1322 core/models.py:1374
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1369 core/models.py:1369
#: build/lib/core/models.py:1371 core/models.py:1371
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1370 core/models.py:1370
#: build/lib/core/models.py:1372 core/models.py:1372
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
#: build/lib/core/models.py:1421 core/models.py:1421
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1423 core/models.py:1423
#: build/lib/core/models.py:1425 core/models.py:1425
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1424 core/models.py:1424
#: build/lib/core/models.py:1426 core/models.py:1426
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1435 core/models.py:1435
msgid "description"
msgstr ""
#: build/lib/core/models.py:1436 core/models.py:1436
msgid "code"
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "css"
msgstr ""
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "public"
msgstr ""
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1447 core/models.py:1447
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1448 core/models.py:1448
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1501 core/models.py:1501
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1502 core/models.py:1502
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1508 core/models.py:1508
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1514 core/models.py:1514
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1591 core/models.py:1591
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1610 core/models.py:1610
#: build/lib/core/models.py:1455 core/models.py:1455
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1611 core/models.py:1611
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1631 core/models.py:1631
#: build/lib/core/models.py:1476 core/models.py:1476
msgid "This email is already associated to a registered user."
msgstr ""
@@ -450,17 +396,12 @@ msgstr ""
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr ""
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:219
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+80 -139
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:28 core/admin.py:28
msgid "Personal info"
msgstr "Información Personal"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
msgid "Permissions"
msgstr "Permisos"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:53 core/admin.py:53
msgid "Important dates"
msgstr "Fechas importantes"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:131 core/admin.py:131
msgid "Tree structure"
msgstr "Estructura en árbol"
@@ -50,36 +50,24 @@ msgstr ""
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
msgid "A new document was created on your behalf!"
msgstr "¡Un nuevo documento se ha creado por ti!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
msgid "You have been granted ownership of a new document:"
msgstr "Se le ha concedido la propiedad de un nuevo documento :"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
msgid "Body"
msgstr "Cuerpo"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
msgid "Body type"
msgstr "Tipo de Cuerpo"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
@@ -147,301 +135,259 @@ msgstr "Izquierda"
msgid "Right"
msgstr "Derecha"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "id"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:82 core/models.py:82
msgid "primary key for the record as UUID"
msgstr "clave primaria para el registro como UUID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "created on"
msgstr "creado el"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:89 core/models.py:89
msgid "date and time at which a record was created"
msgstr "fecha y hora en la que se creó un registro"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:94 core/models.py:94
msgid "updated on"
msgstr "actualizado el"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:95 core/models.py:95
msgid "date and time at which a record was last updated"
msgstr "fecha y hora en la que un registro fue actualizado por última vez"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:131 core/models.py:131
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "No se ha podido encontrar un usuario con este sub (UUID), pero el correo electrónico ya está asociado con un usuario."
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "sub"
msgstr "sub (UUID)"
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:143 core/models.py:143
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Obligatorio. 255 caracteres o menos. Solo caracteres ASCII."
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:151 core/models.py:151
msgid "full name"
msgstr "nombre completo"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:153 core/models.py:153
msgid "short name"
msgstr "nombre abreviado"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:156 core/models.py:156
msgid "identity email address"
msgstr "correo electrónico de identidad"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:161 core/models.py:161
msgid "admin email address"
msgstr "correo electrónico del administrador"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:168 core/models.py:168
msgid "language"
msgstr "idioma"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:169 core/models.py:169
msgid "The language in which the user wants to see the interface."
msgstr "El idioma en el que el usuario desea ver la interfaz."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:177 core/models.py:177
msgid "The timezone in which the user wants to see times."
msgstr "La zona horaria en la que el usuario quiere ver los tiempos."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:180 core/models.py:180
msgid "device"
msgstr "dispositivo"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:182 core/models.py:182
msgid "Whether the user is a device or a real user."
msgstr "Si el usuario es un dispositivo o un usuario real."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:185 core/models.py:185
msgid "staff status"
msgstr "rol en el equipo"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:187 core/models.py:187
msgid "Whether the user can log into this admin site."
msgstr "Si el usuario puede iniciar sesión en esta página web de administración."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:190 core/models.py:190
msgid "active"
msgstr "activo"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Si este usuario debe ser considerado como activo. Deseleccionar en lugar de eliminar cuentas."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:205 core/models.py:205
msgid "user"
msgstr "usuario"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:206 core/models.py:206
msgid "users"
msgstr "usuarios"
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
#: core/models.py:361 core/models.py:1434
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
msgstr "título"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
msgstr "resumen"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
msgstr "Documento"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:413 core/models.py:413
msgid "Documents"
msgstr "Documentos"
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
#: core/models.py:827
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
msgid "Untitled Document"
msgstr "Documento sin título"
#: build/lib/core/models.py:862 core/models.py:862
#: build/lib/core/models.py:829 core/models.py:829
msgid "Open"
msgstr "Abrir"
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "¡{name} ha compartido un documento contigo!"
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:868 core/models.py:868
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "Te ha invitado {name} al siguiente documento con el rol \"{role}\" :"
#: build/lib/core/models.py:872 core/models.py:872
#: build/lib/core/models.py:874 core/models.py:874
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha compartido un documento contigo: {title}"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:975 core/models.py:975
msgid "Document/user link trace"
msgstr "Traza del enlace de documento/usuario"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:976 core/models.py:976
msgid "Document/user link traces"
msgstr "Trazas del enlace de documento/usuario"
#: build/lib/core/models.py:980 core/models.py:980
#: build/lib/core/models.py:982 core/models.py:982
msgid "A link trace already exists for this document/user."
msgstr "Ya existe una traza de enlace para este documento/usuario."
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1005 core/models.py:1005
msgid "Document favorite"
msgstr "Documento favorito"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "Document favorites"
msgstr "Documentos favoritos"
#: build/lib/core/models.py:1010 core/models.py:1010
#: build/lib/core/models.py:1012 core/models.py:1012
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Este documento ya ha sido marcado como favorito por el usuario."
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1034 core/models.py:1034
msgid "Document/user relation"
msgstr "Relación documento/usuario"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "Document/user relations"
msgstr "Relaciones documento/usuario"
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This user is already in this document."
msgstr "Este usuario ya forma parte del documento."
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "This team is already in this document."
msgstr "Este equipo ya forma parte del documento."
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
#: core/models.py:1051 core/models.py:1520
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "Either user or team must be set, not both."
msgstr "Debe establecerse un usuario o un equipo, no ambos."
#: build/lib/core/models.py:1202 core/models.py:1202
#: build/lib/core/models.py:1204 core/models.py:1204
msgid "Document ask for access"
msgstr "Solicitud de acceso"
#: build/lib/core/models.py:1203 core/models.py:1203
#: build/lib/core/models.py:1205 core/models.py:1205
msgid "Document ask for accesses"
msgstr "Solicitud de accesos"
#: build/lib/core/models.py:1209 core/models.py:1209
#: build/lib/core/models.py:1211 core/models.py:1211
msgid "This user has already asked for access to this document."
msgstr "Este usuario ya ha solicitado acceso a este documento."
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1268 core/models.py:1268
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "¡{name} desea acceder a un documento!"
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1272 core/models.py:1272
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} desea acceso al siguiente documento:"
#: build/lib/core/models.py:1276 core/models.py:1276
#: build/lib/core/models.py:1278 core/models.py:1278
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} está pidiendo acceso al documento: {title}"
#: build/lib/core/models.py:1318 core/models.py:1318
#: build/lib/core/models.py:1320 core/models.py:1320
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1319 core/models.py:1319
#: build/lib/core/models.py:1321 core/models.py:1321
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
#: core/models.py:1322 core/models.py:1374
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1369 core/models.py:1369
#: build/lib/core/models.py:1371 core/models.py:1371
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1370 core/models.py:1370
#: build/lib/core/models.py:1372 core/models.py:1372
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
#: build/lib/core/models.py:1421 core/models.py:1421
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1423 core/models.py:1423
#: build/lib/core/models.py:1425 core/models.py:1425
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1424 core/models.py:1424
#: build/lib/core/models.py:1426 core/models.py:1426
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1435 core/models.py:1435
msgid "description"
msgstr "descripción"
#: build/lib/core/models.py:1436 core/models.py:1436
msgid "code"
msgstr "código"
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "public"
msgstr "público"
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "Whether this template is public for anyone to use."
msgstr "Si esta plantilla es pública para que cualquiera la utilice."
#: build/lib/core/models.py:1447 core/models.py:1447
msgid "Template"
msgstr "Plantilla"
#: build/lib/core/models.py:1448 core/models.py:1448
msgid "Templates"
msgstr "Plantillas"
#: build/lib/core/models.py:1501 core/models.py:1501
msgid "Template/user relation"
msgstr "Relación plantilla/usuario"
#: build/lib/core/models.py:1502 core/models.py:1502
msgid "Template/user relations"
msgstr "Relaciones plantilla/usuario"
#: build/lib/core/models.py:1508 core/models.py:1508
msgid "This user is already in this template."
msgstr "Este usuario ya forma parte de la plantilla."
#: build/lib/core/models.py:1514 core/models.py:1514
msgid "This team is already in this template."
msgstr "Este equipo ya se encuentra en esta plantilla."
#: build/lib/core/models.py:1591 core/models.py:1591
msgid "email address"
msgstr "dirección de correo electrónico"
#: build/lib/core/models.py:1610 core/models.py:1610
#: build/lib/core/models.py:1455 core/models.py:1455
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1611 core/models.py:1611
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1631 core/models.py:1631
#: build/lib/core/models.py:1476 core/models.py:1476
msgid "This email is already associated to a registered user."
msgstr "Este correo electrónico está asociado a un usuario registrado."
@@ -450,17 +396,12 @@ msgstr "Este correo electrónico está asociado a un usuario registrado."
msgid "Logo email"
msgstr "Logo de correo electrónico"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Abrir"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:219
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr "Docs, su nueva herramienta esencial para organizar, compartir y colaborar en sus documentos como equipo."
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+80 -139
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:28 core/admin.py:28
msgid "Personal info"
msgstr "Infos Personnelles"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
msgid "Permissions"
msgstr "Permissions"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:53 core/admin.py:53
msgid "Important dates"
msgstr "Dates importantes"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:131 core/admin.py:131
msgid "Tree structure"
msgstr "Arborescence"
@@ -50,36 +50,24 @@ msgstr "Masqué"
msgid "Favorite"
msgstr "Favoris"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
msgid "A new document was created on your behalf!"
msgstr "Un nouveau document a été créé pour vous !"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
msgid "You have been granted ownership of a new document:"
msgstr "Vous avez été déclaré propriétaire d'un nouveau document :"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
msgid "This field is required."
msgstr "Ce champ est obligatoire."
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "La portée du lien '%(link_reach)s' n'est pas autorisée en fonction de la configuration du document parent."
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
msgid "Body"
msgstr "Corps"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
msgid "Body type"
msgstr "Type de corps"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
@@ -147,301 +135,259 @@ msgstr "Gauche"
msgid "Right"
msgstr "Droite"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "id"
msgstr "identifiant/id"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:82 core/models.py:82
msgid "primary key for the record as UUID"
msgstr "clé primaire pour l'enregistrement en tant que UUID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "created on"
msgstr "créé le"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:89 core/models.py:89
msgid "date and time at which a record was created"
msgstr "date et heure de création de l'enregistrement"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:94 core/models.py:94
msgid "updated on"
msgstr "mis à jour le"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:95 core/models.py:95
msgid "date and time at which a record was last updated"
msgstr "date et heure de la dernière mise à jour de l'enregistrement"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:131 core/models.py:131
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Nous n'avons pas pu trouver un utilisateur avec ce sous-groupe mais l'e-mail est déjà associé à un utilisateur enregistré."
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "sub"
msgstr "sous-groupe"
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:143 core/models.py:143
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Obligatoire. 255 caractères ou moins. Caractères ASCII uniquement."
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:151 core/models.py:151
msgid "full name"
msgstr "nom complet"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:153 core/models.py:153
msgid "short name"
msgstr "nom court"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:156 core/models.py:156
msgid "identity email address"
msgstr "adresse e-mail d'identité"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:161 core/models.py:161
msgid "admin email address"
msgstr "adresse e-mail de l'administrateur"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:168 core/models.py:168
msgid "language"
msgstr "langue"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:169 core/models.py:169
msgid "The language in which the user wants to see the interface."
msgstr "La langue dans laquelle l'utilisateur veut voir l'interface."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:177 core/models.py:177
msgid "The timezone in which the user wants to see times."
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:180 core/models.py:180
msgid "device"
msgstr "appareil"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:182 core/models.py:182
msgid "Whether the user is a device or a real user."
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:185 core/models.py:185
msgid "staff status"
msgstr "statut d'équipe"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:187 core/models.py:187
msgid "Whether the user can log into this admin site."
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:190 core/models.py:190
msgid "active"
msgstr "actif"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Si cet utilisateur doit être traité comme actif. Désélectionnez ceci au lieu de supprimer des comptes."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:205 core/models.py:205
msgid "user"
msgstr "utilisateur"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:206 core/models.py:206
msgid "users"
msgstr "utilisateurs"
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
#: core/models.py:361 core/models.py:1434
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
msgstr "titre"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
msgstr "extrait"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
msgstr "Document"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:413 core/models.py:413
msgid "Documents"
msgstr "Documents"
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
#: core/models.py:827
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
msgid "Untitled Document"
msgstr "Document sans titre"
#: build/lib/core/models.py:862 core/models.py:862
#: build/lib/core/models.py:829 core/models.py:829
msgid "Open"
msgstr "Ouvrir"
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} a partagé un document avec vous!"
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:868 core/models.py:868
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} vous a invité avec le rôle \"{role}\" sur le document suivant :"
#: build/lib/core/models.py:872 core/models.py:872
#: build/lib/core/models.py:874 core/models.py:874
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} a partagé un document avec vous : {title}"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:975 core/models.py:975
msgid "Document/user link trace"
msgstr "Trace du lien document/utilisateur"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:976 core/models.py:976
msgid "Document/user link traces"
msgstr "Traces du lien document/utilisateur"
#: build/lib/core/models.py:980 core/models.py:980
#: build/lib/core/models.py:982 core/models.py:982
msgid "A link trace already exists for this document/user."
msgstr "Une trace de lien existe déjà pour ce document/utilisateur."
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1005 core/models.py:1005
msgid "Document favorite"
msgstr "Document favori"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "Document favorites"
msgstr "Documents favoris"
#: build/lib/core/models.py:1010 core/models.py:1010
#: build/lib/core/models.py:1012 core/models.py:1012
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ce document est déjà un favori de cet utilisateur."
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1034 core/models.py:1034
msgid "Document/user relation"
msgstr "Relation document/utilisateur"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "Document/user relations"
msgstr "Relations document/utilisateur"
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This user is already in this document."
msgstr "Cet utilisateur est déjà dans ce document."
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "This team is already in this document."
msgstr "Cette équipe est déjà dans ce document."
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
#: core/models.py:1051 core/models.py:1520
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "Either user or team must be set, not both."
msgstr "L'utilisateur ou l'équipe doivent être définis, pas les deux."
#: build/lib/core/models.py:1202 core/models.py:1202
#: build/lib/core/models.py:1204 core/models.py:1204
msgid "Document ask for access"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1203 core/models.py:1203
#: build/lib/core/models.py:1205 core/models.py:1205
msgid "Document ask for accesses"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1209 core/models.py:1209
#: build/lib/core/models.py:1211 core/models.py:1211
msgid "This user has already asked for access to this document."
msgstr "Cet utilisateur a déjà demandé l'accès à ce document."
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1268 core/models.py:1268
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} souhaiterait accéder au document suivant !"
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1272 core/models.py:1272
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} souhaiterait accéder au document suivant :"
#: build/lib/core/models.py:1276 core/models.py:1276
#: build/lib/core/models.py:1278 core/models.py:1278
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} demande l'accès au document : {title}"
#: build/lib/core/models.py:1318 core/models.py:1318
#: build/lib/core/models.py:1320 core/models.py:1320
msgid "Thread"
msgstr "Conversation"
#: build/lib/core/models.py:1319 core/models.py:1319
#: build/lib/core/models.py:1321 core/models.py:1321
msgid "Threads"
msgstr "Conversations"
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
#: core/models.py:1322 core/models.py:1374
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
msgid "Anonymous"
msgstr "Anonyme"
#: build/lib/core/models.py:1369 core/models.py:1369
#: build/lib/core/models.py:1371 core/models.py:1371
msgid "Comment"
msgstr "Commentaire"
#: build/lib/core/models.py:1370 core/models.py:1370
#: build/lib/core/models.py:1372 core/models.py:1372
msgid "Comments"
msgstr "Commentaires"
#: build/lib/core/models.py:1419 core/models.py:1419
#: build/lib/core/models.py:1421 core/models.py:1421
msgid "This emoji has already been reacted to this comment."
msgstr "Cet émoji a déjà été réagi à ce commentaire."
#: build/lib/core/models.py:1423 core/models.py:1423
#: build/lib/core/models.py:1425 core/models.py:1425
msgid "Reaction"
msgstr "Réaction"
#: build/lib/core/models.py:1424 core/models.py:1424
#: build/lib/core/models.py:1426 core/models.py:1426
msgid "Reactions"
msgstr "Réactions"
#: build/lib/core/models.py:1435 core/models.py:1435
msgid "description"
msgstr "description"
#: build/lib/core/models.py:1436 core/models.py:1436
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "css"
msgstr "CSS"
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "public"
msgstr "public"
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "Whether this template is public for anyone to use."
msgstr "Si ce modèle est public, utilisable par n'importe qui."
#: build/lib/core/models.py:1447 core/models.py:1447
msgid "Template"
msgstr "Modèle"
#: build/lib/core/models.py:1448 core/models.py:1448
msgid "Templates"
msgstr "Modèles"
#: build/lib/core/models.py:1501 core/models.py:1501
msgid "Template/user relation"
msgstr "Relation modèle/utilisateur"
#: build/lib/core/models.py:1502 core/models.py:1502
msgid "Template/user relations"
msgstr "Relations modèle/utilisateur"
#: build/lib/core/models.py:1508 core/models.py:1508
msgid "This user is already in this template."
msgstr "Cet utilisateur est déjà dans ce modèle."
#: build/lib/core/models.py:1514 core/models.py:1514
msgid "This team is already in this template."
msgstr "Cette équipe est déjà modèle."
#: build/lib/core/models.py:1591 core/models.py:1591
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1610 core/models.py:1610
#: build/lib/core/models.py:1455 core/models.py:1455
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1611 core/models.py:1611
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1631 core/models.py:1631
#: build/lib/core/models.py:1476 core/models.py:1476
msgid "This email is already associated to a registered user."
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
@@ -450,17 +396,12 @@ msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
msgid "Logo email"
msgstr "Logo de l'e-mail"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Ouvrir"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:219
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, votre nouvel outil incontournable pour organiser, partager et collaborer sur vos documents en équipe. "
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+80 -139
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:28 core/admin.py:28
msgid "Personal info"
msgstr "Informazioni personali"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
msgid "Permissions"
msgstr "Permessi"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:53 core/admin.py:53
msgid "Important dates"
msgstr "Date importanti"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:131 core/admin.py:131
msgid "Tree structure"
msgstr "Struttura ad albero"
@@ -50,36 +50,24 @@ msgstr ""
msgid "Favorite"
msgstr "Preferiti"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
msgid "A new document was created on your behalf!"
msgstr "Un nuovo documento è stato creato a tuo nome!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
msgid "You have been granted ownership of a new document:"
msgstr "Sei ora proprietario di un nuovo documento:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
msgid "Body"
msgstr "Corpo"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
@@ -147,301 +135,259 @@ msgstr "Sinistra"
msgid "Right"
msgstr "Destra"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "id"
msgstr "Id"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:82 core/models.py:82
msgid "primary key for the record as UUID"
msgstr "chiave primaria per il record come UUID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "created on"
msgstr "creato il"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:89 core/models.py:89
msgid "date and time at which a record was created"
msgstr "data e ora in cui è stato creato un record"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:94 core/models.py:94
msgid "updated on"
msgstr "aggiornato il"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:95 core/models.py:95
msgid "date and time at which a record was last updated"
msgstr "data e ora in cui lultimo record è stato aggiornato"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:131 core/models.py:131
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "sub"
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:143 core/models.py:143
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:151 core/models.py:151
msgid "full name"
msgstr "nome completo"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:153 core/models.py:153
msgid "short name"
msgstr "nome"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:156 core/models.py:156
msgid "identity email address"
msgstr "indirizzo email di identità"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:161 core/models.py:161
msgid "admin email address"
msgstr "Indirizzo email dell'amministratore"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:168 core/models.py:168
msgid "language"
msgstr "lingua"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:169 core/models.py:169
msgid "The language in which the user wants to see the interface."
msgstr "La lingua in cui l'utente vuole vedere l'interfaccia."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:177 core/models.py:177
msgid "The timezone in which the user wants to see times."
msgstr "Il fuso orario in cui l'utente vuole vedere gli orari."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:180 core/models.py:180
msgid "device"
msgstr "dispositivo"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:182 core/models.py:182
msgid "Whether the user is a device or a real user."
msgstr "Se l'utente è un dispositivo o un utente reale."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:185 core/models.py:185
msgid "staff status"
msgstr "stato del personale"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:187 core/models.py:187
msgid "Whether the user can log into this admin site."
msgstr "Indica se l'utente può accedere a questo sito amministratore."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:190 core/models.py:190
msgid "active"
msgstr "attivo"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Indica se questo utente deve essere trattato come attivo. Deseleziona invece di eliminare gli account."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:205 core/models.py:205
msgid "user"
msgstr "utente"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:206 core/models.py:206
msgid "users"
msgstr "utenti"
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
#: core/models.py:361 core/models.py:1434
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
msgstr "titolo"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
msgstr "Documento"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:413 core/models.py:413
msgid "Documents"
msgstr "Documenti"
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
#: core/models.py:827
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
msgid "Untitled Document"
msgstr "Documento senza titolo"
#: build/lib/core/models.py:862 core/models.py:862
#: build/lib/core/models.py:829 core/models.py:829
msgid "Open"
msgstr "Apri"
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ha condiviso un documento con te!"
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:868 core/models.py:868
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} ti ha invitato con il ruolo \"{role}\" nel seguente documento:"
#: build/lib/core/models.py:872 core/models.py:872
#: build/lib/core/models.py:874 core/models.py:874
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha condiviso un documento con te: {title}"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:975 core/models.py:975
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:976 core/models.py:976
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:980 core/models.py:980
#: build/lib/core/models.py:982 core/models.py:982
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1005 core/models.py:1005
msgid "Document favorite"
msgstr "Documento preferito"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "Document favorites"
msgstr "Documenti preferiti"
#: build/lib/core/models.py:1010 core/models.py:1010
#: build/lib/core/models.py:1012 core/models.py:1012
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1034 core/models.py:1034
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This user is already in this document."
msgstr "Questo utente è già presente in questo documento."
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "This team is already in this document."
msgstr "Questo team è già presente in questo documento."
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
#: core/models.py:1051 core/models.py:1520
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1202 core/models.py:1202
#: build/lib/core/models.py:1204 core/models.py:1204
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1203 core/models.py:1203
#: build/lib/core/models.py:1205 core/models.py:1205
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1209 core/models.py:1209
#: build/lib/core/models.py:1211 core/models.py:1211
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1268 core/models.py:1268
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1272 core/models.py:1272
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1276 core/models.py:1276
#: build/lib/core/models.py:1278 core/models.py:1278
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1318 core/models.py:1318
#: build/lib/core/models.py:1320 core/models.py:1320
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1319 core/models.py:1319
#: build/lib/core/models.py:1321 core/models.py:1321
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
#: core/models.py:1322 core/models.py:1374
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1369 core/models.py:1369
#: build/lib/core/models.py:1371 core/models.py:1371
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1370 core/models.py:1370
#: build/lib/core/models.py:1372 core/models.py:1372
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
#: build/lib/core/models.py:1421 core/models.py:1421
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1423 core/models.py:1423
#: build/lib/core/models.py:1425 core/models.py:1425
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1424 core/models.py:1424
#: build/lib/core/models.py:1426 core/models.py:1426
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1435 core/models.py:1435
msgid "description"
msgstr "descrizione"
#: build/lib/core/models.py:1436 core/models.py:1436
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "public"
msgstr "pubblico"
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "Whether this template is public for anyone to use."
msgstr "Indica se questo modello è pubblico per chiunque."
#: build/lib/core/models.py:1447 core/models.py:1447
msgid "Template"
msgstr "Modello"
#: build/lib/core/models.py:1448 core/models.py:1448
msgid "Templates"
msgstr "Modelli"
#: build/lib/core/models.py:1501 core/models.py:1501
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1502 core/models.py:1502
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1508 core/models.py:1508
msgid "This user is already in this template."
msgstr "Questo utente è già in questo modello."
#: build/lib/core/models.py:1514 core/models.py:1514
msgid "This team is already in this template."
msgstr "Questo team è già in questo modello."
#: build/lib/core/models.py:1591 core/models.py:1591
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1610 core/models.py:1610
#: build/lib/core/models.py:1455 core/models.py:1455
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1611 core/models.py:1611
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1631 core/models.py:1631
#: build/lib/core/models.py:1476 core/models.py:1476
msgid "This email is already associated to a registered user."
msgstr "Questa email è già associata a un utente registrato."
@@ -450,17 +396,12 @@ msgstr "Questa email è già associata a un utente registrato."
msgid "Logo email"
msgstr "Logo e-mail"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Apri"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:219
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+80 -139
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:28 core/admin.py:28
msgid "Personal info"
msgstr "Persoonlijke informatie"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
msgid "Permissions"
msgstr "Machtigingen"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:53 core/admin.py:53
msgid "Important dates"
msgstr "Belangrijke data"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:131 core/admin.py:131
msgid "Tree structure"
msgstr "Boomstructuur"
@@ -50,36 +50,24 @@ msgstr "Gemaskeerd"
msgid "Favorite"
msgstr "Favoriet"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
msgid "A new document was created on your behalf!"
msgstr "Een nieuw document is namens u gemaakt!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
msgid "You have been granted ownership of a new document:"
msgstr "U heeft eigenaarschap van een nieuw document gekregen:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
msgid "This field is required."
msgstr "Dit veld is verplicht."
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Link bereik '%(link_reach)s' is niet toegestaan op basis van bovenliggende documentconfiguratie."
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
msgid "Body"
msgstr "Text"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
msgid "Body type"
msgstr "Text type"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
msgid "Format"
msgstr "Formaat"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
@@ -147,301 +135,259 @@ msgstr "Links"
msgid "Right"
msgstr "Rechts"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "id"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:82 core/models.py:82
msgid "primary key for the record as UUID"
msgstr "primaire sleutel voor dossier als UUID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "created on"
msgstr "gecreëerd op"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:89 core/models.py:89
msgid "date and time at which a record was created"
msgstr "datum en tijd waarop dossier is gecreeërd"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:94 core/models.py:94
msgid "updated on"
msgstr "Laatst gewijzigd op"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:95 core/models.py:95
msgid "date and time at which a record was last updated"
msgstr "datum en tijd waarop dossier laatst was gewijzigd"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:131 core/models.py:131
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Wij konden geen gebruiker vinden met dit id, maar de email is al geassocieerd met een geregistreerde gebruiker."
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "sub"
msgstr "id"
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:143 core/models.py:143
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Vereist. 255 tekens of minder. Alleen ASCII tekens."
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:151 core/models.py:151
msgid "full name"
msgstr "volledige naam"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:153 core/models.py:153
msgid "short name"
msgstr "gebruikersnaam"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:156 core/models.py:156
msgid "identity email address"
msgstr "identiteit emailadres"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:161 core/models.py:161
msgid "admin email address"
msgstr "admin emailadres"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:168 core/models.py:168
msgid "language"
msgstr "taal"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:169 core/models.py:169
msgid "The language in which the user wants to see the interface."
msgstr "De taal waarin de gebruiker de interface wil zien."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:177 core/models.py:177
msgid "The timezone in which the user wants to see times."
msgstr "De tijdzone waarin de gebruiker de tijden wil zien."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:180 core/models.py:180
msgid "device"
msgstr "apparaat"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:182 core/models.py:182
msgid "Whether the user is a device or a real user."
msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:185 core/models.py:185
msgid "staff status"
msgstr "beheerder status"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:187 core/models.py:187
msgid "Whether the user can log into this admin site."
msgstr "Of de gebruiker kan inloggen in het beheer gedeelte."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:190 core/models.py:190
msgid "active"
msgstr "actief"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Of een gebruiker als actief moet worden beschouwd. Deselecteer dit in plaats van het account te deleten."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:205 core/models.py:205
msgid "user"
msgstr "gebruiker"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:206 core/models.py:206
msgid "users"
msgstr "gebruikers"
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
#: core/models.py:361 core/models.py:1434
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
msgstr "titel"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
msgstr "uittreksel"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
msgstr "Document"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:413 core/models.py:413
msgid "Documents"
msgstr "Documenten"
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
#: core/models.py:827
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
msgid "Untitled Document"
msgstr "Naamloos Document"
#: build/lib/core/models.py:862 core/models.py:862
#: build/lib/core/models.py:829 core/models.py:829
msgid "Open"
msgstr "Open"
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} heeft een document met u gedeeld!"
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:868 core/models.py:868
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} heeft u uitgenodigd met de rol \"{role}\" op het volgende document:"
#: build/lib/core/models.py:872 core/models.py:872
#: build/lib/core/models.py:874 core/models.py:874
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} heeft een document met u gedeeld: {title}"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:975 core/models.py:975
msgid "Document/user link trace"
msgstr "Document/gebruiker link"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:976 core/models.py:976
msgid "Document/user link traces"
msgstr "Document/gebruiker link"
#: build/lib/core/models.py:980 core/models.py:980
#: build/lib/core/models.py:982 core/models.py:982
msgid "A link trace already exists for this document/user."
msgstr "Een link bestaat al voor dit document/deze gebruiker."
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1005 core/models.py:1005
msgid "Document favorite"
msgstr "Document favoriet"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "Document favorites"
msgstr "Document favorieten"
#: build/lib/core/models.py:1010 core/models.py:1010
#: build/lib/core/models.py:1012 core/models.py:1012
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Dit document is al in gebruik als favoriet door dezelfde gebruiker."
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1034 core/models.py:1034
msgid "Document/user relation"
msgstr "Document/gebruiker relatie"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "Document/user relations"
msgstr "Document/gebruiker relaties"
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This user is already in this document."
msgstr "De gebruiker bestaat al in dit document."
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "This team is already in this document."
msgstr "Dit team bestaat al in dit document."
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
#: core/models.py:1051 core/models.py:1520
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "Either user or team must be set, not both."
msgstr "Een gebruiker of team moet gekozen worden, maar niet beide."
#: build/lib/core/models.py:1202 core/models.py:1202
#: build/lib/core/models.py:1204 core/models.py:1204
msgid "Document ask for access"
msgstr "Document verzoekt om toegang"
#: build/lib/core/models.py:1203 core/models.py:1203
#: build/lib/core/models.py:1205 core/models.py:1205
msgid "Document ask for accesses"
msgstr "Document verzoekt om toegangen"
#: build/lib/core/models.py:1209 core/models.py:1209
#: build/lib/core/models.py:1211 core/models.py:1211
msgid "This user has already asked for access to this document."
msgstr "Deze gebruiker heeft al om toegang tot dit document gevraagd."
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1268 core/models.py:1268
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} verzoekt toegang tot een document!"
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1272 core/models.py:1272
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} verzoekt toegang tot het volgende document:"
#: build/lib/core/models.py:1276 core/models.py:1276
#: build/lib/core/models.py:1278 core/models.py:1278
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} verzoekt toegang tot het document: {title}"
#: build/lib/core/models.py:1318 core/models.py:1318
#: build/lib/core/models.py:1320 core/models.py:1320
msgid "Thread"
msgstr "Kanaal"
#: build/lib/core/models.py:1319 core/models.py:1319
#: build/lib/core/models.py:1321 core/models.py:1321
msgid "Threads"
msgstr "Kanalen"
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
#: core/models.py:1322 core/models.py:1374
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
msgid "Anonymous"
msgstr "Anoniem"
#: build/lib/core/models.py:1369 core/models.py:1369
#: build/lib/core/models.py:1371 core/models.py:1371
msgid "Comment"
msgstr "Reactie"
#: build/lib/core/models.py:1370 core/models.py:1370
#: build/lib/core/models.py:1372 core/models.py:1372
msgid "Comments"
msgstr "Reacties"
#: build/lib/core/models.py:1419 core/models.py:1419
#: build/lib/core/models.py:1421 core/models.py:1421
msgid "This emoji has already been reacted to this comment."
msgstr "Deze emoji is al op deze opmerking gereageerd."
#: build/lib/core/models.py:1423 core/models.py:1423
#: build/lib/core/models.py:1425 core/models.py:1425
msgid "Reaction"
msgstr "Reactie"
#: build/lib/core/models.py:1424 core/models.py:1424
#: build/lib/core/models.py:1426 core/models.py:1426
msgid "Reactions"
msgstr "Reacties"
#: build/lib/core/models.py:1435 core/models.py:1435
msgid "description"
msgstr "omschrijving"
#: build/lib/core/models.py:1436 core/models.py:1436
msgid "code"
msgstr "code"
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "public"
msgstr "publiek"
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "Whether this template is public for anyone to use."
msgstr "Of dit sjabloon door iedereen publiekelijk te gebruiken is."
#: build/lib/core/models.py:1447 core/models.py:1447
msgid "Template"
msgstr "Sjabloon"
#: build/lib/core/models.py:1448 core/models.py:1448
msgid "Templates"
msgstr "Sjabloon"
#: build/lib/core/models.py:1501 core/models.py:1501
msgid "Template/user relation"
msgstr "Sjabloon/gebruiker relatie"
#: build/lib/core/models.py:1502 core/models.py:1502
msgid "Template/user relations"
msgstr "Sjabloon/gebruiker relaties"
#: build/lib/core/models.py:1508 core/models.py:1508
msgid "This user is already in this template."
msgstr "De gebruiker bestaat al in dit sjabloon."
#: build/lib/core/models.py:1514 core/models.py:1514
msgid "This team is already in this template."
msgstr "Het team bestaat al in dit sjabloon."
#: build/lib/core/models.py:1591 core/models.py:1591
msgid "email address"
msgstr "e-mailadres"
#: build/lib/core/models.py:1610 core/models.py:1610
#: build/lib/core/models.py:1455 core/models.py:1455
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1611 core/models.py:1611
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1631 core/models.py:1631
#: build/lib/core/models.py:1476 core/models.py:1476
msgid "This email is already associated to a registered user."
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
@@ -450,17 +396,12 @@ msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
msgid "Logo email"
msgstr "Logo email"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Open"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:219
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, jouw nieuwe essentiële tool voor het organiseren, delen en collaboreren van documenten als team. "
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+80 -139
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:28 core/admin.py:28
msgid "Personal info"
msgstr "Informações Pessoais"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
msgid "Permissions"
msgstr "Permissões"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:53 core/admin.py:53
msgid "Important dates"
msgstr "Datas importantes"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:131 core/admin.py:131
msgid "Tree structure"
msgstr "Estrutura de árvore"
@@ -50,36 +50,24 @@ msgstr ""
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
msgid "A new document was created on your behalf!"
msgstr "Um novo documento foi criado em seu nome!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
msgid "You have been granted ownership of a new document:"
msgstr "A propriedade de um novo documento foi concedida a você:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
msgid "Body"
msgstr "Corpo"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
msgid "Body type"
msgstr "Tipo de corpo"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
msgid "Format"
msgstr "Formato"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#, python-brace-format
msgid "copy of {title}"
msgstr "cópia de {title}"
@@ -147,301 +135,259 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "id"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:82 core/models.py:82
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "created on"
msgstr ""
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:89 core/models.py:89
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:94 core/models.py:94
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:95 core/models.py:95
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:131 core/models.py:131
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "sub"
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:143 core/models.py:143
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:151 core/models.py:151
msgid "full name"
msgstr ""
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:153 core/models.py:153
msgid "short name"
msgstr ""
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:156 core/models.py:156
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:161 core/models.py:161
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:168 core/models.py:168
msgid "language"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:169 core/models.py:169
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:177 core/models.py:177
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:180 core/models.py:180
msgid "device"
msgstr ""
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:182 core/models.py:182
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:185 core/models.py:185
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:187 core/models.py:187
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:190 core/models.py:190
msgid "active"
msgstr ""
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:205 core/models.py:205
msgid "user"
msgstr ""
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:206 core/models.py:206
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
#: core/models.py:361 core/models.py:1434
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
msgstr ""
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
msgstr ""
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:413 core/models.py:413
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
#: core/models.py:827
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:862 core/models.py:862
#: build/lib/core/models.py:829 core/models.py:829
msgid "Open"
msgstr ""
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:868 core/models.py:868
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:872 core/models.py:872
#: build/lib/core/models.py:874 core/models.py:874
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:975 core/models.py:975
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:976 core/models.py:976
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:980 core/models.py:980
#: build/lib/core/models.py:982 core/models.py:982
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1005 core/models.py:1005
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1010 core/models.py:1010
#: build/lib/core/models.py:1012 core/models.py:1012
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1034 core/models.py:1034
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
#: core/models.py:1051 core/models.py:1520
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1202 core/models.py:1202
#: build/lib/core/models.py:1204 core/models.py:1204
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1203 core/models.py:1203
#: build/lib/core/models.py:1205 core/models.py:1205
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1209 core/models.py:1209
#: build/lib/core/models.py:1211 core/models.py:1211
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1268 core/models.py:1268
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1272 core/models.py:1272
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1276 core/models.py:1276
#: build/lib/core/models.py:1278 core/models.py:1278
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1318 core/models.py:1318
#: build/lib/core/models.py:1320 core/models.py:1320
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1319 core/models.py:1319
#: build/lib/core/models.py:1321 core/models.py:1321
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
#: core/models.py:1322 core/models.py:1374
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1369 core/models.py:1369
#: build/lib/core/models.py:1371 core/models.py:1371
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1370 core/models.py:1370
#: build/lib/core/models.py:1372 core/models.py:1372
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
#: build/lib/core/models.py:1421 core/models.py:1421
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1423 core/models.py:1423
#: build/lib/core/models.py:1425 core/models.py:1425
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1424 core/models.py:1424
#: build/lib/core/models.py:1426 core/models.py:1426
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1435 core/models.py:1435
msgid "description"
msgstr ""
#: build/lib/core/models.py:1436 core/models.py:1436
msgid "code"
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "css"
msgstr ""
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "public"
msgstr ""
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1447 core/models.py:1447
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1448 core/models.py:1448
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1501 core/models.py:1501
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1502 core/models.py:1502
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1508 core/models.py:1508
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1514 core/models.py:1514
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1591 core/models.py:1591
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1610 core/models.py:1610
#: build/lib/core/models.py:1455 core/models.py:1455
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1611 core/models.py:1611
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1631 core/models.py:1631
#: build/lib/core/models.py:1476 core/models.py:1476
msgid "This email is already associated to a registered user."
msgstr ""
@@ -450,17 +396,12 @@ msgstr ""
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr ""
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:219
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+80 -139
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:28 core/admin.py:28
msgid "Personal info"
msgstr "Личная информация"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
msgid "Permissions"
msgstr "Разрешения"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:53 core/admin.py:53
msgid "Important dates"
msgstr "Важные даты"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:131 core/admin.py:131
msgid "Tree structure"
msgstr "Древовидная структура"
@@ -50,36 +50,24 @@ msgstr "Скрытый"
msgid "Favorite"
msgstr "Избранное"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
msgid "A new document was created on your behalf!"
msgstr "Новый документ был создан от вашего имени!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
msgid "You have been granted ownership of a new document:"
msgstr "Вы назначены владельцем для нового документа:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
msgid "This field is required."
msgstr "Это поле обязательное."
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Доступ по ссылке '%(link_reach)s' запрещён в соответствии с настройками родительского документа."
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
msgid "Body"
msgstr "Текст сообщения"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
msgid "Body type"
msgstr "Тип сообщения"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#, python-brace-format
msgid "copy of {title}"
msgstr "копия {title}"
@@ -147,301 +135,259 @@ msgstr "Слева"
msgid "Right"
msgstr "Справа"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "id"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:82 core/models.py:82
msgid "primary key for the record as UUID"
msgstr "первичный ключ для записи как UUID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "created on"
msgstr "создано"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:89 core/models.py:89
msgid "date and time at which a record was created"
msgstr "дата и время создания записи"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:94 core/models.py:94
msgid "updated on"
msgstr "обновлено"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:95 core/models.py:95
msgid "date and time at which a record was last updated"
msgstr "дата и время последнего обновления записи"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:131 core/models.py:131
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Мы не смогли найти пользователя с этими данными, но этот адрес уже связан с зарегистрированным пользователем."
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "sub"
msgstr "вложение"
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:143 core/models.py:143
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Обязательно. 255 символов или меньше. Только ASCII символы."
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:151 core/models.py:151
msgid "full name"
msgstr "полное имя"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:153 core/models.py:153
msgid "short name"
msgstr "короткое имя"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:156 core/models.py:156
msgid "identity email address"
msgstr "личный адрес электронной почты"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:161 core/models.py:161
msgid "admin email address"
msgstr "e-mail администратора"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:168 core/models.py:168
msgid "language"
msgstr "язык"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:169 core/models.py:169
msgid "The language in which the user wants to see the interface."
msgstr "Язык, на котором пользователь хочет видеть интерфейс."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:177 core/models.py:177
msgid "The timezone in which the user wants to see times."
msgstr "Часовой пояс, в котором пользователь хочет видеть время."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:180 core/models.py:180
msgid "device"
msgstr "устройство"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:182 core/models.py:182
msgid "Whether the user is a device or a real user."
msgstr "Пользователь является устройством или человеком."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:185 core/models.py:185
msgid "staff status"
msgstr "статус сотрудника"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:187 core/models.py:187
msgid "Whether the user can log into this admin site."
msgstr "Может ли пользователь войти на этот административный сайт."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:190 core/models.py:190
msgid "active"
msgstr "активный"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Должен ли пользователь рассматриваться как активный. Альтернатива удалению учётных записей."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:205 core/models.py:205
msgid "user"
msgstr "пользователь"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:206 core/models.py:206
msgid "users"
msgstr "пользователи"
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
#: core/models.py:361 core/models.py:1434
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
msgstr "заголовок"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
msgstr "отрывок"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
msgstr "Документ"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:413 core/models.py:413
msgid "Documents"
msgstr "Документы"
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
#: core/models.py:827
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
msgid "Untitled Document"
msgstr "Безымянный документ"
#: build/lib/core/models.py:862 core/models.py:862
#: build/lib/core/models.py:829 core/models.py:829
msgid "Open"
msgstr "Открыть"
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} делится с вами документом!"
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:868 core/models.py:868
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
#: build/lib/core/models.py:872 core/models.py:872
#: build/lib/core/models.py:874 core/models.py:874
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} делится с вами документом: {title}"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:975 core/models.py:975
msgid "Document/user link trace"
msgstr "Трассировка связи документ/пользователь"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:976 core/models.py:976
msgid "Document/user link traces"
msgstr "Трассировка связей документ/пользователь"
#: build/lib/core/models.py:980 core/models.py:980
#: build/lib/core/models.py:982 core/models.py:982
msgid "A link trace already exists for this document/user."
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1005 core/models.py:1005
msgid "Document favorite"
msgstr "Избранный документ"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "Document favorites"
msgstr "Избранные документы"
#: build/lib/core/models.py:1010 core/models.py:1010
#: build/lib/core/models.py:1012 core/models.py:1012
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Этот документ уже помечен как избранный для этого пользователя."
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1034 core/models.py:1034
msgid "Document/user relation"
msgstr "Отношение документ/пользователь"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "Document/user relations"
msgstr "Отношения документ/пользователь"
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This user is already in this document."
msgstr "Этот пользователь уже имеет доступ к этому документу."
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "This team is already in this document."
msgstr "Эта команда уже имеет доступ к этому документу."
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
#: core/models.py:1051 core/models.py:1520
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "Either user or team must be set, not both."
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
#: build/lib/core/models.py:1202 core/models.py:1202
#: build/lib/core/models.py:1204 core/models.py:1204
msgid "Document ask for access"
msgstr "Документ запрашивает доступ"
#: build/lib/core/models.py:1203 core/models.py:1203
#: build/lib/core/models.py:1205 core/models.py:1205
msgid "Document ask for accesses"
msgstr "Документ запрашивает доступы"
#: build/lib/core/models.py:1209 core/models.py:1209
#: build/lib/core/models.py:1211 core/models.py:1211
msgid "This user has already asked for access to this document."
msgstr "Этот пользователь уже запросил доступ к этому документу."
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1268 core/models.py:1268
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хочет получить доступ к документу!"
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1272 core/models.py:1272
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} хочет получить доступ к следующему документу:"
#: build/lib/core/models.py:1276 core/models.py:1276
#: build/lib/core/models.py:1278 core/models.py:1278
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запрашивает доступ к документу: {title}"
#: build/lib/core/models.py:1318 core/models.py:1318
#: build/lib/core/models.py:1320 core/models.py:1320
msgid "Thread"
msgstr "Обсуждение"
#: build/lib/core/models.py:1319 core/models.py:1319
#: build/lib/core/models.py:1321 core/models.py:1321
msgid "Threads"
msgstr "Обсуждения"
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
#: core/models.py:1322 core/models.py:1374
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
msgid "Anonymous"
msgstr "Аноним"
#: build/lib/core/models.py:1369 core/models.py:1369
#: build/lib/core/models.py:1371 core/models.py:1371
msgid "Comment"
msgstr "Комментарий"
#: build/lib/core/models.py:1370 core/models.py:1370
#: build/lib/core/models.py:1372 core/models.py:1372
msgid "Comments"
msgstr "Комментарии"
#: build/lib/core/models.py:1419 core/models.py:1419
#: build/lib/core/models.py:1421 core/models.py:1421
msgid "This emoji has already been reacted to this comment."
msgstr "Этот эмодзи уже использован в этом комментарии."
#: build/lib/core/models.py:1423 core/models.py:1423
#: build/lib/core/models.py:1425 core/models.py:1425
msgid "Reaction"
msgstr "Реакция"
#: build/lib/core/models.py:1424 core/models.py:1424
#: build/lib/core/models.py:1426 core/models.py:1426
msgid "Reactions"
msgstr "Реакции"
#: build/lib/core/models.py:1435 core/models.py:1435
msgid "description"
msgstr "описание"
#: build/lib/core/models.py:1436 core/models.py:1436
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "public"
msgstr "доступно всем"
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "Whether this template is public for anyone to use."
msgstr "Этот шаблон доступен всем пользователям."
#: build/lib/core/models.py:1447 core/models.py:1447
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1448 core/models.py:1448
msgid "Templates"
msgstr "Шаблоны"
#: build/lib/core/models.py:1501 core/models.py:1501
msgid "Template/user relation"
msgstr "Отношение шаблон/пользователь"
#: build/lib/core/models.py:1502 core/models.py:1502
msgid "Template/user relations"
msgstr "Отношения шаблон/пользователь"
#: build/lib/core/models.py:1508 core/models.py:1508
msgid "This user is already in this template."
msgstr "Этот пользователь уже указан в этом шаблоне."
#: build/lib/core/models.py:1514 core/models.py:1514
msgid "This team is already in this template."
msgstr "Эта команда уже указана в этом шаблоне."
#: build/lib/core/models.py:1591 core/models.py:1591
msgid "email address"
msgstr "адрес электронной почты"
#: build/lib/core/models.py:1610 core/models.py:1610
#: build/lib/core/models.py:1455 core/models.py:1455
msgid "Document invitation"
msgstr "Приглашение для документа"
#: build/lib/core/models.py:1611 core/models.py:1611
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitations"
msgstr "Приглашения для документов"
#: build/lib/core/models.py:1631 core/models.py:1631
#: build/lib/core/models.py:1476 core/models.py:1476
msgid "This email is already associated to a registered user."
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
@@ -450,17 +396,12 @@ msgstr "Этот адрес уже связан с зарегистрирова
msgid "Logo email"
msgstr "Логотип email"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Открыть"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:219
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, ваш новый инструмент для организации и совместного использования документов в вашей команде. "
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+80 -139
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:28 core/admin.py:28
msgid "Personal info"
msgstr "Osebni podatki"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
msgid "Permissions"
msgstr "Dovoljenja"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:53 core/admin.py:53
msgid "Important dates"
msgstr "Pomembni datumi"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:131 core/admin.py:131
msgid "Tree structure"
msgstr "Drevesna struktura"
@@ -50,36 +50,24 @@ msgstr ""
msgid "Favorite"
msgstr "Priljubljena"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
msgid "A new document was created on your behalf!"
msgstr "Nov dokument je bil ustvarjen v vašem imenu!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
msgid "You have been granted ownership of a new document:"
msgstr "Dodeljeno vam je bilo lastništvo nad novim dokumentom:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
msgid "Body"
msgstr "Telo"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
msgid "Body type"
msgstr "Vrsta telesa"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
msgid "Format"
msgstr "Oblika"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -147,301 +135,259 @@ msgstr "Levo"
msgid "Right"
msgstr "Desno"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "id"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:82 core/models.py:82
msgid "primary key for the record as UUID"
msgstr "primarni ključ za zapis kot UUID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "created on"
msgstr "ustvarjen na"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:89 core/models.py:89
msgid "date and time at which a record was created"
msgstr "datum in čas, ko je bil zapis ustvarjen"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:94 core/models.py:94
msgid "updated on"
msgstr "posodobljeno dne"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:95 core/models.py:95
msgid "date and time at which a record was last updated"
msgstr "datum in čas, ko je bil zapis nazadnje posodobljen"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:131 core/models.py:131
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Nismo mogli najti uporabnika s tem sub, vendar je e-poštni naslov že povezan z registriranim uporabnikom."
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "sub"
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:143 core/models.py:143
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:151 core/models.py:151
msgid "full name"
msgstr "polno ime"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:153 core/models.py:153
msgid "short name"
msgstr "kratko ime"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:156 core/models.py:156
msgid "identity email address"
msgstr "elektronski naslov identitete"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:161 core/models.py:161
msgid "admin email address"
msgstr "elektronski naslov skrbnika"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:168 core/models.py:168
msgid "language"
msgstr "jezik"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:169 core/models.py:169
msgid "The language in which the user wants to see the interface."
msgstr "Jezik, v katerem uporabnik želi videti vmesnik."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:177 core/models.py:177
msgid "The timezone in which the user wants to see times."
msgstr "Časovni pas, v katerem želi uporabnik videti uro."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:180 core/models.py:180
msgid "device"
msgstr "naprava"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:182 core/models.py:182
msgid "Whether the user is a device or a real user."
msgstr "Ali je uporabnik naprava ali pravi uporabnik."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:185 core/models.py:185
msgid "staff status"
msgstr "kadrovski status"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:187 core/models.py:187
msgid "Whether the user can log into this admin site."
msgstr "Ali se uporabnik lahko prijavi na to skrbniško mesto."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:190 core/models.py:190
msgid "active"
msgstr "aktivni"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Ali je treba tega uporabnika obravnavati kot aktivnega. Namesto brisanja računov počistite to izbiro."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:205 core/models.py:205
msgid "user"
msgstr "uporabnik"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:206 core/models.py:206
msgid "users"
msgstr "uporabniki"
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
#: core/models.py:361 core/models.py:1434
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
msgstr "naslov"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
msgstr "odlomek"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:413 core/models.py:413
msgid "Documents"
msgstr "Dokumenti"
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
#: core/models.py:827
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
msgid "Untitled Document"
msgstr "Dokument brez naslova"
#: build/lib/core/models.py:862 core/models.py:862
#: build/lib/core/models.py:829 core/models.py:829
msgid "Open"
msgstr "Odpri"
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} je delil dokument z vami!"
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:868 core/models.py:868
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} vas je povabil z vlogo \"{role}\" na naslednjem dokumentu:"
#: build/lib/core/models.py:872 core/models.py:872
#: build/lib/core/models.py:874 core/models.py:874
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} je delil dokument z vami: {title}"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:975 core/models.py:975
msgid "Document/user link trace"
msgstr "Dokument/sled povezave uporabnika"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:976 core/models.py:976
msgid "Document/user link traces"
msgstr "Sledi povezav dokumenta/uporabnika"
#: build/lib/core/models.py:980 core/models.py:980
#: build/lib/core/models.py:982 core/models.py:982
msgid "A link trace already exists for this document/user."
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1005 core/models.py:1005
msgid "Document favorite"
msgstr "Priljubljeni dokument"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "Document favorites"
msgstr "Priljubljeni dokumenti"
#: build/lib/core/models.py:1010 core/models.py:1010
#: build/lib/core/models.py:1012 core/models.py:1012
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Ta dokument je že ciljno usmerjen s priljubljenim primerkom relacije za istega uporabnika."
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1034 core/models.py:1034
msgid "Document/user relation"
msgstr "Odnos dokument/uporabnik"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "Document/user relations"
msgstr "Odnosi dokument/uporabnik"
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This user is already in this document."
msgstr "Ta uporabnik je že v tem dokumentu."
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "This team is already in this document."
msgstr "Ta ekipa je že v tem dokumentu."
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
#: core/models.py:1051 core/models.py:1520
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "Either user or team must be set, not both."
msgstr "Nastaviti je treba bodisi uporabnika ali ekipo, a ne obojega."
#: build/lib/core/models.py:1202 core/models.py:1202
#: build/lib/core/models.py:1204 core/models.py:1204
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1203 core/models.py:1203
#: build/lib/core/models.py:1205 core/models.py:1205
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1209 core/models.py:1209
#: build/lib/core/models.py:1211 core/models.py:1211
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1268 core/models.py:1268
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1272 core/models.py:1272
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1276 core/models.py:1276
#: build/lib/core/models.py:1278 core/models.py:1278
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1318 core/models.py:1318
#: build/lib/core/models.py:1320 core/models.py:1320
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1319 core/models.py:1319
#: build/lib/core/models.py:1321 core/models.py:1321
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
#: core/models.py:1322 core/models.py:1374
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1369 core/models.py:1369
#: build/lib/core/models.py:1371 core/models.py:1371
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1370 core/models.py:1370
#: build/lib/core/models.py:1372 core/models.py:1372
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
#: build/lib/core/models.py:1421 core/models.py:1421
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1423 core/models.py:1423
#: build/lib/core/models.py:1425 core/models.py:1425
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1424 core/models.py:1424
#: build/lib/core/models.py:1426 core/models.py:1426
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1435 core/models.py:1435
msgid "description"
msgstr "opis"
#: build/lib/core/models.py:1436 core/models.py:1436
msgid "code"
msgstr "koda"
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "public"
msgstr "javno"
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "Whether this template is public for anyone to use."
msgstr "Ali je ta predloga javna za uporabo."
#: build/lib/core/models.py:1447 core/models.py:1447
msgid "Template"
msgstr "Predloga"
#: build/lib/core/models.py:1448 core/models.py:1448
msgid "Templates"
msgstr "Predloge"
#: build/lib/core/models.py:1501 core/models.py:1501
msgid "Template/user relation"
msgstr "Odnos predloga/uporabnik"
#: build/lib/core/models.py:1502 core/models.py:1502
msgid "Template/user relations"
msgstr "Odnosi med predlogo in uporabnikom"
#: build/lib/core/models.py:1508 core/models.py:1508
msgid "This user is already in this template."
msgstr "Ta uporabnik je že v tej predlogi."
#: build/lib/core/models.py:1514 core/models.py:1514
msgid "This team is already in this template."
msgstr "Ta ekipa je že v tej predlogi."
#: build/lib/core/models.py:1591 core/models.py:1591
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1610 core/models.py:1610
#: build/lib/core/models.py:1455 core/models.py:1455
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1611 core/models.py:1611
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1631 core/models.py:1631
#: build/lib/core/models.py:1476 core/models.py:1476
msgid "This email is already associated to a registered user."
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."
@@ -450,17 +396,12 @@ msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."
msgid "Logo email"
msgstr "E-pošta z logotipom"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Odpri"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:219
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Dokumenti, vaše novo bistveno orodje za organiziranje, skupno rabo in skupinsko sodelovanje pri dokumentih. "
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+80 -139
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:28 core/admin.py:28
msgid "Personal info"
msgstr "Personuppgifter"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
msgid "Permissions"
msgstr "Behörigheter"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:53 core/admin.py:53
msgid "Important dates"
msgstr "Viktiga datum"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:131 core/admin.py:131
msgid "Tree structure"
msgstr ""
@@ -50,36 +50,24 @@ msgstr ""
msgid "Favorite"
msgstr "Favoriter"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
msgid "A new document was created on your behalf!"
msgstr "Ett nytt dokument skapades åt dig!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
msgid "You have been granted ownership of a new document:"
msgstr "Du har beviljats äganderätt till ett nytt dokument:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
msgid "Format"
msgstr "Format"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -147,301 +135,259 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "id"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:82 core/models.py:82
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "created on"
msgstr ""
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:89 core/models.py:89
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:94 core/models.py:94
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:95 core/models.py:95
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:131 core/models.py:131
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "sub"
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:143 core/models.py:143
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:151 core/models.py:151
msgid "full name"
msgstr ""
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:153 core/models.py:153
msgid "short name"
msgstr ""
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:156 core/models.py:156
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:161 core/models.py:161
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:168 core/models.py:168
msgid "language"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:169 core/models.py:169
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:177 core/models.py:177
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:180 core/models.py:180
msgid "device"
msgstr ""
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:182 core/models.py:182
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:185 core/models.py:185
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:187 core/models.py:187
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:190 core/models.py:190
msgid "active"
msgstr "aktiv"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:205 core/models.py:205
msgid "user"
msgstr ""
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:206 core/models.py:206
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
#: core/models.py:361 core/models.py:1434
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
msgstr ""
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
msgstr ""
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:413 core/models.py:413
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
#: core/models.py:827
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:862 core/models.py:862
#: build/lib/core/models.py:829 core/models.py:829
msgid "Open"
msgstr "Öppna"
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:868 core/models.py:868
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:872 core/models.py:872
#: build/lib/core/models.py:874 core/models.py:874
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:975 core/models.py:975
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:976 core/models.py:976
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:980 core/models.py:980
#: build/lib/core/models.py:982 core/models.py:982
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1005 core/models.py:1005
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1010 core/models.py:1010
#: build/lib/core/models.py:1012 core/models.py:1012
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1034 core/models.py:1034
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
#: core/models.py:1051 core/models.py:1520
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1202 core/models.py:1202
#: build/lib/core/models.py:1204 core/models.py:1204
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1203 core/models.py:1203
#: build/lib/core/models.py:1205 core/models.py:1205
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1209 core/models.py:1209
#: build/lib/core/models.py:1211 core/models.py:1211
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1268 core/models.py:1268
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1272 core/models.py:1272
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1276 core/models.py:1276
#: build/lib/core/models.py:1278 core/models.py:1278
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1318 core/models.py:1318
#: build/lib/core/models.py:1320 core/models.py:1320
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1319 core/models.py:1319
#: build/lib/core/models.py:1321 core/models.py:1321
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
#: core/models.py:1322 core/models.py:1374
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1369 core/models.py:1369
#: build/lib/core/models.py:1371 core/models.py:1371
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1370 core/models.py:1370
#: build/lib/core/models.py:1372 core/models.py:1372
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
#: build/lib/core/models.py:1421 core/models.py:1421
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1423 core/models.py:1423
#: build/lib/core/models.py:1425 core/models.py:1425
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1424 core/models.py:1424
#: build/lib/core/models.py:1426 core/models.py:1426
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1435 core/models.py:1435
msgid "description"
msgstr ""
#: build/lib/core/models.py:1436 core/models.py:1436
msgid "code"
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "css"
msgstr ""
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "public"
msgstr ""
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1447 core/models.py:1447
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1448 core/models.py:1448
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1501 core/models.py:1501
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1502 core/models.py:1502
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1508 core/models.py:1508
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1514 core/models.py:1514
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1591 core/models.py:1591
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1610 core/models.py:1610
#: build/lib/core/models.py:1455 core/models.py:1455
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1611 core/models.py:1611
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1631 core/models.py:1631
#: build/lib/core/models.py:1476 core/models.py:1476
msgid "This email is already associated to a registered user."
msgstr "Denna e-postadress är redan associerad med en registrerad användare."
@@ -450,17 +396,12 @@ msgstr "Denna e-postadress är redan associerad med en registrerad användare."
msgid "Logo email"
msgstr "Logotyp e-post"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Öppna"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:219
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+80 -139
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:28 core/admin.py:28
msgid "Personal info"
msgstr ""
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
msgid "Permissions"
msgstr ""
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:53 core/admin.py:53
msgid "Important dates"
msgstr ""
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:131 core/admin.py:131
msgid "Tree structure"
msgstr ""
@@ -50,36 +50,24 @@ msgstr ""
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
msgid "Body"
msgstr ""
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
msgid "Body type"
msgstr ""
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
msgid "Format"
msgstr ""
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -147,301 +135,259 @@ msgstr ""
msgid "Right"
msgstr ""
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "id"
msgstr ""
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:82 core/models.py:82
msgid "primary key for the record as UUID"
msgstr ""
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "created on"
msgstr ""
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:89 core/models.py:89
msgid "date and time at which a record was created"
msgstr ""
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:94 core/models.py:94
msgid "updated on"
msgstr ""
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:95 core/models.py:95
msgid "date and time at which a record was last updated"
msgstr ""
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:131 core/models.py:131
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr ""
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "sub"
msgstr ""
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:143 core/models.py:143
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:151 core/models.py:151
msgid "full name"
msgstr ""
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:153 core/models.py:153
msgid "short name"
msgstr ""
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:156 core/models.py:156
msgid "identity email address"
msgstr ""
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:161 core/models.py:161
msgid "admin email address"
msgstr ""
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:168 core/models.py:168
msgid "language"
msgstr ""
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:169 core/models.py:169
msgid "The language in which the user wants to see the interface."
msgstr ""
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:177 core/models.py:177
msgid "The timezone in which the user wants to see times."
msgstr ""
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:180 core/models.py:180
msgid "device"
msgstr ""
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:182 core/models.py:182
msgid "Whether the user is a device or a real user."
msgstr ""
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:185 core/models.py:185
msgid "staff status"
msgstr ""
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:187 core/models.py:187
msgid "Whether the user can log into this admin site."
msgstr ""
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:190 core/models.py:190
msgid "active"
msgstr ""
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr ""
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:205 core/models.py:205
msgid "user"
msgstr ""
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:206 core/models.py:206
msgid "users"
msgstr ""
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
#: core/models.py:361 core/models.py:1434
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
msgstr ""
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
msgstr ""
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:413 core/models.py:413
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
#: core/models.py:827
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:862 core/models.py:862
#: build/lib/core/models.py:829 core/models.py:829
msgid "Open"
msgstr ""
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:868 core/models.py:868
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:872 core/models.py:872
#: build/lib/core/models.py:874 core/models.py:874
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:975 core/models.py:975
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:976 core/models.py:976
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:980 core/models.py:980
#: build/lib/core/models.py:982 core/models.py:982
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1005 core/models.py:1005
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1010 core/models.py:1010
#: build/lib/core/models.py:1012 core/models.py:1012
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1034 core/models.py:1034
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
#: core/models.py:1051 core/models.py:1520
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1202 core/models.py:1202
#: build/lib/core/models.py:1204 core/models.py:1204
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1203 core/models.py:1203
#: build/lib/core/models.py:1205 core/models.py:1205
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1209 core/models.py:1209
#: build/lib/core/models.py:1211 core/models.py:1211
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1268 core/models.py:1268
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1272 core/models.py:1272
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1276 core/models.py:1276
#: build/lib/core/models.py:1278 core/models.py:1278
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1318 core/models.py:1318
#: build/lib/core/models.py:1320 core/models.py:1320
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1319 core/models.py:1319
#: build/lib/core/models.py:1321 core/models.py:1321
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
#: core/models.py:1322 core/models.py:1374
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1369 core/models.py:1369
#: build/lib/core/models.py:1371 core/models.py:1371
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1370 core/models.py:1370
#: build/lib/core/models.py:1372 core/models.py:1372
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1419 core/models.py:1419
#: build/lib/core/models.py:1421 core/models.py:1421
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1423 core/models.py:1423
#: build/lib/core/models.py:1425 core/models.py:1425
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1424 core/models.py:1424
#: build/lib/core/models.py:1426 core/models.py:1426
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1435 core/models.py:1435
msgid "description"
msgstr ""
#: build/lib/core/models.py:1436 core/models.py:1436
msgid "code"
msgstr ""
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "css"
msgstr ""
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "public"
msgstr ""
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "Whether this template is public for anyone to use."
msgstr ""
#: build/lib/core/models.py:1447 core/models.py:1447
msgid "Template"
msgstr ""
#: build/lib/core/models.py:1448 core/models.py:1448
msgid "Templates"
msgstr ""
#: build/lib/core/models.py:1501 core/models.py:1501
msgid "Template/user relation"
msgstr ""
#: build/lib/core/models.py:1502 core/models.py:1502
msgid "Template/user relations"
msgstr ""
#: build/lib/core/models.py:1508 core/models.py:1508
msgid "This user is already in this template."
msgstr ""
#: build/lib/core/models.py:1514 core/models.py:1514
msgid "This team is already in this template."
msgstr ""
#: build/lib/core/models.py:1591 core/models.py:1591
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1610 core/models.py:1610
#: build/lib/core/models.py:1455 core/models.py:1455
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1611 core/models.py:1611
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1631 core/models.py:1631
#: build/lib/core/models.py:1476 core/models.py:1476
msgid "This email is already associated to a registered user."
msgstr ""
@@ -450,17 +396,12 @@ msgstr ""
msgid "Logo email"
msgstr ""
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr ""
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:219
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr ""
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+80 -139
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -17,20 +17,20 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:28 core/admin.py:28
msgid "Personal info"
msgstr "Особисті дані"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
msgid "Permissions"
msgstr "Дозволи"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:53 core/admin.py:53
msgid "Important dates"
msgstr "Важливі дати"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:131 core/admin.py:131
msgid "Tree structure"
msgstr "Ієрархічна структура"
@@ -50,36 +50,24 @@ msgstr "Приховано"
msgid "Favorite"
msgstr "Обране"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
msgid "A new document was created on your behalf!"
msgstr "Новий документ був створений від вашого імені!"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
msgid "You have been granted ownership of a new document:"
msgstr "Ви тепер є власником нового документа:"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
msgid "This field is required."
msgstr "Це поле є обов’язковим."
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Доступ до посилання '%(link_reach)s' заборонено на основі конфігурації батьківського документа."
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
msgid "Body"
msgstr "Вміст"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
msgid "Body type"
msgstr "Тип вмісту"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
msgid "Format"
msgstr "Формат"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#, python-brace-format
msgid "copy of {title}"
msgstr "копія {title}"
@@ -147,301 +135,259 @@ msgstr "Ліворуч"
msgid "Right"
msgstr "Праворуч"
#: build/lib/core/models.py:80 core/models.py:80
#: build/lib/core/models.py:81 core/models.py:81
msgid "id"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
#: build/lib/core/models.py:82 core/models.py:82
msgid "primary key for the record as UUID"
msgstr "первинний ключ для запису як UUID"
#: build/lib/core/models.py:87 core/models.py:87
#: build/lib/core/models.py:88 core/models.py:88
msgid "created on"
msgstr "створено"
#: build/lib/core/models.py:88 core/models.py:88
#: build/lib/core/models.py:89 core/models.py:89
msgid "date and time at which a record was created"
msgstr "дата і час, коли запис було створено"
#: build/lib/core/models.py:93 core/models.py:93
#: build/lib/core/models.py:94 core/models.py:94
msgid "updated on"
msgstr "оновлено"
#: build/lib/core/models.py:94 core/models.py:94
#: build/lib/core/models.py:95 core/models.py:95
msgid "date and time at which a record was last updated"
msgstr "дата і час, коли запис був востаннє оновлений"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:131 core/models.py:131
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Ми не змогли знайти користувача з цими даними, але адреса вже пов'язана з зареєстрованим користувачем."
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "sub"
msgstr "вкладений документ"
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:143 core/models.py:143
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Обов'язкове. 255 символів або менше. Тільки символи ASCII."
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:151 core/models.py:151
msgid "full name"
msgstr "повне ім'я"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:153 core/models.py:153
msgid "short name"
msgstr "коротке ім'я"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:156 core/models.py:156
msgid "identity email address"
msgstr "адреса електронної пошти особи"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:161 core/models.py:161
msgid "admin email address"
msgstr "електронна адреса адміністратора"
#: build/lib/core/models.py:167 core/models.py:167
#: build/lib/core/models.py:168 core/models.py:168
msgid "language"
msgstr "мова"
#: build/lib/core/models.py:168 core/models.py:168
#: build/lib/core/models.py:169 core/models.py:169
msgid "The language in which the user wants to see the interface."
msgstr "Мова, якою користувач хоче бачити інтерфейс."
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:177 core/models.py:177
msgid "The timezone in which the user wants to see times."
msgstr "Часовий пояс, в якому користувач хоче бачити час."
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:180 core/models.py:180
msgid "device"
msgstr "пристрій"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:182 core/models.py:182
msgid "Whether the user is a device or a real user."
msgstr "Чи є користувач пристроєм чи реальним користувачем."
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:185 core/models.py:185
msgid "staff status"
msgstr "статус співробітника"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:187 core/models.py:187
msgid "Whether the user can log into this admin site."
msgstr "Чи може користувач увійти на цей сайт адміністратора."
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:190 core/models.py:190
msgid "active"
msgstr "активний"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Чи слід ставитися до цього користувача як до активного. Зніміть вибір замість видалення облікового запису."
#: build/lib/core/models.py:204 core/models.py:204
#: build/lib/core/models.py:205 core/models.py:205
msgid "user"
msgstr "користувач"
#: build/lib/core/models.py:205 core/models.py:205
#: build/lib/core/models.py:206 core/models.py:206
msgid "users"
msgstr "користувачі"
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
#: core/models.py:361 core/models.py:1434
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
msgstr "заголовок"
#: build/lib/core/models.py:362 core/models.py:362
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
msgstr "уривок"
#: build/lib/core/models.py:411 core/models.py:411
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
msgstr "Документ"
#: build/lib/core/models.py:412 core/models.py:412
#: build/lib/core/models.py:413 core/models.py:413
msgid "Documents"
msgstr "Документи"
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
#: core/models.py:827
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
msgid "Untitled Document"
msgstr "Документ без назви"
#: build/lib/core/models.py:862 core/models.py:862
#: build/lib/core/models.py:829 core/models.py:829
msgid "Open"
msgstr "Відкрити"
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ділиться з вами документом!"
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:868 core/models.py:868
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
#: build/lib/core/models.py:872 core/models.py:872
#: build/lib/core/models.py:874 core/models.py:874
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ділиться з вами документом: {title}"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:975 core/models.py:975
msgid "Document/user link trace"
msgstr "Трасування посилання Документ/користувач"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:976 core/models.py:976
msgid "Document/user link traces"
msgstr "Трасування посилань Документ/користувач"
#: build/lib/core/models.py:980 core/models.py:980
#: build/lib/core/models.py:982 core/models.py:982
msgid "A link trace already exists for this document/user."
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1005 core/models.py:1005
msgid "Document favorite"
msgstr "Обраний документ"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "Document favorites"
msgstr "Обрані документи"
#: build/lib/core/models.py:1010 core/models.py:1010
#: build/lib/core/models.py:1012 core/models.py:1012
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Цей документ вже вказаний як обраний для одного користувача."
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1034 core/models.py:1034
msgid "Document/user relation"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "Document/user relations"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This user is already in this document."
msgstr "Цей користувач вже має доступ до цього документу."
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "This team is already in this document."
msgstr "Ця команда вже має доступ до цього документа."
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
#: core/models.py:1051 core/models.py:1520
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "Either user or team must be set, not both."
msgstr "Вкажіть користувача або команду, а не обох."
#: build/lib/core/models.py:1202 core/models.py:1202
#: build/lib/core/models.py:1204 core/models.py:1204
msgid "Document ask for access"
msgstr "Запит доступу до документа"
#: build/lib/core/models.py:1203 core/models.py:1203
#: build/lib/core/models.py:1205 core/models.py:1205
msgid "Document ask for accesses"
msgstr "Запит доступу для документа"
#: build/lib/core/models.py:1209 core/models.py:1209
#: build/lib/core/models.py:1211 core/models.py:1211
msgid "This user has already asked for access to this document."
msgstr "Цей користувач вже попросив доступ до цього документа."
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1268 core/models.py:1268
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хоче отримати доступ до документа!"
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1272 core/models.py:1272
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} бажає отримати доступ до наступного документа:"
#: build/lib/core/models.py:1276 core/models.py:1276
#: build/lib/core/models.py:1278 core/models.py:1278
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запитує доступ до документа: {title}"
#: build/lib/core/models.py:1318 core/models.py:1318
#: build/lib/core/models.py:1320 core/models.py:1320
msgid "Thread"
msgstr "Обговорення"
#: build/lib/core/models.py:1319 core/models.py:1319
#: build/lib/core/models.py:1321 core/models.py:1321
msgid "Threads"
msgstr "Обговорення"
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
#: core/models.py:1322 core/models.py:1374
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
msgid "Anonymous"
msgstr "Анонім"
#: build/lib/core/models.py:1369 core/models.py:1369
#: build/lib/core/models.py:1371 core/models.py:1371
msgid "Comment"
msgstr "Коментар"
#: build/lib/core/models.py:1370 core/models.py:1370
#: build/lib/core/models.py:1372 core/models.py:1372
msgid "Comments"
msgstr "Коментарі"
#: build/lib/core/models.py:1419 core/models.py:1419
#: build/lib/core/models.py:1421 core/models.py:1421
msgid "This emoji has already been reacted to this comment."
msgstr "Цим емодзі вже відреагували на цей коментар."
#: build/lib/core/models.py:1423 core/models.py:1423
#: build/lib/core/models.py:1425 core/models.py:1425
msgid "Reaction"
msgstr "Реакція"
#: build/lib/core/models.py:1424 core/models.py:1424
#: build/lib/core/models.py:1426 core/models.py:1426
msgid "Reactions"
msgstr "Реакції"
#: build/lib/core/models.py:1435 core/models.py:1435
msgid "description"
msgstr "опис"
#: build/lib/core/models.py:1436 core/models.py:1436
msgid "code"
msgstr "код"
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "public"
msgstr "публічне"
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "Whether this template is public for anyone to use."
msgstr "Чи є цей шаблон публічним для будь-кого користувача."
#: build/lib/core/models.py:1447 core/models.py:1447
msgid "Template"
msgstr "Шаблон"
#: build/lib/core/models.py:1448 core/models.py:1448
msgid "Templates"
msgstr "Шаблони"
#: build/lib/core/models.py:1501 core/models.py:1501
msgid "Template/user relation"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1502 core/models.py:1502
msgid "Template/user relations"
msgstr "Відношення шаблон/користувач"
#: build/lib/core/models.py:1508 core/models.py:1508
msgid "This user is already in this template."
msgstr "Цей користувач вже має доступ до цього шаблону."
#: build/lib/core/models.py:1514 core/models.py:1514
msgid "This team is already in this template."
msgstr "Ця команда вже має доступ до цього шаблону."
#: build/lib/core/models.py:1591 core/models.py:1591
msgid "email address"
msgstr "електронна адреса"
#: build/lib/core/models.py:1610 core/models.py:1610
#: build/lib/core/models.py:1455 core/models.py:1455
msgid "Document invitation"
msgstr "Запрошення до редагування документа"
#: build/lib/core/models.py:1611 core/models.py:1611
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitations"
msgstr "Запрошення до редагування документів"
#: build/lib/core/models.py:1631 core/models.py:1631
#: build/lib/core/models.py:1476 core/models.py:1476
msgid "This email is already associated to a registered user."
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
@@ -450,17 +396,12 @@ msgstr "Ця електронна пошта вже пов'язана з зар
msgid "Logo email"
msgstr "Логотип пошти"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "Відкрити"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:219
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs, ваш новий важливий інструмент для організації, обміну та командної співпраці над вашими документами. "
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
+172 -231
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 15:38+0000\n"
"PO-Revision-Date: 2026-01-13 13:17\n"
"POT-Creation-Date: 2026-01-21 09:53+0000\n"
"PO-Revision-Date: 2026-01-28 20:12\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -17,127 +17,115 @@ msgstr ""
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:36 core/admin.py:36
#: build/lib/core/admin.py:28 core/admin.py:28
msgid "Personal info"
msgstr "个人信息"
msgstr "個人資訊"
#: build/lib/core/admin.py:49 build/lib/core/admin.py:137 core/admin.py:49
#: core/admin.py:137
#: build/lib/core/admin.py:41 build/lib/core/admin.py:121 core/admin.py:41
#: core/admin.py:121
msgid "Permissions"
msgstr "限"
msgstr "限"
#: build/lib/core/admin.py:61 core/admin.py:61
#: build/lib/core/admin.py:53 core/admin.py:53
msgid "Important dates"
msgstr "重要日期"
#: build/lib/core/admin.py:147 core/admin.py:147
#: build/lib/core/admin.py:131 core/admin.py:131
msgid "Tree structure"
msgstr "树状结构"
msgstr "樹狀結構"
#: build/lib/core/api/filters.py:47 core/api/filters.py:47
msgid "Title"
msgstr "标题"
msgstr "標題"
#: build/lib/core/api/filters.py:61 core/api/filters.py:61
msgid "Creator is me"
msgstr "建者是我"
msgstr "建者是我"
#: build/lib/core/api/filters.py:64 core/api/filters.py:64
msgid "Masked"
msgstr "已屏蔽"
msgstr "已隱藏"
#: build/lib/core/api/filters.py:67 core/api/filters.py:67
msgid "Favorite"
msgstr "收藏"
msgstr "我的最愛"
#: build/lib/core/api/serializers.py:497 core/api/serializers.py:497
#: build/lib/core/api/serializers.py:505 core/api/serializers.py:505
msgid "A new document was created on your behalf!"
msgstr "已为您创建了一份新文"
msgstr "已代表您建立新文"
#: build/lib/core/api/serializers.py:501 core/api/serializers.py:501
#: build/lib/core/api/serializers.py:509 core/api/serializers.py:509
msgid "You have been granted ownership of a new document:"
msgstr "您已被授予新文的所有"
msgstr "您已獲得新文的所有"
#: build/lib/core/api/serializers.py:537 core/api/serializers.py:537
#: build/lib/core/api/serializers.py:545 core/api/serializers.py:545
msgid "This field is required."
msgstr "必填字段。"
msgstr "此欄位為必填。"
#: build/lib/core/api/serializers.py:548 core/api/serializers.py:548
#: build/lib/core/api/serializers.py:556 core/api/serializers.py:556
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
msgstr "根據父文件設定,不允許連結範圍「%(link_reach)s」。"
#: build/lib/core/api/serializers.py:694 core/api/serializers.py:694
msgid "Body"
msgstr "正文"
#: build/lib/core/api/serializers.py:697 core/api/serializers.py:697
msgid "Body type"
msgstr "正文类型"
#: build/lib/core/api/serializers.py:703 core/api/serializers.py:703
msgid "Format"
msgstr "格式"
#: build/lib/core/api/viewsets.py:1081 core/api/viewsets.py:1081
#: build/lib/core/api/viewsets.py:1122 core/api/viewsets.py:1122
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
#: build/lib/core/apps.py:12 core/apps.py:12
msgid "Impress core application"
msgstr ""
msgstr "Impress 核心應用程式"
#: build/lib/core/choices.py:35 build/lib/core/choices.py:43 core/choices.py:35
#: core/choices.py:43
msgid "Reader"
msgstr "阅读者"
msgstr "檢視者"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr ""
msgstr "評論者"
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
msgid "Editor"
msgstr "编辑者"
msgstr "編輯者"
#: build/lib/core/choices.py:46 core/choices.py:46
msgid "Administrator"
msgstr "超级管理"
msgstr "管理"
#: build/lib/core/choices.py:47 core/choices.py:47
msgid "Owner"
msgstr "有者"
msgstr "有者"
#: build/lib/core/choices.py:58 core/choices.py:58
msgid "Restricted"
msgstr "受限"
msgstr "受限"
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Authenticated"
msgstr "已验证"
msgstr "已驗證"
#: build/lib/core/choices.py:64 core/choices.py:64
msgid "Public"
msgstr "公"
msgstr "公"
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "第一个子项"
msgstr "第一個子項目"
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "最后一个子项"
msgstr "最後一個子項目"
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr "第一个同级项"
msgstr "第一個同級項目"
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr "最后一个同级项"
msgstr "最後一個同級項目"
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
@@ -147,322 +135,275 @@ msgstr "左"
msgid "Right"
msgstr "右"
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "记录的主密钥为 UUID"
msgid "id"
msgstr "ID"
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "创建时间"
#: build/lib/core/models.py:82 core/models.py:82
msgid "primary key for the record as UUID"
msgstr "記錄的主鍵(UUID"
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "记录的创建日期和时间"
msgid "created on"
msgstr "建立於"
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "更新时间"
#: build/lib/core/models.py:89 core/models.py:89
msgid "date and time at which a record was created"
msgstr "記錄建立的日期與時間"
#: build/lib/core/models.py:94 core/models.py:94
msgid "updated on"
msgstr "更新於"
#: build/lib/core/models.py:95 core/models.py:95
msgid "date and time at which a record was last updated"
msgstr "记录的最后更新时间"
msgstr "記錄最後更新的日期與時間"
#: build/lib/core/models.py:130 core/models.py:130
#: build/lib/core/models.py:131 core/models.py:131
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "未找到具有 sub 的用户,但该邮箱已关联到一个注册用户。"
msgstr "我們找不到具有 sub 的使用者,但此電子郵件地址已與已註冊使用者關聯。"
#: build/lib/core/models.py:141 core/models.py:141
#: build/lib/core/models.py:142 core/models.py:142
msgid "sub"
msgstr "sub"
#: build/lib/core/models.py:142 core/models.py:142
#: build/lib/core/models.py:143 core/models.py:143
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "必填项。限255个字符以内。仅支持ASCII字符。"
msgstr "必填。255 個字元(含)以下。僅限 ASCII 字元。"
#: build/lib/core/models.py:150 core/models.py:150
#: build/lib/core/models.py:151 core/models.py:151
msgid "full name"
msgstr "全名"
#: build/lib/core/models.py:152 core/models.py:152
#: build/lib/core/models.py:153 core/models.py:153
msgid "short name"
msgstr "简称"
msgstr "簡稱"
#: build/lib/core/models.py:155 core/models.py:155
#: build/lib/core/models.py:156 core/models.py:156
msgid "identity email address"
msgstr "身份电子邮件地址"
msgstr "身份驗證電子郵件地址"
#: build/lib/core/models.py:160 core/models.py:160
#: build/lib/core/models.py:161 core/models.py:161
msgid "admin email address"
msgstr "管理员电子邮件地址"
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr "语言"
msgstr "管理員電子郵件地址"
#: build/lib/core/models.py:168 core/models.py:168
msgid "language"
msgstr "語言"
#: build/lib/core/models.py:169 core/models.py:169
msgid "The language in which the user wants to see the interface."
msgstr "用户希望看到的界面语言。"
msgstr "使用者希望介面顯示的語言。"
#: build/lib/core/models.py:176 core/models.py:176
#: build/lib/core/models.py:177 core/models.py:177
msgid "The timezone in which the user wants to see times."
msgstr "用户查看时间希望的时区。"
msgstr "使用者希望時間顯示的時區。"
#: build/lib/core/models.py:179 core/models.py:179
#: build/lib/core/models.py:180 core/models.py:180
msgid "device"
msgstr "设备"
msgstr "裝置"
#: build/lib/core/models.py:181 core/models.py:181
#: build/lib/core/models.py:182 core/models.py:182
msgid "Whether the user is a device or a real user."
msgstr "用户是设备还是真实用户。"
msgstr "使用者是裝置還是真實使用者。"
#: build/lib/core/models.py:184 core/models.py:184
#: build/lib/core/models.py:185 core/models.py:185
msgid "staff status"
msgstr "员工状态"
msgstr "工作人員狀態"
#: build/lib/core/models.py:186 core/models.py:186
#: build/lib/core/models.py:187 core/models.py:187
msgid "Whether the user can log into this admin site."
msgstr "用户是否可以登录该管理员站点。"
msgstr "使用者是否可以登入此管理後台。"
#: build/lib/core/models.py:189 core/models.py:189
#: build/lib/core/models.py:190 core/models.py:190
msgid "active"
msgstr "激活"
msgstr "啟用"
#: build/lib/core/models.py:192 core/models.py:192
#: build/lib/core/models.py:193 core/models.py:193
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "是否应将此用户视为活跃用户。取消选择此选项而不是删除账户。"
#: build/lib/core/models.py:204 core/models.py:204
msgid "user"
msgstr "用户"
msgstr "此使用者是否應被視為處於啟用狀態。請取消勾選此項而非刪除帳號。"
#: build/lib/core/models.py:205 core/models.py:205
msgid "users"
msgstr "个用户"
msgid "user"
msgstr "使用者"
#: build/lib/core/models.py:361 build/lib/core/models.py:1434
#: core/models.py:361 core/models.py:1434
msgid "title"
msgstr "标题"
#: build/lib/core/models.py:206 core/models.py:206
msgid "users"
msgstr "使用者"
#: build/lib/core/models.py:362 core/models.py:362
msgid "title"
msgstr "標題"
#: build/lib/core/models.py:363 core/models.py:363
msgid "excerpt"
msgstr "摘要"
#: build/lib/core/models.py:411 core/models.py:411
msgid "Document"
msgstr "文档"
#: build/lib/core/models.py:412 core/models.py:412
msgid "Document"
msgstr "文件"
#: build/lib/core/models.py:413 core/models.py:413
msgid "Documents"
msgstr "个文档"
msgstr "文件"
#: build/lib/core/models.py:424 build/lib/core/models.py:827 core/models.py:424
#: core/models.py:827
#: build/lib/core/models.py:425 build/lib/core/models.py:828 core/models.py:425
#: core/models.py:828
msgid "Untitled Document"
msgstr "未命名文"
msgstr "未命名文"
#: build/lib/core/models.py:862 core/models.py:862
#: build/lib/core/models.py:829 core/models.py:829
msgid "Open"
msgstr "開啟"
#: build/lib/core/models.py:864 core/models.py:864
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} 与您共享了一个文档"
msgstr "{name} 與您分享了一份文件"
#: build/lib/core/models.py:866 core/models.py:866
#: build/lib/core/models.py:868 core/models.py:868
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} 邀您以{role}角色访问以下文"
msgstr "{name} 邀您以{role}角色參與以下文"
#: build/lib/core/models.py:872 core/models.py:872
#: build/lib/core/models.py:874 core/models.py:874
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} 与您共享了一个文档{title}"
msgstr "{name} 與您分享了一份文件{title}"
#: build/lib/core/models.py:973 core/models.py:973
#: build/lib/core/models.py:975 core/models.py:975
msgid "Document/user link trace"
msgstr "文档/用户链接跟踪"
msgstr "文件/使用者連結追蹤"
#: build/lib/core/models.py:974 core/models.py:974
#: build/lib/core/models.py:976 core/models.py:976
msgid "Document/user link traces"
msgstr "个文档/用户链接跟踪"
msgstr "文件/使用者連結追蹤"
#: build/lib/core/models.py:980 core/models.py:980
#: build/lib/core/models.py:982 core/models.py:982
msgid "A link trace already exists for this document/user."
msgstr "此文档/用户的链接跟踪已存在。"
msgstr "此文件/使用者已存在連結追蹤。"
#: build/lib/core/models.py:1003 core/models.py:1003
#: build/lib/core/models.py:1005 core/models.py:1005
msgid "Document favorite"
msgstr "文收藏"
msgstr "文收藏"
#: build/lib/core/models.py:1004 core/models.py:1004
#: build/lib/core/models.py:1006 core/models.py:1006
msgid "Document favorites"
msgstr "文收藏"
msgstr "文收藏"
#: build/lib/core/models.py:1010 core/models.py:1010
#: build/lib/core/models.py:1012 core/models.py:1012
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "该文档已被同一用户的收藏关系实例关联。"
msgstr "此使用者已將此文件加入收藏。"
#: build/lib/core/models.py:1032 core/models.py:1032
#: build/lib/core/models.py:1034 core/models.py:1034
msgid "Document/user relation"
msgstr "文档/用户关系"
msgstr "文件/使用者關聯"
#: build/lib/core/models.py:1033 core/models.py:1033
#: build/lib/core/models.py:1035 core/models.py:1035
msgid "Document/user relations"
msgstr "文档/用户关系集"
msgstr "文件/使用者關聯"
#: build/lib/core/models.py:1039 core/models.py:1039
#: build/lib/core/models.py:1041 core/models.py:1041
msgid "This user is already in this document."
msgstr "该用户已在此文中。"
msgstr "此使用者已在此文中。"
#: build/lib/core/models.py:1045 core/models.py:1045
#: build/lib/core/models.py:1047 core/models.py:1047
msgid "This team is already in this document."
msgstr "该团队已在此文中。"
msgstr "此團隊已在此文中。"
#: build/lib/core/models.py:1051 build/lib/core/models.py:1520
#: core/models.py:1051 core/models.py:1520
#: build/lib/core/models.py:1053 core/models.py:1053
msgid "Either user or team must be set, not both."
msgstr "必须设置用户或团队之一,不能同时设置两者。"
msgstr "必須設定使用者或團隊其中之一,不能同時設定兩者。"
#: build/lib/core/models.py:1202 core/models.py:1202
#: build/lib/core/models.py:1204 core/models.py:1204
msgid "Document ask for access"
msgstr "文档需要访问权限"
msgstr "要求文件存取權"
#: build/lib/core/models.py:1203 core/models.py:1203
#: build/lib/core/models.py:1205 core/models.py:1205
msgid "Document ask for accesses"
msgstr "文档需要访问权限"
msgstr "要求文件存取權"
#: build/lib/core/models.py:1209 core/models.py:1209
#: build/lib/core/models.py:1211 core/models.py:1211
msgid "This user has already asked for access to this document."
msgstr "用户已申请该文档的访问权限。"
msgstr "此使用者已要求過存取此文件的權限。"
#: build/lib/core/models.py:1266 core/models.py:1266
#: build/lib/core/models.py:1268 core/models.py:1268
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} 申请访问文档"
msgstr "{name} 想要存取文件"
#: build/lib/core/models.py:1270 core/models.py:1270
#: build/lib/core/models.py:1272 core/models.py:1272
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} 申请访问以下文"
msgstr "{name} 想要存取以下文"
#: build/lib/core/models.py:1276 core/models.py:1276
#: build/lib/core/models.py:1278 core/models.py:1278
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name}申请文档{title}的访问权限"
msgstr "{name} 正要求存取文件{title}"
#: build/lib/core/models.py:1318 core/models.py:1318
#: build/lib/core/models.py:1320 core/models.py:1320
msgid "Thread"
msgstr ""
msgstr "對話串"
#: build/lib/core/models.py:1319 core/models.py:1319
#: build/lib/core/models.py:1321 core/models.py:1321
msgid "Threads"
msgstr ""
msgstr "對話串"
#: build/lib/core/models.py:1322 build/lib/core/models.py:1374
#: core/models.py:1322 core/models.py:1374
#: build/lib/core/models.py:1324 build/lib/core/models.py:1376
#: core/models.py:1324 core/models.py:1376
msgid "Anonymous"
msgstr ""
msgstr "匿名"
#: build/lib/core/models.py:1369 core/models.py:1369
#: build/lib/core/models.py:1371 core/models.py:1371
msgid "Comment"
msgstr ""
msgstr "評論"
#: build/lib/core/models.py:1370 core/models.py:1370
#: build/lib/core/models.py:1372 core/models.py:1372
msgid "Comments"
msgstr ""
msgstr "評論"
#: build/lib/core/models.py:1419 core/models.py:1419
#: build/lib/core/models.py:1421 core/models.py:1421
msgid "This emoji has already been reacted to this comment."
msgstr ""
msgstr "此評論已標記過此表情符號。"
#: build/lib/core/models.py:1423 core/models.py:1423
#: build/lib/core/models.py:1425 core/models.py:1425
msgid "Reaction"
msgstr ""
msgstr "回應"
#: build/lib/core/models.py:1424 core/models.py:1424
#: build/lib/core/models.py:1426 core/models.py:1426
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1435 core/models.py:1435
msgid "description"
msgstr "说明"
msgstr "回應"
#: build/lib/core/models.py:1436 core/models.py:1436
msgid "code"
msgstr "代码"
#: build/lib/core/models.py:1437 core/models.py:1437
msgid "css"
msgstr "css"
#: build/lib/core/models.py:1439 core/models.py:1439
msgid "public"
msgstr "公开"
#: build/lib/core/models.py:1441 core/models.py:1441
msgid "Whether this template is public for anyone to use."
msgstr "该模板是否公开供任何人使用。"
#: build/lib/core/models.py:1447 core/models.py:1447
msgid "Template"
msgstr "模板"
#: build/lib/core/models.py:1448 core/models.py:1448
msgid "Templates"
msgstr "模板"
#: build/lib/core/models.py:1501 core/models.py:1501
msgid "Template/user relation"
msgstr "模板/用户关系"
#: build/lib/core/models.py:1502 core/models.py:1502
msgid "Template/user relations"
msgstr "模板/用户关系集"
#: build/lib/core/models.py:1508 core/models.py:1508
msgid "This user is already in this template."
msgstr "该用户已在此模板中。"
#: build/lib/core/models.py:1514 core/models.py:1514
msgid "This team is already in this template."
msgstr "该团队已在此模板中。"
#: build/lib/core/models.py:1591 core/models.py:1591
msgid "email address"
msgstr "电子邮件地址"
msgstr "電子郵件地址"
#: build/lib/core/models.py:1610 core/models.py:1610
#: build/lib/core/models.py:1455 core/models.py:1455
msgid "Document invitation"
msgstr "文档邀请"
msgstr "文件邀請"
#: build/lib/core/models.py:1611 core/models.py:1611
#: build/lib/core/models.py:1456 core/models.py:1456
msgid "Document invitations"
msgstr "文档邀请"
msgstr "文件邀請"
#: build/lib/core/models.py:1631 core/models.py:1631
#: build/lib/core/models.py:1476 core/models.py:1476
msgid "This email is already associated to a registered user."
msgstr "此电子邮件已经与现有注册用户关联。"
msgstr "此電子郵件地址已與已註冊使用者關聯。"
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "徽标邮件"
msgstr "電子郵件標誌"
#: core/templates/mail/html/template.html:200
#: core/templates/mail/text/template.txt:10
msgid "Open"
msgstr "打开"
#: core/templates/mail/html/template.html:217
#: core/templates/mail/html/template.html:219
#: core/templates/mail/text/template.txt:14
msgid " Docs, your new essential tool for organizing, sharing and collaborating on your documents as a team. "
msgstr " Docs——您的全新必工具,帮助团队组织、共享和协作处理文档。 "
msgstr " Docs,您團隊組織、分享及協作文件的全新必工具。 "
#: core/templates/mail/html/template.html:224
#: core/templates/mail/html/template.html:226
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr " 由 %(brandname)s 倾力打造。 "
msgstr " 由 %(brandname)s 提供 "
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "4.4.0"
version = "4.5.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -192,10 +192,10 @@ endobj
(react-pdf)
endobj
55 0 obj
(D:20260120141652Z)
(D:20260128100716Z)
endobj
56 0 obj
(chromium-1944-0-doc-export-override-content)
(chromium-8039-0-doc-export-override-content)
endobj
52 0 obj
<<
@@ -216,7 +216,7 @@ endobj
58 0 obj
<<
/Type /FontDescriptor
/FontName /NRSKJK+Inter18pt-Regular
/FontName /FDAZSC+Inter18pt-Regular
/Flags 4
/FontBBox [-742.1875 -323.242187 2579.589844 1109.375]
/ItalicAngle 0
@@ -232,7 +232,7 @@ endobj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /NRSKJK+Inter18pt-Regular
/BaseFont /FDAZSC+Inter18pt-Regular
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Identity)
@@ -247,7 +247,7 @@ endobj
<<
/Type /Font
/Subtype /Type0
/BaseFont /NRSKJK+Inter18pt-Regular
/BaseFont /FDAZSC+Inter18pt-Regular
/Encoding /Identity-H
/DescendantFonts [59 0 R]
/ToUnicode 60 0 R
@@ -256,7 +256,7 @@ endobj
62 0 obj
<<
/Type /FontDescriptor
/FontName /XTJBQL+Inter18pt-Bold
/FontName /UEJHFC+Inter18pt-Bold
/Flags 4
/FontBBox [-790.527344 -334.472656 2580.566406 1114.746094]
/ItalicAngle 0
@@ -272,7 +272,7 @@ endobj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /XTJBQL+Inter18pt-Bold
/BaseFont /UEJHFC+Inter18pt-Bold
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Identity)
@@ -287,7 +287,7 @@ endobj
<<
/Type /Font
/Subtype /Type0
/BaseFont /XTJBQL+Inter18pt-Bold
/BaseFont /UEJHFC+Inter18pt-Bold
/Encoding /Identity-H
/DescendantFonts [63 0 R]
/ToUnicode 64 0 R
@@ -296,7 +296,7 @@ endobj
66 0 obj
<<
/Type /FontDescriptor
/FontName /EDRVHV+Inter18pt-Italic
/FontName /EUMTON+Inter18pt-Italic
/Flags 68
/FontBBox [-747.558594 -323.242187 2595.703125 1109.375]
/ItalicAngle -9.398804
@@ -312,7 +312,7 @@ endobj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /EDRVHV+Inter18pt-Italic
/BaseFont /EUMTON+Inter18pt-Italic
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Identity)
@@ -327,7 +327,7 @@ endobj
<<
/Type /Font
/Subtype /Type0
/BaseFont /EDRVHV+Inter18pt-Italic
/BaseFont /EUMTON+Inter18pt-Italic
/Encoding /Identity-H
/DescendantFonts [67 0 R]
/ToUnicode 68 0 R
@@ -336,7 +336,7 @@ endobj
70 0 obj
<<
/Type /FontDescriptor
/FontName /JIDLHQ+GeistMono-Regular
/FontName /HIJACG+GeistMono-Regular
/Flags 5
/FontBBox [-1738 -247 654 1012]
/ItalicAngle 0
@@ -352,7 +352,7 @@ endobj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /JIDLHQ+GeistMono-Regular
/BaseFont /HIJACG+GeistMono-Regular
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Identity)
@@ -367,7 +367,7 @@ endobj
<<
/Type /Font
/Subtype /Type0
/BaseFont /JIDLHQ+GeistMono-Regular
/BaseFont /HIJACG+GeistMono-Regular
/Encoding /Identity-H
/DescendantFonts [71 0 R]
/ToUnicode 72 0 R
@@ -376,7 +376,7 @@ endobj
74 0 obj
<<
/Type /FontDescriptor
/FontName /SELAIX+Inter18pt-BoldItalic
/FontName /IKVFNP+Inter18pt-BoldItalic
/Flags 68
/FontBBox [-795.898437 -334.472656 2596.191406 1114.746094]
/ItalicAngle -9.398804
@@ -392,7 +392,7 @@ endobj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /SELAIX+Inter18pt-BoldItalic
/BaseFont /IKVFNP+Inter18pt-BoldItalic
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Identity)
@@ -407,7 +407,7 @@ endobj
<<
/Type /Font
/Subtype /Type0
/BaseFont /SELAIX+Inter18pt-BoldItalic
/BaseFont /IKVFNP+Inter18pt-BoldItalic
/Encoding /Identity-H
/DescendantFonts [75 0 R]
/ToUnicode 76 0 R
@@ -692,21 +692,24 @@ endstream
endobj
6 0 obj
<<
/Length 1381
/Length 1393
/Filter /FlateDecode
>>
stream
xœíËnÜ6ð®¯à¾À‡¤m€
¤6ÐCC"ÛMm€Ö€ûûáCÒj¹»*°©³®-SšGÃá¼HPtm€nÁ€ Q)°¢ßv mù¿tT¤(±`'€tûꇻÇ?ú»_ß½ýC§ÅCÿµ{õîÄï4êý8²!Û›×Y× qþ  ¸¹ï>¼V„¤†Ôti†š¥æ®Dp2QN &Ä+ZF´>@Bø+zsÖ|„+¡>Š›Ÿ»oH¼*€µÒ è‰ö>É^¯…¢¬— U
+r¡*©ŒíÑš¨ :åƒJ.qŽpîlÕÍIBVâ&Jô^“TæX‘¬:jÄP}*ÄŸ›ª略`iINë6T©ZfçI?þ›ÛΫ¥¾­%íØ¢÷k´dTËäBKMZ:_5Âz¨ œ * JSýënÏ6ƒÛù#É£¢ðÎxeC@6Ö8ýÔ–ºÓ
Ü“ìßxu ùδ@¤ÛìøËUªŪõªÔ·íE›T·bÑü÷Pmsz¬h¡]\3¹Ø ‚ÓôòE±íèÅ(†ü˜n=#ÀäzÎï=L]é1ÝúÄqH÷ÚúîK÷›ø:—‚E ËÖž”8Ô1ÑQÍÂ’õ>Q±¬dï £i†›üxìµáÎð€‚óÄd(`ÔuX—‡1»`H&~HàÐñˆfúq£» 8UqÌfÈ ³.ê‹’^ÁÀã4·ô”~ðm›¥Èì˜ç‘f`&ø’»e
PæÛsõ_Ÿ9æ–PpKíŽK­fx]žó=w t©«ÖQzg]¡Äê™ é2ÁK¤n߬~ƒ¨ž×ÊV“rbc#¿ÛYZK¾ ™€oC§°V¶+ËØ€•€9Œ¶ùâ„GœpÄoçøõ‹Ÿ’ɶý4J§U°ÍÜÖòSƒÇóÛe§otãäH8ÌÂ)aRá½åø‘f:ŒÐòÙwz¤Ù‡–O¦Î¼‡Z>ç4jZ>køšË|ˆrÍlfK÷öšk`Þ’Û1
™+0×oéœø»»î>|$Ö·EñSñÈ{˜a?2í S;—± +¶Õ¥26-Wkšž³Rƒ1vM]¬áDéxÙ®¥Í3Êð9UsÂÑš@—2­4”Ë3à %¸ F%Wq\ xtB{—ÇÎ(Ì I¹:Ñ9ÚNmlrPGÓÜXpÂ¥l«˜µC"¢È@P#Î…Lg,³Nc mî?£9§· Mµ¿µÉÒ0P$L8–šéÊLЫ<;ZôŠÓôÎD§S1êæÒîêó3ê["Ä’T͉} :¦÷JcCäÓ­UiW»gvuX±g~ÚI0¾]CI¥£:}¬ÇG43Â'ž%3ðÙ ”Z1^ °´óÇòÓJ[?˾¸8h6 Ó1jÕË+P$p&˜f$ر·Cø?·žv¹@ x»Öxôњ䲃ª±'rÆwáío%NZÚ“ê“_È`æ„O<—>;…šW ῌža¤_iÿÇâ_ är ¤ #JHeíZ9õ9ã²C¤Nˤ¶m¸$’Î1Šr:²ÄœúÝÏGX½Ç$£N#xóZ‡Whùì«àCvu³[u¿÷½=8ôƪ¨4E°üQsŽWTí)À9½ß9àÙ )³- ý/%Ùæž ӟφ̡= 9lTk=Æzû¬=&†Ö)U±\uíxOÁ ÜÌŽ‘ªöyÆ­Á¤#£Êb÷%Z‚ûÀt[nSŸSi{¶¢6>†ÅÃǰ“<ÛC‹ïPÉgõñž¯Tð©Ÿ
xœíËnÜ6ð®¯àá›@àCÒ6@Rè!È!‘í¦€6@kÀýýÎ II+q³*°©µeJ³Ãáh8œ)
¯à-Z1)NôûîÏr€0ŽþKGE*¡¥.Ø  }ƾúáæþþæ×woD×)¦¸ë¿v¯ÞÝøýG6dxs5ã:ëºBÎ?iZ\Ýv^+DbÓØLi›Ãæ/Dô2B™´ .ðÍQ:g} Œ‰B}W?w?^¡xUç¤Õ&ꈿP´÷,{½ŠrA6T¦¡+] UIe ˜ M
’W!Z¡äççϦQ#´=©QÈJÜ%©C0Ö²Ê<)T‡ ªO…øsS•4.Éi]؆*UËìê'|w»3âyµÔ·µd<¹Q
a‹–\Xk)¶”d¤WPñU¤…ÚÊ˨¢V†4êœøëfeœÑü¡„èRÉoƒr1j²Ö4ý4»y nQøï¼<gZ!Tnöüå2ÕŽ‰bÓ‚UêëöªMÚÛ°j $[€y´Õ5g‰A ×Û§ sÔhXîäó…±ïðíZ ù1ÝzB`,Ês~ïq$wñcºõÌqà{m}÷¥ûM|KA"`À%ãgMN vLtq³°hI¨T­HV´}Áâ4äÇ}G¨u2@
. “¡€ÉÔa|Fì¢E™èÜw4‚Á¡£ fúq¡˜]¼ª8b3dX—aUHfG¢ð+¸Ÿæ6 žøÝöYŠÌŽÁ<™à ;yË 6êêË>S.aáÛ Õ29C6ðº8?Ùï-uh鹫
Iï|¡Ô9î5@Óob»C³ˆh9æ€ÓâI9ÅÔ°# ¢M)/v.Ñ»½Ã%¥Û è6tëL$ór„º‡ÑD_|ñ¾8â÷süŒúÅ]ÑdÛ7*ºfžk¹«Ã„M)ÆgœÏ#Ö,sDu–Q Ë…ùžb Ox¡å³ïÌH³†–O¢Î¼‡Z>ç4j-Ÿ5”Íe>F¹e6³|{I%2mÙÆí†Ï ˜Ë·¿t^üÝ]v>"ëë¢ÿ©¶¤Šü_˜‘ä=#·sɰaÛ]
gÛr»¦z' Xë¶”ÍÆ˜gïfÆù…›=ýÌ4¥pJDÆ è9³áª#ˆ9>ÞbâË`R‚邊„£
!h/Lð¸ï¬Ò´œÃ™Îc­´s쬧¹sà…LåNk¯Iˆ¤E¢q>f:ëˆ5µ¸d~ÖPŽæ·hÇ5½Y›¥! HÈ8’šèÊLtPyv¸öLg¸°Òñæ³6ìê’lí‰Ü}¤xÆ÷Jëb¢“°MéØ„çŸŽ­Z¦ããqâaç
@øv%•Iêôq ìÌx>˜ä dw€XêÉt!ÀI0>@‹ÌOíþ,[L á¨õ´,Hoɦ‚ôQ  ÞFÛ ÿç&Ô®ÅpÄà¶ZÝR«<ñ8ë·ä’Gá%í.^:ÜÈš“Ÿ¯Ðxæ„<—>;ˆš—ñ¿ §q¤ßè éÉäã;9cÌLZ—¾íÄÁóßÃ:³Ì enû7hR£þuåde‰9õ»ŸpfÅ$£N#h³[‡Whùì«àCus\—`õ-?z¬S)bùªé[íV-¬Z)À}”½=8: /³-#Ä•-i‰vº2% úó™’=¶g´Çmk³÷`”}îÞãù;ö±ƒV,x©@;𤂸-˜B­yÇmÁðqSe1‚k`I§–à˜sËÀý·©Ï©´ÕÁ¯¨ŽsõñãÜIžýQÀÒ3³sºþÂÒÀ.
endstream
endobj
15 0 obj
<<
/Length 5411
/Length 5425
/Filter /FlateDecode
>>
stream
@@ -730,10 +733,8 @@ AZ
¨¡V;@ ÔP@ ¨¡²ÑÿÍSCEù;)ß•!±ü;p0áH&à`¦°¸&à`ºj(p0p0Ó­Kû}×Âw^ÅÀÍ'©HªˆÉŽ0Yû TGþêHÕPÕPÕÑ
¶µ :Ê-wÑ ÌŒ¼¬ó€êÈf{òåÂumÀ0Ó¹ùw3ï¢:õ“ŽóÈç?¢Ét£s‚‘ªïÿQ,ÿð¹/åÿðÿQûPþ#à?þ#à?þ£Ûò½\˜®æo]š/h’mpP#ÅÄ*0IX ɱåZ€)–€ ˜‘\òÌHÀŒÌHíC˜‘€ ˜‘š¢ÌHÀŒt˜éåÂuŽ/‰Ät) ¨ŸËò)]ðA)‰˜h( €1)’€1 “ÜBŒIÀ˜ŒIíC“€1 “€1 “n˘¤Ë:0ÁGîÊ‚¹VHß"í¸ôI@Ÿ„ƒ@ŸôI@ŸôI§ è“#è“€>éÁòðôö»ÐÑf7dÜÀšKÀšT2°&d1Àš¬IÀš¬IÀš¬IÀš¬I7eMz¹ŒhÒ}&ß|g'O¹ã˜bn¤ åÐ)­¿útJ©°Ð)Ð)­v€N 蔀N 蔀Né*:¥Ö/Üjìj;æ"[ ¸•.À­´Çßš„·R¬À­Ô…Þ-§n¥Ã±n%àVn%àVêÀè[æVjú’¬>šT YŠ$ Yš¥Y4K@³t¿[4K@³4K@³´
Ð,ÍÒU4KÑïçd¿(«ëQD“´«9î%ª{‹ñÚ_¿Çä!¼€ÆdguˆÑQÌÕ>~ÿ(5Amëy¸¾Õ/fý cÛÚ˜%ˆ}Ø>*wµºýa\úù¹|ÿÏÝðèí^»CÐË噣ÔÈHkOÊ­'] ¹H>(ÞaG°¥ËøÜ­CræÐ ú <®ð7‡bªÊj1å«Í†{|ž)9ðçd¯¥tð{¼äGíHõÀåÞ¬”¹¥I£ä Ç©¹,ÊÛrª¶º»R÷`.üÊ@Ábƒq&³S>
ÁB7Òÿ'…ihÁŒ1õÑ=ì™ûÀh(Üö<ÉÐø®—FD)òWëŸÏ¾“E
êF
KSóŒƒ>Š&(pò¾¡ Y3”ÙƒwÀ‚ÒÉj ºÛsôû áNÃéÝå‘ ʲ»ËžßF¬Þ5.Fm½ÂüzÑ=p!¬'. ñT±¼ÑŽåðq¾‘ð£†ÞµpëÁwžàïJž^^”3zW´Ú7ç”~µü}¬„þxGØ{éÈø^:ÂßKGÄ{éˆ|¬Ž¼Ãê|Õú¯sn íOóºh+s4¯±v;5]|­;óÓu-ë$'ém¸Ç¨X ×»í¬Î±N=8wl æKƒºT7âœCà/óQÏ:'6´» ÆA¶1'¬ú¡\¹:nµï-òg÷Z89³ÿ)Ø@騨lñ¶®{Ê«öÏÖ›¯Obþ4§}Úþ™ŒBŒóÁÿ†7×¾åŸàô1ï¼ý¾ÅË­ƒµžeFºÚFz¸›û
O}ý
ÁB7Òÿ'…ihÁŒ1õÑ=ì™ûÀh(Üö<ɰx×Ë£*‰ù«õÏgßÉ"u#…¥©yÆAŸNEÄûΘJg
)((³/“Õ2t·çè÷A†ӻË#$”ew—=¿æêÈûÅÆÈÆdú˜Â‡0¿btëŠ l<e,o4d9|œïH$üǨ¡y-Ü‚ð¨'æ4ü-D±öɱ±Ì(gö®xµoΩýjùúX‰ýñް÷Ò‘ñ½t„¿—Žˆ÷ÒùXy‡U<ùªõ_çjÜÜŸæuÑVèh^cí¶jº7[xæ'ìZÖIn¿o÷¨•«á|·ÕY#Ö©çŽÔ|yP—ìFœsüe>êY DØDÒî†?ÙÆœ¸ê‡reë¸9Ô¾·ÈŸÝkáäÌ>¨`¥£¢²ÅÛ‚VVퟭ7_ŸÄü©Nû´ý3ç7‚ÿ ®}Ë?Áéc>Þy~‹—=Zk=ËŒtµôpW÷?Löw
endstream
endobj
77 0 obj
@@ -1325,10 +1326,10 @@ xref
0000000059 00000 n
0000005563 00000 n
0000006178 00000 n
0000026254 00000 n
0000026266 00000 n
0000001770 00000 n
0000001585 00000 n
0000033101 00000 n
0000033127 00000 n
0000002627 00000 n
0000007053 00000 n
0000007208 00000 n
@@ -1336,24 +1337,24 @@ xref
0000000526 00000 n
0000000650 00000 n
0000000752 00000 n
0000032010 00000 n
0000032036 00000 n
0000000883 00000 n
0000000985 00000 n
0000001116 00000 n
0000001218 00000 n
0000001350 00000 n
0000001452 00000 n
0000037952 00000 n
0000045271 00000 n
0000054156 00000 n
0000062501 00000 n
0000071744 00000 n
0000080223 00000 n
0000082133 00000 n
0000037978 00000 n
0000045297 00000 n
0000054182 00000 n
0000062527 00000 n
0000071770 00000 n
0000080249 00000 n
0000082159 00000 n
0000024508 00000 n
0000002219 00000 n
0000002079 00000 n
0000091285 00000 n
0000091311 00000 n
0000007311 00000 n
0000007428 00000 n
0000007558 00000 n
@@ -1387,23 +1388,23 @@ xref
0000006330 00000 n
0000006609 00000 n
0000024118 00000 n
0000031739 00000 n
0000032307 00000 n
0000036923 00000 n
0000044355 00000 n
0000052437 00000 n
0000061643 00000 n
0000070822 00000 n
0000079503 00000 n
0000081429 00000 n
0000083219 00000 n
0000031765 00000 n
0000032333 00000 n
0000036949 00000 n
0000044381 00000 n
0000052463 00000 n
0000061669 00000 n
0000070848 00000 n
0000079529 00000 n
0000081455 00000 n
0000083245 00000 n
trailer
<<
/Size 87
/Root 3 0 R
/Info 52 0 R
/ID [<6a2a704b01cba44185a92d8d4bcaa9d7> <6a2a704b01cba44185a92d8d4bcaa9d7>]
/ID [<2f4ec8da7e87471807031f721b6c9ac2> <2f4ec8da7e87471807031f721b6c9ac2>]
>>
startxref
101700
101726
%%EOF
@@ -397,7 +397,11 @@ export const comparePDFWithAssetFolder = async (download: Download) => {
expect(genPage.width).toBe(refPage.width);
expect(genPage.height).toBe(refPage.height);
expect(genPage.data).toStrictEqual(refPage.data);
try {
expect(genPage.data).toStrictEqual(refPage.data);
} catch {
throw new Error(`PDF page ${i + 1} screenshot does not match reference.`);
}
}
};
@@ -232,6 +232,7 @@ const data = [
depth: 1,
excerpt: null,
is_favorite: false,
is_encrypted: false,
link_role: 'reader',
link_reach: 'restricted',
nb_accesses_ancestors: 1,
@@ -281,6 +282,7 @@ const data = [
depth: 1,
excerpt: null,
is_favorite: false,
is_encrypted: false,
link_role: 'reader',
link_reach: 'restricted',
nb_accesses_ancestors: 1,
@@ -329,6 +331,7 @@ const data = [
depth: 1,
excerpt: null,
is_favorite: false,
is_encrypted: false,
link_role: 'reader',
link_reach: 'restricted',
nb_accesses_ancestors: 14,
@@ -7,7 +7,12 @@ import {
mockedDocument,
verifyDocName,
} from './utils-common';
import { mockedAccesses, mockedInvitations } from './utils-share';
import {
connectOtherUserToDoc,
mockedAccesses,
mockedInvitations,
updateShareLink,
} from './utils-share';
import { createRootSubPage, getTreeRow } from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
@@ -52,13 +57,54 @@ test.describe('Doc Header', () => {
).toBeVisible();
});
test('it updates the title doc', async ({ page, browserName }) => {
await createDoc(page, 'doc-update', browserName, 1);
const docTitle = page.getByRole('textbox', { name: 'Document title' });
await expect(docTitle).toBeVisible();
await docTitle.fill('Hello World');
await docTitle.blur();
test('it updates the title doc and check the broadcast', async ({
page,
browserName,
}) => {
const [docTitle] = await createDoc(
page,
'doc-title-update',
browserName,
1,
);
await page.getByRole('button', { name: 'Share' }).click();
await updateShareLink(page, 'Public', 'Editing');
const docUrl = page.url();
const { otherPage, cleanup } = await connectOtherUserToDoc({
docUrl,
browserName,
withoutSignIn: true,
docTitle,
});
// Wait for other page to sync
await page.waitForTimeout(1000);
await page.keyboard.press('Escape');
const elTitle = page.getByRole('textbox', { name: 'Document title' });
await expect(elTitle).toBeVisible();
await elTitle.fill('Hello World');
await elTitle.blur();
await verifyDocName(page, 'Hello World');
// Wait for other page to sync
await page.waitForTimeout(1000);
// Check other user page
await verifyDocName(otherPage, 'Hello World');
const elTitleOther = otherPage.getByRole('textbox', {
name: 'Document title',
});
await elTitleOther.fill('Hello Other World');
await elTitleOther.blur();
// Check first user page
await verifyDocName(page, 'Hello Other World');
await cleanup();
});
test('it updates the title doc adding a leading emoji', async ({
@@ -45,7 +45,7 @@ test.describe('Document search', () => {
const listSearch = page.getByRole('listbox').getByRole('group');
const rowdoc = listSearch.getByRole('option').first();
await expect(rowdoc.getByText('keyboard_return')).toBeVisible();
await expect(rowdoc.getByText(/seconds? ago/)).toBeVisible();
await expect(rowdoc.getByText(/just now/)).toBeVisible();
await expect(
listSearch.getByRole('option').getByText(doc1Title),
@@ -0,0 +1,22 @@
import { expect, test } from '@playwright/test';
import { overrideConfig } from './utils-common';
test.describe('Login: Not logged', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('It tries silent login', async ({ page }) => {
await overrideConfig(page, {
FRONTEND_SILENT_LOGIN_ENABLED: true,
});
const silentLoginRequest = page.waitForRequest((request) =>
request.url().includes('/api/v1.0/authenticate/?silent=true'),
);
await page.goto('/');
await silentLoginRequest;
expect(silentLoginRequest).toBeDefined();
});
});
@@ -14,6 +14,7 @@ export const CONFIG = {
FRONTEND_CSS_URL: null,
FRONTEND_JS_URL: null,
FRONTEND_HOMEPAGE_FEATURE_ENABLED: true,
FRONTEND_SILENT_LOGIN_ENABLED: false,
FRONTEND_THEME: null,
MEDIA_BASE_URL: 'http://localhost:8083',
LANGUAGES: [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "4.4.0",
"version": "4.5.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -1,3 +1,5 @@
NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
NEXT_PUBLIC_PUBLISH_AS_MIT=false
NEXT_PUBLIC_SW_DEACTIVATED=true
NEXT_PUBLIC_VAULT_URL=http://localhost:7201
NEXT_PUBLIC_INTERFACE_URL=http://localhost:7202
+1 -2
View File
@@ -38,5 +38,4 @@ service-worker.js
# Font embedding
public/assets/fonts/emoji/*
!public/assets/fonts/emoji/fallback.png
public/assets/fonts/Marianne/*
!public/assets/fonts/emoji/fallback.png
-8
View File
@@ -55,14 +55,6 @@ const nextConfig = {
to: path.resolve(__dirname, 'public/assets/fonts/emoji'),
force: true,
},
{
from: path.resolve(
__dirname,
'../../node_modules/@gouvfr-lasuite/ui-kit/dist/assets/fonts/Marianne',
),
to: path.resolve(__dirname, 'public/assets/fonts/Marianne'),
force: true,
},
],
}),
);
+29 -28
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "4.4.0",
"version": "4.5.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -19,60 +19,61 @@
},
"dependencies": {
"@ag-media/react-pdf-table": "2.0.3",
"@blocknote/code-block": "0.45.0",
"@blocknote/core": "0.45.0",
"@blocknote/mantine": "0.45.0",
"@blocknote/react": "0.45.0",
"@blocknote/xl-docx-exporter": "0.45.0",
"@blocknote/xl-multi-column": "0.45.0",
"@blocknote/xl-odt-exporter": "0.45.0",
"@blocknote/xl-pdf-exporter": "0.45.0",
"@blocknote/code-block": "0.46.2",
"@blocknote/core": "0.46.2",
"@blocknote/mantine": "0.46.2",
"@blocknote/react": "0.46.2",
"@blocknote/xl-docx-exporter": "0.46.2",
"@blocknote/xl-multi-column": "0.46.2",
"@blocknote/xl-odt-exporter": "0.46.2",
"@blocknote/xl-pdf-exporter": "0.46.2",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "9.0.0",
"@emoji-mart/data": "1.2.1",
"@emoji-mart/react": "1.1.1",
"@fontsource-variable/inter": "5.2.8",
"@fontsource-variable/material-symbols-outlined": "5.2.30",
"@fontsource-variable/material-symbols-outlined": "5.2.31",
"@fontsource/material-icons": "5.2.7",
"@gouvfr-lasuite/cunningham-react": "4.1.0",
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "0.18.6",
"@gouvfr-lasuite/ui-kit": "0.18.7",
"@hocuspocus/provider": "3.4.3",
"@mantine/core": "8.3.10",
"@mantine/hooks": "8.3.10",
"@mantine/core": "8.3.12",
"@mantine/hooks": "8.3.12",
"@react-pdf/renderer": "4.3.1",
"@sentry/nextjs": "10.32.1",
"@tanstack/react-query": "5.90.16",
"@sentry/nextjs": "10.34.0",
"@tanstack/react-query": "5.90.18",
"@tiptap/extensions": "*",
"async-mutex": "^0.5.0",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"crisp-sdk-web": "1.0.26",
"docx": "*",
"crisp-sdk-web": "1.0.27",
"emoji-datasource-apple": "16.0.0",
"emoji-mart": "5.6.0",
"emoji-regex": "10.6.0",
"i18next": "25.7.3",
"i18next": "25.7.4",
"i18next-browser-languagedetector": "8.2.0",
"idb": "8.0.3",
"lodash": "4.17.23",
"luxon": "3.7.2",
"next": "15.5.9",
"posthog-js": "1.312.0",
"posthog-js": "1.325.0",
"react": "*",
"react-aria-components": "1.14.0",
"react-dom": "*",
"react-dropzone": "14.3.8",
"react-i18next": "16.5.1",
"react-intersection-observer": "10.0.0",
"react-i18next": "16.5.3",
"react-intersection-observer": "10.0.2",
"react-resizable-panels": "3.0.6",
"react-select": "5.10.2",
"styled-components": "6.1.19",
"use-debounce": "10.0.6",
"styled-components": "6.3.8",
"use-debounce": "10.1.0",
"uuid": "13.0.0",
"y-protocols": "1.0.7",
"y-websocket": "^3.0.0",
"yjs": "*",
"zustand": "5.0.9"
"zustand": "5.0.10"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
@@ -81,7 +82,7 @@
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.1",
"@testing-library/user-event": "14.6.1",
"@types/lodash": "4.17.21",
"@types/lodash": "4.17.23",
"@types/luxon": "3.7.1",
"@types/node": "*",
"@types/react": "*",
@@ -94,13 +95,13 @@
"fetch-mock": "9.11.0",
"jsdom": "27.4.0",
"node-fetch": "2.7.0",
"prettier": "3.7.4",
"prettier": "3.8.0",
"stylelint": "16.26.1",
"stylelint-config-standard": "39.0.1",
"stylelint-prettier": "5.0.3",
"typescript": "*",
"vite-tsconfig-paths": "6.0.3",
"vitest": "4.0.16",
"vite-tsconfig-paths": "6.0.4",
"vitest": "4.0.17",
"webpack": "5.104.1",
"workbox-webpack-plugin": "7.1.0"
},
@@ -1,6 +1,5 @@
import { ComponentPropsWithRef, HTMLElementType } from 'react';
import styled from 'styled-components';
import { CSSProperties, RuleSet } from 'styled-components/dist/types';
import { ComponentPropsWithRef, ElementType } from 'react';
import styled, { CSSProperties, RuleSet } from 'styled-components';
import {
MarginPadding,
@@ -13,7 +12,7 @@ import {
import { hideEffect, showEffect } from './Effect';
export interface BoxProps {
as?: HTMLElementType;
as?: ElementType;
$align?: CSSProperties['alignItems'];
$background?: CSSProperties['background'];
$border?: CSSProperties['border'];
@@ -70,7 +69,7 @@ export const Box = styled('div')<BoxProps>`
${({ $cursor }) => $cursor && `cursor: ${$cursor};`}
${({ $direction }) => `flex-direction: ${$direction || 'column'};`}
${({ $display, as }) =>
`display: ${$display || (as?.match('span|input') ? 'inline-flex' : 'flex')};`}
`display: ${$display || (typeof as === 'string' && as.match('span|input') ? 'inline-flex' : 'flex')};`}
${({ $flex }) => $flex && `flex: ${$flex};`}
${({ $gap }) => $gap && `gap: ${spacingValue($gap)};`}
${({ $height }) => $height && `height: ${$height};`}
@@ -1,4 +1,4 @@
import { CSSProperties, ComponentPropsWithRef, forwardRef } from 'react';
import React, { CSSProperties, ComponentPropsWithRef, forwardRef } from 'react';
import styled from 'styled-components';
import { tokens } from '@/cunningham';
@@ -34,7 +34,9 @@ export const TextStyled = styled(Box)<TextProps>`
const Text = forwardRef<HTMLElement, ComponentPropsWithRef<typeof TextStyled>>(
(props, ref) => {
return <TextStyled ref={ref} as="span" {...props} />;
return (
<TextStyled ref={ref as React.Ref<HTMLDivElement>} as="span" {...props} />
);
},
);
@@ -9,6 +9,8 @@ import { useEffect } from 'react';
import { useCunninghamTheme } from '@/cunningham';
import { Auth, KEY_AUTH, setAuthUrl } from '@/features/auth';
import { UserEncryptionProvider } from '@/features/docs/doc-collaboration';
import { VaultClientProvider } from '@/features/docs/doc-collaboration/vault';
import { useResponsiveStore } from '@/stores/';
import { ConfigProvider } from './config/';
@@ -74,7 +76,11 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
<QueryClientProvider client={queryClient}>
<CunninghamProvider theme={theme}>
<ConfigProvider>
<Auth>{children}</Auth>
<Auth>
<VaultClientProvider>
<UserEncryptionProvider>{children}</UserEncryptionProvider>
</VaultClientProvider>
</Auth>
</ConfigProvider>
</CunninghamProvider>
</QueryClientProvider>
@@ -12,7 +12,7 @@ import {
useSynchronizedLanguage,
} from '@/features/language';
import { useAnalytics } from '@/libs';
import { CrispProvider, PostHogAnalytic } from '@/services';
import { CrispAnalytic, PostHogAnalytic } from '@/services';
import { useSentryStore } from '@/stores/useSentryStore';
import { useConfig } from './api/useConfig';
@@ -73,6 +73,14 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
new PostHogAnalytic(conf.POSTHOG_KEY);
}, [conf?.POSTHOG_KEY]);
useEffect(() => {
if (!conf?.CRISP_WEBSITE_ID) {
return;
}
new CrispAnalytic({ websiteId: conf.CRISP_WEBSITE_ID });
}, [conf?.CRISP_WEBSITE_ID]);
if (!conf) {
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
@@ -91,11 +99,7 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
{conf?.FRONTEND_JS_URL && (
<Script src={conf?.FRONTEND_JS_URL} strategy="afterInteractive" />
)}
<AnalyticsProvider>
<CrispProvider websiteId={conf?.CRISP_WEBSITE_ID}>
{children}
</CrispProvider>
</AnalyticsProvider>
<AnalyticsProvider>{children}</AnalyticsProvider>
</>
);
};
@@ -25,6 +25,7 @@ export interface ConfigResponse {
FRONTEND_CSS_URL?: string;
FRONTEND_HOMEPAGE_FEATURE_ENABLED?: boolean;
FRONTEND_JS_URL?: string;
FRONTEND_SILENT_LOGIN_ENABLED?: boolean;
FRONTEND_THEME?: Theme;
LANGUAGES: [string, string][];
LANGUAGE_CODE: string;
@@ -1,33 +1,40 @@
import fetchMock from 'fetch-mock';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { gotoLogout } from '../utils';
import { SILENT_LOGIN_RETRY } from '../conf';
import { gotoLogout, gotoSilentLogin } from '../utils';
// Mock the Crisp service
vi.mock('@/services/Crisp', () => ({
terminateCrispSession: vi.fn(),
}));
// Add mock on window.location.replace
const mockReplace = vi.fn();
Object.defineProperty(window, 'location', {
value: {
...window.location,
replace: mockReplace,
href: 'http://test.jest/',
},
writable: true,
configurable: true,
});
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem');
describe('utils', () => {
afterEach(() => {
vi.clearAllMocks();
fetchMock.restore();
mockReplace.mockClear();
setItemSpy.mockClear();
localStorage.clear();
});
it('checks support session is terminated when logout', async () => {
const { terminateCrispSession } = await import('@/services/Crisp');
// Mock window.location.replace
const mockReplace = vi.fn();
Object.defineProperty(window, 'location', {
value: {
...window.location,
replace: mockReplace,
},
writable: true,
configurable: true,
});
gotoLogout();
expect(terminateCrispSession).toHaveBeenCalled();
@@ -35,4 +42,13 @@ describe('utils', () => {
'http://test.jest/api/v1.0/logout/',
);
});
it('checks the gotoSilentLogin', async () => {
gotoSilentLogin();
expect(mockReplace).toHaveBeenCalledWith(
'http://test.jest/api/v1.0/authenticate/?silent=true&next=http%3A%2F%2Ftest.jest%2F',
);
expect(setItemSpy).toHaveBeenCalledWith(SILENT_LOGIN_RETRY, 'true');
});
});
@@ -3,11 +3,12 @@
* @interface User
* @property {string} id - The id of the user.
* @property {string} email - The email of the user.
* @property {string} name - The name of the user.
* @property {string} full_name - The full name of the user.
* @property {string} language - The language of the user. e.g. 'en-us', 'fr-fr', 'de-de'.
*/
export interface User {
id: string;
suite_user_id: string | null;
email: string;
full_name: string;
short_name: string;
@@ -0,0 +1,107 @@
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, DropdownMenu, DropdownMenuOption, Icon } from '@/components';
import { useVaultClient } from '@/features/docs/doc-collaboration/vault';
import { useAuth } from '../hooks';
import { gotoLogout } from '../utils';
import { ModalEncryptionOnboarding } from './ModalEncryptionOnboarding';
import { ModalEncryptionSettings } from './ModalEncryptionSettings';
export const AccountMenu = () => {
const { t } = useTranslation();
const { user } = useAuth();
const { hasKeys } = useVaultClient();
const [isOnboardingOpen, setIsOnboardingOpen] = useState(false);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
// hasKeys comes from the vault — true if the user has encryption keys on this device
const hasEncryptionSetup = hasKeys === true;
const encryptionOption: DropdownMenuOption = useMemo(() => {
if (hasEncryptionSetup) {
return {
label: t('Encryption settings'),
icon: 'lock',
callback: () => setIsSettingsOpen(true),
showSeparator: true,
};
}
return {
label: t('Enable encryption'),
icon: 'lock_open',
callback: () => setIsOnboardingOpen(true),
showSeparator: true,
};
}, [hasEncryptionSetup, t]);
const options: DropdownMenuOption[] = useMemo(
() => [
encryptionOption,
{
label: t('Logout'),
icon: 'logout',
callback: () => gotoLogout(),
},
],
[encryptionOption, t],
);
return (
<>
<DropdownMenu
options={options}
showArrow
label={t('My account')}
buttonCss={css`
transition: all var(--c--globals--transitions--duration)
var(--c--globals--transitions--ease-out) !important;
border-radius: var(--c--globals--spacings--st);
padding: 0.5rem 0.6rem;
& > div {
gap: 0.2rem;
display: flex;
}
& .material-icons {
color: var(
--c--contextuals--content--palette--brand--primary
) !important;
}
`}
>
<Box
$theme="brand"
$variation="tertiary"
$direction="row"
$gap="0.5rem"
$align="center"
>
<Icon iconName="person" $color="inherit" $size="xl" />
{t('My account')}
</Box>
</DropdownMenu>
{user && isOnboardingOpen && (
<ModalEncryptionOnboarding
isOpen
onClose={() => setIsOnboardingOpen(false)}
onSuccess={() => setIsOnboardingOpen(false)}
/>
)}
{user && isSettingsOpen && (
<ModalEncryptionSettings
isOpen
onClose={() => setIsSettingsOpen(false)}
onRequestReOnboard={() => {
setIsSettingsOpen(false);
setIsOnboardingOpen(true);
}}
/>
)}
</>
);
};
@@ -1,19 +1,47 @@
import { useRouter } from 'next/router';
import { PropsWithChildren, useEffect, useState } from 'react';
import { PropsWithChildren, useEffect, useMemo, useState } from 'react';
import { Loading } from '@/components';
import { useConfig } from '@/core';
import { HOME_URL } from '../conf';
import { useAuth } from '../hooks';
import { getAuthUrl, gotoLogin } from '../utils';
import {
getAuthUrl,
gotoLogin,
gotoSilentLogin,
hasTrySilent,
resetSilent,
} from '../utils';
export const Auth = ({ children }: PropsWithChildren) => {
const { isLoading, pathAllowed, isFetchedAfterMount, authenticated } =
useAuth();
const { replace, pathname } = useRouter();
const { data: config } = useConfig();
const {
isLoading: isAuthLoading,
pathAllowed,
isFetchedAfterMount,
authenticated,
fetchStatus,
} = useAuth();
const isLoading = fetchStatus !== 'idle' || isAuthLoading;
const [isRedirecting, setIsRedirecting] = useState(false);
const { data: config } = useConfig();
const shouldTrySilentLogin = useMemo(
() =>
!authenticated &&
!hasTrySilent() &&
!isLoading &&
!isRedirecting &&
config?.FRONTEND_SILENT_LOGIN_ENABLED,
[
authenticated,
isLoading,
isRedirecting,
config?.FRONTEND_SILENT_LOGIN_ENABLED,
],
);
const shouldTryLogin =
!authenticated && !isLoading && !isRedirecting && !pathAllowed;
const { replace, pathname } = useRouter();
/**
* If the user is authenticated and initially wanted to access a specific page, redirect him to that page now.
@@ -23,6 +51,10 @@ export const Auth = ({ children }: PropsWithChildren) => {
return;
}
if (hasTrySilent()) {
resetSilent();
}
const authUrl = getAuthUrl();
if (authUrl) {
setIsRedirecting(true);
@@ -34,7 +66,13 @@ export const Auth = ({ children }: PropsWithChildren) => {
* If the user is not authenticated and not on a allowed pages
*/
useEffect(() => {
if (isLoading || authenticated || pathAllowed || isRedirecting) {
if (shouldTrySilentLogin) {
setIsRedirecting(true);
gotoSilentLogin();
return;
}
if (!shouldTryLogin) {
return;
}
@@ -56,19 +94,17 @@ export const Auth = ({ children }: PropsWithChildren) => {
setIsRedirecting(true);
gotoLogin();
}, [
authenticated,
pathAllowed,
config?.FRONTEND_HOMEPAGE_FEATURE_ENABLED,
replace,
isLoading,
isRedirecting,
pathname,
shouldTryLogin,
shouldTrySilentLogin,
]);
const shouldShowLoader =
(isLoading && !isFetchedAfterMount) ||
isRedirecting ||
(!authenticated && !pathAllowed);
(!authenticated && !pathAllowed) ||
shouldTrySilentLogin;
if (shouldShowLoader) {
return <Loading $height="100vh" $width="100vw" />;
@@ -2,17 +2,16 @@ import { Button } from '@gouvfr-lasuite/cunningham-react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, BoxButton } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { BoxButton } from '@/components';
import ProConnectImg from '../assets/button-proconnect.svg';
import { useAuth } from '../hooks';
import { gotoLogin, gotoLogout } from '../utils';
import { gotoLogin } from '../utils';
import { AccountMenu } from './AccountMenu';
export const ButtonLogin = () => {
const { t } = useTranslation();
const { authenticated } = useAuth();
const { colorsTokens } = useCunninghamTheme();
if (!authenticated) {
return (
@@ -28,26 +27,7 @@ export const ButtonLogin = () => {
);
}
return (
<Box
$css={css`
.--docs--button-logout:focus-visible {
box-shadow: 0 0 0 2px ${colorsTokens['brand-400']} !important;
border-radius: var(--c--globals--spacings--st);
}
`}
>
<Button
onClick={gotoLogout}
color="brand"
variant="tertiary"
aria-label={t('Logout')}
className="--docs--button-logout"
>
{t('Logout')}
</Button>
</Box>
);
return <AccountMenu />;
};
export const ProConnectButton = () => {
@@ -0,0 +1,94 @@
/**
* Encryption onboarding modal delegates to the centralized encryption service.
*
* Opens the encryption service's interface iframe which handles everything:
* key generation, backup, restore, device transfer, and server registration.
* The product (Docs) doesn't manage public keys it only stores fingerprints
* on document accesses for UI purposes.
*/
import { Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Box } from '@/components';
import { useUserEncryption } from '@/docs/doc-collaboration';
import { useVaultClient } from '@/features/docs/doc-collaboration/vault';
interface ModalEncryptionOnboardingProps {
isOpen: boolean;
onClose: () => void;
onSuccess?: () => void;
}
export const ModalEncryptionOnboarding = ({
isOpen,
onClose,
onSuccess,
}: ModalEncryptionOnboardingProps) => {
const { client: vaultClient, refreshKeyState } = useVaultClient();
const { refreshEncryption } = useUserEncryption();
const onboardingOpenedRef = useRef(false);
const [containerEl, setContainerEl] = useState<HTMLDivElement | null>(null);
useEffect(() => {
if (!isOpen || !vaultClient || !containerEl || onboardingOpenedRef.current) {
return;
}
onboardingOpenedRef.current = true;
vaultClient.openOnboarding(containerEl);
}, [isOpen, vaultClient, containerEl]);
useEffect(() => {
if (!vaultClient) return;
const handleComplete = async () => {
// The encryption service registered the public key on its central server.
// Docs doesn't need to store it — just refresh the vault key state.
await refreshKeyState();
refreshEncryption();
onSuccess?.();
};
const handleClosed = () => {
onboardingOpenedRef.current = false;
onClose();
};
vaultClient.on('onboarding:complete', handleComplete);
vaultClient.on('interface:closed', handleClosed);
return () => {
vaultClient.off('onboarding:complete', handleComplete);
vaultClient.off('interface:closed', handleClosed);
};
}, [vaultClient, refreshKeyState, refreshEncryption, onSuccess, onClose]);
const handleClose = useCallback(() => {
vaultClient?.closeInterface();
onboardingOpenedRef.current = false;
onClose();
}, [vaultClient, onClose]);
useEffect(() => {
if (!isOpen) {
onboardingOpenedRef.current = false;
}
}, [isOpen]);
return (
<Modal
isOpen={isOpen}
closeOnClickOutside={false}
onClose={handleClose}
size={ModalSize.LARGE}
hideCloseButton
>
<Box $minHeight="400px">
<div
ref={setContainerEl}
style={{ width: '100%', minHeight: '400px' }}
/>
</Box>
</Modal>
);
};
@@ -0,0 +1,91 @@
/**
* Encryption settings modal delegates to the centralized encryption service.
*
* Opens the encryption service's settings interface iframe which handles:
* fingerprint display, key deletion, device transfer export, and server key management.
*/
import { Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
import { useCallback, useEffect, useState } from 'react';
import { Box } from '@/components';
import { useUserEncryption } from '@/docs/doc-collaboration';
import { useVaultClient } from '@/features/docs/doc-collaboration/vault';
interface ModalEncryptionSettingsProps {
isOpen: boolean;
onClose: () => void;
onRequestReOnboard: () => void;
}
export const ModalEncryptionSettings = ({
isOpen,
onClose,
onRequestReOnboard,
}: ModalEncryptionSettingsProps) => {
const { client: vaultClient, refreshKeyState } = useVaultClient();
const { refreshEncryption } = useUserEncryption();
const [containerEl, setContainerEl] = useState<HTMLDivElement | null>(null);
const [settingsOpened, setSettingsOpened] = useState(false);
// Open the vault's settings interface when container is mounted
useEffect(() => {
if (!isOpen || !vaultClient || !containerEl || settingsOpened) {
return;
}
setSettingsOpened(true);
vaultClient.openSettings(containerEl);
}, [isOpen, vaultClient, containerEl, settingsOpened]);
// Listen for interface close and key changes
useEffect(() => {
if (!vaultClient) return;
const handleClosed = () => {
setSettingsOpened(false);
refreshKeyState().then(() => refreshEncryption());
onClose();
};
const handleKeysDestroyed = () => {
refreshKeyState().then(() => refreshEncryption());
};
vaultClient.on('interface:closed', handleClosed);
vaultClient.on('keys-destroyed', handleKeysDestroyed);
return () => {
vaultClient.off('interface:closed', handleClosed);
vaultClient.off('keys-destroyed', handleKeysDestroyed);
};
}, [vaultClient, refreshKeyState, refreshEncryption, onClose]);
const handleClose = useCallback(() => {
vaultClient?.closeInterface();
setSettingsOpened(false);
onClose();
}, [vaultClient, onClose]);
useEffect(() => {
if (!isOpen) {
setSettingsOpened(false);
}
}, [isOpen]);
return (
<Modal
isOpen={isOpen}
closeOnClickOutside={false}
onClose={handleClose}
size={ModalSize.LARGE}
hideCloseButton
>
<Box $minHeight="400px">
<div
ref={setContainerEl}
style={{ width: '100%', minHeight: '400px' }}
/>
</Box>
</Modal>
);
};
@@ -1,3 +1,6 @@
export * from './AccountMenu';
export * from './Auth';
export * from './ButtonLogin';
export * from './ModalEncryptionOnboarding';
export * from './ModalEncryptionSettings';
export * from './UserAvatar';
@@ -4,3 +4,4 @@ export const HOME_URL = '/home/';
export const LOGIN_URL = `${baseApiUrl()}authenticate/`;
export const LOGOUT_URL = `${baseApiUrl()}logout/`;
export const PATH_AUTH_LOCAL_STORAGE = 'docs-path-auth';
export const SILENT_LOGIN_RETRY = 'silent-login-retry';
@@ -1,19 +1,21 @@
import { terminateCrispSession } from '@/services/Crisp';
import { safeLocalStorage } from '@/utils/storages';
import {
HOME_URL,
LOGIN_URL,
LOGOUT_URL,
PATH_AUTH_LOCAL_STORAGE,
SILENT_LOGIN_RETRY,
} from './conf';
/**
* Get the stored auth URL from local storage
*/
export const getAuthUrl = () => {
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
const path_auth = safeLocalStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
if (path_auth) {
localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE);
safeLocalStorage.removeItem(PATH_AUTH_LOCAL_STORAGE);
return path_auth;
}
};
@@ -27,7 +29,7 @@ export const setAuthUrl = () => {
window.location.pathname !== '/' &&
window.location.pathname !== `${HOME_URL}/`
) {
localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.href);
safeLocalStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.href);
}
};
@@ -39,6 +41,29 @@ export const gotoLogin = (withRedirect = true) => {
window.location.replace(LOGIN_URL);
};
export const gotoSilentLogin = () => {
// Already tried silent login, dont try again
if (!hasTrySilent()) {
const params = new URLSearchParams({
silent: 'true',
next: window.location.href,
});
safeLocalStorage.setItem(SILENT_LOGIN_RETRY, 'true');
const REDIRECT = `${LOGIN_URL}?${params.toString()}`;
window.location.replace(REDIRECT);
}
};
export const hasTrySilent = () => {
return !!safeLocalStorage.getItem(SILENT_LOGIN_RETRY);
};
export const resetSilent = () => {
safeLocalStorage.removeItem(SILENT_LOGIN_RETRY);
};
export const gotoLogout = () => {
terminateCrispSession();
window.location.replace(LOGOUT_URL);
@@ -0,0 +1,95 @@
/**
* User encryption context provider.
*
* MIGRATION NOTE: This provider now bridges between the VaultClient SDK
* and the existing encryption context interface used by downstream components.
* The vault handles all key storage and crypto operations we no longer
* expose raw CryptoKey objects. Instead, `encryptionSettings` signals that
* encryption is available, and components use the VaultClient directly for
* encrypt/decrypt operations.
*/
import { createContext, useCallback, useContext, useState } from 'react';
import { useAuth } from '@/features/auth';
import { useVaultClient } from './vault';
export type EncryptionError =
| 'missing_private_key'
| 'missing_public_key'
| null;
interface UserEncryptionContextValue {
encryptionLoading: boolean;
/**
* Non-null when the user has encryption keys available.
* NOTE: userPrivateKey and userPublicKey are no longer raw CryptoKey objects.
* They are kept as null placeholders for type compatibility. Use the VaultClient
* directly for all crypto operations.
*/
encryptionSettings: {
userId: string;
userPrivateKey: null;
userPublicKey: null;
} | null;
encryptionError: EncryptionError;
refreshEncryption: () => void;
}
const UserEncryptionContext = createContext<UserEncryptionContextValue>({
encryptionLoading: true,
encryptionSettings: null,
encryptionError: null,
refreshEncryption: () => {},
});
export const UserEncryptionProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const { user } = useAuth();
const { isReady, isLoading, hasKeys, error, refreshKeyState } =
useVaultClient();
const [, setRefreshTrigger] = useState(0);
const refreshEncryption = useCallback(() => {
setRefreshTrigger((prev) => prev + 1);
void refreshKeyState();
}, [refreshKeyState]);
// Derive the legacy context value from the VaultClient state
let encryptionSettings: UserEncryptionContextValue['encryptionSettings'] =
null;
let encryptionError: EncryptionError = null;
if (isReady && user?.suite_user_id) {
if (hasKeys) {
encryptionSettings = {
userId: user.suite_user_id,
userPrivateKey: null, // Keys are in the vault — use VaultClient for crypto
userPublicKey: null,
};
} else {
encryptionError = 'missing_private_key';
}
} else if (!isLoading && error) {
encryptionError = 'missing_private_key';
}
return (
<UserEncryptionContext.Provider
value={{
encryptionLoading: isLoading,
encryptionSettings,
encryptionError,
refreshEncryption,
}}
>
{children}
</UserEncryptionContext.Provider>
);
};
export const useUserEncryption = (): UserEncryptionContextValue =>
useContext(UserEncryptionContext);
@@ -0,0 +1,125 @@
/**
* Encrypted WebSocket wrapper for real-time Yjs collaboration.
*
* Uses the VaultClient SDK for all encrypt/decrypt operations via postMessage
* to the vault iframe. All data transfers use ArrayBuffer for zero-copy
* performance. The vault caches the decrypted symmetric key per session
* so only the first message incurs the hybrid decapsulation cost.
*/
export class EncryptedWebSocket extends WebSocket {
protected readonly vaultClient!: VaultClient;
protected readonly encryptedSymmetricKey!: ArrayBuffer;
protected readonly onSystemMessage?: (message: string) => void;
constructor(address: string | URL, protocols?: string | string[]) {
super(address, protocols);
const originalAddEventListener = this.addEventListener.bind(this);
this.addEventListener = function <K extends keyof WebSocketEventMap>(
type: K,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
): void {
if (type === 'message') {
const wrappedListener: typeof listener = async (event) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const messageEvent = event as any;
// System messages (strings) bypass encryption
if (typeof messageEvent.data === 'string') {
this.onSystemMessage?.(messageEvent.data as string);
return;
}
if (!(messageEvent.data instanceof ArrayBuffer)) {
throw new Error(
'WebSocket data should always be ArrayBuffer (binaryType)',
);
}
try {
// Decrypt directly with ArrayBuffer — no base64 conversion
const { data: decryptedBuffer } =
await this.vaultClient.decryptWithKey(
messageEvent.data as ArrayBuffer,
this.encryptedSymmetricKey,
);
const decryptedData = new Uint8Array(decryptedBuffer);
if (typeof listener === 'function') {
listener.call(this, { ...event, data: decryptedData });
} else {
listener.handleEvent.call(this, {
...event,
data: decryptedData,
});
}
} catch (err) {
console.error('WebSocket decrypt error:', err);
}
};
originalAddEventListener('message', wrappedListener, options);
} else {
originalAddEventListener(type, listener, options);
}
};
// Block direct onmessage assignment
let explicitlySetListener: // eslint-disable-next-line @typescript-eslint/no-explicit-any
((this: WebSocket, handlerEvent: MessageEvent) => any) | null;
null;
Object.defineProperty(this, 'onmessage', {
configurable: true,
enumerable: true,
get() {
return explicitlySetListener;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
set(handler: ((handlerEvent: MessageEvent) => any) | null) {
explicitlySetListener = null;
throw new Error(
'"onmessage" should not be set directly. Use addEventListener instead. Run "yarn run patch-package"!',
);
},
});
}
sendSystemMessage(message: string) {
super.send(message);
}
send(message: Uint8Array<ArrayBuffer>) {
// Encrypt directly with ArrayBuffer — no base64 conversion
this.vaultClient
.encryptWithKey(
message.buffer as ArrayBuffer,
this.encryptedSymmetricKey,
)
.then(({ encryptedData }) => {
super.send(new Uint8Array(encryptedData));
})
.catch((error) => {
console.error('WebSocket encrypt error:', error);
});
}
}
export function createAdaptedEncryptedWebsocketClass(options: {
vaultClient: VaultClient;
encryptedSymmetricKey: ArrayBuffer;
onSystemMessage?: (message: string) => void;
}) {
return class extends EncryptedWebSocket {
protected readonly vaultClient = options.vaultClient;
protected readonly encryptedSymmetricKey = options.encryptedSymmetricKey;
protected readonly onSystemMessage = options.onSystemMessage;
};
}
@@ -0,0 +1,69 @@
import { userKeyPairAlgorithm } from './encryption';
export async function exportPrivateKeyAsJwk(
privateKey: CryptoKey,
): Promise<JsonWebKey> {
return await crypto.subtle.exportKey('jwk', privateKey);
}
export async function importPrivateKeyFromJwk(
jwk: JsonWebKey,
): Promise<CryptoKey> {
return await crypto.subtle.importKey(
'jwk',
jwk,
{ name: userKeyPairAlgorithm, hash: 'SHA-256' },
true,
['decrypt'],
);
}
export async function importPublicKeyFromJwk(
jwk: JsonWebKey,
): Promise<CryptoKey> {
return await crypto.subtle.importKey(
'jwk',
jwk,
{ name: userKeyPairAlgorithm, hash: 'SHA-256' },
true,
['encrypt'],
);
}
export async function exportPublicKeyAsBase64(
publicKey: CryptoKey,
): Promise<string> {
const rawPublicKey = await crypto.subtle.exportKey('spki', publicKey);
return Buffer.from(new Uint8Array(rawPublicKey)).toString('base64');
}
// Derive a public JWK from a private JWK by removing private fields.
export function derivePublicJwkFromPrivate(privateJwk: JsonWebKey): JsonWebKey {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { d, p, q, dp, dq, qi, ...publicJwk } = privateJwk;
return { ...publicJwk, key_ops: ['encrypt'] };
}
/**
* Serialize a JWK to a compact passphrase-like string.
* This is a base64url encoding of the full JWK JSON - not a mnemonic,
* but compact enough to be stored in a password manager.
*/
export function jwkToPassphrase(jwk: JsonWebKey): string {
const json = JSON.stringify(jwk);
const base64 = Buffer.from(json).toString('base64');
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
/**
* Deserialize a passphrase string back to a JWK.
*/
export function passphraseToJwk(passphrase: string): JsonWebKey {
const base64 = passphrase.replace(/-/g, '+').replace(/_/g, '/');
const json = Buffer.from(base64, 'base64').toString('utf-8');
return JSON.parse(json) as JsonWebKey;
}
@@ -0,0 +1,146 @@
export const userKeyPairAlgorithm = 'RSA-OAEP';
export const documentSymmetricKeyAlgorithm = 'AES-GCM';
export async function generateUserKeyPair(): Promise<CryptoKeyPair> {
return await crypto.subtle.generateKey(
{
name: userKeyPairAlgorithm,
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256',
},
true,
['encrypt', 'decrypt'],
);
}
// generate a symmetric key for document encryption
export async function generateSymmetricKey(): Promise<CryptoKey> {
return await crypto.subtle.generateKey(
{ name: documentSymmetricKeyAlgorithm, length: 256 },
true,
['encrypt', 'decrypt'],
);
}
// Encrypt a symmetric key with a user's public key
export async function encryptSymmetricKey(
symmetricKey: CryptoKey,
publicKey: CryptoKey,
): Promise<ArrayBuffer> {
const raw = await crypto.subtle.exportKey('raw', symmetricKey);
// TODO:
// TODO: should use something better than RSA-OAEP, but maybe WebCrypto is not enough (use downloaded library? "libsodium-wrappers" or so)
// TODO:
return await crypto.subtle.encrypt(
{ name: userKeyPairAlgorithm },
publicKey,
raw,
);
}
// decrypt a symmetric key with the local private key
export async function decryptSymmetricKey(
encryptedSymmetricKey: ArrayBuffer,
privateKey: CryptoKey,
): Promise<CryptoKey> {
const raw = await crypto.subtle.decrypt(
{ name: userKeyPairAlgorithm },
privateKey,
encryptedSymmetricKey,
);
return await crypto.subtle.importKey(
'raw',
raw,
{ name: documentSymmetricKeyAlgorithm },
true,
['encrypt', 'decrypt'],
);
}
// encrypt content with a symmetric key
export async function encryptContent(
content: Uint8Array<ArrayBuffer>,
symmetricKey: CryptoKey,
): Promise<Uint8Array<ArrayBuffer>> {
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{
name: documentSymmetricKeyAlgorithm,
iv,
},
symmetricKey,
content,
);
// Prepend IV to ciphertext so the recipient can extract it for decryption
const result = new Uint8Array(iv.length + ciphertext.byteLength);
result.set(iv);
result.set(new Uint8Array(ciphertext), iv.length);
return result;
}
// decrypt content with a symmetric key
export async function decryptContent(
encryptedContent: Uint8Array<ArrayBuffer>,
symmetricKey: CryptoKey,
): Promise<Uint8Array<ArrayBufferLike>> {
const iv = encryptedContent.slice(0, 12);
const ciphertext = encryptedContent.slice(12);
const arrayBuffer = await crypto.subtle.decrypt(
{
name: documentSymmetricKeyAlgorithm,
iv,
},
symmetricKey,
ciphertext,
);
return new Uint8Array(arrayBuffer);
}
/**
* Compute a SHA-256 fingerprint of a base64-encoded public key.
* Returns a formatted hex string like "A1B2 C3D4 E5F6 7890".
*/
export async function computeKeyFingerprint(
base64Key: string,
): Promise<string> {
const raw = Uint8Array.from(atob(base64Key), (c) => c.charCodeAt(0));
const hash = await crypto.subtle.digest('SHA-256', raw);
const hex = Array.from(new Uint8Array(hash))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
return hex
.slice(0, 16)
.replace(/(.{4})/g, '$1 ')
.trim()
.toUpperCase();
}
// prepare encrypted symmetric keys for all users with access to a document
export async function prepareEncryptedSymmetricKeysForUsers(
symmetricKey: CryptoKey,
accessesPublicKeysPerUser: Record<string, ArrayBuffer>,
): Promise<Record<string, ArrayBuffer>> {
const result: Record<string, ArrayBuffer> = {};
// encrypt the symmetric key for each user's public key
for (const [userId, publicKey] of Object.entries(accessesPublicKeysPerUser)) {
const usablePublicKey = await crypto.subtle.importKey(
'spki',
publicKey,
{ name: userKeyPairAlgorithm, hash: 'SHA-256' },
true,
['encrypt'],
);
result[userId] = await encryptSymmetricKey(symmetricKey, usablePublicKey);
}
return result;
}
@@ -0,0 +1,35 @@
import { IDBPDatabase, openDB } from 'idb';
const DB_NAME = 'encryption';
const DB_VERSION = 1;
// Store names
export const STORE_PRIVATE_KEY = 'privateKey';
export const STORE_PUBLIC_KEY = 'publicKey';
export const STORE_KNOWN_PUBLIC_KEYS = 'knownPublicKeys';
let dbPromise: Promise<IDBPDatabase> | null = null;
/**
* Opens (or reuses) the encryption IndexedDB with all required object stores.
* Uses a singleton promise so the upgrade callback only runs once.
*/
export function getEncryptionDB(): Promise<IDBPDatabase> {
if (!dbPromise) {
dbPromise = openDB(DB_NAME, DB_VERSION, {
upgrade(db) {
if (!db.objectStoreNames.contains(STORE_PRIVATE_KEY)) {
db.createObjectStore(STORE_PRIVATE_KEY);
}
if (!db.objectStoreNames.contains(STORE_PUBLIC_KEY)) {
db.createObjectStore(STORE_PUBLIC_KEY);
}
if (!db.objectStoreNames.contains(STORE_KNOWN_PUBLIC_KEYS)) {
db.createObjectStore(STORE_KNOWN_PUBLIC_KEYS);
}
},
});
}
return dbPromise;
}
@@ -0,0 +1,109 @@
/**
* Hook to manage document-level encryption state.
*
* Stores the user's encrypted symmetric key as an ArrayBuffer.
* Components pass this to VaultClient.encryptWithKey() / decryptWithKey()
* for all crypto operations. The vault decrypts the symmetric key internally
* using the user's private key (with session caching for performance).
*/
import { useEffect, useMemo, useState } from 'react';
import { useUserEncryption } from '../UserEncryptionProvider';
export type DocumentEncryptionError =
| 'missing_symmetric_key'
| 'decryption_failed'
| null;
export interface DocumentEncryptionSettings {
/**
* The user's encrypted symmetric key as ArrayBuffer.
* Pass this to VaultClient.encryptWithKey() / decryptWithKey().
*/
encryptedSymmetricKey: ArrayBuffer;
}
/** Convert a base64 string to ArrayBuffer */
function base64ToArrayBuffer(base64: string): ArrayBuffer {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer as ArrayBuffer;
}
export function useDocumentEncryption(
isDocumentEncrypted: boolean | undefined,
userEncryptedSymmetricKeyBase64: string | undefined,
): {
documentEncryptionLoading: boolean;
documentEncryptionSettings: DocumentEncryptionSettings | null;
documentEncryptionError: DocumentEncryptionError;
} {
const { encryptionLoading, encryptionSettings } = useUserEncryption();
const [loading, setLoading] = useState(true);
const [error, setError] = useState<DocumentEncryptionError>(null);
// Convert the base64 key from the API to ArrayBuffer (memoized)
const encryptedSymmetricKey = useMemo(() => {
if (!userEncryptedSymmetricKeyBase64) return null;
try {
return base64ToArrayBuffer(userEncryptedSymmetricKeyBase64);
} catch {
return null;
}
}, [userEncryptedSymmetricKeyBase64]);
const settings = useMemo<DocumentEncryptionSettings | null>(() => {
if (!encryptedSymmetricKey) return null;
return { encryptedSymmetricKey };
}, [encryptedSymmetricKey]);
useEffect(() => {
if (!encryptionLoading && !encryptionSettings) {
setLoading(false);
return;
}
if (encryptionLoading || isDocumentEncrypted === undefined) {
setLoading(true);
setError(null);
return;
}
if (isDocumentEncrypted === false) {
setLoading(false);
setError(null);
return;
}
if (!encryptedSymmetricKey) {
setError('missing_symmetric_key');
setLoading(false);
return;
}
setError(null);
setLoading(false);
}, [
encryptionLoading,
encryptionSettings,
isDocumentEncrypted,
encryptedSymmetricKey,
]);
return {
documentEncryptionLoading: loading,
documentEncryptionSettings: error ? null : settings,
documentEncryptionError: error,
};
}
@@ -0,0 +1,114 @@
import { useEffect, useState } from 'react';
import { getEncryptionDB } from '../encryptionDB';
export type EncryptionError =
| 'missing_private_key'
| 'missing_public_key'
| null;
export function useEncryption(
userId?: string,
refreshTrigger?: number,
): {
encryptionLoading: boolean;
encryptionSettings: {
userId: string;
userPrivateKey: CryptoKey;
userPublicKey: CryptoKey;
} | null;
encryptionError: EncryptionError;
} {
const [loading, setLoading] = useState(true);
const [settings, setSettings] = useState<{
userId: string;
userPrivateKey: CryptoKey;
userPublicKey: CryptoKey;
} | null>(null);
const [error, setError] = useState<EncryptionError>(null);
const enableEncryption: boolean = true; // TODO: this could be toggled for instances not needing encryption to save some requests
useEffect(() => {
let cancelled = false;
async function initEncryption() {
// Waiting for asynchronous data before initializing encryption stuff
if (!userId) {
setLoading(true);
setSettings(null);
setError(null);
return;
} else if (enableEncryption === false) {
setLoading(false);
setSettings(null);
setError(null);
return;
}
try {
setLoading(true);
setError(null);
// We must first retrieve user keys locally
const encryptionDatabase = await getEncryptionDB();
const userPrivateKey = await encryptionDatabase.get(
'privateKey',
`user:${userId}`,
);
if (!userPrivateKey) {
if (!cancelled) {
setError('missing_private_key');
setSettings(null);
}
return;
}
const userPublicKey = await encryptionDatabase.get(
'publicKey',
`user:${userId}`,
);
if (!userPublicKey) {
if (!cancelled) {
setError('missing_public_key');
setSettings(null);
}
return;
}
if (!cancelled) {
setSettings({
userId: userId,
userPrivateKey: userPrivateKey,
userPublicKey: userPublicKey,
});
}
} catch (error) {
console.error(error);
if (!cancelled) {
setSettings(null);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
initEncryption();
return () => {
cancelled = true;
};
}, [userId, enableEncryption, refreshTrigger]);
return {
encryptionLoading: loading,
encryptionSettings: settings,
encryptionError: error,
};
}
@@ -0,0 +1,34 @@
import { useEffect, useState } from 'react';
import { computeKeyFingerprint } from '../encryption';
/**
* Computes a SHA-256 fingerprint of a base64-encoded public key.
* Returns a formatted hex string like "A1B2 C3D4 E5F6 7890", or null
* if the key is not provided or still computing.
*/
export function useKeyFingerprint(
base64Key: string | null | undefined,
): string | null {
const [fingerprint, setFingerprint] = useState<string | null>(null);
useEffect(() => {
if (!base64Key) {
setFingerprint(null);
return;
}
let cancelled = false;
computeKeyFingerprint(base64Key).then((fp) => {
if (!cancelled) {
setFingerprint(fp);
}
});
return () => {
cancelled = true;
};
}, [base64Key]);
return fingerprint;
}
@@ -0,0 +1,133 @@
import { useCallback, useEffect, useState } from 'react';
import { STORE_KNOWN_PUBLIC_KEYS, getEncryptionDB } from '../encryptionDB';
export interface PublicKeyMismatch {
userId: string;
knownKey: string;
currentKey: string;
}
// module-level listener set to keep all hook instances in sync
const registryListeners = new Set<() => void>();
function notifyRegistryUpdated() {
registryListeners.forEach((fn) => fn());
}
/**
* TOFU (Trust On First Use) public key registry.
*
* - On first encounter, a user's public key is stored locally in IndexedDB.
* - On subsequent encounters, if the key differs from the stored one, it is
* flagged as a mismatch.
* - The caller can accept a new key via `acceptNewKey(userId)`, which updates
* the locally stored key.
*
* All instances stay in sync via a module-level listener set.
*/
export function usePublicKeyRegistry(
accessesPublicKeysPerUser: Record<string, string> | undefined,
currentUserId?: string,
) {
const [mismatches, setMismatches] = useState<PublicKeyMismatch[]>([]);
const [loading, setLoading] = useState(true);
const [refreshTrigger, setRefreshTrigger] = useState(0);
// listen for updates from other hook instances
useEffect(() => {
const handler = () => setRefreshTrigger((prev) => prev + 1);
registryListeners.add(handler);
return () => {
registryListeners.delete(handler);
};
}, []);
useEffect(() => {
if (!accessesPublicKeysPerUser) {
setMismatches([]);
setLoading(false);
return;
}
let cancelled = false;
async function checkKeys() {
try {
const db = await getEncryptionDB();
const newMismatches: PublicKeyMismatch[] = [];
for (const [userId, currentKey] of Object.entries(
accessesPublicKeysPerUser!,
)) {
// Skip the current user — they know about their own key changes
if (currentUserId && userId === currentUserId) {
// Still store the key so it stays up to date locally
await db.put(STORE_KNOWN_PUBLIC_KEYS, currentKey, `user:${userId}`);
continue;
}
const knownKey: string | undefined = await db.get(
STORE_KNOWN_PUBLIC_KEYS,
`user:${userId}`,
);
if (!knownKey) {
// First time seeing this user's key — trust on first use
await db.put(STORE_KNOWN_PUBLIC_KEYS, currentKey, `user:${userId}`);
} else if (knownKey !== currentKey) {
newMismatches.push({ userId, knownKey, currentKey });
}
}
if (!cancelled) {
setMismatches(newMismatches);
}
} catch (error) {
console.error('usePublicKeyRegistry: failed to check keys', error);
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
setLoading(true);
checkKeys();
return () => {
cancelled = true;
};
}, [accessesPublicKeysPerUser, currentUserId, refreshTrigger]);
const acceptNewKey = useCallback(
async (userId: string) => {
const mismatch = mismatches.find((m) => m.userId === userId);
if (!mismatch) {
return;
}
const db = await getEncryptionDB();
await db.put(
STORE_KNOWN_PUBLIC_KEYS,
mismatch.currentKey,
`user:${userId}`,
);
setMismatches((prev) => prev.filter((m) => m.userId !== userId));
// notify other instances to re-check
notifyRegistryUpdated();
},
[mismatches],
);
return {
mismatches,
hasMismatches: mismatches.length > 0,
loading,
acceptNewKey,
};
}
@@ -0,0 +1,30 @@
export {
computeKeyFingerprint,
decryptContent,
encryptContent,
generateSymmetricKey,
generateUserKeyPair,
prepareEncryptedSymmetricKeysForUsers,
encryptSymmetricKey,
} from './encryption';
export { getEncryptionDB } from './encryptionDB';
export {
useDocumentEncryption,
type DocumentEncryptionError,
} from './hook/useDocumentEncryption';
export { useEncryption, type EncryptionError } from './hook/useEncryption';
export {
UserEncryptionProvider,
useUserEncryption,
} from './UserEncryptionProvider';
export { useKeyFingerprint } from './hook/useKeyFingerprint';
export { usePublicKeyRegistry } from './hook/usePublicKeyRegistry';
export {
exportPrivateKeyAsJwk,
importPrivateKeyFromJwk,
importPublicKeyFromJwk,
exportPublicKeyAsBase64,
derivePublicJwkFromPrivate,
jwkToPassphrase,
passphraseToJwk,
} from './encryption-backup';
@@ -0,0 +1,15 @@
import { WebsocketProvider } from 'y-websocket';
export class RelayProvider extends WebsocketProvider {
// since the RelayProvider has been added to manage encryption that skips Hocuspocus logic
// we mimic the needed properties for `SwitchableProvider` to be usable and to avoid use extra intermediaries
get document() {
return this.doc;
}
get configuration() {
return {
name: this.roomname,
};
}
}
@@ -0,0 +1,277 @@
/**
* React context provider for the centralized encryption VaultClient SDK.
*
* The client SDK is loaded at runtime via a <script> tag from the vault domain
* (data.encryption). Type declarations are provided by encryption-client.d.ts.
*
* This provider:
* - Loads the client.js script from the vault URL
* - Creates and initializes the VaultClient instance
* - Sets auth context when the user logs in
* - Tracks key state (hasKeys, publicKey)
* - Provides the client to all downstream components
*/
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { useCunninghamTheme } from '@/cunningham';
import { useAuth } from '@/features/auth';
// Environment configuration
const VAULT_URL =
process.env.NEXT_PUBLIC_VAULT_URL ?? 'http://localhost:7201';
const INTERFACE_URL =
process.env.NEXT_PUBLIC_INTERFACE_URL ?? 'http://localhost:7202';
export interface VaultClientContextValue {
/** The VaultClient instance, or null if not yet initialized */
client: VaultClient | null;
/** True once the vault iframe is ready AND auth context has been set */
isReady: boolean;
/** True while the vault is initializing */
isLoading: boolean;
/** Error message if initialization failed */
error: string | null;
/** Whether the current user has encryption keys on this device */
hasKeys: boolean | null;
/** The current user's public key, or null */
publicKey: ArrayBuffer | null;
/** Re-check key state (after onboarding, restore, etc.) */
refreshKeyState: () => Promise<void>;
}
const VaultClientContext = createContext<VaultClientContextValue>({
client: null,
isReady: false,
isLoading: true,
error: null,
hasKeys: null,
publicKey: null,
refreshKeyState: async () => {},
});
/** Load the encryption client SDK script from the vault domain */
function loadClientScript(): Promise<void> {
return new Promise((resolve, reject) => {
// Check if already loaded
if (window.EncryptionClient?.VaultClient) {
resolve();
return;
}
// Check if script tag already exists
const existing = document.querySelector(
`script[src="${VAULT_URL}/client.js"]`,
);
if (existing) {
existing.addEventListener('load', () => resolve());
existing.addEventListener('error', () =>
reject(new Error('Failed to load encryption client SDK')),
);
return;
}
const script = document.createElement('script');
script.src = `${VAULT_URL}/client.js`;
script.async = true;
script.onload = () => resolve();
script.onerror = () =>
reject(new Error('Failed to load encryption client SDK'));
document.head.appendChild(script);
});
}
export function VaultClientProvider({
children,
}: {
children: React.ReactNode;
}) {
const { user, authenticated } = useAuth();
const { i18n } = useTranslation();
const { theme: cunninghamTheme } = useCunninghamTheme();
const clientRef = useRef<VaultClient | null>(null);
const [clientInitialized, setClientInitialized] = useState(false);
const [isReady, setIsReady] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [hasKeys, setHasKeys] = useState<boolean | null>(null);
const [publicKey, setPublicKey] = useState<ArrayBuffer | null>(null);
const initRef = useRef(false);
// Load script + initialize VaultClient once
useEffect(() => {
if (initRef.current) return;
initRef.current = true;
let destroyed = false;
async function init() {
try {
await loadClientScript();
if (destroyed) return;
const client = new window.EncryptionClient.VaultClient({
vaultUrl: VAULT_URL,
interfaceUrl: INTERFACE_URL,
theme: cunninghamTheme,
lang: i18n.language,
});
clientRef.current = client;
client.on('onboarding:complete', () => {
setHasKeys(true);
client
.getPublicKey()
.then(({ publicKey: pk }) => setPublicKey(pk))
.catch(() => {});
});
client.on('keys-changed', () => {
client
.hasKeys()
.then(({ hasKeys: exists }) => {
setHasKeys(exists);
if (exists) {
client
.getPublicKey()
.then(({ publicKey: pk }) => setPublicKey(pk))
.catch(() => {});
}
})
.catch(() => {});
});
client.on('keys-destroyed', () => {
setHasKeys(false);
setPublicKey(null);
});
await client.init();
if (destroyed) {
client.destroy();
} else {
setClientInitialized(true);
}
} catch (err) {
if (!destroyed) {
setError((err as Error).message);
setIsLoading(false);
}
}
}
void init();
return () => {
destroyed = true;
if (clientRef.current) {
clientRef.current.destroy();
clientRef.current = null;
}
};
}, []);
// Set auth context whenever user changes or client finishes initializing
useEffect(() => {
const client = clientRef.current;
if (
!client ||
!clientInitialized ||
!authenticated ||
!user?.id ||
!user?.suite_user_id
) {
return;
}
let cancelled = false;
async function setupAuth() {
if (cancelled || !client) return;
client.setAuthContext({
suiteUserId: user!.suite_user_id!,
});
setIsLoading(true);
try {
const { hasKeys: exists } = await client.hasKeys();
setHasKeys(exists);
if (exists) {
const { publicKey: pk } = await client.getPublicKey();
setPublicKey(pk);
}
setIsReady(true);
} catch (err) {
setError((err as Error).message);
} finally {
setIsLoading(false);
}
}
void setupAuth();
return () => {
cancelled = true;
};
}, [clientInitialized, authenticated, user?.id, user?.suite_user_id]);
const refreshKeyState = useCallback(async () => {
const client = clientRef.current;
if (!client) return;
try {
const { hasKeys: exists } = await client.hasKeys();
setHasKeys(exists);
if (exists) {
const { publicKey: pk } = await client.getPublicKey();
setPublicKey(pk);
} else {
setPublicKey(null);
}
} catch {
// Vault not available
}
}, []);
return (
<VaultClientContext.Provider
value={{
client: clientRef.current,
isReady,
isLoading,
error,
hasKeys,
publicKey,
refreshKeyState,
}}
>
{children}
</VaultClientContext.Provider>
);
}
export const useVaultClient = (): VaultClientContextValue =>
useContext(VaultClientContext);
@@ -0,0 +1,44 @@
// Re-export types from encryption-client.d.ts for global availability
export {};
declare global {
interface VaultClient {
init(): Promise<void>;
destroy(): void;
setTheme(theme: string): void;
setAuthContext(context: { suiteUserId: string }): void;
hasKeys(): Promise<{ hasKeys: boolean }>;
getPublicKey(): Promise<{ publicKey: ArrayBuffer }>;
encrypt(data: ArrayBuffer): Promise<{ encryptedData: ArrayBuffer }>;
decrypt(encryptedData: ArrayBuffer): Promise<{ data: ArrayBuffer }>;
encryptForUsers(data: ArrayBuffer, userPublicKeys: Record<string, ArrayBuffer>): Promise<{ encryptedContent: ArrayBuffer; encryptedKeys: Record<string, ArrayBuffer> }>;
encryptWithKey(data: ArrayBuffer, encryptedSymmetricKey: ArrayBuffer): Promise<{ encryptedData: ArrayBuffer }>;
decryptWithKey(encryptedData: ArrayBuffer, encryptedSymmetricKey: ArrayBuffer): Promise<{ data: ArrayBuffer }>;
rewrapKey(encryptedSymmetricKey: ArrayBuffer, targetUserPublicKey: ArrayBuffer): Promise<{ rewrappedKey: ArrayBuffer }>;
fetchPublicKeys(userIds: string[]): Promise<{ publicKeys: Record<string, ArrayBuffer> }>;
checkFingerprints(userFingerprints: Record<string, string>, currentUserId?: string): Promise<{ results: Array<{ userId: string; knownFingerprint: string | null; providedFingerprint: string; status: 'trusted' | 'refused' | 'unknown' }> }>;
acceptFingerprint(userId: string, fingerprint: string): Promise<void>;
refuseFingerprint(userId: string, fingerprint: string): Promise<void>;
getKnownFingerprints(): Promise<{ fingerprints: Record<string, { fingerprint: string; status: 'trusted' | 'refused' | 'unknown' }> }>;
openOnboarding(container: HTMLElement): void;
openBackup(container: HTMLElement): void;
openRestore(container: HTMLElement): void;
openDeviceTransfer(container: HTMLElement): void;
openSettings(container: HTMLElement): void;
closeInterface(): void;
on<K extends string>(event: K, listener: (data: any) => void): void;
off<K extends string>(event: K, listener: (data: any) => void): void;
}
interface Window {
EncryptionClient: {
VaultClient: new (options: {
vaultUrl: string;
interfaceUrl: string;
timeout?: number;
theme?: string;
lang?: string;
}) => VaultClient;
};
}
}
@@ -0,0 +1,2 @@
export { VaultClientProvider, useVaultClient } from './VaultClientProvider';
export type { VaultClientContextValue } from './VaultClientProvider';
@@ -0,0 +1,120 @@
import { render } from '@testing-library/react';
import React from 'react';
import { describe, expect, test, vi } from 'vitest';
import { AppWrapper } from '@/tests/utils';
import { LinkReach } from '../../doc-management';
import { DocEditor } from '../components/DocEditor';
vi.mock('@/stores', () => ({
useResponsiveStore: () => ({ isDesktop: false }),
}));
vi.mock('@/features/skeletons', () => ({
useSkeletonStore: () => ({
setIsSkeletonVisible: vi.fn(),
}),
}));
vi.mock('../../doc-management', async () => {
const actual = await vi.importActual<any>('../../doc-management');
return {
...actual,
useIsCollaborativeEditable: () => ({ isEditable: true, isLoading: false }),
useProviderStore: () => ({
provider: {
configuration: { name: 'test-doc-id' },
document: {
getXmlFragment: () => null,
},
},
isReady: true,
}),
getDocLinkReach: (doc: any) => doc.computed_link_reach,
};
});
vi.mock('../../doc-table-content', () => ({
TableContent: () => null,
}));
vi.mock('../../doc-header', () => ({
DocHeader: () => null,
}));
vi.mock('../components/BlockNoteEditor', () => ({
BlockNoteEditor: () => null,
BlockNoteReader: () => null,
}));
vi.mock('../../../auth', async () => {
const actual = await vi.importActual<any>('../../../auth');
return {
...actual,
useAuth: () => ({ authenticated: true }),
};
});
const TrackEventMock = vi.fn();
vi.mock('../../../../libs', async () => {
const actual = await vi.importActual<any>('../../../../libs');
return {
...actual,
useAnalytics: () => ({
trackEvent: TrackEventMock,
}),
};
});
describe('DocEditor', () => {
test('it checks that trackevent is called with correct parameters', () => {
const doc = {
id: 'test-doc-id-1',
computed_link_reach: LinkReach.PUBLIC,
deleted_at: null,
abilities: {
partial_update: true,
},
} as any;
const { rerender } = render(<DocEditor doc={doc} documentEncryptionSettings={null} />, {
wrapper: AppWrapper,
});
expect(TrackEventMock).toHaveBeenCalledWith({
eventName: 'doc',
isPublic: true,
authenticated: true,
});
// Rerender with same doc to check that event is not tracked again
rerender(
<DocEditor doc={{ ...doc, computed_link_reach: LinkReach.RESTRICTED }} documentEncryptionSettings={null} />,
);
expect(TrackEventMock).toHaveBeenNthCalledWith(1, {
eventName: 'doc',
isPublic: true,
authenticated: true,
});
// Rerender with different doc to check that event is tracked again
rerender(
<DocEditor
doc={{
...doc,
id: 'test-doc-id-2',
computed_link_reach: LinkReach.RESTRICTED,
}}
documentEncryptionSettings={null}
/>,
);
expect(TrackEventMock).toHaveBeenNthCalledWith(2, {
eventName: 'doc',
isPublic: false,
authenticated: true,
});
});
});
@@ -12,7 +12,6 @@ import * as locales from '@blocknote/core/locales';
import { BlockNoteView } from '@blocknote/mantine';
import '@blocknote/mantine/style.css';
import { useCreateBlockNote } from '@blocknote/react';
import { HocuspocusProvider } from '@hocuspocus/provider';
import { useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -20,8 +19,13 @@ import type { Awareness } from 'y-protocols/awareness';
import * as Y from 'yjs';
import { Box, TextErrors } from '@/components';
import { DocumentEncryptionSettings } from '@/docs/doc-collaboration/hook/useDocumentEncryption';
import { useCunninghamTheme } from '@/cunningham';
import { Doc, useProviderStore } from '@/docs/doc-management';
import {
Doc,
SwitchableProvider,
useProviderStore,
} from '@/docs/doc-management';
import { avatarUrlFromName, useAuth } from '@/features/auth';
import {
@@ -37,13 +41,17 @@ import { DocsBlockNoteEditor } from '../types';
import { randomColor } from '../utils';
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
import { EncryptedDocBanner } from './EncryptedDocBanner';
import { EncryptionProvider } from './EncryptionProvider';
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
import { cssComments, useComments } from './comments/';
import {
AccessibleImageBlock,
AudioBlock,
CalloutBlock,
PdfBlock,
UploadLoaderBlock,
VideoBlock,
} from './custom-blocks';
import {
InterlinkingLinkInlineContent,
@@ -58,11 +66,13 @@ const baseBlockNoteSchema = withPageBreak(
BlockNoteSchema.create({
blockSpecs: {
...defaultBlockSpecs,
audio: AudioBlock(),
callout: CalloutBlock(),
codeBlock: createCodeBlockSpec(codeBlockOptions),
image: AccessibleImageBlock(),
pdf: PdfBlock(),
uploadLoader: UploadLoaderBlock(),
video: VideoBlock(),
},
inlineContentSpecs: {
...defaultInlineContentSpecs,
@@ -77,10 +87,15 @@ export const blockNoteSchema = (withMultiColumn?.(baseBlockNoteSchema) ||
interface BlockNoteEditorProps {
doc: Doc;
provider: HocuspocusProvider;
provider: SwitchableProvider;
documentEncryptionSettings: DocumentEncryptionSettings | null;
}
export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
export const BlockNoteEditor = ({
doc,
provider,
documentEncryptionSettings,
}: BlockNoteEditorProps) => {
const { user } = useAuth();
const { setEditor } = useEditorStore();
const { t } = useTranslation();
@@ -91,14 +106,21 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
// Determine if comments should be visible in the UI
const showComments = canSeeComment;
useSaveDoc(doc.id, provider.document, isConnectedToCollabServer);
useSaveDoc(
doc.id,
provider.document,
isConnectedToCollabServer,
doc.is_encrypted,
documentEncryptionSettings,
);
const { i18n } = useTranslation();
let lang = i18n.resolvedLanguage;
if (!lang || !(lang in locales)) {
lang = 'en';
}
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
const encryptedSymmetricKey = documentEncryptionSettings?.encryptedSymmetricKey;
const { uploadFile, errorAttachment } = useUploadFile(doc.id, encryptedSymmetricKey);
const collabName = user?.full_name || user?.email;
const cursorName = collabName || t('Anonymous');
@@ -200,7 +222,15 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
uploadFile,
schema: blockNoteSchema,
},
[cursorName, lang, provider, uploadFile, threadStore, resolveUsers],
[
cursorName,
lang,
provider,
uploadFile,
encryptedSymmetricKey,
threadStore,
resolveUsers,
],
);
useHeadings(editor);
@@ -218,35 +248,38 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
}, [setEditor, editor]);
return (
<Box
ref={refEditorContainer}
$css={css`
${cssEditor};
${cssComments(showComments, currentUserAvatarUrl)}
`}
>
{errorAttachment && (
<Box $margin={{ bottom: 'big', top: 'none', horizontal: 'large' }}>
<TextErrors
causes={errorAttachment.cause}
canClose
$textAlign="left"
/>
</Box>
)}
<BlockNoteView
className="--docs--main-editor"
editor={editor}
formattingToolbar={false}
slashMenu={false}
theme="light"
comments={showComments}
aria-label={t('Document editor')}
<EncryptionProvider encryptedSymmetricKey={encryptedSymmetricKey}>
<EncryptedDocBanner />
<Box
ref={refEditorContainer}
$css={css`
${cssEditor};
${cssComments(showComments, currentUserAvatarUrl)}
`}
>
<BlockNoteSuggestionMenu />
<BlockNoteToolbar />
</BlockNoteView>
</Box>
{errorAttachment && (
<Box $margin={{ bottom: 'big', top: 'none', horizontal: 'large' }}>
<TextErrors
causes={errorAttachment.cause}
canClose
$textAlign="left"
/>
</Box>
)}
<BlockNoteView
className="--docs--main-editor"
editor={editor}
formattingToolbar={false}
slashMenu={false}
theme="light"
comments={showComments}
aria-label={t('Document editor')}
>
<BlockNoteSuggestionMenu />
<BlockNoteToolbar />
</BlockNoteView>
</Box>
</EncryptionProvider>
);
};
@@ -24,6 +24,7 @@ import {
DocsInlineContentSchema,
DocsStyleSchema,
} from '../../types';
import { useEncryption } from '../EncryptionProvider';
export const FileDownloadButton = ({
open,
@@ -32,6 +33,7 @@ export const FileDownloadButton = ({
}) => {
const dict = useDictionary();
const Components = useComponentsContext();
const { isEncrypted, decryptFileUrl } = useEncryption();
const editor = useBlockNoteEditor<
DocsBlockSchema,
@@ -75,30 +77,34 @@ export const FileDownloadButton = ({
/**
* If not hosted on our domain, means not a file uploaded by the user,
* we do what Blocknote was doing initially.
*
* For this case, no need of adding decryption logic
*/
if (!url.includes(window.location.hostname) && !url.includes('base64')) {
if (!editor.resolveFileUrl) {
if (!isSafeUrl(url)) {
return;
}
window.open(url, '_blank', 'noopener,noreferrer');
} else {
void editor
.resolveFileUrl(url)
.then((downloadUrl) => window.open(downloadUrl));
if (!isSafeUrl(url)) {
return;
}
window.open(url, '_blank', 'noopener,noreferrer');
return;
}
const fetchAndDownload = async (fileName: string) => {
if (isEncrypted) {
const blobUrl = await decryptFileUrl(url);
const blob = await fetch(blobUrl).then((r) => r.blob());
downloadFile(blob, fileName);
} else {
const blob = (await exportResolveFileUrl(url)) as Blob;
downloadFile(blob, fileName);
}
};
if (!url.includes('-unsafe')) {
const blob = (await exportResolveFileUrl(url)) as Blob;
downloadFile(blob, name || url.split('/').pop() || 'file');
await fetchAndDownload(name || url.split('/').pop() || 'file');
} else {
const onConfirm = async () => {
const blob = (await exportResolveFileUrl(url)) as Blob;
const baseName = name || url.split('/').pop() || 'file';
const regFindLastDot = /(\.[^/.]+)$/;
@@ -106,13 +112,13 @@ export const FileDownloadButton = ({
? baseName.replace(regFindLastDot, '-unsafe$1')
: baseName + '-unsafe';
downloadFile(blob, unsafeName);
await fetchAndDownload(unsafeName);
};
open(onConfirm);
}
}
}, [editor, fileBlock, open]);
}, [editor, fileBlock, open, isEncrypted, decryptFileUrl]);
if (!fileBlock || fileBlock.props.url === '' || !Components) {
return null;
@@ -1,15 +1,20 @@
import clsx from 'clsx';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { Box, Loading } from '@/components';
import { DocumentEncryptionSettings } from '@/docs/doc-collaboration/hook/useDocumentEncryption';
import { DocHeader } from '@/docs/doc-header/';
import {
Doc,
LinkReach,
getDocLinkReach,
useIsCollaborativeEditable,
useProviderStore,
} from '@/docs/doc-management';
import { TableContent } from '@/docs/doc-table-content/';
import { useAuth } from '@/features/auth/';
import { useSkeletonStore } from '@/features/skeletons';
import { useAnalytics } from '@/libs';
import { useResponsiveStore } from '@/stores';
import { BlockNoteEditor, BlockNoteReader } from './BlockNoteEditor';
@@ -72,9 +77,13 @@ export const DocEditorContainer = ({
interface DocEditorProps {
doc: Doc;
documentEncryptionSettings: DocumentEncryptionSettings | null;
}
export const DocEditor = ({ doc }: DocEditorProps) => {
export const DocEditor = ({
doc,
documentEncryptionSettings,
}: DocEditorProps) => {
const { isDesktop } = useResponsiveStore();
const { provider, isReady } = useProviderStore();
const { isEditable, isLoading } = useIsCollaborativeEditable(doc);
@@ -83,6 +92,10 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
!doc.abilities.partial_update || !isEditable || isLoading || isDeletedDoc;
const { setIsSkeletonVisible } = useSkeletonStore();
const isProviderReady = isReady && provider;
const { trackEvent } = useAnalytics();
const [hasTracked, setHasTracked] = useState(false);
const { authenticated } = useAuth();
const isPublicDoc = getDocLinkReach(doc) === LinkReach.PUBLIC;
useEffect(() => {
if (isProviderReady) {
@@ -90,6 +103,30 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
}
}, [isProviderReady, setIsSkeletonVisible]);
/**
* Track doc view event only once per doc change
*/
useEffect(() => {
setHasTracked(false);
}, [doc.id]);
/**
* Track doc view event
*/
useEffect(() => {
if (hasTracked) {
return;
}
setHasTracked(true);
trackEvent({
eventName: 'doc',
isPublic: isPublicDoc,
authenticated,
});
}, [authenticated, hasTracked, isPublicDoc, trackEvent]);
if (!isProviderReady || provider?.configuration.name !== doc.id) {
return <Loading />;
}
@@ -98,7 +135,12 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
<>
{isDesktop && <TableContent />}
<DocEditorContainer
docHeader={<DocHeader doc={doc} />}
docHeader={
<DocHeader
doc={doc}
documentEncryptionSettings={documentEncryptionSettings}
/>
}
docEditor={
readOnly ? (
<BlockNoteReader
@@ -108,7 +150,11 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
docId={doc.id}
/>
) : (
<BlockNoteEditor doc={doc} provider={provider} />
<BlockNoteEditor
doc={doc}
provider={provider}
documentEncryptionSettings={documentEncryptionSettings}
/>
)
}
isDeletedDoc={isDeletedDoc}
@@ -0,0 +1,34 @@
import { Button } from '@gouvfr-lasuite/cunningham-react';
import { useTranslation } from 'react-i18next';
import { Box, Icon } from '@/components';
import { useEncryption } from './EncryptionProvider';
export const EncryptedDocBanner = () => {
const { t } = useTranslation();
const { isEncrypted, pendingPlaceholders, requestRevealAll } =
useEncryption();
if (!isEncrypted || pendingPlaceholders === 0) {
return null;
}
return (
<Box
$direction="row"
$align="center"
$justify="flex-end"
$padding={{ horizontal: '54px', vertical: '3xs' }}
>
<Button
size="small"
variant="tertiary"
onClick={requestRevealAll}
icon={<Icon iconName="visibility" $size="sm" $color="inherit" />}
>
{t('Reveal all media')}
</Button>
</Box>
);
};
@@ -0,0 +1,76 @@
import { css } from 'styled-components';
import { Box, Icon, Loading } from '@/components';
interface EncryptedMediaPlaceholderProps {
label: string;
errorLabel: string;
minHeight?: string;
isLoading: boolean;
hasError: boolean;
onDecrypt: () => void;
}
export const EncryptedMediaPlaceholder = ({
label,
errorLabel,
minHeight = '200px',
isLoading,
hasError,
onDecrypt,
}: EncryptedMediaPlaceholderProps) => {
if (hasError) {
return (
<Box
$align="center"
$justify="center"
$color="#666"
$background="#f5f5f5"
$border="1px solid #ddd"
$minHeight={minHeight}
$padding="20px"
$css={css`
text-align: center;
`}
contentEditable={false}
>
{errorLabel}
</Box>
);
}
return (
<Box
$align="center"
$justify="center"
$color="#666"
$background="#f5f5f5"
$border="1px solid #ddd"
$minHeight={minHeight}
$padding="20px"
$css={css`
text-align: center;
cursor: ${isLoading ? 'wait' : 'pointer'};
position: relative;
`}
contentEditable={false}
onClick={() => !isLoading && onDecrypt()}
>
<Icon iconName="lock" $size="24px" />
<Box $margin={{ top: '2px' }}>{label}</Box>
{isLoading && (
<Box
$align="center"
$justify="center"
$css={css`
position: absolute;
inset: 0;
background: rgba(245, 245, 245, 0.8);
`}
>
<Loading />
</Box>
)}
</Box>
);
};
@@ -0,0 +1,166 @@
/**
* Encryption context for document content and file attachments.
* Uses VaultClient SDK with ArrayBuffer for all decrypt operations.
*/
import {
ReactNode,
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useVaultClient } from '@/features/docs/doc-collaboration/vault';
const MIME_MAP: Record<string, string> = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
webp: 'image/webp',
svg: 'image/svg+xml',
mp3: 'audio/mpeg',
wav: 'audio/wav',
ogg: 'audio/ogg',
flac: 'audio/flac',
aac: 'audio/aac',
mp4: 'video/mp4',
webm: 'video/webm',
ogv: 'video/ogg',
mov: 'video/quicktime',
avi: 'video/x-msvideo',
pdf: 'application/pdf',
};
interface EncryptionContextValue {
isEncrypted: boolean;
decryptFileUrl: (url: string) => Promise<string>;
revealAllCounter: number;
requestRevealAll: () => void;
pendingPlaceholders: number;
registerPlaceholder: () => void;
unregisterPlaceholder: () => void;
}
const noop = () => {};
const DEFAULT_VALUE: EncryptionContextValue = {
isEncrypted: false,
decryptFileUrl: async (url: string) => url,
revealAllCounter: 0,
requestRevealAll: noop,
pendingPlaceholders: 0,
registerPlaceholder: noop,
unregisterPlaceholder: noop,
};
const EncryptionContext = createContext<EncryptionContextValue>(DEFAULT_VALUE);
interface EncryptionProviderProps {
encryptedSymmetricKey: ArrayBuffer | undefined;
children: ReactNode;
}
export const EncryptionProvider = ({
encryptedSymmetricKey,
children,
}: EncryptionProviderProps) => {
const { client: vaultClient } = useVaultClient();
const blobUrlCacheRef = useRef<Map<string, string>>(new Map());
const [revealAllCounter, setRevealAllCounter] = useState(0);
const [pendingPlaceholders, setPendingPlaceholders] = useState(0);
const requestRevealAll = useCallback(() => {
setRevealAllCounter((c) => c + 1);
}, []);
const registerPlaceholder = useCallback(() => {
setPendingPlaceholders((c) => c + 1);
}, []);
const unregisterPlaceholder = useCallback(() => {
setPendingPlaceholders((c) => Math.max(0, c - 1));
}, []);
const decryptFileUrl = useCallback(
async (url: string): Promise<string> => {
if (!encryptedSymmetricKey || !vaultClient) {
return url;
}
const cached = blobUrlCacheRef.current.get(url);
if (cached) {
return cached;
}
const response = await fetch(url, { credentials: 'include' });
if (!response.ok) {
throw new Error(
`Failed to fetch encrypted attachment: ${response.status}`,
);
}
// Get file as ArrayBuffer directly — no base64 conversion
const encryptedBuffer = await response.arrayBuffer();
const { data: decryptedBuffer } = await vaultClient.decryptWithKey(
encryptedBuffer,
encryptedSymmetricKey,
);
const ext = url.split('.').pop()?.toLowerCase() || '';
const mime = MIME_MAP[ext] || 'application/octet-stream';
const blob = new Blob([decryptedBuffer], { type: mime });
const blobUrl = URL.createObjectURL(blob);
blobUrlCacheRef.current.set(url, blobUrl);
return blobUrl;
},
[encryptedSymmetricKey, vaultClient],
);
useEffect(() => {
return () => {
blobUrlCacheRef.current.forEach((blobUrl) =>
URL.revokeObjectURL(blobUrl),
);
blobUrlCacheRef.current.clear();
};
}, [encryptedSymmetricKey]);
const value = useMemo(
() => ({
isEncrypted: !!encryptedSymmetricKey,
decryptFileUrl,
revealAllCounter,
requestRevealAll,
pendingPlaceholders,
registerPlaceholder,
unregisterPlaceholder,
}),
[
encryptedSymmetricKey,
decryptFileUrl,
revealAllCounter,
requestRevealAll,
pendingPlaceholders,
registerPlaceholder,
unregisterPlaceholder,
],
);
return (
<EncryptionContext.Provider value={value}>
{children}
</EncryptionContext.Provider>
);
};
export const useEncryption = (): EncryptionContextValue =>
useContext(EncryptionContext);
@@ -1,130 +1,294 @@
/**
* AccessibleImageBlock.tsx
*
* This file defines a custom BlockNote block specification for an accessible image block.
* It extends the default image block to ensure compliance with accessibility standards,
* specifically RGAA 1.9.1, by using <figure> and <figcaption> elements when a caption is provided.
* Custom BlockNote block for accessible images with encryption support.
*
* The accessible image block ensures that:
* Accessibility (RGAA 1.9.1):
* - Images with captions are wrapped in <figure> and <figcaption> elements.
* - The <img> element has an appropriate alt attribute based on the caption.
* - Accessibility attributes such as role and aria-label are added for better screen reader support.
* - Images without captions have alt="" and are marked as decorative with aria-hidden="true".
*
* This implementation leverages BlockNote's existing image block functionality while enhancing it for accessibility.
* Encryption:
* - Images < 2MB are auto-decrypted inline.
* - Images >= 2MB show a "click to decrypt" placeholder.
*
* https://github.com/TypeCellOS/BlockNote/blob/main/packages/core/src/blocks/Image/block.ts
*/
import {
BlockFromConfig,
BlockNoDefaults,
BlockNoteEditor,
ImageOptions,
InlineContentSchema,
InlineContentSchemaFromSpecs,
StyleSchema,
createBlockSpec,
createImageBlockConfig,
defaultInlineContentSpecs,
imageParse,
imageRender,
imageToExternalHTML,
} from '@blocknote/core';
import { t } from 'i18next';
import {
ResizableFileBlockWrapper,
createReactBlockSpec,
} from '@blocknote/react';
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
type CreateImageBlockConfig = ReturnType<typeof createImageBlockConfig>;
import { Icon, Loading } from '@/components';
export const accessibleImageRender =
(config: ImageOptions) =>
(
block: BlockFromConfig<
CreateImageBlockConfig,
InlineContentSchema,
StyleSchema
>,
editor: BlockNoteEditor<
Record<'image', CreateImageBlockConfig>,
InlineContentSchemaFromSpecs<typeof defaultInlineContentSpecs>,
StyleSchema
>,
) => {
const imageRenderComputed = imageRender(config);
const dom = imageRenderComputed(block, editor).dom;
const imgSelector = dom.querySelector('img');
import { ANALYZE_URL } from '../../conf';
import { EncryptedMediaPlaceholder } from '../EncryptedMediaPlaceholder';
import { useEncryption } from '../EncryptionProvider';
// Fix RGAA 1.9.1: Convert to figure/figcaption structure if caption exists
const accessibleImageWithCaption = () => {
imgSelector?.setAttribute('alt', block.props.caption);
imgSelector?.removeAttribute('aria-hidden');
imgSelector?.setAttribute('tabindex', '0');
type ImageBlockConfig = ReturnType<typeof createImageBlockConfig>;
const figureElement = document.createElement('figure');
const AUTOMATIC_DECRYPTION_MAX_SIZE = 2 * 1024 * 1024; // 2 MB
// Copy all attributes from the original div
figureElement.className = dom.className;
const styleAttr = dom.getAttribute('style');
if (styleAttr) {
figureElement.setAttribute('style', styleAttr);
}
figureElement.style.setProperty('margin', '0');
interface AccessibleImageProps {
src: string;
caption: string;
}
Array.from(dom.children).forEach((child) => {
figureElement.appendChild(child.cloneNode(true));
const AccessibleImage = ({ src, caption }: AccessibleImageProps) => {
const { t } = useTranslation();
if (caption) {
return (
<figure
style={{ margin: 0 }}
role="img"
aria-label={t('Image: {{title}}', { title: caption })}
>
<img
className="bn-visual-media"
src={src}
alt={caption}
tabIndex={0}
contentEditable={false}
draggable={false}
/>
<figcaption className="bn-file-caption">{caption}</figcaption>
</figure>
);
}
return (
<img
className="bn-visual-media"
src={src}
alt=""
role="presentation"
aria-hidden="true"
tabIndex={-1}
contentEditable={false}
draggable={false}
/>
);
};
interface ImageBlockComponentProps {
block: BlockNoDefaults<
Record<'image', ImageBlockConfig>,
InlineContentSchema,
StyleSchema
>;
contentRef: (node: HTMLElement | null) => void;
editor: BlockNoteEditor<
Record<'image', ImageBlockConfig>,
InlineContentSchema,
StyleSchema
>;
}
const ImageBlockComponent = ({
editor,
block,
...rest
}: ImageBlockComponentProps) => {
const { t } = useTranslation();
const { isEncrypted, decryptFileUrl } = useEncryption();
const url = block.props.url;
const caption = block.props.caption || '';
const isAnalyzing = !!url && url.includes(ANALYZE_URL);
// Encrypted state
const [resolvedUrl, setResolvedUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const [showClickPlaceholder, setShowClickPlaceholder] = useState(false);
// Auto-decrypt small files, show placeholder for large ones
useEffect(() => {
if (!isEncrypted || !url || isAnalyzing) {
return;
}
let cancelled = false;
setIsLoading(true);
fetch(url, { method: 'HEAD', credentials: 'include' })
.then(async (headResponse) => {
if (cancelled) {
return;
}
const contentLength = Number(
headResponse.headers.get('content-length'),
);
// Larger images show a "click to decrypt" placeholder instead to save decryption processing
// (needed since photos taken from a smartphone can easily be over 15MB)
if (contentLength < AUTOMATIC_DECRYPTION_MAX_SIZE) {
try {
const blobUrl = await decryptFileUrl(url);
if (!cancelled) {
setResolvedUrl(blobUrl);
}
} catch {
if (!cancelled) {
setShowClickPlaceholder(true);
}
}
} else {
if (!cancelled) {
setShowClickPlaceholder(true);
}
}
})
.catch(() => {
if (!cancelled) {
setShowClickPlaceholder(true);
}
})
.finally(() => {
if (!cancelled) {
setIsLoading(false);
}
});
// Replace the <p> caption with <figcaption>
const figcaptionElement = document.createElement('figcaption');
const originalCaption = figureElement.querySelector('.bn-file-caption');
if (originalCaption) {
figcaptionElement.className = originalCaption.className;
figcaptionElement.textContent = originalCaption.textContent;
originalCaption.parentNode?.replaceChild(
figcaptionElement,
originalCaption,
);
return () => {
cancelled = true;
};
}, [isEncrypted, url, isAnalyzing, decryptFileUrl]);
// Add explicit role and aria-label for better screen reader support
figureElement.setAttribute('role', 'img');
figureElement.setAttribute(
'aria-label',
t(`Image: {{title}}`, { title: figcaptionElement.textContent }),
);
const handleDecrypt = useCallback(async () => {
if (!url) {
return;
}
setIsLoading(true);
setHasError(false);
try {
const blobUrl = await decryptFileUrl(url);
setResolvedUrl(blobUrl);
setShowClickPlaceholder(false);
} catch {
setHasError(true);
} finally {
setIsLoading(false);
}
}, [url, decryptFileUrl]);
// Remove the duplicate <p class="bn-file-caption"> added by ResizableFileBlockWrapper
// when we render our own <figcaption> inside a <figure>.
const wrapperRef = useRef<HTMLElement>(null);
useLayoutEffect(() => {
if (!wrapperRef.current || !caption) {
return;
}
const wrapper = wrapperRef.current.closest(
'.bn-file-block-content-wrapper',
);
if (!wrapper) {
return;
}
const pCaption = wrapper.querySelector(':scope > p.bn-file-caption');
if (pCaption) {
pCaption.remove();
}
}, [caption]);
const effectiveUrl = isEncrypted ? resolvedUrl : url;
const showMedia = !!effectiveUrl && !isAnalyzing;
const showEncryptedPlaceholder =
isEncrypted && (showClickPlaceholder || hasError) && !resolvedUrl;
return (
<ResizableFileBlockWrapper
{...({ editor, block, ...rest } as any)}
buttonIcon={
<Icon iconName="image" $size="24px" $css="line-height: normal;" />
}
>
{isEncrypted && isLoading && !resolvedUrl && !showClickPlaceholder && (
<Loading />
)}
{showEncryptedPlaceholder && (
<EncryptedMediaPlaceholder
label={t('Click to decrypt and view image')}
errorLabel={t('Failed to decrypt image.')}
isLoading={isLoading}
hasError={hasError}
onDecrypt={() => void handleDecrypt()}
/>
)}
{showMedia && (
<span ref={wrapperRef}>
<AccessibleImage src={effectiveUrl} caption={caption} />
</span>
)}
</ResizableFileBlockWrapper>
);
};
// Return the figure element as the new dom
return {
...imageRenderComputed,
dom: figureElement,
};
};
const ImageToExternalHTML = ({
block,
}: {
block: BlockNoDefaults<
Record<'image', ImageBlockConfig>,
InlineContentSchema,
StyleSchema
>;
}) => {
if (!block.props.url) {
return <p>Add image</p>;
}
const accessibleImage = () => {
imgSelector?.setAttribute('alt', '');
imgSelector?.setAttribute('role', 'presentation');
imgSelector?.setAttribute('aria-hidden', 'true');
imgSelector?.setAttribute('tabindex', '-1');
const img = (
<img
src={block.props.url}
alt={block.props.caption || ''}
width={block.props.previewWidth}
/>
);
return {
...imageRenderComputed,
dom,
};
};
if (block.props.caption) {
return (
<figure role="img" aria-label={block.props.caption}>
{img}
<figcaption>{block.props.caption}</figcaption>
</figure>
);
}
const withCaption =
block.props.caption && dom.querySelector('.bn-file-caption');
return img;
};
// Set accessibility attributes for the image
return withCaption ? accessibleImageWithCaption() : accessibleImage();
};
export const AccessibleImageBlock = createBlockSpec(
export const AccessibleImageBlock = createReactBlockSpec(
createImageBlockConfig,
(config) => ({
meta: {
fileBlockAccept: ['image/*'],
},
render: accessibleImageRender(config),
render: (props) => <ImageBlockComponent {...(props as any)} />,
parse: imageParse(config),
toExternalHTML: imageToExternalHTML(config),
toExternalHTML: (props) => <ImageToExternalHTML {...(props as any)} />,
runsBefore: ['file'],
}),
);
@@ -0,0 +1,105 @@
import {
BlockNoDefaults,
BlockNoteEditor,
InlineContentSchema,
StyleSchema,
audioParse,
createAudioBlockConfig,
} from '@blocknote/core';
import { FileBlockWrapper, createReactBlockSpec } from '@blocknote/react';
import { useTranslation } from 'react-i18next';
import { Icon } from '@/components';
import { useDecryptMedia } from '../../hook';
import { EncryptedMediaPlaceholder } from '../EncryptedMediaPlaceholder';
type AudioBlockConfig = ReturnType<typeof createAudioBlockConfig>;
interface AudioBlockComponentProps {
block: BlockNoDefaults<
Record<'audio', AudioBlockConfig>,
InlineContentSchema,
StyleSchema
>;
contentRef: (node: HTMLElement | null) => void;
editor: BlockNoteEditor<
Record<'audio', AudioBlockConfig>,
InlineContentSchema,
StyleSchema
>;
}
const AudioBlockComponent = ({
editor,
block,
...rest
}: AudioBlockComponentProps) => {
const { t } = useTranslation();
const {
showPlaceholder,
showMedia,
isLoading,
hasError,
decrypt,
resolvedUrl,
} = useDecryptMedia(block.props.url);
return (
<FileBlockWrapper
{...({ editor, block, ...rest } as any)}
buttonIcon={
<Icon iconName="audiotrack" $size="24px" $css="line-height: normal;" />
}
>
{showPlaceholder && (
<EncryptedMediaPlaceholder
label={t('Click to decrypt and play audio')}
errorLabel={t('Failed to decrypt audio file.')}
minHeight="80px"
isLoading={isLoading}
hasError={hasError}
onDecrypt={() => void decrypt()}
/>
)}
{showMedia && (
<audio
className="bn-audio"
src={resolvedUrl || block.props.url}
controls
contentEditable={false}
draggable={false}
/>
)}
</FileBlockWrapper>
);
};
const AudioToExternalHTML = ({
block,
}: {
block: BlockNoDefaults<
Record<'audio', AudioBlockConfig>,
InlineContentSchema,
StyleSchema
>;
}) => {
if (!block.props.url) {
return <p>Add audio</p>;
}
return <audio src={block.props.url} controls />;
};
export const AudioBlock = createReactBlockSpec(
createAudioBlockConfig,
(config) => ({
meta: {
fileBlockAccept: ['audio/*'],
},
render: (props) => <AudioBlockComponent {...(props as any)} />,
parse: audioParse(config),
toExternalHTML: (props) => <AudioToExternalHTML {...(props as any)} />,
runsBefore: ['file'],
}),
);
@@ -13,7 +13,7 @@ import {
createReactBlockSpec,
} from '@blocknote/react';
import { TFunction } from 'i18next';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { createGlobalStyle, css } from 'styled-components';
@@ -21,6 +21,8 @@ import { Box, Icon, Loading } from '@/components';
import { ANALYZE_URL } from '../../conf';
import { DocsBlockNoteEditor } from '../../types';
import { EncryptedMediaPlaceholder } from '../EncryptedMediaPlaceholder';
import { useEncryption } from '../EncryptionProvider';
const PDFBlockStyle = createGlobalStyle`
.bn-block-content[data-content-type="pdf"] .bn-file-block-content-wrapper[style*="fit-content"] {
@@ -67,9 +69,12 @@ const PdfBlockComponent = ({
const pdfUrl = block.props.url;
const { i18n, t } = useTranslation();
const lang = i18n.resolvedLanguage;
const { isEncrypted, decryptFileUrl } = useEncryption();
const [isPDFContent, setIsPDFContent] = useState<boolean | null>(null);
const [isPDFContentLoading, setIsPDFContentLoading] =
useState<boolean>(false);
const [resolvedPdfUrl, setResolvedPdfUrl] = useState<string | null>(null);
useEffect(() => {
if (lang && locales[lang as keyof typeof locales]) {
@@ -86,8 +91,9 @@ const PdfBlockComponent = ({
}
}, [lang, t]);
// For non-encrypted docs, validate PDF content on mount
useEffect(() => {
if (!pdfUrl || pdfUrl.includes(ANALYZE_URL)) {
if (isEncrypted || !pdfUrl || pdfUrl.includes(ANALYZE_URL)) {
return;
}
@@ -101,6 +107,7 @@ const PdfBlockComponent = ({
if (response.ok && contentType?.includes('application/pdf')) {
setIsPDFContent(true);
setResolvedPdfUrl(pdfUrl);
} else {
setIsPDFContent(false);
}
@@ -112,12 +119,45 @@ const PdfBlockComponent = ({
};
void validatePDFContent();
}, [pdfUrl]);
}, [pdfUrl, isEncrypted]);
const handleDecryptPdf = useCallback(async () => {
if (!pdfUrl) {
return;
}
setIsPDFContentLoading(true);
try {
const blobUrl = await decryptFileUrl(pdfUrl);
setResolvedPdfUrl(blobUrl);
setIsPDFContent(true);
} catch {
setIsPDFContent(false);
} finally {
setIsPDFContentLoading(false);
}
}, [pdfUrl, decryptFileUrl]);
const showEncryptedPlaceholder =
isEncrypted &&
isPDFContent === null &&
pdfUrl &&
!pdfUrl.includes(ANALYZE_URL);
return (
<Box ref={contentRef} className="bn-file-block-content-wrapper">
<PDFBlockStyle />
{isPDFContentLoading && <Loading />}
{!isEncrypted && isPDFContentLoading && <Loading />}
{showEncryptedPlaceholder && (
<EncryptedMediaPlaceholder
label={t('Click to decrypt and view PDF')}
errorLabel={t('Invalid or missing PDF file.')}
minHeight="300px"
isLoading={isPDFContentLoading}
hasError={false}
onDecrypt={() => void handleDecryptPdf()}
/>
)}
{!isPDFContentLoading && isPDFContent !== null && !isPDFContent && (
<Box
$align="center"
@@ -142,7 +182,7 @@ const PdfBlockComponent = ({
block={block as unknown as FileBlockBlock}
editor={editor as unknown as FileBlockEditor}
>
{!isPDFContentLoading && isPDFContent && (
{!isPDFContentLoading && isPDFContent && resolvedPdfUrl && (
<Box
as="embed"
className="bn-visual-media"
@@ -150,7 +190,7 @@ const PdfBlockComponent = ({
$width="100%"
$height="450px"
type="application/pdf"
src={pdfUrl}
src={resolvedPdfUrl}
aria-label={block.props.name || t('PDF document')}
contentEditable={false}
draggable={false}
@@ -0,0 +1,108 @@
import {
BlockNoDefaults,
BlockNoteEditor,
InlineContentSchema,
StyleSchema,
createVideoBlockConfig,
videoParse,
} from '@blocknote/core';
import {
ResizableFileBlockWrapper,
createReactBlockSpec,
} from '@blocknote/react';
import { useTranslation } from 'react-i18next';
import { Icon } from '@/components';
import { useDecryptMedia } from '../../hook';
import { EncryptedMediaPlaceholder } from '../EncryptedMediaPlaceholder';
type VideoBlockConfig = ReturnType<typeof createVideoBlockConfig>;
interface VideoBlockComponentProps {
block: BlockNoDefaults<
Record<'video', VideoBlockConfig>,
InlineContentSchema,
StyleSchema
>;
contentRef: (node: HTMLElement | null) => void;
editor: BlockNoteEditor<
Record<'video', VideoBlockConfig>,
InlineContentSchema,
StyleSchema
>;
}
const VideoBlockComponent = ({
editor,
block,
...rest
}: VideoBlockComponentProps) => {
const { t } = useTranslation();
const {
showPlaceholder,
showMedia,
isLoading,
hasError,
decrypt,
resolvedUrl,
} = useDecryptMedia(block.props.url);
return (
<ResizableFileBlockWrapper
{...({ editor, block, ...rest } as any)}
buttonIcon={
<Icon iconName="videocam" $size="24px" $css="line-height: normal;" />
}
>
{showPlaceholder && (
<EncryptedMediaPlaceholder
label={t('Click to decrypt and play video')}
errorLabel={t('Failed to decrypt video file.')}
minHeight="300px"
isLoading={isLoading}
hasError={hasError}
onDecrypt={() => void decrypt()}
/>
)}
{showMedia && (
<video
className="bn-visual-media"
src={resolvedUrl || block.props.url}
controls
contentEditable={false}
draggable={false}
/>
)}
</ResizableFileBlockWrapper>
);
};
const VideoToExternalHTML = ({
block,
}: {
block: BlockNoDefaults<
Record<'video', VideoBlockConfig>,
InlineContentSchema,
StyleSchema
>;
}) => {
if (!block.props.url) {
return <p>Add video</p>;
}
return <video src={block.props.url} controls />;
};
export const VideoBlock = createReactBlockSpec(
createVideoBlockConfig,
(config) => ({
meta: {
fileBlockAccept: ['video/*'],
},
render: (props) => <VideoBlockComponent {...(props as any)} />,
parse: videoParse(config),
toExternalHTML: (props) => <VideoToExternalHTML {...(props as any)} />,
runsBefore: ['file'],
}),
);
@@ -1,4 +1,6 @@
export * from './AccessibleImageBlock';
export * from './AudioBlock';
export * from './CalloutBlock';
export * from './PdfBlock';
export * from './UploadLoaderBlock';
export * from './VideoBlock';
@@ -122,8 +122,8 @@ export const SearchPage = ({
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
e.preventDefault();
// Keep the trigger character ('@' or '/') in the editor when closing with Escape
closeSearch(trigger);
// Keep the trigger character and typed text in the editor when closing with Escape
closeSearch(`${trigger}${search}`);
} else if (e.key === 'Backspace' && search.length === 0) {
e.preventDefault();
closeSearch('');
@@ -43,7 +43,7 @@ describe('useSaveDoc', () => {
const addEventListenerSpy = vi.spyOn(window, 'addEventListener');
renderHook(() => useSaveDoc(docId, yDoc, true), {
renderHook(() => useSaveDoc(docId, yDoc, true, false, null), {
wrapper: AppWrapper,
});
@@ -75,7 +75,7 @@ describe('useSaveDoc', () => {
}),
});
renderHook(() => useSaveDoc(docId, yDoc, true), {
renderHook(() => useSaveDoc(docId, yDoc, true, false, null), {
wrapper: AppWrapper,
});
@@ -112,7 +112,7 @@ describe('useSaveDoc', () => {
}),
});
renderHook(() => useSaveDoc(docId, yDoc, true), {
renderHook(() => useSaveDoc(docId, yDoc, true, false, null), {
wrapper: AppWrapper,
});
@@ -132,7 +132,7 @@ describe('useSaveDoc', () => {
const docId = 'test-doc-id';
const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener');
const { unmount } = renderHook(() => useSaveDoc(docId, yDoc, true), {
const { unmount } = renderHook(() => useSaveDoc(docId, yDoc, true, false, null), {
wrapper: AppWrapper,
});
@@ -1,3 +1,4 @@
export * from './useDecryptMedia';
export * from './useHeadings';
export * from './useSaveDoc';
export * from './useShortcuts';
@@ -0,0 +1,81 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useEncryption } from '../components/EncryptionProvider';
import { ANALYZE_URL } from '../conf';
export const useDecryptMedia = (url: string | undefined) => {
const {
isEncrypted,
decryptFileUrl,
revealAllCounter,
registerPlaceholder,
unregisterPlaceholder,
} = useEncryption();
const [resolvedUrl, setResolvedUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const isAnalyzing = !!url && url.includes(ANALYZE_URL);
const decrypt = useCallback(async () => {
if (!url || resolvedUrl || isLoading) {
return;
}
setIsLoading(true);
setHasError(false);
try {
const blobUrl = await decryptFileUrl(url);
setResolvedUrl(blobUrl);
} catch {
setHasError(true);
} finally {
setIsLoading(false);
}
}, [url, resolvedUrl, isLoading, decryptFileUrl]);
// Auto-decrypt when "Reveal all" is requested
const decryptRef = useRef(decrypt);
decryptRef.current = decrypt;
useEffect(() => {
if (revealAllCounter > 0 && isEncrypted && url && !isAnalyzing) {
void decryptRef.current();
}
}, [revealAllCounter, isEncrypted, url, isAnalyzing]);
const showPlaceholder =
isEncrypted && !resolvedUrl && !hasError && !!url && !isAnalyzing;
const showMedia = !!url && !isAnalyzing && (!isEncrypted || !!resolvedUrl);
// Track pending placeholders in the provider
const wasShowingPlaceholder = useRef(false);
useEffect(() => {
if (showPlaceholder && !wasShowingPlaceholder.current) {
registerPlaceholder();
wasShowingPlaceholder.current = true;
} else if (!showPlaceholder && wasShowingPlaceholder.current) {
unregisterPlaceholder();
wasShowingPlaceholder.current = false;
}
}, [showPlaceholder, registerPlaceholder, unregisterPlaceholder]);
useEffect(() => {
return () => {
if (wasShowingPlaceholder.current) {
unregisterPlaceholder();
}
};
}, [unregisterPlaceholder]);
return {
isEncrypted,
resolvedUrl,
isLoading,
hasError,
decrypt,
showPlaceholder,
showMedia,
};
};
@@ -2,8 +2,10 @@ import { useRouter } from 'next/router';
import { useCallback, useEffect, useState } from 'react';
import * as Y from 'yjs';
import { useUpdateDoc } from '@/docs/doc-management/';
import { DocumentEncryptionSettings } from '@/docs/doc-collaboration/hook/useDocumentEncryption';
import { useUpdateDoc, useProviderStore } from '@/docs/doc-management/';
import { KEY_LIST_DOC_VERSIONS } from '@/docs/doc-versioning';
import { useVaultClient } from '@/features/docs/doc-collaboration/vault';
import { isFirefox } from '@/utils/userAgent';
import { toBase64 } from '../utils';
@@ -14,7 +16,11 @@ export const useSaveDoc = (
docId: string,
yDoc: Y.Doc,
isConnectedToCollabServer: boolean,
isEncrypted: boolean,
documentEncryptionSettings: DocumentEncryptionSettings | null,
) => {
const { encryptionTransition } = useProviderStore();
const { client: vaultClient } = useVaultClient();
const { mutate: updateDoc } = useUpdateDoc({
listInvalidQueries: [KEY_LIST_DOC_VERSIONS],
onSuccess: () => {
@@ -23,11 +29,6 @@ export const useSaveDoc = (
});
const [isLocalChange, setIsLocalChange] = useState<boolean>(false);
/**
* Update initial doc when doc is updated by other users,
* so only the user typing will trigger the save.
* This is to avoid saving the same doc multiple time.
*/
useEffect(() => {
const onUpdate = (
_uintArray: Uint8Array,
@@ -48,16 +49,53 @@ export const useSaveDoc = (
const saveDoc = useCallback(() => {
if (!isLocalChange) {
return false;
} else if (encryptionTransition) {
return false;
} else if (isEncrypted && (!documentEncryptionSettings || !vaultClient)) {
return false;
}
updateDoc({
id: docId,
content: toBase64(Y.encodeStateAsUpdate(yDoc)),
websocket: isConnectedToCollabServer,
});
const state = Y.encodeStateAsUpdate(yDoc);
if (isEncrypted && documentEncryptionSettings && vaultClient) {
// Encrypt via vault with ArrayBuffer — zero-copy
vaultClient
.encryptWithKey(
state.buffer as ArrayBuffer,
documentEncryptionSettings.encryptedSymmetricKey,
)
.then(({ encryptedData }) => {
updateDoc({
id: docId,
content: toBase64(new Uint8Array(encryptedData)),
contentEncrypted: true,
websocket: isConnectedToCollabServer,
});
})
.catch((err) => {
console.error('Failed to encrypt document for save:', err);
});
} else {
updateDoc({
id: docId,
content: toBase64(state),
contentEncrypted: false,
websocket: isConnectedToCollabServer,
});
}
return true;
}, [isLocalChange, updateDoc, docId, yDoc, isConnectedToCollabServer]);
}, [
isLocalChange,
encryptionTransition,
updateDoc,
docId,
yDoc,
isConnectedToCollabServer,
isEncrypted,
documentEncryptionSettings,
vaultClient,
]);
const router = useRouter();
@@ -65,13 +103,6 @@ export const useSaveDoc = (
const onSave = (e?: Event) => {
const isSaving = saveDoc();
/**
* Firefox does not trigger the request every time the user leaves the page.
* Plus the request is not intercepted by the service worker.
* So we prevent the default behavior to have the popup asking the user
* if he wants to leave the page, by adding the popup, we let the time to the
* request to be sent, and intercepted by the service worker (for the offline part).
*/
if (
isSaving &&
typeof e !== 'undefined' &&
@@ -82,16 +113,12 @@ export const useSaveDoc = (
}
};
// Save every minute
const timeout = setInterval(onSave, SAVE_INTERVAL);
// Save when the user leaves the page
addEventListener('beforeunload', onSave);
// Save when the user navigates to another page
router.events.on('routeChangeStart', onSave);
return () => {
clearInterval(timeout);
removeEventListener('beforeunload', onSave);
router.events.off('routeChangeStart', onSave);
};

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