Compare commits

..

17 Commits

Author SHA1 Message Date
Thomas Ramé
d081a2d3b4 TO REMOVE: ugly pnpm setup replacement for e2esdk local tests (since package linking impossible from yarn to pnpm, same issues with adding >1 bundled tarballs...) 2026-02-03 18:16:34 +01: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
101 changed files with 24561 additions and 21083 deletions

View File

@@ -6,7 +6,6 @@ on:
push:
branches:
- 'main'
- 'refacto/blocknote-ai'
tags:
- 'v*'
pull_request:
@@ -37,12 +36,12 @@ jobs:
with:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main
with:
docker-build-args: '--target backend-production -f Dockerfile'
docker-image-name: 'docker.io/lasuite/impress-backend:${{ github.sha }}'
# -
# name: Run trivy scan
# uses: numerique-gouv/action-trivy-cache@main
# with:
# docker-build-args: '--target backend-production -f Dockerfile'
# docker-image-name: 'docker.io/lasuite/impress-backend:${{ github.sha }}'
-
name: Build and push
uses: docker/build-push-action@v6
@@ -147,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:

View File

@@ -6,13 +6,16 @@ and this project adheres to
## [Unreleased]
## [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
- ✨(frontend) integrate new Blocknote AI feature #1016
### Changed
@@ -27,6 +30,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
@@ -36,7 +40,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
@@ -64,7 +68,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
@@ -83,7 +87,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
@@ -107,7 +111,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
@@ -126,7 +130,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
@@ -149,7 +153,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
@@ -183,7 +187,7 @@ and this project adheres to
- 🔥(backend) remove api managing templates
## [3.9.0] - 2025-11-10
## [v3.9.0] - 2025-11-10
### Added
@@ -209,13 +213,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
@@ -229,7 +233,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
@@ -282,7 +286,7 @@ and this project adheres to
- 🔥(frontend) remove custom DividerBlock ##1375
## [3.7.0] - 2025-09-12
## [v3.7.0] - 2025-09-12
### Added
@@ -314,7 +318,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
@@ -350,7 +354,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
@@ -378,7 +382,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
@@ -388,7 +392,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
@@ -399,7 +403,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
@@ -443,7 +447,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
@@ -475,14 +479,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
@@ -509,7 +513,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
@@ -527,7 +531,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
@@ -543,7 +547,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
@@ -561,7 +565,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
@@ -591,7 +595,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
@@ -605,7 +609,7 @@ and this project adheres to
- 🐛(frontend) fix collaboration error #684
## [2.3.0] - 2025-03-03
## [v2.3.0] - 2025-03-03
## Added
@@ -632,7 +636,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
@@ -651,7 +655,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
@@ -680,14 +684,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
@@ -718,7 +722,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
@@ -739,7 +743,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
@@ -760,19 +764,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
@@ -800,7 +804,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
@@ -827,7 +831,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
@@ -849,13 +853,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
@@ -883,7 +887,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
@@ -903,7 +907,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
@@ -927,14 +931,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
@@ -960,7 +964,7 @@ and this project adheres to
- 🔥(helm) remove htaccess #181
## [1.1.0] - 2024-07-15
## [v1.1.0] - 2024-07-15
## Added
@@ -975,7 +979,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
@@ -1013,14 +1017,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
@@ -1057,12 +1062,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

View File

@@ -11,7 +11,6 @@ These are the environment variables you can set for the `impress-backend` contai
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
| AI_API_KEY | AI key to be used for AI Base url | |
| AI_BASE_URL | OpenAI compatible AI base url | |
| AI_BOT | Information to give to the frontend about the AI bot | { "name": "Docs AI", "color": "#8bc6ff" }
| AI_FEATURE_ENABLED | Enable AI options | false |
| AI_MODEL | AI Model to use | |
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |
@@ -24,7 +23,7 @@ These are the environment variables you can set for the `impress-backend` contai
| AWS_S3_SECRET_ACCESS_KEY | Access key for s3 endpoint | |
| 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 |

View File

@@ -48,7 +48,7 @@ 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.

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

View File

@@ -17,6 +17,7 @@ from rest_framework import serializers
from core import choices, enums, models, utils, validators
from core.services import mime_types
from core.services.ai_services import AI_ACTIONS
from core.services.converter_services import (
ConversionError,
Converter,
@@ -791,38 +792,33 @@ class VersionFilterSerializer(serializers.Serializer):
)
class AIProxySerializer(serializers.Serializer):
"""Serializer for AI proxy requests."""
class AITransformSerializer(serializers.Serializer):
"""Serializer for AI transform requests."""
messages = serializers.ListField(
required=True,
child=serializers.DictField(
child=serializers.CharField(required=True),
),
allow_empty=False,
action = serializers.ChoiceField(choices=AI_ACTIONS, required=True)
text = serializers.CharField(required=True)
def validate_text(self, value):
"""Ensure the text field is not empty."""
if len(value.strip()) == 0:
raise serializers.ValidationError("Text field cannot be empty.")
return value
class AITranslateSerializer(serializers.Serializer):
"""Serializer for AI translate requests."""
language = serializers.ChoiceField(
choices=tuple(enums.ALL_LANGUAGES.items()), required=True
)
model = serializers.CharField(required=True)
text = serializers.CharField(required=True)
def validate_messages(self, messages):
"""Validate messages structure."""
# Ensure each message has the required fields
for message in messages:
if (
not isinstance(message, dict)
or "role" not in message
or "content" not in message
):
raise serializers.ValidationError(
"Each message must have 'role' and 'content' fields"
)
return messages
def validate_model(self, value):
"""Validate model value is the same than settings.AI_MODEL"""
if value != settings.AI_MODEL:
raise serializers.ValidationError(f"{value} is not a valid model")
def validate_text(self, value):
"""Ensure the text field is not empty."""
if len(value.strip()) == 0:
raise serializers.ValidationError("Text field cannot be empty.")
return value

View File

@@ -338,8 +338,21 @@ class DocumentViewSet(
9. **Media Auth**: Authorize access to document media.
Example: GET /documents/media-auth/
10. **AI Proxy**: Proxy an AI request to an external AI service.
Example: POST /api/v1.0/documents/<resource_id>/ai-proxy
10. **AI Transform**: Apply a transformation action on a piece of text with AI.
Example: POST /documents/{id}/ai-transform/
Expected data:
- text (str): The input text.
- action (str): The transformation type, one of [prompt, correct, rephrase, summarize].
Returns: JSON response with the processed text.
Throttled by: AIDocumentRateThrottle, AIUserRateThrottle.
11. **AI Translate**: Translate a piece of text with AI.
Example: POST /documents/{id}/ai-translate/
Expected data:
- text (str): The input text.
- language (str): The target language, chosen from settings.LANGUAGES.
Returns: JSON response with the translated text.
Throttled by: AIDocumentRateThrottle, AIUserRateThrottle.
### Ordering: created_at, updated_at, is_favorite, title
@@ -378,6 +391,7 @@ class DocumentViewSet(
throttle_scope = "document"
queryset = models.Document.objects.select_related("creator").all()
serializer_class = serializers.DocumentSerializer
ai_translate_serializer_class = serializers.AITranslateSerializer
all_serializer_class = serializers.ListDocumentSerializer
children_serializer_class = serializers.ListDocumentSerializer
descendants_serializer_class = serializers.ListDocumentSerializer
@@ -1631,42 +1645,58 @@ class DocumentViewSet(
@drf.decorators.action(
detail=True,
methods=["post"],
name="Proxy AI requests to the AI provider",
url_path="ai-proxy",
# throttle_classes=[utils.AIDocumentRateThrottle, utils.AIUserRateThrottle],
name="Apply a transformation action on a piece of text with AI",
url_path="ai-transform",
throttle_classes=[utils.AIDocumentRateThrottle, utils.AIUserRateThrottle],
)
def ai_proxy(self, request, *args, **kwargs):
def ai_transform(self, request, *args, **kwargs):
"""
POST /api/v1.0/documents/<resource_id>/ai-proxy
Proxy AI requests to the configured AI provider.
This endpoint forwards requests to the AI provider and returns the complete response.
POST /api/v1.0/documents/<resource_id>/ai-transform
with expected data:
- text: str
- action: str [prompt, correct, rephrase, summarize]
Return JSON response with the processed text.
"""
# Check permissions first
self.get_object()
if not settings.AI_FEATURE_ENABLED:
raise ValidationError("AI feature is not enabled.")
serializer = serializers.AITransformSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
ai_service = AIService()
text = serializer.validated_data["text"]
action = serializer.validated_data["action"]
if settings.AI_STREAM:
stream_gen = ai_service.stream_proxy(
url=settings.AI_BASE_URL.rstrip("/") + "/chat/completions",
method="POST",
headers={"Content-Type": "application/json"},
body=json.dumps(request.data, ensure_ascii=False).encode("utf-8"),
)
response = AIService().transform(text, action)
resp = StreamingHttpResponse(
streaming_content=stream_gen,
content_type="text/event-stream",
status=200,
)
resp["X-Accel-Buffering"] = "no"
resp["Cache-Control"] = "no-cache"
return resp
return drf.response.Response(response, status=drf.status.HTTP_200_OK)
@drf.decorators.action(
detail=True,
methods=["post"],
name="Translate a piece of text with AI",
url_path="ai-translate",
throttle_classes=[utils.AIDocumentRateThrottle, utils.AIUserRateThrottle],
)
def ai_translate(self, request, *args, **kwargs):
"""
POST /api/v1.0/documents/<resource_id>/ai-translate
with expected data:
- text: str
- language: str [settings.LANGUAGES]
Return JSON response with the translated text.
"""
# Check permissions first
self.get_object()
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
text = serializer.validated_data["text"]
language = serializer.validated_data["language"]
response = AIService().translate(text, language)
return drf.response.Response(response, status=drf.status.HTTP_200_OK)
def _reject_invalid_ips(self, ips):
"""
@@ -2307,10 +2337,7 @@ class ConfigView(drf.views.APIView):
Return a dictionary of public settings.
"""
array_settings = [
"AI_BOT",
"AI_FEATURE_ENABLED",
"AI_MODEL",
"AI_STREAM",
"COLLABORATION_WS_URL",
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY",
"CONVERSION_FILE_EXTENSIONS_ALLOWED",
@@ -2320,6 +2347,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",

View File

@@ -783,7 +783,8 @@ class Document(MP_Node, BaseModel):
return {
"accesses_manage": is_owner_or_admin,
"accesses_view": has_access_role,
"ai_proxy": ai_access,
"ai_transform": ai_access,
"ai_translate": ai_access,
"attachment_upload": can_update,
"media_check": can_get,
"can_edit": can_update,

View File

@@ -1,168 +1,98 @@
# core/services/ai_services.py
from __future__ import annotations
"""AI services."""
import json
from typing import Any, Dict, Generator
from urllib.parse import urlparse
import httpx
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from core import enums
BLOCKNOTE_TOOL_STRICT_PROMPT = """You are editing a BlockNote document via the tool applyDocumentOperations.
You MUST respond ONLY by calling applyDocumentOperations.
The tool input MUST be valid JSON:
{ "operations": [ ... ] }
Each operation MUST include "type" and it MUST be one of:
- "update" (requires: id, block)
- "add" (requires: referenceId, position, blocks)
- "delete" (requires: id)
VALID SHAPES (FOLLOW EXACTLY):
Update:
{ "type":"update", "id":"<id$>", "block":"<p>...</p>" }
IMPORTANT: "block" MUST be a STRING containing a SINGLE valid HTML element.
Add:
{ "type":"add", "referenceId":"<id$>", "position":"before|after", "blocks":["<p>...</p>"] }
IMPORTANT: "blocks" MUST be an ARRAY OF STRINGS.
Each item MUST be a STRING containing a SINGLE valid HTML element.
Delete:
{ "type":"delete", "id":"<id$>" }
IDs ALWAYS end with "$". Use ids EXACTLY as provided.
Return ONLY the JSON tool input. No prose, no markdown.
"""
if settings.LANGFUSE_PUBLIC_KEY:
from langfuse.openai import OpenAI
else:
from openai import OpenAI
def _drop_nones(obj: Any) -> Any:
if isinstance(obj, dict):
return {k: _drop_nones(v) for k, v in obj.items() if v is not None}
if isinstance(obj, list):
return [_drop_nones(v) for v in obj]
return obj
AI_ACTIONS = {
"prompt": (
"Answer the prompt using markdown formatting for structure and emphasis. "
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
"Preserve the language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"correct": (
"Correct grammar and spelling of the markdown text, "
"preserving language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"rephrase": (
"Rephrase the given markdown text, "
"preserving language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"summarize": (
"Summarize the markdown text, preserving language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
"beautify": (
"Add formatting to the text to make it more readable. "
"Do not provide any other information. "
"Preserve the language."
),
"emojify": (
"Add emojis to the important parts of the text. "
"Do not provide any other information. "
"Preserve the language."
),
}
AI_TRANSLATE = (
"Keep the same html structure and formatting. "
"Translate the content in the html to the specified language {language:s}. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information."
)
class AIService:
"""
Backward-compatible proxy service for your existing viewset:
"""Service class for AI-related operations."""
stream_proxy(provider, url, method, headers, body) -> yields bytes
def __init__(self):
"""Ensure that the AI configuration is set properly."""
if (
settings.AI_BASE_URL is None
or settings.AI_API_KEY is None
or settings.AI_MODEL is None
):
raise ImproperlyConfigured("AI configuration not set")
self.client = OpenAI(base_url=settings.AI_BASE_URL, api_key=settings.AI_API_KEY)
Plus: hardening payload so BlockNote tool calls are valid.
"""
def call_ai_api(self, system_content, text):
"""Helper method to call the OpenAI API and process the response."""
response = self.client.chat.completions.create(
model=settings.AI_MODEL,
messages=[
{"role": "system", "content": system_content},
{"role": "user", "content": text},
],
)
def __init__(self) -> None:
if not settings.AI_BASE_URL or not settings.AI_API_KEY:
raise ImproperlyConfigured("AI_BASE_URL and AI_API_KEY must be set")
content = response.choices[0].message.content
self.base_url = str(settings.AI_BASE_URL).rstrip("/")
self.api_key = str(settings.AI_API_KEY)
self.allowed_host = urlparse(self.base_url).netloc
if not content:
raise RuntimeError("AI response does not contain an answer")
def _assert_allowed_target(self, target_url: str) -> None:
t = urlparse(target_url)
if t.scheme not in ("http", "https"):
raise ValueError("Target URL not allowed")
if t.netloc != self.allowed_host:
raise ValueError("Target URL not allowed")
return {"answer": content}
def _filtered_headers(self, incoming_headers: Dict[str, str]) -> Dict[str, str]:
hop_by_hop = {"host", "connection", "content-length", "accept-encoding"}
out: Dict[str, str] = {}
for k, v in incoming_headers.items():
lk = k.lower()
if lk in hop_by_hop:
continue
if lk == "authorization":
# Client auth is for Django only, not upstream
continue
out[k] = v
def transform(self, text, action):
"""Transform text based on specified action."""
system_content = AI_ACTIONS[action]
return self.call_ai_api(system_content, text)
out["Authorization"] = f"Bearer {self.api_key}"
return out
def _normalize_tools(self, tools: list) -> list:
normalized = []
for tool in tools:
if isinstance(tool, dict) and tool.get("type") == "function":
fn = tool.get("function") or {}
if isinstance(fn, dict) and not fn.get("description"):
fn["description"] = f"Tool {fn.get('name', 'unknown')}."
tool["function"] = fn
normalized.append(_drop_nones(tool))
return normalized
def _harden_payload(self, payload: Dict[str, Any]) -> Dict[str, Any]:
payload = dict(payload)
# Enforce server model (important with Albert routing)
if getattr(settings, "AI_MODEL", None):
payload["model"] = settings.AI_MODEL
# Compliance
payload["temperature"] = 0
# Tools normalization
if isinstance(payload.get("tools"), list):
payload["tools"] = self._normalize_tools(payload["tools"])
# Force tool call if tools exist
if payload.get("tools"):
payload["tool_choice"] = {"type": "function", "function": {"name": "applyDocumentOperations"}}
# Convert non-standard "required"
if payload.get("tool_choice") == "required":
payload["tool_choice"] = {"type": "function", "function": {"name": "applyDocumentOperations"}}
# Inject strict system prompt once
msgs = payload.get("messages")
if isinstance(msgs, list):
need = True
if msgs and isinstance(msgs[0], dict) and msgs[0].get("role") == "system":
c = msgs[0].get("content") or ""
if isinstance(c, str) and "applyDocumentOperations" in c and "blocks" in c:
need = False
if need:
payload["messages"] = [{"role": "system", "content": BLOCKNOTE_TOOL_STRICT_PROMPT}] + msgs
return _drop_nones(payload)
def _maybe_harden_json_body(self, body: bytes, headers: Dict[str, str]) -> bytes:
ct = (headers.get("Content-Type") or headers.get("content-type") or "").lower()
if "application/json" not in ct:
return body
try:
payload = json.loads(body.decode("utf-8"))
except Exception:
return body
if isinstance(payload, dict):
payload = self._harden_payload(payload)
return json.dumps(payload, ensure_ascii=False).encode("utf-8")
return body
def stream_proxy(
self,
*,
url: str,
method: str,
headers: Dict[str, str],
body: bytes,
) -> Generator[bytes, None, None]:
self._assert_allowed_target(url)
req_headers = self._filtered_headers(dict(headers))
req_body = self._maybe_harden_json_body(body, req_headers)
timeout = httpx.Timeout(connect=10.0, read=300.0, write=60.0, pool=10.0)
with httpx.Client(timeout=timeout, follow_redirects=False) as client:
with client.stream(method.upper(), url, headers=req_headers, content=req_body) as r:
for chunk in r.iter_bytes():
if chunk:
yield chunk
def translate(self, text, language):
"""Translate text to a specified language."""
language_display = enums.ALL_LANGUAGES.get(language, language)
system_content = AI_TRANSLATE.format(language=language_display)
return self.call_ai_api(system_content, text)

View File

@@ -1,686 +0,0 @@
"""
Test AI proxy API endpoint for users in impress's core app.
"""
import random
from unittest.mock import MagicMock, patch
from django.test import override_settings
import pytest
from rest_framework.test import APIClient
from core import factories
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@pytest.fixture(autouse=True)
def ai_settings(settings):
"""Fixture to set AI settings."""
settings.AI_MODEL = "llama"
settings.AI_BASE_URL = "http://example.com"
settings.AI_API_KEY = "test-key"
settings.AI_FEATURE_ENABLED = True
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"reach, role",
[
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("authenticated", "editor"),
("public", "reader"),
],
)
def test_api_documents_ai_proxy_anonymous_forbidden(reach, role):
"""
Anonymous users should not be able to request AI proxy if the link reach
and role don't allow it.
"""
document = factories.DocumentFactory(link_reach=reach, link_role=role)
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = APIClient().post(
url,
{
"messages": [{"role": "user", "content": "Hello"}],
"model": "llama",
},
format="json",
)
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
@override_settings(AI_ALLOW_REACH_FROM="public")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_proxy_anonymous_success(mock_create):
"""
Anonymous users should be able to request AI proxy to a document
if the link reach and role permit it.
"""
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_response = MagicMock()
mock_response.model_dump.return_value = {
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1677652288,
"model": "llama",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I help you?",
},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 9, "completion_tokens": 12, "total_tokens": 21},
}
mock_create.return_value = mock_response
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = APIClient().post(
url,
{
"messages": [{"role": "user", "content": "Hello"}],
"model": "llama",
},
format="json",
)
assert response.status_code == 200
response_data = response.json()
assert response_data["id"] == "chatcmpl-123"
assert response_data["model"] == "llama"
assert len(response_data["choices"]) == 1
assert (
response_data["choices"][0]["message"]["content"]
== "Hello! How can I help you?"
)
mock_create.assert_called_once_with(
messages=[{"role": "user", "content": "Hello"}],
model="llama",
stream=False,
)
@override_settings(AI_ALLOW_REACH_FROM=random.choice(["authenticated", "restricted"]))
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_proxy_anonymous_limited_by_setting(mock_create):
"""
Anonymous users should not be able to request AI proxy to a document
if AI_ALLOW_REACH_FROM setting restricts it.
"""
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_response = MagicMock()
mock_response.model_dump.return_value = {"content": "Hello!"}
mock_create.return_value = mock_response
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = APIClient().post(
url,
{
"messages": [{"role": "user", "content": "Hello"}],
"model": "llama",
},
format="json",
)
assert response.status_code == 401
@pytest.mark.parametrize(
"reach, role",
[
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("public", "reader"),
],
)
def test_api_documents_ai_proxy_authenticated_forbidden(reach, role):
"""
Users who are not related to a document can't request AI proxy if the
link reach and role don't allow it.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach, link_role=role)
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
{
"messages": [{"role": "user", "content": "Hello"}],
"model": "llama",
},
format="json",
)
assert response.status_code == 403
@pytest.mark.parametrize(
"reach, role",
[
("authenticated", "editor"),
("public", "editor"),
],
)
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_proxy_authenticated_success(mock_create, reach, role):
"""
Authenticated users should be able to request AI proxy to a document
if the link reach and role permit it.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach, link_role=role)
mock_response = MagicMock()
mock_response.model_dump.return_value = {
"id": "chatcmpl-456",
"object": "chat.completion",
"model": "llama",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Hi there!"},
"finish_reason": "stop",
}
],
}
mock_create.return_value = mock_response
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
{
"messages": [{"role": "user", "content": "Hello"}],
"model": "llama",
},
format="json",
)
assert response.status_code == 200
response_data = response.json()
assert response_data["id"] == "chatcmpl-456"
assert response_data["choices"][0]["message"]["content"] == "Hi there!"
mock_create.assert_called_once_with(
messages=[{"role": "user", "content": "Hello"}],
model="llama",
stream=False,
)
@pytest.mark.parametrize("via", VIA)
def test_api_documents_ai_proxy_reader(via, mock_user_teams):
"""Users with reader access should not be able to request AI proxy."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="restricted")
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="reader")
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="reader"
)
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
{
"messages": [{"role": "user", "content": "Hello"}],
"model": "llama",
},
format="json",
)
assert response.status_code == 403
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
@pytest.mark.parametrize("via", VIA)
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_proxy_success(mock_create, via, role, mock_user_teams):
"""Users with sufficient permissions should be able to request AI proxy."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="restricted")
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
mock_response = MagicMock()
mock_response.model_dump.return_value = {
"id": "chatcmpl-789",
"object": "chat.completion",
"model": "llama",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Success!"},
"finish_reason": "stop",
}
],
}
mock_create.return_value = mock_response
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
{
"messages": [{"role": "user", "content": "Test message"}],
"model": "llama",
},
format="json",
)
assert response.status_code == 200
response_data = response.json()
assert response_data["id"] == "chatcmpl-789"
assert response_data["choices"][0]["message"]["content"] == "Success!"
mock_create.assert_called_once_with(
messages=[{"role": "user", "content": "Test message"}],
model="llama",
stream=False,
)
def test_api_documents_ai_proxy_empty_messages():
"""The messages should not be empty when requesting AI proxy."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(url, {"messages": [], "model": "llama"}, format="json")
assert response.status_code == 400
assert response.json() == {"messages": ["This list may not be empty."]}
def test_api_documents_ai_proxy_missing_model():
"""The model should be required when requesting AI proxy."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url, {"messages": [{"role": "user", "content": "Hello"}]}, format="json"
)
assert response.status_code == 400
assert response.json() == {"model": ["This field is required."]}
def test_api_documents_ai_proxy_invalid_message_format():
"""Messages should have the correct format when requesting AI proxy."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
# Test with invalid message format (missing role)
response = client.post(
url,
{
"messages": [{"content": "Hello"}],
"model": "llama",
},
format="json",
)
assert response.status_code == 400
assert response.json() == {
"messages": ["Each message must have 'role' and 'content' fields"]
}
# Test with invalid message format (missing content)
response = client.post(
url,
{
"messages": [{"role": "user"}],
"model": "llama",
},
format="json",
)
assert response.status_code == 400
assert response.json() == {
"messages": ["Each message must have 'role' and 'content' fields"]
}
# Test with non-dict message
response = client.post(
url,
{
"messages": ["invalid"],
"model": "llama",
},
format="json",
)
assert response.status_code == 400
assert response.json() == {
"messages": {"0": ['Expected a dictionary of items but got type "str".']}
}
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_proxy_stream_disabled(mock_create):
"""Stream should be automatically disabled in AI proxy requests."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_response = MagicMock()
mock_response.model_dump.return_value = {"content": "Success!"}
mock_create.return_value = mock_response
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
{
"messages": [{"role": "user", "content": "Hello"}],
"model": "llama",
"stream": True, # This should be overridden to False
},
format="json",
)
assert response.status_code == 200
# Verify that stream was set to False
mock_create.assert_called_once_with(
messages=[{"role": "user", "content": "Hello"}],
model="llama",
stream=False,
)
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_proxy_additional_parameters(mock_create):
"""AI proxy should pass through additional parameters to the AI service."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_response = MagicMock()
mock_response.model_dump.return_value = {"content": "Success!"}
mock_create.return_value = mock_response
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
{
"messages": [{"role": "user", "content": "Hello"}],
"model": "llama",
"temperature": 0.7,
"max_tokens": 100,
"top_p": 0.9,
},
format="json",
)
assert response.status_code == 200
# Verify that additional parameters were passed through
mock_create.assert_called_once_with(
messages=[{"role": "user", "content": "Hello"}],
model="llama",
temperature=0.7,
max_tokens=100,
top_p=0.9,
stream=False,
)
@override_settings(AI_DOCUMENT_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_proxy_throttling_document(mock_create):
"""
Throttling per document should be triggered on the AI transform endpoint.
For full throttle class test see: `test_api_utils_ai_document_rate_throttles`
"""
client = APIClient()
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_response = MagicMock()
mock_response.model_dump.return_value = {"content": "Success!"}
mock_create.return_value = mock_response
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
for _ in range(3):
user = factories.UserFactory()
client.force_login(user)
response = client.post(
url,
{
"messages": [{"role": "user", "content": "Test message"}],
"model": "llama",
},
format="json",
)
assert response.status_code == 200
assert response.json() == {"content": "Success!"}
user = factories.UserFactory()
client.force_login(user)
response = client.post(
url,
{
"messages": [{"role": "user", "content": "Test message"}],
"model": "llama",
},
)
assert response.status_code == 429
assert response.json() == {
"detail": "Request was throttled. Expected available in 60 seconds."
}
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_proxy_complex_conversation(mock_create):
"""AI proxy should handle complex conversations with multiple messages."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_response = MagicMock()
mock_response.model_dump.return_value = {
"id": "chatcmpl-complex",
"object": "chat.completion",
"model": "llama",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "I understand your question about Python.",
},
"finish_reason": "stop",
}
],
}
mock_create.return_value = mock_response
complex_messages = [
{"role": "system", "content": "You are a helpful programming assistant."},
{"role": "user", "content": "How do I write a for loop in Python?"},
{
"role": "assistant",
"content": "You can write a for loop using: for item in iterable:",
},
{"role": "user", "content": "Can you give me a concrete example?"},
]
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
{
"messages": complex_messages,
"model": "llama",
},
format="json",
)
assert response.status_code == 200
response_data = response.json()
assert response_data["id"] == "chatcmpl-complex"
assert (
response_data["choices"][0]["message"]["content"]
== "I understand your question about Python."
)
mock_create.assert_called_once_with(
messages=complex_messages,
model="llama",
stream=False,
)
@override_settings(AI_USER_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_proxy_throttling_user(mock_create):
"""
Throttling per user should be triggered on the AI proxy endpoint.
For full throttle class test see: `test_api_utils_ai_user_rate_throttles`
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
mock_response = MagicMock()
mock_response.model_dump.return_value = {"content": "Success!"}
mock_create.return_value = mock_response
for _ in range(3):
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
{
"messages": [{"role": "user", "content": "Hello"}],
"model": "llama",
},
format="json",
)
assert response.status_code == 200
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-proxy/"
response = client.post(
url,
{
"messages": [{"role": "user", "content": "Hello"}],
"model": "llama",
},
format="json",
)
assert response.status_code == 429
assert response.json() == {
"detail": "Request was throttled. Expected available in 60 seconds."
}
@override_settings(AI_USER_RATE_THROTTLE_RATES={"minute": 10, "hour": 6, "day": 10})
def test_api_documents_ai_proxy_different_models():
"""AI proxy should work with different AI models."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="public", link_role="editor")
models_to_test = ["gpt-3.5-turbo", "gpt-4", "claude-3", "llama-2"]
for model_name in models_to_test:
response = client.post(
f"/api/v1.0/documents/{document.id!s}/ai-proxy/",
{
"messages": [{"role": "user", "content": "Hello"}],
"model": model_name,
},
format="json",
)
assert response.status_code == 400
assert response.json() == {"model": [f"{model_name} is not a valid model"]}
def test_api_documents_ai_proxy_ai_feature_disabled(settings):
"""When the settings AI_FEATURE_ENABLED is set to False, the endpoint is not reachable."""
settings.AI_FEATURE_ENABLED = False
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="public", link_role="editor")
response = client.post(
f"/api/v1.0/documents/{document.id!s}/ai-proxy/",
{
"messages": [{"role": "user", "content": "Hello"}],
"model": "llama",
},
format="json",
)
assert response.status_code == 400
assert response.json() == ["AI feature is not enabled."]

View File

@@ -0,0 +1,362 @@
"""
Test AI transform API endpoint for users in impress's core app.
"""
import random
from unittest.mock import MagicMock, patch
from django.test import override_settings
import pytest
from rest_framework.test import APIClient
from core import factories
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@pytest.fixture
def ai_settings():
"""Fixture to set AI settings."""
with override_settings(
AI_BASE_URL="http://example.com", AI_API_KEY="test-key", AI_MODEL="llama"
):
yield
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"reach, role",
[
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("authenticated", "editor"),
("public", "reader"),
],
)
def test_api_documents_ai_transform_anonymous_forbidden(reach, role):
"""
Anonymous users should not be able to request AI transform if the link reach
and role don't allow it.
"""
document = factories.DocumentFactory(link_reach=reach, link_role=role)
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
response = APIClient().post(url, {"text": "hello", "action": "prompt"})
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
@override_settings(AI_ALLOW_REACH_FROM="public")
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_anonymous_success(mock_create):
"""
Anonymous users should be able to request AI transform to a document
if the link reach and role permit it.
"""
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content="Salut"))]
)
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
response = APIClient().post(url, {"text": "Hello", "action": "summarize"})
assert response.status_code == 200
assert response.json() == {"answer": "Salut"}
mock_create.assert_called_once_with(
model="llama",
messages=[
{
"role": "system",
"content": (
"Summarize the markdown text, preserving language and markdown formatting. "
"Do not provide any other information. Preserve the language."
),
},
{"role": "user", "content": "Hello"},
],
)
@override_settings(AI_ALLOW_REACH_FROM=random.choice(["authenticated", "restricted"]))
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_anonymous_limited_by_setting(mock_create):
"""
Anonymous users should be able to request AI transform to a document
if the link reach and role permit it.
"""
document = factories.DocumentFactory(link_reach="public", link_role="editor")
answer = '{"answer": "Salut"}'
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content=answer))]
)
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
response = APIClient().post(url, {"text": "Hello", "action": "summarize"})
assert response.status_code == 401
@pytest.mark.parametrize(
"reach, role",
[
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("public", "reader"),
],
)
def test_api_documents_ai_transform_authenticated_forbidden(reach, role):
"""
Users who are not related to a document can't request AI transform if the
link reach and role don't allow it.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach, link_role=role)
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
response = client.post(url, {"text": "Hello", "action": "prompt"})
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
@pytest.mark.parametrize(
"reach, role",
[
("authenticated", "editor"),
("public", "editor"),
],
)
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_authenticated_success(mock_create, reach, role):
"""
Authenticated who are not related to a document should be able to request AI transform
if the link reach and role permit it.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach, link_role=role)
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content="Salut"))]
)
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
response = client.post(url, {"text": "Hello", "action": "prompt"})
assert response.status_code == 200
assert response.json() == {"answer": "Salut"}
mock_create.assert_called_once_with(
model="llama",
messages=[
{
"role": "system",
"content": (
"Answer the prompt using markdown formatting for structure and emphasis. "
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
"Preserve the language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
},
{"role": "user", "content": "Hello"},
],
)
@pytest.mark.parametrize("via", VIA)
def test_api_documents_ai_transform_reader(via, mock_user_teams):
"""
Users who are simple readers on a document should not be allowed to request AI transform.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_role="reader")
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="reader")
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="reader"
)
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
response = client.post(url, {"text": "Hello", "action": "prompt"})
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
@pytest.mark.parametrize("via", VIA)
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_success(mock_create, via, role, mock_user_teams):
"""
Editors, administrators and owners of a document should be able to request AI transform.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content="Salut"))]
)
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
response = client.post(url, {"text": "Hello", "action": "prompt"})
assert response.status_code == 200
assert response.json() == {"answer": "Salut"}
mock_create.assert_called_once_with(
model="llama",
messages=[
{
"role": "system",
"content": (
"Answer the prompt using markdown formatting for structure and emphasis. "
"Return the content directly without wrapping it in code blocks or markdown delimiters. "
"Preserve the language and markdown formatting. "
"Do not provide any other information. "
"Preserve the language."
),
},
{"role": "user", "content": "Hello"},
],
)
def test_api_documents_ai_transform_empty_text():
"""The text should not be empty when requesting AI transform."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
response = client.post(url, {"text": " ", "action": "prompt"})
assert response.status_code == 400
assert response.json() == {"text": ["This field may not be blank."]}
def test_api_documents_ai_transform_invalid_action():
"""The action should valid when requesting AI transform."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
response = client.post(url, {"text": "Hello", "action": "invalid"})
assert response.status_code == 400
assert response.json() == {"action": ['"invalid" is not a valid choice.']}
@override_settings(AI_DOCUMENT_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_throttling_document(mock_create):
"""
Throttling per document should be triggered on the AI transform endpoint.
For full throttle class test see: `test_api_utils_ai_document_rate_throttles`
"""
client = APIClient()
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content="Salut"))]
)
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
for _ in range(3):
user = factories.UserFactory()
client.force_login(user)
response = client.post(url, {"text": "Hello", "action": "summarize"})
assert response.status_code == 200
assert response.json() == {"answer": "Salut"}
user = factories.UserFactory()
client.force_login(user)
response = client.post(url, {"text": "Hello", "action": "summarize"})
assert response.status_code == 429
assert response.json() == {
"detail": "Request was throttled. Expected available in 60 seconds."
}
@override_settings(AI_USER_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_transform_throttling_user(mock_create):
"""
Throttling per user should be triggered on the AI transform endpoint.
For full throttle class test see: `test_api_utils_ai_user_rate_throttles`
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content="Salut"))]
)
for _ in range(3):
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
response = client.post(url, {"text": "Hello", "action": "summarize"})
assert response.status_code == 200
assert response.json() == {"answer": "Salut"}
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-transform/"
response = client.post(url, {"text": "Hello", "action": "summarize"})
assert response.status_code == 429
assert response.json() == {
"detail": "Request was throttled. Expected available in 60 seconds."
}

View File

@@ -0,0 +1,384 @@
"""
Test AI translate API endpoint for users in impress's core app.
"""
import random
from unittest.mock import MagicMock, patch
from django.test import override_settings
import pytest
from rest_framework.test import APIClient
from core import factories
from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
@pytest.fixture
def ai_settings():
"""Fixture to set AI settings."""
with override_settings(
AI_BASE_URL="http://example.com", AI_API_KEY="test-key", AI_MODEL="llama"
):
yield
def test_api_documents_ai_translate_viewset_options_metadata():
"""The documents endpoint should give us the list of available languages."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
factories.DocumentFactory(link_reach="public", link_role="editor")
response = APIClient().options("/api/v1.0/documents/")
assert response.status_code == 200
metadata = response.json()
assert metadata["name"] == "Document List"
assert metadata["actions"]["POST"]["language"]["choices"][0] == {
"value": "af",
"display_name": "Afrikaans",
}
@override_settings(
AI_ALLOW_REACH_FROM=random.choice(["public", "authenticated", "restricted"])
)
@pytest.mark.parametrize(
"reach, role",
[
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("authenticated", "editor"),
("public", "reader"),
],
)
def test_api_documents_ai_translate_anonymous_forbidden(reach, role):
"""
Anonymous users should not be able to request AI translate if the link reach
and role don't allow it.
"""
document = factories.DocumentFactory(link_reach=reach, link_role=role)
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
response = APIClient().post(url, {"text": "hello", "language": "es"})
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
@override_settings(AI_ALLOW_REACH_FROM="public")
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_anonymous_success(mock_create):
"""
Anonymous users should be able to request AI translate to a document
if the link reach and role permit it.
"""
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content="Ola"))]
)
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
response = APIClient().post(url, {"text": "Hello", "language": "es"})
assert response.status_code == 200
assert response.json() == {"answer": "Ola"}
mock_create.assert_called_once_with(
model="llama",
messages=[
{
"role": "system",
"content": (
"Keep the same html structure and formatting. "
"Translate the content in the html to the specified language Spanish. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information."
),
},
{"role": "user", "content": "Hello"},
],
)
@override_settings(AI_ALLOW_REACH_FROM=random.choice(["authenticated", "restricted"]))
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_anonymous_limited_by_setting(mock_create):
"""
Anonymous users should be able to request AI translate to a document
if the link reach and role permit it.
"""
document = factories.DocumentFactory(link_reach="public", link_role="editor")
answer = '{"answer": "Salut"}'
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content=answer))]
)
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
response = APIClient().post(url, {"text": "Hello", "language": "es"})
assert response.status_code == 401
@pytest.mark.parametrize(
"reach, role",
[
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("public", "reader"),
],
)
def test_api_documents_ai_translate_authenticated_forbidden(reach, role):
"""
Users who are not related to a document can't request AI translate if the
link reach and role don't allow it.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach, link_role=role)
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
response = client.post(url, {"text": "Hello", "language": "es"})
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
@pytest.mark.parametrize(
"reach, role",
[
("authenticated", "editor"),
("public", "editor"),
],
)
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_authenticated_success(mock_create, reach, role):
"""
Authenticated who are not related to a document should be able to request AI translate
if the link reach and role permit it.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach, link_role=role)
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content="Salut"))]
)
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
response = client.post(url, {"text": "Hello", "language": "es-co"})
assert response.status_code == 200
assert response.json() == {"answer": "Salut"}
mock_create.assert_called_once_with(
model="llama",
messages=[
{
"role": "system",
"content": (
"Keep the same html structure and formatting. "
"Translate the content in the html to the "
"specified language Colombian Spanish. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information."
),
},
{"role": "user", "content": "Hello"},
],
)
@pytest.mark.parametrize("via", VIA)
def test_api_documents_ai_translate_reader(via, mock_user_teams):
"""
Users who are simple readers on a document should not be allowed to request AI translate.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_role="reader")
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role="reader")
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role="reader"
)
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
response = client.post(url, {"text": "Hello", "language": "es"})
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
@pytest.mark.parametrize("via", VIA)
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_success(mock_create, via, role, mock_user_teams):
"""
Editors, administrators and owners of a document should be able to request AI translate.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
if via == USER:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document, team="lasuite", role=role
)
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content="Salut"))]
)
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
response = client.post(url, {"text": "Hello", "language": "es-co"})
assert response.status_code == 200
assert response.json() == {"answer": "Salut"}
mock_create.assert_called_once_with(
model="llama",
messages=[
{
"role": "system",
"content": (
"Keep the same html structure and formatting. "
"Translate the content in the html to the "
"specified language Colombian Spanish. "
"Check the translation for accuracy and make any necessary corrections. "
"Do not provide any other information."
),
},
{"role": "user", "content": "Hello"},
],
)
def test_api_documents_ai_translate_empty_text():
"""The text should not be empty when requesting AI translate."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
response = client.post(url, {"text": " ", "language": "es"})
assert response.status_code == 400
assert response.json() == {"text": ["This field may not be blank."]}
def test_api_documents_ai_translate_invalid_action():
"""The action should valid when requesting AI translate."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
response = client.post(url, {"text": "Hello", "language": "invalid"})
assert response.status_code == 400
assert response.json() == {"language": ['"invalid" is not a valid choice.']}
@override_settings(AI_DOCUMENT_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_throttling_document(mock_create):
"""
Throttling per document should be triggered on the AI translate endpoint.
For full throttle class test see: `test_api_utils_ai_document_rate_throttles`
"""
client = APIClient()
document = factories.DocumentFactory(link_reach="public", link_role="editor")
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content="Salut"))]
)
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
for _ in range(3):
user = factories.UserFactory()
client.force_login(user)
response = client.post(url, {"text": "Hello", "language": "es"})
assert response.status_code == 200
assert response.json() == {"answer": "Salut"}
user = factories.UserFactory()
client.force_login(user)
response = client.post(url, {"text": "Hello", "language": "es"})
assert response.status_code == 429
assert response.json() == {
"detail": "Request was throttled. Expected available in 60 seconds."
}
@override_settings(AI_USER_RATE_THROTTLE_RATES={"minute": 3, "hour": 6, "day": 10})
@pytest.mark.usefixtures("ai_settings")
@patch("openai.resources.chat.completions.Completions.create")
def test_api_documents_ai_translate_throttling_user(mock_create):
"""
Throttling per user should be triggered on the AI translate endpoint.
For full throttle class test see: `test_api_utils_ai_user_rate_throttles`
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content="Salut"))]
)
for _ in range(3):
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
response = client.post(url, {"text": "Hello", "language": "es"})
assert response.status_code == 200
assert response.json() == {"answer": "Salut"}
document = factories.DocumentFactory(link_reach="public", link_role="editor")
url = f"/api/v1.0/documents/{document.id!s}/ai-translate/"
response = client.post(url, {"text": "Hello", "language": "es"})
assert response.status_code == 429
assert response.json() == {
"detail": "Request was throttled. Expected available in 60 seconds."
}

View File

@@ -29,7 +29,8 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": document.link_role == "editor",
"can_edit": document.link_role == "editor",
"children_create": False,
@@ -106,7 +107,8 @@ def test_api_documents_retrieve_anonymous_public_parent():
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": grand_parent.link_role == "editor",
"can_edit": grand_parent.link_role == "editor",
"children_create": False,
@@ -213,7 +215,8 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": document.link_role == "editor",
"ai_transform": document.link_role == "editor",
"ai_translate": document.link_role == "editor",
"attachment_upload": document.link_role == "editor",
"can_edit": document.link_role == "editor",
"children_create": document.link_role == "editor",
@@ -297,7 +300,8 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": grand_parent.link_role == "editor",
"ai_transform": grand_parent.link_role == "editor",
"ai_translate": grand_parent.link_role == "editor",
"attachment_upload": grand_parent.link_role == "editor",
"can_edit": grand_parent.link_role == "editor",
"children_create": grand_parent.link_role == "editor",
@@ -494,7 +498,6 @@ def test_api_documents_retrieve_authenticated_related_parent():
"abilities": {
"accesses_manage": access.role in ["administrator", "owner"],
"accesses_view": True,
"ai_proxy": access.role not in ["reader", "commenter"],
"ai_transform": access.role not in ["reader", "commenter"],
"ai_translate": access.role not in ["reader", "commenter"],
"attachment_upload": access.role not in ["reader", "commenter"],

View File

@@ -72,7 +72,8 @@ def test_api_documents_trashbin_format():
"abilities": {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"can_edit": False,
"children_create": False,

View File

@@ -19,10 +19,7 @@ pytestmark = pytest.mark.django_db
@override_settings(
AI_BOT={"name": "Test Bot", "color": "#000000"},
AI_FEATURE_ENABLED=False,
AI_MODEL="test-model",
AI_STREAM=False,
COLLABORATION_WS_URL="http://testcollab/",
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY=True,
CRISP_WEBSITE_ID="123",
@@ -46,11 +43,7 @@ def test_api_config(is_authenticated):
response = client.get("/api/v1.0/config/")
assert response.status_code == HTTP_200_OK
assert response.json() == {
"AI_BOT": {"name": "Test Bot", "color": "#000000"},
"AI_FEATURE_ENABLED": False,
"AI_MODEL": "test-model",
"AI_FEATURE_ENABLED": False,
"AI_STREAM": False,
"COLLABORATION_WS_URL": "http://testcollab/",
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY": True,
"CONVERSION_FILE_EXTENSIONS_ALLOWED": [".docx", ".md"],
@@ -60,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"],

View File

@@ -155,7 +155,8 @@ def test_models_documents_get_abilities_forbidden(
expected_abilities = {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"can_edit": False,
"children_create": False,
@@ -219,7 +220,8 @@ def test_models_documents_get_abilities_reader(
expected_abilities = {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"can_edit": False,
"children_create": False,
@@ -355,7 +357,8 @@ def test_models_documents_get_abilities_editor(
expected_abilities = {
"accesses_manage": False,
"accesses_view": False,
"ai_proxy": is_authenticated,
"ai_transform": is_authenticated,
"ai_translate": is_authenticated,
"attachment_upload": True,
"can_edit": True,
"children_create": is_authenticated,
@@ -410,7 +413,8 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
expected_abilities = {
"accesses_manage": True,
"accesses_view": True,
"ai_proxy": True,
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
"can_edit": True,
"children_create": True,
@@ -497,7 +501,8 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
expected_abilities = {
"accesses_manage": True,
"accesses_view": True,
"ai_proxy": True,
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
"can_edit": True,
"children_create": True,
@@ -552,7 +557,8 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
expected_abilities = {
"accesses_manage": False,
"accesses_view": True,
"ai_proxy": True,
"ai_transform": True,
"ai_translate": True,
"attachment_upload": True,
"can_edit": True,
"children_create": True,
@@ -614,7 +620,8 @@ def test_models_documents_get_abilities_reader_user(
"accesses_view": True,
# If you get your editor rights from the link role and not your access role
# You should not access AI if it's restricted to users with specific access
"ai_proxy": access_from_link and ai_access_setting != "restricted",
"ai_transform": access_from_link and ai_access_setting != "restricted",
"ai_translate": access_from_link and ai_access_setting != "restricted",
"attachment_upload": access_from_link,
"can_edit": access_from_link,
"children_create": access_from_link,
@@ -740,7 +747,8 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
assert abilities == {
"accesses_manage": False,
"accesses_view": True,
"ai_proxy": False,
"ai_transform": False,
"ai_translate": False,
"attachment_upload": False,
"can_edit": False,
"children_create": False,
@@ -870,7 +878,8 @@ def test_models_document_get_abilities_ai_access_authenticated(is_authenticated,
document = factories.DocumentFactory(link_reach=reach, link_role="editor")
abilities = document.get_abilities(user)
assert abilities["ai_proxy"] is True
assert abilities["ai_transform"] is True
assert abilities["ai_translate"] is True
@override_settings(AI_ALLOW_REACH_FROM="authenticated")
@@ -888,7 +897,8 @@ def test_models_document_get_abilities_ai_access_public(is_authenticated, reach)
document = factories.DocumentFactory(link_reach=reach, link_role="editor")
abilities = document.get_abilities(user)
assert abilities["ai_proxy"] == is_authenticated
assert abilities["ai_transform"] == is_authenticated
assert abilities["ai_translate"] == is_authenticated
def test_models_documents_get_versions_slice_pagination(settings):

View File

@@ -2,9 +2,10 @@
Test ai API endpoints in the impress core app.
"""
from unittest.mock import patch
from unittest.mock import MagicMock, patch
from django.core.exceptions import ImproperlyConfigured
from django.test.utils import override_settings
import pytest
from openai import OpenAIError
@@ -14,15 +15,6 @@ from core.services.ai_services import AIService
pytestmark = pytest.mark.django_db
@pytest.fixture(autouse=True)
def ai_settings(settings):
"""Fixture to set AI settings."""
settings.AI_MODEL = "llama"
settings.AI_BASE_URL = "http://example.com"
settings.AI_API_KEY = "test-key"
settings.AI_FEATURE_ENABLED = True
@pytest.mark.parametrize(
"setting_name, setting_value",
[
@@ -31,105 +23,62 @@ def ai_settings(settings):
("AI_MODEL", None),
],
)
def test_services_ai_setting_missing(setting_name, setting_value, settings):
def test_api_ai_setting_missing(setting_name, setting_value):
"""Setting should be set"""
setattr(settings, setting_name, setting_value)
with pytest.raises(
ImproperlyConfigured,
match="AI configuration not set",
):
AIService()
with override_settings(**{setting_name: setting_value}):
with pytest.raises(
ImproperlyConfigured,
match="AI configuration not set",
):
AIService()
@override_settings(
AI_BASE_URL="http://example.com", AI_API_KEY="test-key", AI_MODEL="test-model"
)
@patch("openai.resources.chat.completions.Completions.create")
def test_services_ai_proxy_client_error(mock_create):
def test_api_ai__client_error(mock_create):
"""Fail when the client raises an error"""
mock_create.side_effect = OpenAIError("Mocked client error")
with pytest.raises(
RuntimeError,
match="Failed to proxy AI request: Mocked client error",
OpenAIError,
match="Mocked client error",
):
AIService().proxy({"messages": [{"role": "user", "content": "hello"}]})
AIService().transform("hello", "prompt")
@override_settings(
AI_BASE_URL="http://example.com", AI_API_KEY="test-key", AI_MODEL="test-model"
)
@patch("openai.resources.chat.completions.Completions.create")
def test_services_ai_proxy_success(mock_create):
def test_api_ai__client_invalid_response(mock_create):
"""Fail when the client response is invalid"""
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content=None))]
)
with pytest.raises(
RuntimeError,
match="AI response does not contain an answer",
):
AIService().transform("hello", "prompt")
@override_settings(
AI_BASE_URL="http://example.com", AI_API_KEY="test-key", AI_MODEL="test-model"
)
@patch("openai.resources.chat.completions.Completions.create")
def test_api_ai__success(mock_create):
"""The AI request should work as expect when called with valid arguments."""
mock_create.return_value = {
"id": "chatcmpl-test",
"object": "chat.completion",
"created": 1234567890,
"model": "test-model",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Salut"},
"finish_reason": "stop",
}
],
}
response = AIService().proxy({"messages": [{"role": "user", "content": "hello"}]})
expected_response = {
"id": "chatcmpl-test",
"object": "chat.completion",
"created": 1234567890,
"model": "test-model",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Salut"},
"finish_reason": "stop",
}
],
}
assert response == expected_response
mock_create.assert_called_once_with(
messages=[{"role": "user", "content": "hello"}], stream=False
mock_create.return_value = MagicMock(
choices=[MagicMock(message=MagicMock(content="Salut"))]
)
response = AIService().transform("hello", "prompt")
@patch("openai.resources.chat.completions.Completions.create")
def test_services_ai_proxy_with_stream(mock_create):
"""The AI request should work as expect when called with valid arguments."""
mock_create.return_value = {
"id": "chatcmpl-test",
"object": "chat.completion",
"created": 1234567890,
"model": "test-model",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Salut"},
"finish_reason": "stop",
}
],
}
response = AIService().proxy(
{"messages": [{"role": "user", "content": "hello"}]}, stream=True
)
expected_response = {
"id": "chatcmpl-test",
"object": "chat.completion",
"created": 1234567890,
"model": "test-model",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Salut"},
"finish_reason": "stop",
}
],
}
assert response == expected_response
mock_create.assert_called_once_with(
messages=[{"role": "user", "content": "hello"}], stream=True
)
assert response == {"answer": "Salut"}

View File

@@ -507,7 +507,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 +551,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",
@@ -670,35 +682,24 @@ class Base(Configuration):
default=True, environ_name="ALLOW_LOGOUT_GET_METHOD", environ_prefix=None
)
# AI settings
# AI service
AI_FEATURE_ENABLED = values.BooleanValue(
default=False, environ_name="AI_FEATURE_ENABLED", environ_prefix=None
)
AI_API_KEY = SecretFileValue(None, environ_name="AI_API_KEY", environ_prefix=None)
AI_BASE_URL = values.Value(None, environ_name="AI_BASE_URL", environ_prefix=None)
AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None)
AI_ALLOW_REACH_FROM = values.Value(
choices=("public", "authenticated", "restricted"),
default="authenticated",
environ_name="AI_ALLOW_REACH_FROM",
environ_prefix=None,
)
AI_API_KEY = SecretFileValue(None, environ_name="AI_API_KEY", environ_prefix=None)
AI_BASE_URL = values.Value(None, environ_name="AI_BASE_URL", environ_prefix=None)
AI_BOT = values.DictValue(
default={
"name": _("Docs AI"),
"color": "#8bc6ff",
},
environ_name="AI_BOT",
environ_prefix=None,
)
AI_DOCUMENT_RATE_THROTTLE_RATES = {
"minute": 5,
"hour": 100,
"day": 500,
}
AI_FEATURE_ENABLED = values.BooleanValue(
default=False, environ_name="AI_FEATURE_ENABLED", environ_prefix=None
)
AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None)
AI_STREAM = values.BooleanValue(
default=False, environ_name="AI_STREAM", environ_prefix=None
)
AI_USER_RATE_THROTTLE_RATES = {
"minute": 3,
"hour": 50,
@@ -1082,6 +1083,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"
@@ -1089,11 +1091,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,
),
@@ -1107,10 +1110,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",
},
},
}

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 "

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 "

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 "

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 "

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 "

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 "

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 "

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 "

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 "

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 "

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 "

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 "

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 "

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 提供 "

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

View File

@@ -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ÐC<EFBFBD>C"ÛMm€Ö€ûû<C3BB>áCÒj¹»*°©³®-SšGÃá¼H<C2BC>Ptm€nÁ€ Q)°¢ßv mù¿tT¤(±`'€tûꇻÇ?ú»_ß½ýC§ÅCÿµ{õîÄï4êý8²!Û›×Y× ¸¹ï>¼V„¤†Ôti†š¥æ®Dp2QN &Ä+<2B>ZF´>@Bø+zs<7A>Ö|„+¡>Š›Ÿ»oH¼*€µÒ è‰ö>É^¯…¢¬— U<>
+r¡*©ŒíÑš¨ :åƒJ.qŽpîlÕÍI<C38D>BVâ&Jô^“TæX¬:jÄP}*ÄŸ›ª略`iINë6T©ZfçI?þÛ<E280BA>Ϋ¥¾­%íØ<C3AD>¢÷k´dTËäBKMZ:_5Âz¨ œ * JSýënÏ6ƒÛù#É£¢ðÎxeC@6Ö8ýÔºÓ
Ü“ìßxu ùδ@¤ÛìøËUªŪõªÔ·íET·bÑü÷Pmsz¬h¡]\3¹Ø ÓôòE±íèÅ(†ü˜n=#ÀäzÎï=<3D>L]é1ÝúÄqH÷ÚúîK÷ø:—E ËÖž”8<E2809D>Ô1ÑQ<C391>ÍÂõ<E28099>>Q±¬dï £i†üxìµáÎð€óÄd(`ÔuX—‡1»`H&~<03><1D>HàÐñˆfúq£» 8UqÌfÈ ³.ê<C383>^ÁÀã4·<34>ô”~ðm¥Èì˜ç‘f`&ø’»e
PæÛsõ_Ÿ9æPpKíŽK­fx]žó=w t©«ÖQzg]¡Äê™ é2ÁK¤n߬~ƒ¨ž<C2A8>×ÊV“rbc#¿ÛYZK¾ ™€oC§<43>°V¶+ËØ€•€9Œ¶ùâ„GœpÄoçøõŸÉ¶ý4J§U°ÍÜÖòSƒÇóÛe§otãäH8ÌÂ)aRá½åøf:ŒÐòÙwz¤Ù‡O¦Î¼‡Z>ç4jZ>køšË|ˆrÍlfK÷öšk`Þ<C39E>Û1
™+0×oéœø»»î>|$Ö·EñSñÈ{˜a?2í S;—± +¶Õ¥26-Wkšž³Rƒ1vM]¬áDéxÙ®¥Í3Êð9UsÂÑš@—2­4<C2AD>”Ë3à %¸ F%<12>W<EFBFBD>q\ xtB{—<>ÇÎ(Ì I¹:Ñ9ÚNmlrPGÓÜXpÂ¥l«˜µC"¢È@P#Î…Lg,³Nc mî?£9§· ¿µÉÒ0P$L8šéÊLЫ<;ZôŠÓôÎD§S1êæÒîêó3ê<00>["ÄT͉} :¦÷JcCäÓ­UiW»g<C2BB>vuX±g~ÚI0¾]CI¥£:}¬ÇG43Â'ž%3ðÙ ”Z1^ °´óÇòÓJ[?˾¸8h6 Ó1jÕ<6A>Ë+P$p&˜f$ر·Cø?·žv¹@ x»Öxôњ䲃ª±'rÆwáío%NZÚ“ê“_<E2809C>È`æ„O<—>;…šW ῌža¤_iÿÇ<C3BF>â_ är ¤ #JHeíZ9õ9ã²C¤<43>N<EFBFBD>ˤ¶m¸$Î1Šr:²ÄœúÝÏGX½Ç$£N#xóZ‡Whùì«àCvu³[u¿÷½=8ôƪ¨4E°üQsŽWTí)À9½ß9àÙ )³- ý/<2F>%Ùæž ӟφ̡= 9lTk=Æzû¬=&†Ö)U±\uíxOÁ ÜÌŽªöyÆ­Á¤#£Êb÷<>%<25>ZûÀt[n<>SŸSi{¶¢6>†ÅÃǰ“<ÛCïPÉgõñž¯Tð©Ÿ
xœíËnÜ6ð®¯àá›@àCÒ6@Rè!È!í¦€6@kÀýýÎ II+q³*°©µeJ³Ãáh8œ)
¯à-Z<>1)NôûîÏr€0ŽþKGE*¡¥.Ø  }ƾúáæþ<C3A6>þæ×woD׸ë¿v¯ÞÝ<C39E>øýG6dxs5ã:ëºBÎ?iZ\Ýv^+DbÓØLi›Ãæ/Dô2<C3B4>B™´ .ðÍQ:g} Œ‰B}W?w?^¡xUç¤Õ&ꈿP´÷,{½ŠrA6T¦¡+] UIe ˜ <CB9C>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øï¼<<16>gZ!Tnöüå2ÕŽ‰bÓUêëöªMÚÛ°j $[€y´<79>Õ5g‰A ×Û§ sÔhXîäó…±ïðíZ ù1ÝzB`,Ês~ïq$wñcºõÌqà{m}÷¥ûM|<7C>KA"`À%ãgMN vLtq³°hI¨T­HV´}Áâ4äÇ}G¨u2@
. “¡€ÉÔa|Fì¢E™èÜw4Á¡£ fúq¡˜]¼ª8b3d<33>X—aUHfG¢ð+¸Ÿæ6 žøÝöYŠÌŽÁ<&˜<>™à ;yË 6êêË>S.aáÛ Õ29C6ðº8?Ùï-uh鹫
Iï|¡Ô9î5@Óob<6F>»C³ˆh9æ€ÓâI9<49>ÅÔ°# ¢M)/v.Ñ»½Ã%¥Û<C2A5> è6tëL$ór„<72>º‡ÑD_|ñ¾8â÷süŒúÅ]ÑdÛ7*ºfžk¹«Ã<C2AB>„M)ÆgœÏ#Ö,sDuQ Ë…ùž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<CB86>µ¸d~ÖPŽæ·hÇ5½Y›¥! HÈ8šèÊLtPyv¸ö<67>Lg¸°Òñæ³6ìêlí‰Ü}¤xÆ÷Jëb¢“°MéØ„çŸŽ­Z¦ããqâaç
@øv<C3B8>%•Iêôq <20>ìÌx>˜ä dw€XêÉt!ÀI0>@ÌOíþ,[L á¨õ´,Hoɦ<C389>ÂôQ  ÞFÛ ÿç&Ô®ÅpÄà¶Z<C2B6>ÝR«<ñ8ë·äGá%í<>.^:ÜÈš“Ÿ¯Ðxæ„<—>;ˆš—ñ¿ §q¤ßè éÉäã;9cÌLZ—¾íÄÁóßÃ:³Ì enû7hR£þuåde‰9õ»Ÿ<C2BB>pfÅ$£N#h³[‡Whùì«àCus\—`õ-?z¬S)bùªé[íV-¬Z)À}”½=8: /³-#Ä•-i‰vº2% úó™’=¶g´Çmk³÷`”}îÞãù;ö±ƒV,x©@;𤂸-˜<03>B<EFBFBD>­yÇmÁðqSe1k`I§à˜sËÀý·©Ï©´ÕÁ¯¨<C2AF>Ž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<C389>£<EFBFBD>sªïÿQ,ÿð¹/åÿðÿQûPþ#à?þ#à?þ£Ûò½\˜®æo]š/hmpP#ÅÄ*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©°<02>Ð)<01>Ð)­v€N 蔀N 蔀Né*:¥Ö/Üjìj;æ"<22>[ ¸•.À­´Çßš„·R¬À­Ô…Þ-§n¥Ã±n%àVn%àVêÀè[æVjú¬>šT YŠ$ Yš¥Y4K@³t¿[4K@³4K@³´<C2B3>
Ð,ÍÒU4KÑïçd¿(«ë<C2AB>QD“´«9î%ª{ñÚ_¿Çä!¼€ÆdguˆÑQÌÕ>~ÿ(5Amëy¸¾Õ/fý cÛÚ˜%ˆ}Ø>*wµºýa\úù¹|ÿÏÝðèí^»CÐË噣ÔÈHkOÊ­'] ¹H>(ÞaG°¥ËøÜ­CræÐ ú <®ð7‡bªÊj1å«Í†{|ž)9ðçd¯¥tð{¼äGíHõÀåÞ¬”¹¥I<C2A5>£ä Ç©¹<C2A9>,ÊÛrª¶º»R÷`.üÊ@Ábƒq&³S>
ÁB7Òÿ'…ihÁŒ1õÑ=ì<>™ûÀh(Üö<ÉÐø®—FD)òWëŸÏ¾“E
êF
KSóŒƒ><3E>Š&(pò¾¡ Y3”ÙƒÒÉj ºÛsôû áNÃéÝå ʲ»ËžßF¬Þ5.Fm½ÂüzÑ=p!¬'. ñT±¼ÑŽåðq¾‘ð£†ÞµpëÁwžàïJž^^”3zW´Ú7ç”~µü}¬„þxGØ{éÈø^:ÂßKGÄ{éˆ|¬Ž¼Ãê<C383>|Õú¯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Â<41>†Ó»Ë#$”ew—=¿<>æêÈûÅÆÈÆ<C388>˜Â‡0¿btëŠ l<e,o4d9|œïH$üǨ¡y-Ü‚ð¨'<27>æ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µ<74>ô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

View File

@@ -93,7 +93,9 @@ test.describe('Config', () => {
expect(
await page.locator('button[data-test="convertMarkdown"]').count(),
).toBe(1);
await expect(page.getByRole('button', { name: 'Ask AI' })).toBeHidden();
expect(await page.locator('button[data-test="ai-actions"]').count()).toBe(
0,
);
});
test('it checks that Crisp is trying to init from config endpoint', async ({

View File

@@ -1,3 +1,4 @@
/* eslint-disable playwright/no-conditional-expect */
import path from 'path';
import { expect, test } from '@playwright/test';
@@ -388,72 +389,13 @@ test.describe('Doc Editor', () => {
await expect(image).toHaveAttribute('aria-hidden', 'true');
});
test('it checks the AI feature', async ({ page, browserName }) => {
await overrideConfig(page, {
AI_BOT: {
name: 'Albert AI',
color: '#8bc6ff',
},
});
await page.goto('/');
await page.route(/.*\/ai-proxy\//, async (route) => {
test('it checks the AI buttons', async ({ page, browserName }) => {
await page.route(/.*\/ai-translate\//, async (route) => {
const request = route.request();
if (request.method().includes('POST')) {
await route.fulfill({
json: {
id: 'chatcmpl-b1e7a9e456ca41f78fec130d552a6bf5',
choices: [
{
finish_reason: 'stop',
index: 0,
logprobs: null,
message: {
content: '',
refusal: null,
role: 'assistant',
annotations: null,
audio: null,
function_call: null,
tool_calls: [
{
id: 'chatcmpl-tool-2e3567dfecf94a4c85e27a3528337718',
function: {
arguments:
'{"operations": [{"type": "update", "id": "initialBlockId$", "block": "<p>Bonjour le monde</p>"}]}',
name: 'json',
},
type: 'function',
},
],
reasoning_content: null,
},
stop_reason: null,
},
],
created: 1749549477,
model: 'neuralmagic/Meta-Llama-3.1-70B-Instruct-FP8',
object: 'chat.completion',
service_tier: null,
system_fingerprint: null,
usage: {
completion_tokens: 0,
prompt_tokens: 204,
total_tokens: 204,
completion_tokens_details: null,
prompt_tokens_details: null,
details: [
{
id: 'chatcmpl-b1e7a9e456ca41f78fec130d552a6bf5',
model: 'neuralmagic/Meta-Llama-3.1-70B-Instruct-FP8',
prompt_tokens: 204,
completion_tokens: 0,
total_tokens: 204,
},
],
},
prompt_logprobs: null,
answer: 'Bonjour le monde',
},
});
} else {
@@ -466,84 +408,118 @@ test.describe('Doc Editor', () => {
await page.locator('.bn-block-outer').last().fill('Hello World');
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello World').selectText();
await editor.getByText('Hello').selectText();
// Check from toolbar
await page.getByRole('button', { name: 'Ask AI' }).click();
await page.getByRole('button', { name: 'AI' }).click();
await expect(
page.getByRole('option', { name: 'Improve Writing' }),
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
await expect(
page.getByRole('option', { name: 'Fix Spelling' }),
page.getByRole('menuitem', { name: 'Rephrase' }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Summarize' }),
).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Correct' })).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
await expect(page.getByRole('option', { name: 'Translate' })).toBeVisible();
await page.getByRole('option', { name: 'Translate' }).click();
await page.getByPlaceholder('Ask AI anything…').fill('French');
await page.getByPlaceholder('Ask AI anything…').press('Enter');
await expect(editor.getByText('Albert AI')).toBeVisible();
await page
.locator('p.bn-mt-suggestion-menu-item-title')
.getByText('Accept')
.click();
await page.getByRole('menuitem', { name: 'Language' }).hover();
await expect(
page.getByRole('menuitem', { name: 'English', exact: true }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'French', exact: true }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'German', exact: true }),
).toBeVisible();
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
await page.getByRole('menuitem', { name: 'English', exact: true }).click();
// Check Suggestion menu
await page.locator('.bn-block-outer').last().fill('/');
await expect(page.getByText('Write with AI')).toBeVisible();
// Reload the page to check that the AI change is still there
await page.goto(page.url());
await expect(editor.getByText('Bonjour le monde')).toBeVisible();
});
test(`it checks ai_proxy ability`, async ({ page, browserName }) => {
await mockedDocument(page, {
accesses: [
{
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
role: 'owner',
user: {
email: 'super@owner.com',
full_name: 'Super Owner',
},
},
],
abilities: {
destroy: true, // Means owner
link_configuration: true,
ai_proxy: false,
accesses_manage: true,
accesses_view: true,
update: true,
partial_update: true,
retrieve: true,
},
link_reach: 'restricted',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
title: '',
});
const [randomDoc] = await createDoc(
[
{ ai_transform: false, ai_translate: false },
{ ai_transform: true, ai_translate: false },
{ ai_transform: false, ai_translate: true },
].forEach(({ ai_transform, ai_translate }) => {
test(`it checks AI buttons when can transform is at "${ai_transform}" and can translate is at "${ai_translate}"`, async ({
page,
'doc-editor-ai-proxy',
browserName,
1,
);
}) => {
await mockedDocument(page, {
accesses: [
{
id: 'b0df4343-c8bd-4c20-9ff6-fbf94fc94egg',
role: 'owner',
user: {
email: 'super@owner.com',
full_name: 'Super Owner',
},
},
],
abilities: {
destroy: true, // Means owner
link_configuration: true,
ai_transform,
ai_translate,
accesses_manage: true,
accesses_view: true,
update: true,
partial_update: true,
retrieve: true,
},
link_reach: 'restricted',
link_role: 'editor',
created_at: '2021-09-01T09:00:00Z',
title: '',
});
await verifyDocName(page, randomDoc);
const [randomDoc] = await createDoc(
page,
'doc-editor-ai',
browserName,
1,
);
await page.locator('.bn-block-outer').last().fill('Hello World');
await verifyDocName(page, randomDoc);
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
await page.locator('.bn-block-outer').last().fill('Hello World');
await expect(page.getByRole('button', { name: 'Ask AI' })).toBeHidden();
await page.locator('.bn-block-outer').last().fill('/');
await expect(page.getByText('Write with AI')).toBeHidden();
const editor = page.locator('.ProseMirror');
await editor.getByText('Hello').selectText();
if (!ai_transform && !ai_translate) {
await expect(page.getByRole('button', { name: 'AI' })).toBeHidden();
return;
}
await page.getByRole('button', { name: 'AI' }).click();
if (ai_transform) {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeHidden();
}
if (ai_translate) {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
} else {
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeHidden();
}
});
});
test('it downloads unsafe files', async ({ page, browserName }) => {

View File

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

View File

@@ -121,7 +121,7 @@ test.describe('Language', () => {
LANGUAGE_CODE: 'en-us',
});
await createDoc(page, 'doc-translations-slash', browserName, 1);
await createDoc(page, 'doc-toolbar', browserName, 1);
const { editor, suggestionMenu } = await openSuggestionMenu({ page });
await expect(

View File

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

View File

@@ -4,13 +4,7 @@ export type BrowserName = 'chromium' | 'firefox' | 'webkit';
export const BROWSERS: BrowserName[] = ['chromium', 'webkit', 'firefox'];
export const CONFIG = {
AI_BOT: {
name: 'Docs AI',
color: '#8bc6ff',
},
AI_FEATURE_ENABLED: true,
AI_MODEL: 'llama',
AI_STREAM: false,
CRISP_WEBSITE_ID: null,
COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/',
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: true,
@@ -20,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: [

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",
@@ -18,7 +18,7 @@
"@playwright/test": "1.57.0",
"@types/node": "*",
"@types/pdf-parse": "1.1.5",
"eslint-plugin-docs": "*",
"eslint-plugin-docs": "workspace:^",
"typescript": "*"
},
"dependencies": {

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

View File

@@ -50,19 +50,11 @@ const nextConfig = {
{
from: path.resolve(
__dirname,
'../../node_modules/emoji-datasource-apple/img/apple/64',
'./node_modules/emoji-datasource-apple/img/apple/64',
),
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,
},
],
}),
);

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,66 +19,59 @@
},
"dependencies": {
"@ag-media/react-pdf-table": "2.0.3",
"@ai-sdk/groq": "^3.0.15",
"@ai-sdk/openai": "^3.0.19",
"@ai-sdk/openai-compatible": "2.0.18",
"@blocknote/code-block": "0.46.1",
"@blocknote/core": "0.46.1",
"@blocknote/mantine": "0.46.1",
"@blocknote/react": "0.46.1",
"@blocknote/xl-ai": "0.46.1",
"@blocknote/xl-docx-exporter": "0.46.1",
"@blocknote/xl-multi-column": "0.46.1",
"@blocknote/xl-odt-exporter": "0.46.1",
"@blocknote/xl-pdf-exporter": "0.46.1",
"@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": "*",
"ai": "6.0.49",
"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",
"yjs": "*",
"zod": "3.25.28",
"zustand": "5.0.9"
"zustand": "5.0.10"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
@@ -87,7 +80,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": "*",
@@ -96,19 +89,19 @@
"copy-webpack-plugin": "13.0.1",
"cross-env": "10.1.0",
"dotenv": "17.2.3",
"eslint-plugin-docs": "*",
"eslint-plugin-docs": "workspace:^",
"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"
},
"packageManager": "yarn@1.22.22"
"packageManager": "pnpm@10.28.2"
}

View File

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

View File

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

View File

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

View File

@@ -15,10 +15,7 @@ interface ThemeCustomization {
}
export interface ConfigResponse {
AI_BOT: { name: string; color: string };
AI_FEATURE_ENABLED?: boolean;
AI_MODEL?: string;
AI_STREAM: boolean;
COLLABORATION_WS_URL?: string;
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY?: boolean;
CONVERSION_FILE_EXTENSIONS_ALLOWED: string[];
@@ -28,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;

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,119 @@
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} />, {
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 }} />,
);
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,
}}
/>,
);
expect(TrackEventMock).toHaveBeenNthCalledWith(2, {
eventName: 'doc',
isPublic: false,
authenticated: true,
});
});
});

View File

@@ -1,2 +1,4 @@
export * from './checkDocMediaStatus';
export * from './useCreateDocUpload';
export * from './useDocAITransform';
export * from './useDocAITranslate';

View File

@@ -0,0 +1,48 @@
import { useMutation } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
export type AITransformActions =
| 'correct'
| 'prompt'
| 'rephrase'
| 'summarize'
| 'beautify'
| 'emojify';
export type DocAITransform = {
docId: string;
text: string;
action: AITransformActions;
};
export type DocAITransformResponse = {
answer: string;
};
export const docAITransform = async ({
docId,
...params
}: DocAITransform): Promise<DocAITransformResponse> => {
const response = await fetchAPI(`documents/${docId}/ai-transform/`, {
method: 'POST',
body: JSON.stringify({
...params,
}),
});
if (!response.ok) {
throw new APIError(
'Failed to request ai transform',
await errorCauses(response),
);
}
return response.json() as Promise<DocAITransformResponse>;
};
export function useDocAITransform() {
return useMutation<DocAITransformResponse, APIError, DocAITransform>({
mutationFn: docAITransform,
});
}

View File

@@ -0,0 +1,40 @@
import { useMutation } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
export type DocAITranslate = {
docId: string;
text: string;
language: string;
};
export type DocAITranslateResponse = {
answer: string;
};
export const docAITranslate = async ({
docId,
...params
}: DocAITranslate): Promise<DocAITranslateResponse> => {
const response = await fetchAPI(`documents/${docId}/ai-translate/`, {
method: 'POST',
body: JSON.stringify({
...params,
}),
});
if (!response.ok) {
throw new APIError(
'Failed to request ai translate',
await errorCauses(response),
);
}
return response.json() as Promise<DocAITranslateResponse>;
};
export function useDocAITranslate() {
return useMutation<DocAITranslateResponse, APIError, DocAITranslate>({
mutationFn: docAITranslate,
});
}

View File

@@ -1,6 +0,0 @@
<svg viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.89904 7.9473C6.0847 7.9473 6.19816 7.84673 6.23941 7.6456C6.31677 7.17114 6.39671 6.76887 6.47922 6.43881C6.56174 6.10359 6.66746 5.82768 6.79639 5.61108C6.92532 5.38932 7.09551 5.21139 7.30696 5.0773C7.5184 4.93806 7.78916 4.82718 8.11922 4.74466C8.44928 4.66215 8.85928 4.58737 9.34922 4.52033C9.56066 4.49454 9.66639 4.3785 9.66639 4.17221C9.66639 3.98139 9.56066 3.87051 9.34922 3.83957C8.8696 3.77768 8.46475 3.70548 8.13469 3.62297C7.80979 3.53529 7.53903 3.42441 7.32243 3.29033C7.11098 3.15624 6.93822 2.98089 6.80413 2.76429C6.6752 2.54253 6.5669 2.26662 6.47922 1.93656C6.39671 1.60649 6.31677 1.20165 6.23941 0.722032C6.19816 0.515743 6.0847 0.412598 5.89904 0.412598C5.70306 0.412598 5.58702 0.515743 5.55092 0.722032C5.47872 1.1965 5.40136 1.60134 5.31885 1.93656C5.23633 2.26662 5.12803 2.53995 4.99394 2.75655C4.86501 2.97316 4.69483 3.15108 4.48338 3.29033C4.27193 3.42441 4.00118 3.53272 3.67112 3.61523C3.34106 3.69775 2.93364 3.77253 2.44886 3.83957C2.23741 3.87051 2.13169 3.98139 2.13169 4.17221C2.13169 4.36819 2.23741 4.48422 2.44886 4.52033C3.04709 4.60284 3.52929 4.70083 3.89546 4.81429C4.26162 4.92774 4.54784 5.0902 4.75413 5.30164C4.96042 5.51309 5.1203 5.80705 5.23376 6.18353C5.35237 6.55485 5.45809 7.04221 5.55092 7.6456C5.59218 7.84673 5.70822 7.9473 5.89904 7.9473ZM2.53395 9.27786C2.6732 9.27786 2.75829 9.20824 2.78923 9.06899C2.83565 8.77503 2.87691 8.54296 2.91301 8.37277C2.95426 8.20774 3.01357 8.08138 3.09093 7.99371C3.17345 7.90604 3.30238 7.839 3.47772 7.79258C3.65307 7.74617 3.90061 7.69459 4.22036 7.63786C4.35961 7.61208 4.42923 7.52956 4.42923 7.39032C4.42923 7.25623 4.35961 7.17629 4.22036 7.15051C3.90061 7.09894 3.65307 7.05252 3.47772 7.01126C3.30753 6.96485 3.1786 6.90038 3.09093 6.81787C3.00841 6.73019 2.94911 6.60126 2.91301 6.43107C2.87691 6.26089 2.83565 6.02623 2.78923 5.72711C2.75313 5.57756 2.66804 5.50278 2.53395 5.50278C2.40502 5.50278 2.3225 5.57756 2.2864 5.72711C2.23483 6.02108 2.19099 6.25057 2.15489 6.4156C2.11879 6.58063 2.05949 6.70699 1.97697 6.79466C1.89961 6.87717 1.77326 6.94164 1.59791 6.98805C1.42773 7.03447 1.18276 7.08862 0.863011 7.15051C0.713451 7.17629 0.638672 7.25623 0.638672 7.39032C0.638672 7.52956 0.721187 7.61208 0.886218 7.63786C1.19565 7.68944 1.43546 7.73843 1.60565 7.78485C1.77584 7.8261 1.89961 7.89057 1.97697 7.97824C2.05949 8.06592 2.11879 8.19227 2.15489 8.3573C2.19099 8.52749 2.23483 8.75956 2.2864 9.05352C2.32766 9.20308 2.41018 9.27786 2.53395 9.27786Z"
fill="currentColor"
/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,6 +0,0 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M4.4 21L3 19.6L10.525 12.05L6 10.925L10.95 7.85L10.525 2L15 5.775L20.4 3.575L18.225 9L22 13.45L16.15 13.05L13.05 18L11.925 13.475L4.4 21ZM5 8L3 6L5 4L7 6L5 8ZM13.875 12.925L15.075 10.95L17.4 11.125L15.9 9.35L16.775 7.2L14.625 8.075L12.85 6.6L13.025 8.9L11.05 10.125L13.3 10.675L13.875 12.925ZM18 21L16 19L18 17L20 19L18 21Z"
fill="#303030"
/>
</svg>

Before

Width:  |  Height:  |  Size: 444 B

View File

@@ -1,190 +0,0 @@
import { FormattingToolbarExtension } from '@blocknote/core/extensions';
import {
useBlockNoteEditor,
useComponentsContext,
useExtension,
} from '@blocknote/react';
import {
AIExtension,
AIMenu as AIMenuDefault,
getDefaultAIMenuItems,
} from '@blocknote/xl-ai';
import '@blocknote/xl-ai/style.css';
import { useTranslation } from 'react-i18next';
import { createGlobalStyle, css } from 'styled-components';
import { Box, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import IconAI from '../../assets/IconAI.svg';
import IconWandStar from '../../assets/wand_stars.svg';
import {
DocsBlockNoteEditor,
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema,
} from '../../types';
const AIMenuStyle = createGlobalStyle`
#ai-suggestion-menu .bn-suggestion-menu-item-small .bn-mt-suggestion-menu-item-section[data-position=left] svg {
height: 18px;
width: 18px;
}
`;
export function AIMenu() {
return (
<>
<AIMenuStyle />
<AIMenuDefault
items={(editor: DocsBlockNoteEditor, aiResponseStatus) => {
if (aiResponseStatus === 'user-input') {
let aiMenuItems = getDefaultAIMenuItems(editor, aiResponseStatus);
if (editor.getSelection()) {
aiMenuItems = aiMenuItems.filter(
(item) => ['simplify'].indexOf(item.key) === -1,
);
aiMenuItems = aiMenuItems.map((item) => {
if (item.key === 'improve_writing') {
return {
...item,
icon: <IconWandStar />,
};
} else if (item.key === 'translate') {
return {
...item,
icon: (
<Icon
iconName="translate"
$color="inherit"
$size="18px"
/>
),
};
}
return item;
});
} else {
aiMenuItems = aiMenuItems.filter(
(item) =>
['action_items', 'write_anything'].indexOf(item.key) === -1,
);
}
return aiMenuItems;
} else if (aiResponseStatus === 'user-reviewing') {
return getDefaultAIMenuItems(editor, aiResponseStatus).map(
(item) => {
if (item.key === 'accept') {
return {
...item,
icon: (
<Icon
iconName="check_circle"
$color="inherit"
$size="18px"
/>
),
};
}
return item;
},
);
}
return getDefaultAIMenuItems(editor, aiResponseStatus);
}}
/>
</>
);
}
export const AIToolbarButton = () => {
const { t } = useTranslation();
const Components = useComponentsContext();
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const editor = useBlockNoteEditor<
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema
>();
const ai = useExtension(AIExtension);
const formattingToolbar = useExtension(FormattingToolbarExtension);
if (!editor.isEditable || !Components) {
return null;
}
const onClick = () => {
const selection = editor.getSelection();
if (!selection) {
throw new Error('No selection');
}
const position = selection.blocks[selection.blocks.length - 1].id;
ai.openAIMenuAtBlock(position);
formattingToolbar.store.setState(false);
};
return (
<Box
$css={css`
& > button.mantine-Button-root {
padding-inline: ${spacingsTokens['2xs']};
transition: all 0.1s ease-in;
&:hover,
&:hover {
background-color: ${colorsTokens['gray-050']};
}
&:hover .--docs--icon-bg {
background-color: #5858e1;
border: 1px solid #8484f5;
color: #ffffff;
}
}
`}
$direction="row"
className="--docs--ai-toolbar-button"
>
<Components.Generic.Toolbar.Button
className="bn-button"
onClick={onClick}
>
<Box
$direction="row"
$align="center"
$gap={spacingsTokens['xs']}
$padding={{ right: '2xs' }}
>
<Text
className="--docs--icon-bg"
$theme="greyscale"
$variation="600"
$css={css`
border: 1px solid var(--c--theme--colors--greyscale-100);
transition: all 0.1s ease-in;
`}
$radius="100%"
$padding="0.15rem"
>
<IconAI width="16px" />
</Text>
{t('Ask AI')}
</Box>
</Components.Generic.Toolbar.Button>
<Box
$background={colorsTokens['gray-100']}
$width="1px"
$height="70%"
$margin={{ left: '2px' }}
$css={css`
align-self: center;
`}
/>
</Box>
);
};

View File

@@ -1,25 +0,0 @@
/**
* To import AI modules you must import from the index file.
* This is to ensure that the AI modules are only loaded when
* the application is not published as MIT.
*/
import * as XLAI from '@blocknote/xl-ai';
import * as localesAI from '@blocknote/xl-ai/locales';
import * as AIUI from './AIUI';
import * as useAI from './useAI';
let modulesAI = undefined;
if (process.env.NEXT_PUBLIC_PUBLISH_AS_MIT === 'false') {
modulesAI = {
...XLAI,
...AIUI,
localesAI: localesAI,
...useAI,
};
}
type ModulesAI = typeof XLAI &
typeof AIUI & { localesAI: typeof localesAI } & typeof useAI;
export default modulesAI as ModulesAI;

View File

@@ -1,43 +0,0 @@
import { createOpenAI } from '@ai-sdk/openai';
import { AIExtension, ClientSideTransport } from '@blocknote/xl-ai';
import { useMemo } from 'react';
import { baseApiUrl, fetchAPI } from '@/api';
import { useConfig } from '@/core';
import { Doc } from '@/docs/doc-management';
//import { usePromptAI } from './usePromptAI';
export const useAI = (docId: Doc['id'], aiAllowed: boolean) => {
const conf = useConfig().data;
//const promptBuilder = usePromptAI();
return useMemo(() => {
if (!aiAllowed || !conf?.AI_MODEL) {
return;
}
const aIprovider = createOpenAI({
apiKey: '',
baseURL: `${baseApiUrl('1.0')}documents/${docId}/ai-proxy/`,
fetch: (input, init) => {
// Create a new headers object without the Authorization header
const headers = new Headers(init?.headers);
headers.delete('Authorization');
return fetchAPI(`documents/${docId}/ai-proxy/`, {
...init,
headers,
});
},
});
const model = aIprovider.chat(conf.AI_MODEL);
const extension = AIExtension({
agentCursor: conf.AI_BOT,
transport: new ClientSideTransport({ model }),
});
return extension;
}, [aiAllowed, conf, docId]);
};

View File

@@ -20,7 +20,6 @@ import type { Awareness } from 'y-protocols/awareness';
import * as Y from 'yjs';
import { Box, TextErrors } from '@/components';
import { useConfig } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { Doc, useProviderStore } from '@/docs/doc-management';
import { avatarUrlFromName, useAuth } from '@/features/auth';
@@ -37,7 +36,6 @@ import { cssEditor } from '../styles';
import { DocsBlockNoteEditor } from '../types';
import { randomColor } from '../utils';
import BlockNoteAI from './AI';
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
import { cssComments, useComments } from './comments/';
@@ -47,10 +45,6 @@ import {
PdfBlock,
UploadLoaderBlock,
} from './custom-blocks';
const AIMenu = BlockNoteAI?.AIMenu;
const AIMenuController = BlockNoteAI?.AIMenuController;
const useAI = BlockNoteAI?.useAI;
const localesAI = BlockNoteAI?.localesAI;
import {
InterlinkingLinkInlineContent,
InterlinkingSearchInlineContent,
@@ -89,6 +83,7 @@ interface BlockNoteEditorProps {
export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
const { user } = useAuth();
const { setEditor } = useEditorStore();
const { t } = useTranslation();
const { themeTokens } = useCunninghamTheme();
const { isSynced: isConnectedToCollabServer } = useProviderStore();
const refEditorContainer = useRef<HTMLDivElement>(null);
@@ -97,16 +92,13 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
const showComments = canSeeComment;
useSaveDoc(doc.id, provider.document, isConnectedToCollabServer);
const { i18n, t } = useTranslation();
const { i18n } = useTranslation();
let lang = i18n.resolvedLanguage;
if (!lang || !(lang in locales)) {
lang = 'en';
}
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
const conf = useConfig().data;
const aiAllowed = !!(conf?.AI_FEATURE_ENABLED && doc.abilities?.ai_proxy);
const aiExtension = useAI?.(doc.id, aiAllowed);
const collabName = user?.full_name || user?.email;
const cursorName = collabName || t('Anonymous');
@@ -176,7 +168,6 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
...(multiColumnLocales && {
multi_column:
multiColumnLocales[lang as keyof typeof multiColumnLocales],
ai: localesAI?.[lang as keyof typeof localesAI],
}),
},
pasteHandler: ({ event, defaultPasteHandler }) => {
@@ -199,15 +190,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
return defaultPasteHandler();
},
extensions: [
CommentsExtension({ threadStore, resolveUsers }),
...(aiExtension ? [aiExtension] : []),
],
visualMedia: {
image: {
maxWidth: 760,
},
},
extensions: [CommentsExtension({ threadStore, resolveUsers })],
tables: {
splitCells: true,
cellBackgroundColor: true,
@@ -217,15 +200,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
uploadFile,
schema: blockNoteSchema,
},
[
aiExtension,
cursorName,
lang,
provider,
uploadFile,
threadStore,
resolveUsers,
],
[cursorName, lang, provider, uploadFile, threadStore, resolveUsers],
);
useHeadings(editor);
@@ -268,11 +243,8 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
comments={showComments}
aria-label={t('Document editor')}
>
{aiAllowed && AIMenuController && AIMenu && (
<AIMenuController aiMenu={AIMenu} />
)}
<BlockNoteSuggestionMenu aiAllowed={aiAllowed} />
<BlockNoteToolbar aiAllowed={aiAllowed} />
<BlockNoteSuggestionMenu />
<BlockNoteToolbar />
</BlockNoteView>
</Box>
);
@@ -339,7 +311,7 @@ export const BlockNoteReader = ({
slashMenu={false}
comments={false}
>
<BlockNoteToolbar aiAllowed={false} />
<BlockNoteToolbar />
</BlockNoteView>
</Box>
);

View File

@@ -17,7 +17,6 @@ import {
DocsStyleSchema,
} from '../types';
import BlockNoteAI from './AI';
import {
getCalloutReactSlashMenuItems,
getPdfReactSlashMenuItems,
@@ -28,13 +27,7 @@ import XLMultiColumn from './xl-multi-column';
const getMultiColumnSlashMenuItems =
XLMultiColumn?.getMultiColumnSlashMenuItems;
const getAISlashMenuItems = BlockNoteAI?.getAISlashMenuItems;
export const BlockNoteSuggestionMenu = ({
aiAllowed,
}: {
aiAllowed: boolean;
}) => {
export const BlockNoteSuggestionMenu = () => {
const editor = useBlockNoteEditor<
DocsBlockSchema,
DocsInlineContentSchema,
@@ -57,7 +50,6 @@ export const BlockNoteSuggestionMenu = ({
getMultiColumnSlashMenuItems?.(editor) || [],
getPdfReactSlashMenuItems(editor, t, fileBlocksName),
getCalloutReactSlashMenuItems(editor, t, basicBlocksName),
aiAllowed && getAISlashMenuItems ? getAISlashMenuItems(editor) : [],
);
const index = combinedMenu.findIndex(
@@ -74,14 +66,7 @@ export const BlockNoteSuggestionMenu = ({
return async (query: string) =>
Promise.resolve(filterSuggestionItems(newSlashMenuItems, query));
}, [
editor,
t,
fileBlocksName,
basicBlocksName,
aiAllowed,
getInterlinkingMenuItems,
]);
}, [basicBlocksName, editor, getInterlinkingMenuItems, t, fileBlocksName]);
return (
<SuggestionMenuController

View File

@@ -0,0 +1,371 @@
import { Block } from '@blocknote/core';
import {
ComponentProps,
useBlockNoteEditor,
useComponentsContext,
useSelectedBlocks,
} from '@blocknote/react';
import {
Loader,
VariantType,
useToastProvider,
} from '@gouvfr-lasuite/cunningham-react';
import { PropsWithChildren, ReactNode, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { isAPIError } from '@/api';
import { Box, Icon } from '@/components';
import { useDocOptions, useDocStore } from '@/docs/doc-management/';
import {
AITransformActions,
useDocAITransform,
useDocAITranslate,
} from '../../api';
type LanguageTranslate = {
value: string;
display_name: string;
};
const sortByPopularLanguages = (
languages: LanguageTranslate[],
popularLanguages: string[],
) => {
languages.sort((a, b) => {
const indexA = popularLanguages.indexOf(a.value);
const indexB = popularLanguages.indexOf(b.value);
// If both languages are in the popular list, sort based on their order in popularLanguages
if (indexA !== -1 && indexB !== -1) {
return indexA - indexB;
}
// If only a is in the popular list, it should come first
if (indexA !== -1) {
return -1;
}
// If only b is in the popular list, it should come first
if (indexB !== -1) {
return 1;
}
// If neither a nor b is in the popular list, maintain their relative order
return 0;
});
};
export function AIGroupButton() {
const editor = useBlockNoteEditor();
const Components = useComponentsContext();
const selectedBlocks = useSelectedBlocks(editor);
const { t } = useTranslation();
const { currentDoc } = useDocStore();
const { data: docOptions } = useDocOptions();
const languages = useMemo(() => {
const languages = docOptions?.actions.POST.language.choices;
if (!languages) {
return;
}
sortByPopularLanguages(languages, [
'fr',
'en',
'de',
'es',
'it',
'pt',
'nl',
'pl',
]);
return languages;
}, [docOptions?.actions.POST.language.choices]);
const show = useMemo(() => {
return !!selectedBlocks.find((block) => block.content !== undefined);
}, [selectedBlocks]);
if (!show || !editor.isEditable || !Components || !currentDoc || !languages) {
return null;
}
const canAITransform = currentDoc.abilities.ai_transform;
const canAITranslate = currentDoc.abilities.ai_translate;
if (!canAITransform && !canAITranslate) {
return null;
}
return (
<Components.Generic.Menu.Root>
<Components.Generic.Menu.Trigger>
<Components.FormattingToolbar.Button
className="bn-button bn-menu-item --docs--ai-actions-menu-trigger"
data-test="ai-actions"
label="AI"
mainTooltip={t('AI Actions')}
icon={<Icon iconName="auto_awesome" $size="md" />}
/>
</Components.Generic.Menu.Trigger>
<Components.Generic.Menu.Dropdown
className="bn-menu-dropdown bn-drag-handle-menu --docs--ai-actions-menu"
sub={true}
>
{canAITransform && (
<>
<AIMenuItemTransform
action="prompt"
docId={currentDoc.id}
icon={<Icon iconName="text_fields" $size="s" />}
>
{t('Use as prompt')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="rephrase"
docId={currentDoc.id}
icon={<Icon iconName="refresh" $size="s" />}
>
{t('Rephrase')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="summarize"
docId={currentDoc.id}
icon={<Icon iconName="summarize" $size="s" />}
>
{t('Summarize')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="correct"
docId={currentDoc.id}
icon={<Icon iconName="check" $size="s" />}
>
{t('Correct')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="beautify"
docId={currentDoc.id}
icon={<Icon iconName="draw" $size="s" />}
>
{t('Beautify')}
</AIMenuItemTransform>
<AIMenuItemTransform
action="emojify"
docId={currentDoc.id}
icon={<Icon iconName="emoji_emotions" $size="s" />}
>
{t('Emojify')}
</AIMenuItemTransform>
</>
)}
{canAITranslate && (
<Components.Generic.Menu.Root position="right" sub={true}>
<Components.Generic.Menu.Trigger sub={false}>
<Components.Generic.Menu.Item
className="bn-menu-item --docs--ai-translate-menu-trigger"
subTrigger={true}
>
<Box $direction="row" $gap="0.6rem">
<Icon iconName="translate" $size="s" />
{t('Language')}
</Box>
</Components.Generic.Menu.Item>
</Components.Generic.Menu.Trigger>
<Components.Generic.Menu.Dropdown
sub={true}
className="bn-menu-dropdown --docs--ai-translate-menu"
>
{languages.map((language) => (
<AIMenuItemTranslate
key={language.value}
language={language.value}
docId={currentDoc.id}
>
{language.display_name}
</AIMenuItemTranslate>
))}
</Components.Generic.Menu.Dropdown>
</Components.Generic.Menu.Root>
)}
</Components.Generic.Menu.Dropdown>
</Components.Generic.Menu.Root>
);
}
/**
* Item is derived from Mantime, some props seem lacking or incorrect.
*/
type ItemDefault = ComponentProps['Generic']['Menu']['Item'];
type ItemProps = Omit<ItemDefault, 'onClick'> & {
rightSection?: ReactNode;
closeMenuOnClick?: boolean;
onClick: (e: React.MouseEvent) => void;
};
interface AIMenuItemTransformProps {
action: AITransformActions;
docId: string;
icon?: ReactNode;
}
const AIMenuItemTransform = ({
docId,
action,
children,
icon,
}: PropsWithChildren<AIMenuItemTransformProps>) => {
const { mutateAsync: requestAI, isPending } = useDocAITransform();
const editor = useBlockNoteEditor();
const requestAIAction = async (selectedBlocks: Block[]) => {
const text = await editor.blocksToMarkdownLossy(selectedBlocks);
const responseAI = await requestAI({
text,
action,
docId,
});
if (!responseAI?.answer) {
throw new Error('No response from AI');
}
const markdown = await editor.tryParseMarkdownToBlocks(responseAI.answer);
editor.replaceBlocks(selectedBlocks, markdown);
};
return (
<AIMenuItem icon={icon} requestAI={requestAIAction} isPending={isPending}>
{children}
</AIMenuItem>
);
};
interface AIMenuItemTranslateProps {
language: string;
docId: string;
icon?: ReactNode;
}
const AIMenuItemTranslate = ({
children,
docId,
icon,
language,
}: PropsWithChildren<AIMenuItemTranslateProps>) => {
const { mutateAsync: requestAI, isPending } = useDocAITranslate();
const editor = useBlockNoteEditor();
const requestAITranslate = async (selectedBlocks: Block[]) => {
let fullHtml = '';
for (const block of selectedBlocks) {
if (Array.isArray(block.content) && block.content.length === 0) {
fullHtml += '<p><br/></p>';
continue;
}
fullHtml += await editor.blocksToHTMLLossy([block]);
}
const responseAI = await requestAI({
text: fullHtml,
language,
docId,
});
if (!responseAI || !responseAI.answer) {
throw new Error('No response from AI');
}
try {
const blocks = await editor.tryParseHTMLToBlocks(responseAI.answer);
editor.replaceBlocks(selectedBlocks, blocks);
} catch {
editor.replaceBlocks(selectedBlocks, selectedBlocks);
}
};
return (
<AIMenuItem
icon={icon}
requestAI={requestAITranslate}
isPending={isPending}
>
{children}
</AIMenuItem>
);
};
interface AIMenuItemProps {
requestAI: (blocks: Block[]) => Promise<void>;
isPending: boolean;
icon?: ReactNode;
}
const AIMenuItem = ({
requestAI,
isPending,
children,
icon,
}: PropsWithChildren<AIMenuItemProps>) => {
const Components = useComponentsContext();
const { toast } = useToastProvider();
const { t } = useTranslation();
const editor = useBlockNoteEditor();
const handleAIError = useHandleAIError();
const handleAIAction = async () => {
const selectedBlocks = editor.getSelection()?.blocks ?? [
editor.getTextCursorPosition().block,
];
if (!selectedBlocks?.length) {
toast(t('No text selected'), VariantType.WARNING);
return;
}
try {
await requestAI(selectedBlocks);
} catch (error) {
handleAIError(error);
}
};
if (!Components) {
return null;
}
const Item = Components.Generic.Menu.Item as React.FC<ItemProps>;
return (
<Item
closeMenuOnClick={false}
icon={icon}
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
void handleAIAction();
}}
rightSection={isPending ? <Loader size="small" /> : undefined}
>
{children}
</Item>
);
};
const useHandleAIError = () => {
const { toast } = useToastProvider();
const { t } = useTranslation();
return (error: unknown) => {
if (isAPIError(error) && error.status === 429) {
toast(t('Too many requests. Please wait 60 seconds.'), VariantType.ERROR);
return;
}
toast(t('AI seems busy! Please try again.'), VariantType.ERROR);
};
};

View File

@@ -8,21 +8,22 @@ import {
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import BlockNoteAI from '../AI';
import { useConfig } from '@/core/config/api';
import { CommentToolbarButton } from '../comments/CommentToolbarButton';
import { getCalloutFormattingToolbarItems } from '../custom-blocks';
import { AIGroupButton } from './AIButton';
import { FileDownloadButton } from './FileDownloadButton';
import { MarkdownButton } from './MarkdownButton';
import { ModalConfirmDownloadUnsafe } from './ModalConfirmDownloadUnsafe';
const AIToolbarButton = BlockNoteAI?.AIToolbarButton;
export const BlockNoteToolbar = ({ aiAllowed }: { aiAllowed: boolean }) => {
export const BlockNoteToolbar = () => {
const dict = useDictionary();
const [confirmOpen, setIsConfirmOpen] = useState(false);
const [onConfirm, setOnConfirm] = useState<() => void | Promise<void>>();
const { t } = useTranslation();
const { data: conf } = useConfig();
const toolbarItems = useMemo(() => {
let toolbarItems = getFormattingToolbarItems([
@@ -68,17 +69,18 @@ export const BlockNoteToolbar = ({ aiAllowed }: { aiAllowed: boolean }) => {
const formattingToolbar = useCallback(() => {
return (
<FormattingToolbar>
{aiAllowed && AIToolbarButton && <AIToolbarButton />}
<CommentToolbarButton />
{toolbarItems}
{/* Extra button to do some AI powered actions */}
{conf?.AI_FEATURE_ENABLED && <AIGroupButton key="AIButton" />}
{/* Extra button to convert from markdown to json */}
<MarkdownButton key="customButton" />
</FormattingToolbar>
);
}, [toolbarItems, aiAllowed]);
}, [toolbarItems, conf?.AI_FEATURE_ENABLED]);
return (
<>

View File

@@ -1,15 +1,19 @@
import clsx from 'clsx';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { Box, Loading } from '@/components';
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';
@@ -83,6 +87,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 +98,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 />;
}

View File

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

View File

@@ -35,26 +35,7 @@ export const useSaveDoc = (
_updatedDoc: Y.Doc,
transaction: Y.Transaction,
) => {
/**
* When the AI edit the doc transaction.local is false,
* so we check if the origin constructor to know where
* the transaction comes from.
* "PluginKey" constructor comes from the current user, but transaction.local is more reliable
* "HocuspocusProvider" constructor comes from other users from the collaboration server,
* it seems quite reliable too.
* The AI constructor name seems to not be reliable enough, but by deduction if it's not local
* and not from other users, it has to be from the AI.
*
* TODO: see if we can get the local changes from the AI
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const transactionOrigin = transaction?.origin?.constructor?.name;
const PROVIDER_ORIGIN_CONSTRUCTOR = 'HocuspocusProvider';
const isAIChange =
!transaction.local && transactionOrigin !== PROVIDER_ORIGIN_CONSTRUCTOR;
setIsLocalChange(transaction.local || isAIChange);
setIsLocalChange(transaction.local);
};
yDoc.on('update', onUpdate);

View File

@@ -1,11 +1,6 @@
import { css } from 'styled-components';
export const cssEditor = css`
.mantine-Menu-itemLabel,
.mantine-Button-label {
font-family: var(--c--components--button--font-family);
}
&,
& > .bn-container,
& .ProseMirror {
@@ -153,16 +148,6 @@ export const cssEditor = css`
font-style: italic;
}
/**
* AI
*/
ins,
[data-type='modification'] {
background: var(--c--globals--colors--primary-100);
border-bottom: 2px solid var(--c--globals--colors--primary-300);
color: var(--c--globals--colors--primary-700);
}
/**
* Divider
*/

View File

@@ -57,6 +57,7 @@ export const blockMappingHeadingPDF: DocsExporterPDF['mappings']['blockMapping']
style={{
fontSize: levelFontSizeEM * FONT_SIZE * PIXELS_PER_POINT,
fontWeight: 700,
lineHeight: 1.25,
marginTop: `${fontSizeEM * MERGE_RATIO}px`,
marginBottom: `${fontSizeEM * MERGE_RATIO}px`,
}}

View File

@@ -9,7 +9,7 @@ import {
useIsCollaborativeEditable,
useTrans,
} from '@/docs/doc-management';
import { useDate } from '@/hook';
import { useDate } from '@/hooks';
import { useResponsiveStore } from '@/stores';
interface DocHeaderInfoProps {

View File

@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useClipboard } from '@/hook';
import { useClipboard } from '@/hooks';
import { Doc } from '../types';

View File

@@ -74,7 +74,8 @@ export interface Doc {
abilities: {
accesses_manage: boolean;
accesses_view: boolean;
ai_proxy: boolean;
ai_transform: boolean;
ai_translate: boolean;
attachment_upload: boolean;
children_create: boolean;
children_list: boolean;

View File

@@ -12,7 +12,7 @@ import {
TextErrors,
} from '@/components';
import { Doc } from '@/docs/doc-management';
import { useDate } from '@/hook';
import { useDate } from '@/hooks';
import { useDocVersionsInfiniteQuery } from '../api/useDocVersions';
import { Versions } from '../types';

View File

@@ -9,7 +9,7 @@ import { useConfig } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { Doc, LinkReach, SimpleDocItem } from '@/docs/doc-management';
import { DocShareModal } from '@/docs/doc-share';
import { useDate } from '@/hook';
import { useDate } from '@/hooks';
import { useResponsiveStore } from '@/stores';
import { useResponsiveDocGrid } from '../hooks/useResponsiveDocGrid';

View File

@@ -7,7 +7,7 @@ import { Box, Icon, SeparatedSection } from '@/components';
import { useDocStore } from '@/docs/doc-management';
import { DocSearchModal } from '@/docs/doc-search/';
import { useAuth } from '@/features/auth';
import { useCmdK } from '@/hook/useCmdK';
import { useCmdK } from '@/hooks/useCmdK';
import { useLeftPanelStore } from '../stores';

View File

@@ -182,7 +182,8 @@ export class ApiPlugin implements WorkboxPlugin {
abilities: {
accesses_manage: true,
accesses_view: true,
ai_proxy: true,
ai_transform: true,
ai_translate: true,
attachment_upload: true,
children_create: true,
children_list: true,

View File

@@ -1,2 +0,0 @@
export * from './useDate';
export * from './useClipboard';

View File

@@ -1 +1,4 @@
export * from './useClipboard';
export * from './useCmdK';
export * from './useDate';
export * from './useKeyboardAction';

View File

@@ -82,7 +82,6 @@
"Editor": "Embanner",
"Editor unavailable": "Aozer dihegerz",
"Emojify": "Emojifiañ",
"Empty template": "Patrom ebet",
"Error during delete invitation": "Fazi en ur zilemel ar bedadenn",
"Error during update invitation": "Fazi e-pad hizivadur ar bedadenn",
"Error while deleting invitation": "Fazi e-pad ma oa o tilemel ur bedadenn",
@@ -201,7 +200,6 @@
"Start Writing": "Stagañ da skrivañ",
"Summarize": "Diverrañ",
"Summary": "Taolenn",
"Template": "Patrom",
"The antivirus has detected an anomaly in your file.": "An enepvirus en deus detektet un direizhder en ho restr.",
"The document has been deleted.": "Dilamet eo bet ar restr.",
"The document visibility has been updated.": "Hizivaet eo bet gweled ar restr.",
@@ -338,7 +336,6 @@
"Editor unavailable": "Editor nicht verfügbar",
"Embed a PDF file": "Eine PDF-Datei einbetten",
"Emojify": "Emojifizieren",
"Empty template": "Leere Vorlage",
"Error during delete invitation": "Fehler beim Löschen der Einladung",
"Error during update invitation": "Fehler beim Aktualisieren der Einladung",
"Error while deleting invitation": "Fehler beim Löschen der Einladung",
@@ -354,6 +351,7 @@
"Flexible export.": "Flexibler Export.",
"Format": "Format",
"Govs ❤️ Open Source.": "Regierungen ❤️ Open Source.",
"HTML": "HTML",
"History": "Versionsverlauf",
"Home": "Start",
"I understand": "Verstanden",
@@ -364,6 +362,10 @@
"Image: {{title}}": "Abbildung: {{title}}",
"Insufficient access rights to view the document.": "Unzureichende Zugriffsrechte zur Ansicht des Dokuments.",
"Invite": "Einladen",
"Invite {{count}} members_many": "{{count}} Mitglieder einladen",
"Invite {{count}} members_one": "{{count}} Mitglieder einladen",
"Invite {{count}} members_other": "{{count}} Mitglieder einladen",
"Invite {{name}}": "{{name}} einladen",
"It is the card information about the document.": "Es handelt sich um die Karteninformationen zum Dokument.",
"It is the document title": "Es ist der Titel des Dokuments",
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Es scheint, dass die von Ihnen gesuchte Seite nicht existiert oder nicht korrekt angezeigt werden kann.",
@@ -416,6 +418,7 @@
"Others are editing. Your network prevent changes.": "Ihre Änderung konnte nicht übernommen werden, da andere Benutzer diesen Bereich zurzeit bearbeiten.",
"Owner": "Besitzer",
"PDF": "PDF",
"PDF document": "PDF-Dokument",
"Page Not Found - Error 404": "Seite nicht gefunden - Fehler 404",
"Pending invitations": "Ausstehende Einladungen",
"People with access via the parent document": "Benutzer, die den Zugriff von einem übergeordneten Dokument erben",
@@ -465,7 +468,6 @@
"Start Writing": "Beginne das Schreiben",
"Summarize": "Zusammenfassen",
"Summary": "Zusammenfassung",
"Template": "Vorlage",
"The antivirus has detected an anomaly in your file.": "Das Antivirus hat eine Anomalie in Ihrer Datei festgestellt.",
"The document has been deleted.": "Das Dokument wurde gelöscht.",
"The document has been restored.": "Das Dokument wurde wiederhergestellt.",
@@ -582,7 +584,6 @@
"Editor": "Editor",
"Editor unavailable": "Editor no disponible",
"Emojify": "Emojizar",
"Empty template": "Plantilla vacía",
"Error during delete invitation": "Error al eliminar la invitación",
"Error during update invitation": "Error al actualizar la invitación",
"Export": "Exportar",
@@ -666,7 +667,6 @@
"Start Writing": "Comenzar a escribir",
"Summarize": "Resumir",
"Summary": "Resumen",
"Template": "Plantilla",
"The document has been deleted.": "El documento ha sido eliminado.",
"The document visibility has been updated.": "Se ha actualizado la visibilidad del documento.",
"The export failed": "Error al exportar",
@@ -799,7 +799,6 @@
"Editor unavailable": "Éditeur indisponible",
"Embed a PDF file": "Intégrer un fichier PDF",
"Emojify": "Emojifier",
"Empty template": "Sans modèle",
"Error during delete invitation": "Erreur lors de la suppression de l'invitation",
"Error during update invitation": "Erreur lors de la mise à jour de l'invitation",
"Error while deleting invitation": "Erreur lors de la suppression de l'invitation",
@@ -825,6 +824,7 @@
"Illustration": "Image",
"Image 403": "Image 403",
"Image: {{title}}": "Image : {{title}}",
"Import Docx or Markdown files": "Importer des fichiers Docx ou Markdown",
"Insufficient access rights to view the document.": "Droits d'accès insuffisants pour voir le document.",
"Invalid or missing PDF file.": "Fichier PDF non valide ou manquant.",
"Invite": "Inviter",
@@ -880,6 +880,7 @@
"Open the document options": "Ouvrir les options du document",
"Open the menu of actions for the document: {{title}}": "Ouvrir le menu des actions du document : {{title}}",
"Open the sharing settings for the document": "Ouvrir les paramètres de partage pour le document",
"Open the upload dialog": "Ouvrir le dialogue de téléversement",
"Organize": "Organiser",
"Others are editing this document. Unfortunately your network blocks WebSockets, the technology enabling real-time co-editing.": "D'autres sont en train de modifier ce document. Malheureusement, votre réseau bloque les web sockets, la technologie permettant la coédition en temps réel.",
"Others are editing. Your network prevent changes.": "D'autres sont en cours d'édition. Votre réseau empêche les changements.",
@@ -937,8 +938,11 @@
"Start Writing": "Commencer à écrire",
"Summarize": "Résumer",
"Summary": "Sommaire",
"Template": "Modèle",
"The antivirus has detected an anomaly in your file.": "L'antivirus a détecté une anomalie dans votre fichier.",
"The document \"{{documentName}}\" has been successfully imported": "Le document \"{{documentName}}\" a été importé avec succès",
"The document \"{{documentName}}\" import has failed": "L'importation du document \"{{documentName}}\" a échoué",
"The document \"{{documentName}}\" import has failed (only .docx and .md files are allowed)": "L'importation du document \"{{documentName}}\" a échoué (seuls les fichiers .docx et .md sont autorisés)",
"The document \"{{documentName}}\" is too large. Maximum file size is {{maxFileSize}}.": "Le document \"{{documentName}}\" est trop grand. La taille maximale du fichier est {{maxFileSize}}.",
"The document has been deleted.": "Le document a bien été supprimé.",
"The document has been restored.": "Le document a été restauré.",
"The document visibility has been updated.": "La visibilité du document a été mise à jour.",
@@ -967,16 +971,6 @@
"Warning": "Attention",
"Why you can't edit the document?": "Pourquoi vous ne pouvez pas modifier le document ?",
"Write": "Écrire",
"You are an AI assistant that helps users to edit their documents.": "Tu es un assistant IA qui aide les utilisateurs à éditer leurs documents.",
"Answer the user prompt in markdown format.": "Réponds à la demande de l'utilisateur au format markdown.",
"Add formatting to the text to make it more readable.": "Ajoute du formatage au texte pour le rendre plus lisible.",
"Keep adding to the document, do not delete or modify existing blocks.": "Continue d'ajouter au document, ne supprime ni ne modifie les blocs existants.",
"Your answer must be in the same language as the document.": "Ta réponse doit être dans la même langue que le document.",
"Fix the spelling and grammar mistakes in the selected text.": "Corrige les fautes d'orthographe et de grammaire dans le texte sélectionné.",
"Improve the writing of the selected text. Make it more professional and clear.": "Améliore l'écriture du texte sélectionné. Rends-le plus professionnel et clair.",
"Summarize the document into a concise paragraph.": "Résume le document en un paragraphe concis.",
"Keep writing about the content sent in the prompt, expanding on the ideas.": "Continue à écrire sur le contenu envoyé dans la demande, en développant les idées.",
"Important, verified the language of the document! Your answer MUST be in the same language as the document. If the document is in English, your answer MUST be in English. If the document is in Spanish, your answer MUST be in Spanish, etc.": "Important, vérifie la langue du document ! Ta réponse DOIT être dans la même langue que le document. Si le document est en anglais, ta réponse DOIT être en anglais. Si le document est en espagnol, ta réponse DOIT être en espagnol, etc.",
"You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.": "Vous êtes le seul propriétaire de ce groupe, faites d'un autre membre le propriétaire du groupe, avant de pouvoir modifier votre propre rôle ou vous supprimer du document.",
"You can view this document but need additional access to see its members or modify settings.": "Vous pouvez voir ce document mais vous avez besoin d'un accès supplémentaire pour voir ses membres ou modifier les paramètres.",
"You cannot restrict access to a subpage relative to its parent page.": "Vous ne pouvez pas restreindre l'accès à une sous-page par rapport à sa page parente.",
@@ -1049,7 +1043,6 @@
"Editor": "Editor",
"Editor unavailable": "Editor non disponibile",
"Emojify": "Emojify",
"Empty template": "Modello vuoto",
"Error during delete invitation": "Errore durante l'eliminazione dell'invito",
"Error during update invitation": "Errore durante l'aggiornamento dell'invito",
"Export": "Esporta",
@@ -1119,7 +1112,6 @@
"Start Writing": "Avvia Scrittura",
"Summarize": "Riassumi",
"Summary": "Sommario",
"Template": "Modello",
"The document has been deleted.": "Il documento è stato eliminato con successo.",
"The document visibility has been updated.": "La visibilità del documento è stata aggiornata.",
"The export failed": "Esportazione fallita",
@@ -1229,6 +1221,7 @@
"Document deleted": "Document verwijderd",
"Document duplicated successfully!": "Document met succes gedupliceerd!",
"Document editor": "Documenteditor",
"Document emoji": "Document emoji",
"Document owner": "Document eigenaar",
"Document role text": "Document roluitleg",
"Document sections": "Document secties",
@@ -1248,7 +1241,6 @@
"Editor unavailable": "Editor niet beschikbaar",
"Embed a PDF file": "PDF bestand invoegen",
"Emojify": "Maak met emoji's",
"Empty template": "Leeg sjabloon",
"Error during delete invitation": "Fout bij verwijderen uitnodiging",
"Error during update invitation": "Fout tijdens bijwerken uitnodiging",
"Error while deleting invitation": "Fout bij verwijderen uitnodiging",
@@ -1274,6 +1266,7 @@
"Illustration": "Illustratie",
"Image 403": "Afbeelding 403",
"Image: {{title}}": "Afbeelding: {{title}}",
"Import Docx or Markdown files": "Importeer Docx of Markdown bestanden",
"Insufficient access rights to view the document.": "Onvoldoende toegangsrechten om het document te bekijken.",
"Invalid or missing PDF file.": "Ongeldig of ontbrekend PDF-bestand.",
"Invite": "Uitnodigen",
@@ -1329,6 +1322,7 @@
"Open the document options": "Open document opties",
"Open the menu of actions for the document: {{title}}": "Open het menu van acties voor het document: {{title}}",
"Open the sharing settings for the document": "Open de instellingen voor delen van het document",
"Open the upload dialog": "Open het upload dialoogvenster",
"Organize": "Organiseer",
"Others are editing this document. Unfortunately your network blocks WebSockets, the technology enabling real-time co-editing.": "Anderen bewerken dit document. Helaas blokkeert uw netwerk WebSockets, de technologie voor real-time co-editing.",
"Others are editing. Your network prevent changes.": "Anderen zijn aan het bewerken. Uw netwerk voorkomt wijzigingen.",
@@ -1386,8 +1380,11 @@
"Start Writing": "Begin met schrijven",
"Summarize": "Vat samen",
"Summary": "Samenvatting",
"Template": "Sjabloon",
"The antivirus has detected an anomaly in your file.": "Antivirus heeft een afwijking in uw bestand ontdekt.",
"The document \"{{documentName}}\" has been successfully imported": "Het document \"{{documentName}}\" is succesvol geïmporteerd",
"The document \"{{documentName}}\" import has failed": "Het document \"{{documentName}}\" import is mislukt",
"The document \"{{documentName}}\" import has failed (only .docx and .md files are allowed)": "Het document \"{{documentName}}\" import is mislukt (alleen zijn .docx en .md bestanden toegestaan)",
"The document \"{{documentName}}\" is too large. Maximum file size is {{maxFileSize}}.": "Het document \"{{documentName}}\" is te groot. Maximale bestandsgrootte is {{maxFileSize}}.",
"The document has been deleted.": "Het document is verwijderd",
"The document has been restored.": "Het document is hersteld.",
"The document visibility has been updated.": "De toegang van het document is bijgewerkt.",
@@ -1549,7 +1546,6 @@
"Editor unavailable": "Редактор недоступен",
"Embed a PDF file": "Вложить PDF файл",
"Emojify": "Сделать эмодзи",
"Empty template": "Пустой шаблон",
"Error during delete invitation": "Ошибка при удалении приглашения",
"Error during update invitation": "Ошибка при обновлении приглашения",
"Error while deleting invitation": "Ошибка в процессе удаления приглашения",
@@ -1575,6 +1571,7 @@
"Illustration": "Иллюстрация",
"Image 403": "Изображение 403",
"Image: {{title}}": "Изображение: {{title}}",
"Import Docx or Markdown files": "Импорт файлов Docx или Markdown",
"Insufficient access rights to view the document.": "Недостаточно прав доступа для просмотра документа.",
"Invalid or missing PDF file.": "Повреждённый или отсутствующий PDF-файл.",
"Invite": "Приглашение",
@@ -1630,6 +1627,7 @@
"Open the document options": "Открыть параметры документа",
"Open the menu of actions for the document: {{title}}": "Открыть меню действий для документа: {{title}}",
"Open the sharing settings for the document": "Открыть настройки общего доступа к документу",
"Open the upload dialog": "Открыть диалоговое окно загрузки",
"Organize": "Организация данных",
"Others are editing this document. Unfortunately your network blocks WebSockets, the technology enabling real-time co-editing.": "Другие пользователи редактируют этот документ. К сожалению, ваша сеть блокирует WebSockets, технологию, позволяющую совместное редактирование в реальном времени.",
"Others are editing. Your network prevent changes.": "Другие участники редактируют этот документ. Ваша сеть не позволяет вам присоединиться.",
@@ -1687,8 +1685,11 @@
"Start Writing": "Начать писать",
"Summarize": "Обобщить",
"Summary": "Сводка",
"Template": "Шаблон",
"The antivirus has detected an anomaly in your file.": "Антивирус обнаружил что-то странное в вашем файле.",
"The document \"{{documentName}}\" has been successfully imported": "Документ \"{{documentName}}\" успешно импортирован",
"The document \"{{documentName}}\" import has failed": "Не удалось импортировать документ \"{{documentName}}\"",
"The document \"{{documentName}}\" import has failed (only .docx and .md files are allowed)": "Не удалось импортировать документ \"{{documentName}}\" (разрешены только файлы форматов .docx и .md)",
"The document \"{{documentName}}\" is too large. Maximum file size is {{maxFileSize}}.": "Документ \"{{documentName}}\" слишком большой. Максимальный размер файла - {{maxFileSize}}.",
"The document has been deleted.": "Документ удалён.",
"The document has been restored.": "Документ восстановлен.",
"The document visibility has been updated.": "Видимость документа обновлена.",
@@ -1806,7 +1807,6 @@
"Editor": "Editör",
"Editor unavailable": "Editör mevcut değil",
"Emojify": "Emojileştir",
"Empty template": "Boş taslak",
"Export": "Dışa Aktar",
"Failed to copy link": "Bağlantı kopyalanamadı",
"Format": "Format",
@@ -1834,7 +1834,6 @@
"Show more": "Daha fazla göster",
"Start Writing": "Yazmaya Başla",
"Summarize": "Özetle",
"Template": "Şablon",
"The export failed": "Dışa aktarma başarısız oldu",
"This file is flagged as unsafe.": "Bu dosya güvensiz olarak işaretlenmiş.",
"Type the name of a document": "Belgenin ismini yazın",
@@ -1953,7 +1952,6 @@
"Editor unavailable": "Редактор недоступний",
"Embed a PDF file": "Вкласти PDF-файл",
"Emojify": "Зробити емодзі",
"Empty template": "Порожній шаблон",
"Error during delete invitation": "Під час видалення запрошення сталася помилка",
"Error during update invitation": "Помилка при оновленні запрошення",
"Error while deleting invitation": "Помилка при видаленні запрошення",
@@ -1979,6 +1977,7 @@
"Illustration": "Ілюстрація",
"Image 403": "Зображення 403",
"Image: {{title}}": "Зображення: {{title}}",
"Import Docx or Markdown files": "Імпортувати файли Docx або Markdown",
"Insufficient access rights to view the document.": "Недостатньо прав для перегляду документа.",
"Invalid or missing PDF file.": "Неприпустимий або відсутній PDF-файл.",
"Invite": "Запрошення",
@@ -2034,6 +2033,7 @@
"Open the document options": "Відкрити параметри документа",
"Open the menu of actions for the document: {{title}}": "Відкрити меню дій для документа: {{title}}",
"Open the sharing settings for the document": "Відкрити параметри спільного доступу для документа",
"Open the upload dialog": "Відкрити діалогове вікно вивантаження",
"Organize": "Організуйте",
"Others are editing this document. Unfortunately your network blocks WebSockets, the technology enabling real-time co-editing.": "Інші учасники редагують цей документ. На жаль, ваша мережа блокує WebSockets, технологію, що дозволяє редагувати в реальному часі.",
"Others are editing. Your network prevent changes.": "Інші учасники редагують документ. Ваша мережа не дозволяє вам вносити зміни.",
@@ -2091,8 +2091,11 @@
"Start Writing": "Почати писати",
"Summarize": "Підсумувати",
"Summary": "Зведення",
"Template": "Шаблон",
"The antivirus has detected an anomaly in your file.": "Антивірус виявив аномалію у вашому файлі.",
"The document \"{{documentName}}\" has been successfully imported": "Документ \"{{documentName}}\" успішно імпортовано",
"The document \"{{documentName}}\" import has failed": "Імпорт документу \"{{documentName}}\" не вдався",
"The document \"{{documentName}}\" import has failed (only .docx and .md files are allowed)": "Імпорт документу \"{{documentName}}\" пройшов невдало (допускаються тільки файли .docx і .md)",
"The document \"{{documentName}}\" is too large. Maximum file size is {{maxFileSize}}.": "Документ \"{{documentName}}\" занадто великий. Максимальний розмір файлу - {{maxFileSize}}.",
"The document has been deleted.": "Документ був видалений.",
"The document has been restored.": "Документ був відновлений.",
"The document visibility has been updated.": "Видимість документа оновлено.",
@@ -2147,182 +2150,300 @@
},
"zh": {
"translation": {
"\"{{email}}\" is already invited to the document.": "{{email}}已经被邀请过。",
"\"{{email}}\" is already member of the document.": "{{email}}已经在成员列表中。",
"401 Unauthorized": "未授权",
"A new way to organize knowledge.": "一种组织知识的新方法",
"AI Actions": "使用AI",
"AI seems busy! Please try again.": "AI看起来很忙!请重试。",
"Accessible to anyone": "对所有人开放",
"Accessible to authenticated users": "对授权用户开放",
"Add": "新增",
"Add a callout block": "增加一个标注模块",
"Administrator": "管理员",
"All docs": "所有文档",
"An uncompromising writing experience.": "极致的写作体验。",
"Analyzing file...": "正在分析文件……",
"Anonymous": "匿名用户",
"Anyone with the link can edit the document": "被分享人可以编辑文档",
"Anyone with the link can edit the document if they are logged in": "已登录的被分享人可以编辑文档内容",
"Anyone with the link can see the document": "被分享人可以查看文档",
"Anyone with the link can view the document if they are logged in": "已登录的被分享人可以查看文档内容",
"Available soon": "即将推出",
"Back to homepage": "返回主页",
"Banner image": "Banner图像",
"\"{{email}}\" is already invited to the document.": "{{email}}」已被邀請加入此文件",
"\"{{email}}\" is already member of the document.": "{{email}}」已是此文件的成員",
"401 Unauthorized": "401 未經授權",
"A new way to organize knowledge.": "組織知識的新方法",
"AI Actions": "AI 動作",
"AI seems busy! Please try again.": "AI 似乎很忙!請再試一次",
"Access Denied - Error 403": "存取被拒 - 403 錯誤",
"Access Requests": "存取要求",
"Access request sent successfully.": "存取要求已成功發送",
"Accessible to anyone": "任何人均可存取",
"Accessible to authenticated users": "已驗證的使用者可存取",
"Actions for {{title}}": "{{title}} 的動作",
"Add": "加入",
"Add PDF": "加入 PDF",
"Add a callout block": "加入註釋區塊",
"Add a sub page": "增加子頁面",
"Add emoji": "加入表情符號",
"Administrator": "管理員",
"Alert deleted document": "文件刪除警報",
"All docs": "所有文件",
"An error occurred while restoring the document: {{error}}": "還原文件時發生錯誤:{{error}}",
"An uncompromising writing experience.": "不妥協的寫作體驗",
"An unexpected error occurred.": "發生非預期錯誤",
"Analyzing file...": "正在分析檔案...",
"Anonymous": "匿名",
"Anyone with the link can edit the document": "任何擁有連結的使用者均可編輯文件",
"Anyone with the link can edit the document if they are logged in": "任何擁有連結且已登入的使用者均可編輯文件",
"Anyone with the link can see the document": "任何擁有連結的使用者均可檢視文件",
"Anyone with the link can view the document if they are logged in": "任何擁有連結且已登入的使用者均可檢視文件",
"Approve": "核准",
"As this is a sub-document, please request access to the parent document to enable these features.": "由於這是子文件,請向父文件申請存取權以啟用這些功能",
"Available soon": "即將推出",
"Back to homepage": "返回首頁",
"Banner image": "橫幅圖片",
"Beautify": "美化",
"Callout": "标注",
"Can't load this page, please check your internet connection.": "无法加载此页面,请检查您的网络连接。",
"By moving this document to <strong>{{targetDocumentTitle}}</strong>, it will lose its current access rights and inherit the permissions of that document. <strong>This access change cannot be undone.</strong>": "將此文件移至 <strong>{{targetDocumentTitle}}</strong> 後,它將失去目前的存取權限並繼承該文件的權限<strong>此權限變更無法還原</strong>",
"Callout": "註釋",
"Can't load this page, please check your internet connection.": "無法載入此頁面,請檢查您的網路連線",
"Cancel": "取消",
"Cancel the download": "取消下载",
"Collaborate": "协作",
"Collaborate and write in real time, without layout constraints.": "实时协作和写入,不受布局限制。",
"Collaborative writing, Simplified.": "协作写入,简体字样。",
"Confirm": "确定",
"Connected": "已连接",
"Content modal to export the document": "导出文档",
"Convert Markdown": "转换为Markdown格式",
"Copied to clipboard": "已复制到剪贴板",
"Copy as {{format}}": "复制为{{format}}",
"Copy link": "复制链接",
"Correct": "更正",
"Current doc": "当前文档",
"Delete": "删除",
"Delete a doc": "删除文档",
"Delete document": "删除文档",
"Doc visibility card": "文档可见列表",
"Cancel the deletion": "取消刪除",
"Cancel the download": "取消下載",
"Change role for {{email}}": "更改 {{email}} 的角色",
"Change role for {{name}}": "更改 {{name}} 的角色",
"Close the access request modal": "關閉存取要求彈窗",
"Close the delete modal": "關閉刪除彈窗",
"Close the download modal": "關閉下載彈窗",
"Close the search modal": "關閉搜尋彈窗",
"Close the share modal": "關閉分享彈窗",
"Close the version history modal": "關閉版本紀錄彈窗",
"Collaborate": "協作",
"Collaborate and write in real time, without layout constraints.": "即時協作與寫作,不受排版限制",
"Collaborative writing, Simplified.": "協作寫作,化繁為簡",
"Comment": "評論",
"Confirm": "確認",
"Connected": "已登入",
"Contains {{count}} sub-documents_many": "包含 {{count}} 個子文件",
"Contains {{count}} sub-documents_one": "包含 {{count}} 個子文件",
"Contains {{count}} sub-documents_other": "包含 {{count}} 個子文件",
"Content modal to explain why the user cannot edit": "解釋使用者為何無法編輯的內容彈窗",
"Content modal to export the document": "匯出文件的內容彈窗",
"Convert Markdown": "轉換 Markdown",
"Copied to clipboard": "已複製到剪貼簿",
"Copy as {{format}}": "複製為 {{format}}",
"Copy link": "複製連結",
"Correct": "糾正",
"Create a new sub-doc": "建立新子文件",
"Current doc": "目前文件",
"Days remaining": "剩餘天數",
"Days remaining:": "剩餘天數:",
"Delete": "刪除",
"Delete a doc": "刪除文件",
"Delete document": "刪除文件",
"Delete sub-document": "刪除子文件",
"Doc visibility card": "文件可見度卡片",
"Docs": "Docs",
"Docs Logo": "Docs 徽标",
"Docs is already available, log in to use it now.": "Docs已可用,请登录使用",
"Docs makes real-time collaboration simple. Invite collaborators - public officials or external partners - with one click to see their changes live, while maintaining precise access control for data security.": "Docs 支持精确权限控制的实时协作,协作者可一键加入并查看实时修改。",
"Docs offers an intuitive writing experience. Its minimalist interface favors content over layout, while offering the essentials: media import, offline mode and keyboard shortcuts for greater efficiency.": "Docs 的极简界面专注于内容创作,支持媒体导入、离线模式及快捷键操作,提升写作效率。",
"Docs transforms your documents into knowledge bases thanks to subpages, powerful search and the ability to pin your important documents.": "Docs 通过子页面、大的搜功能以及固定重要文的能力,将您的文档转化为知识库。",
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs 为您提供高效、直且安全的文档协作解决方案。",
"Document accessible to any connected person": "任何来访的人都可以访问文档",
"Document owner": "文档所有者",
"Document title": "文档标题",
"Docs is already available, log in to use it now.": "Docs 已上線,立即登入開始使用",
"Docs makes real-time collaboration simple. Invite collaborators - public officials or external partners - with one click to see their changes live, while maintaining precise access control for data security.": "Docs 讓即時協作變得簡單只需一鍵即可邀請協作者(包括公務人員或外部合作夥伴)並即時查看變更,同時透過精密的權限控管確保數據安全",
"Docs offers an intuitive writing experience. Its minimalist interface favors content over layout, while offering the essentials: media import, offline mode and keyboard shortcuts for greater efficiency.": "Docs 提供直覺的寫作體驗極簡的介面讓使用者專注於內容而非排版,同時提供核心功能:媒體匯入、離線模式及提升效率的快捷鍵",
"Docs transforms your documents into knowledge bases thanks to subpages, powerful search and the ability to pin your important documents.": "憑藉子頁面、大的搜功能以及置頂重要文的能力,Docs 能將您的文件轉化為知識庫",
"Docs: Your new companion to collaborate on documents efficiently, intuitively, and securely.": "Docs:您高效、直且安全的文件協作新夥伴",
"Document access mode": "文件存取模式",
"Document accessible to any connected person": "任何已連線的人員均可存取的文件",
"Document deleted": "文件已刪除",
"Document duplicated successfully!": "文件副本建立成功!",
"Document editor": "文件編輯器",
"Document emoji": "文件表情符號",
"Document owner": "文件所有者",
"Document role text": "文件角色文字",
"Document sections": "文件章節",
"Document title": "文件標題",
"Document tree": "文件樹狀結構",
"Document visibility": "文件可見度",
"Documents grid": "文件網格",
"Docx": "Doc",
"Download": "下",
"Download anyway": "仍要下",
"Editing": "正在编辑",
"Editor": "编辑者",
"Editor unavailable": "编辑功能不可用",
"Emojify": "表情符",
"Empty template": "空模板",
"Error during delete invitation": "更新邀请时出错",
"Error during update invitation": "更新邀请时出错",
"Export": "导出",
"Export the document": "导出文档",
"Failed to add the member in the document.": "添加成员失败。",
"Failed to copy link": "复制链接失败",
"Failed to copy to clipboard": "无法复制到剪贴板",
"Failed to create the invitation for {{email}}.": "邀请{{email}}失败。",
"Flexible export.": "灵活的导出。",
"Format": "格式化",
"Govs ❤️ Open Source.": "德法政府资助、开放源码。",
"History": "历史记录",
"Home": "首页",
"I understand": "我已知晓",
"If a member is editing, his works can be lost.": "如果其他成员在编辑,他的内容可能丢失。",
"Illustration": "图",
"Image 403": "图片 403",
"Invite": "邀请",
"It is the card information about the document.": "这是关于该文件的卡片信息。",
"It is the document title": "这是文档标题",
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "您要找的页面似乎不存在或无法正确显示。",
"Language": "语言",
"Last update: {{update}}": "上次更新: {{update}}",
"Learn more": "了解更多",
"Link Copied !": "链接已复制!",
"List invitation card": "邀请列表",
"List members card": "成员列表",
"Load more": "加载更多",
"Log in to access the document.": "登录以访问文档。",
"Login": "登录",
"Logo": "Logo",
"Logout": "退出登录",
"Modal confirmation to download the attachment": "下载附件",
"More docs": "更多文档",
"Move": "移动",
"My docs": "我的文档",
"Name": "名称",
"New doc": "新建文档",
"No active search": "未搜索到",
"No document found": "找不到文档",
"Download": "下",
"Download anyway": "仍要下",
"Download your document in a .docx, .odt, .pdf or .html(zip) format.": "下載您的文件,格式支援 .docx、.odt、.pdf 或 .html(zip)",
"Drag and drop status": "拖放狀態",
"Duplicate": "建立副本",
"Edit document emoji": "編輯文件表情符",
"Editing": "編輯",
"Editor": "編輯者",
"Editor unavailable": "編輯器無法使用",
"Embed a PDF file": "內嵌 PDF 檔案",
"Emojify": "加入表情符號",
"Error during delete invitation": "刪除邀請時發生錯誤",
"Error during update invitation": "更新邀請時發生錯誤",
"Error while deleting invitation": "刪除邀請時發生錯誤",
"Error while removing the request.": "移除請求時發生錯誤",
"Error while updating the member role.": "更新成員角色時發生錯誤",
"Export": "匯出",
"Export the document": "匯出文件",
"Failed to add the member in the document.": "無法將成員加入文件",
"Failed to copy link": "連結複製失敗",
"Failed to copy to clipboard": "無法複製到剪貼簿",
"Failed to create the invitation for {{email}}.": "無法為 {{email}} 建立邀請",
"Failed to duplicate the document...": "建立文件副本失敗...",
"Flexible export.": "彈性的匯出功能",
"Format": "格式",
"Go to content": "前往內容",
"Govs ❤️ Open Source.": "政府 ❤️ 開源軟體",
"HTML": "HTML",
"History": "歷史紀錄",
"Home": "首頁",
"I understand": "我理解",
"If a member is editing, his works can be lost.": "如果成員正在進行編輯,其工作內容可能會遺失",
"If you wish to be able to co-edit in real-time, contact your Information Systems Security Manager about allowing WebSockets.": "如果您希望能夠即時共同編輯,請聯絡您的資訊系統安全管理員以允許 WebSockets",
"Illustration": "插圖",
"Image 403": "圖片 403",
"Image: {{title}}": "圖片:{{title}}",
"Insufficient access rights to view the document.": "存取權限不足,無法檢視文件",
"Invalid or missing PDF file.": "無效的 PDF 檔案",
"Invite": "邀請",
"Invite new members": "邀請新成員",
"Invite {{count}} members_many": "邀請 {{count}} 位成員",
"Invite {{count}} members_one": "邀請 {{count}} 位成員",
"Invite {{count}} members_other": "邀請 {{count}} 位成員",
"Invite {{name}}": "邀請 {{name}}",
"It is the card information about the document.": "這是關於文件的資訊卡片",
"It is the document title": "這是文件標題",
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "您尋找的頁面似乎不存在,或無法正確顯示",
"Language": "語言",
"Last update: {{update}}": "最後更新:{{update}}",
"Learn more": "深入瞭解",
"Link Copied !": "連結已複製!",
"Link a doc": "連結文件",
"Link settings": "連結設定",
"Link this doc to another doc": "將此文件連結到另一個文件",
"Links": "連結",
"List invitation card": "邀請列表卡片",
"List members card": "成員列表卡片",
"List request access card": "存取要求列表卡片",
"List search user result card": "使用者搜尋結果列表卡片",
"Load more": "載入更多",
"Log in to access the document.": "請登入以存取文件",
"Login": "登入",
"Logo": "標誌",
"Logout": "登出",
"Main content": "主要內容",
"Modal confirmation to download the attachment": "下載附件的確認彈窗",
"More docs": "更多文件",
"More options": "更多選項",
"Move": "移動",
"Move document": "移動文件",
"Move to my docs": "移至我的文件",
"My docs": "我的文件",
"Name": "名稱",
"New doc": "新文件",
"New sub-doc": "新子文件",
"No active search": "無進行中的搜尋",
"No document found": "找不到文件",
"No documents found": "找不到文档",
"No text selected": "未选择文本",
"No versions": "暂无版本",
"OK": "OK",
"Offline ?!": "离线?",
"Only invited people can access": "只有受邀人可以访问",
"Open Source": "开放源代码",
"Open document {{title}}": "打开文档{{title}}",
"Open the document options": "打开文档选项",
"Organize": "组织",
"No text selected": "未選擇文字",
"No versions": "無版本紀錄",
"ODT": "ODT",
"OK": "確定",
"Offline ?!": "離線了嗎?!",
"Only invited people can access": "僅限受邀人員存取",
"Open Source": "開源軟體",
"Open document {{title}}": "開啟文件 {{title}}",
"Open document: {{title}}": "開啟文件:{{title}}",
"Open root document": "開啟根文件",
"Open the document options": "開啟文件選項",
"Open the menu of actions for the document: {{title}}": "開啟文件的動作選單:{{title}}",
"Open the sharing settings for the document": "開啟文件的分享設定",
"Organize": "組織",
"Others are editing this document. Unfortunately your network blocks WebSockets, the technology enabling real-time co-editing.": "其他人正在編輯此文件可惜您的網路封鎖了 WebSockets這是實現即時共同編輯的技術",
"Others are editing. Your network prevent changes.": "其他人正在編輯您的網路環境阻止了變更彙整",
"Owner": "所有者",
"PDF": "PDF",
"Page Not Found - Error 404": "页面未找到",
"Pending invitations": "待邀请",
"Pin": "固定",
"Pinned documents": "置顶文档",
"Please download it only if it comes from a trusted source.": "请从可信的来源下载。",
"Private": "私有",
"Proconnect Login": "安全登录",
"Public": "公开",
"Public document": "公开文档",
"Quick search input": "快速搜索",
"Reader": "阅读者",
"Reading": "阅读中",
"Rename": "重命名",
"Rephrase": "改写",
"Reset": "重",
"Restore": "恢复",
"Search": "搜索",
"Search by title": "根据标题搜索",
"Search docs": "搜索文档",
"Search modal": "搜索模式",
"Search user result": "查找用户",
"Select a document": "选择一个文档",
"Select a version on the right to restore": "选择要恢复的版本",
"Select language": "选择语言",
"Share": "共享",
"Share the document": "共享文档",
"Share with {{count}} users_many": "与{{count}}共享",
"Share with {{count}} users_one": "与{{count}}共享",
"Share with {{count}} users_other": "与{{count}}共享",
"Shared with me": "共享给我的",
"Shared with {{count}} users_many": "与{{count}}共享",
"Shared with {{count}} users_one": "与{{count}}共享",
"Shared with {{count}} users_other": "与{{count}}共享",
"Show more": "显示更多",
"Simple and secure collaboration.": "简单而安全的协作。",
"Something bad happens, please retry.": "出错了,请重试。",
"Start Writing": "开始记录",
"Summarize": "摘要",
"Summary": "概要",
"Template": "模板",
"The document has been deleted.": "文档已被删除。",
"The document visibility has been updated.": "文档可见性已更新。",
"The export failed": "导出失败",
"This file is flagged as unsafe.": "此文件被标记为不安全的。",
"To facilitate the circulation of documents, Docs allows you to export your content to the most common formats: PDF, Word or OpenDocument.": "Docs 支持导出为 PDF、Word、OpenDocument 等常见格式,方便文件共享与协作。",
"Too many requests. Please wait 60 seconds.": "太多的请求。请等待 60 秒。",
"Type a name or email": "输入姓名或邮件地址",
"Type the name of a document": "输入文档名称",
"Unpin": "取消固定",
"Untitled document": "无标题文档",
"Updated": "更新",
"Updated at": "更新于",
"Use as prompt": "用作提示",
"Version history": "历史版本",
"Version restored successfully": "已成功还原版本",
"PDF document": "PDF 文件",
"Page Not Found - Error 404": "找不到頁面 - 錯誤 404",
"Pending invitations": "待處理的邀請",
"People with access via the parent document": "透過父文件獲得存取權的人員",
"Pin": "置頂",
"Pinned documents": "置頂文件",
"Please download it only if it comes from a trusted source.": "請僅在來源可信的情況下下載",
"Private": "私人",
"Proconnect Login": "Proconnect 登入",
"Public": "公開",
"Public document": "公開文件",
"Quick search input": "快速搜尋輸入",
"Reader": "檢視者",
"Reading": "僅限閱讀",
"Refresh page": "重新整理頁面",
"Remove access": "移除存取權",
"Remove emoji": "移除表情符號",
"Remove {{name}} from the invite list": "將 {{name}} 從邀請名單中移除",
"Rename": "重新命名",
"Rephrase": "換句話說",
"Request access": "要求存取權",
"Reset": "重設",
"Restore": "還原",
"Root document {{title}}": "{{title}} 的根文件",
"Search": "搜尋",
"Search by title": "依標題搜尋",
"Search docs": "搜尋文件",
"Search modal": "搜尋彈窗",
"Search results": "搜尋結果",
"Search user result": "使用者搜尋結果",
"Select a doc": "選擇文件",
"Select a document": "選擇文件",
"Select a version on the right to restore": "從右側選擇要還原的版本",
"Select language": "選擇語言",
"Share": "分享",
"Share button": "分享按鈕",
"Share modal content": "用於分享內容的彈窗",
"Share the document": "分享文件",
"Share with {{count}} users_many": "與 {{count}} 位使用者分享",
"Share with {{count}} users_one": "與 {{count}} 位使用者分享",
"Share with {{count}} users_other": "與 {{count}} 位使用者分享",
"Shared with me": "與我分享",
"Shared with {{count}} users_many": "與 {{count}} 位使用者分享",
"Shared with {{count}} users_one": "與 {{count}} 位使用者分享",
"Shared with {{count}} users_other": "與 {{count}} 位使用者分享",
"Show more": "顯示更多",
"Simple and secure collaboration.": "簡單且安全的協作",
"Simple document icon": "簡單文件圖示",
"Something bad happens, please retry.": "發生錯誤,請重試",
"Start Writing": "開始寫作",
"Summarize": "總結",
"Summary": "摘要/目錄",
"The antivirus has detected an anomaly in your file.": "防毒軟體偵測到檔案異常",
"The document has been deleted.": "文件已刪除",
"The document has been restored.": "文件已還原",
"The document visibility has been updated.": "文件可見度已更新",
"The document visibility restored.": "文件可見度已還原",
"The export failed": "匯出失敗",
"The link sharing rules differ from the parent document": "連結分享規則與父文件不同",
"This document and <strong>any sub-documents</strong> will be placed in the trashbin. You can restore it within {{days}} days.": "此文件及其<strong>所有子文件</strong>將被移至垃圾桶您可以在 {{days}} 天內還原",
"This document will be placed in the trashbin. You can restore it within {{days}} days.": "此文件將被移至垃圾桶您可以在 {{days}} 天內還原",
"This file is flagged as unsafe.": "此檔案被標記為不安全",
"This means you can't edit until others leave.": "這意味著在其他人離開之前,您無法進行編輯",
"This user has access inherited from a parent page.": "此使用者的存取權繼承自父頁面",
"To facilitate the circulation of documents, Docs allows you to export your content to the most common formats: PDF, Word or OpenDocument.": "為方便文件流通Docs 支援將內容匯出為主流格式PDF、Word 或 OpenDocument",
"Too many requests. Please wait 60 seconds.": "太多的請求數請稍候 60 秒",
"Trashbin": "垃圾桶",
"Type a name or email": "輸入姓名或電子郵件",
"Type the name of a document": "輸入文件名稱",
"Unpin": "取消置頂",
"Untitled document": "未命名文件",
"Updated": "已更新",
"Updated at": "更新於",
"Upload PDF": "上傳 PDF",
"Use arrow keys to navigate between documents. Press Enter to open a document. Press F2 to focus the emoji button when available, then press F2 again to access document actions.": "使用方向鍵在文件間導覽按 Enter 開啟文件可用時按 F2 聚焦表情符號按鈕,再按一次 F2 進入文件動作",
"Use as prompt": "作為提示詞使用",
"Version history": "版本紀錄",
"Version restored successfully": "版本還原成功",
"Warning": "警告",
"Write": "写入",
"You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.": "您是当前群组唯一所有者,需先指定另一管理员,才能更改自身角色或退出文档。",
"Your current document will revert to this version.": "您当前的文档将恢复到这个版本。",
"Your {{format}} was downloaded succesfully": "你的{{format}}已成功下载",
"Why you can't edit the document?": "為什麼您無法編輯此文件?",
"Write": "寫作",
"You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.": "您是此群組的唯一所有者在更改您自己的角色或從文件中移除之前,請先將另一位成員設為群組所有者",
"You can view this document but need additional access to see its members or modify settings.": "您可以檢視此文件,但需要額外的權限才能查看成員或修改設定",
"You cannot restrict access to a subpage relative to its parent page.": "您無法將子頁面的存取權限設為比父頁面更嚴格",
"You must be at least the administrator of the target document": "您必須至少是目標文件的管理員",
"You must be the owner to move the document": "您必須是所有者才能移動文件",
"You're currently viewing a sub-document. To gain access, please request permission from the main document.": "您目前正在檢視子文件如需存取權限,請向主文件提出申請",
"Your access request for this document is pending.": "您的文件存取要求正在等待審核",
"Your current document will revert to this version.": "您的目前文件將還原至此版本",
"Your {{format}} was downloaded succesfully": "您的 {{format}} 已成功下載",
"days_many": "天",
"days_one": "天",
"days_other": "天",
"document": "文件",
"embed": "內嵌",
"file": "檔案",
"home-content-open-source-part1": "Docs基于<2>Django Rest Framework</2>和<6>Next.js</6>构建。我们还使用<9>Yjs</9>和<13>BlockNote.js</13>,这两个项目我们很自豪能够赞助。",
"home-content-open-source-part2": "您可以轻松自托管Docs请参阅我们的<2>安装文档</2>)。<br/>Docs使用适合创新和企业的<7>许可证</7>MIT。<br/>欢迎贡献(请参阅我们的路线图<13>此处</13>)。",
"home-content-open-source-part3": "Docs是法国政府🇫🇷🥖<1>DINUM</1>和德国政府🇩🇪🥨<5>ZenDiS</5>联合努力的结果。"
"home-content-open-source-part3": "Docs是法国政府🇫🇷🥖<1>DINUM</1>和德国政府🇩🇪🥨<5>ZenDiS</5>联合努力的结果。",
"pdf": "pdf",
"{{action}}, current role: {{role}}": "{{action}},目前角色:{{role}}",
"{{name}} added to invite list. Add more members or press Tab to select role and invite.": "{{name}} 已加入邀請名單加入更多成員,或按 Tab 選擇角色並發送邀請",
"{{name}} removed from invite list": "{{name}} 已從邀請名單移除"
}
}
}

View File

@@ -8,8 +8,16 @@ type AnalyticEventUser = {
id: string;
email: string;
};
type AnalyticEventDoc = {
eventName: 'doc';
isPublic: boolean;
authenticated: boolean;
};
export type AnalyticEvent = AnalyticEventClick | AnalyticEventUser;
export type AnalyticEvent =
| AnalyticEventClick
| AnalyticEventUser
| AnalyticEventDoc;
export abstract class AbstractAnalytic {
public constructor() {

View File

@@ -171,19 +171,25 @@ const DocPage = ({ id }: DocProps) => {
});
}, [addTask, doc?.id, queryClient]);
if (isError && error) {
if ([404, 401].includes(error.status)) {
let replacePath = `/${error.status}`;
useEffect(() => {
if (!isError || !error?.status || ![404, 401].includes(error.status)) {
return;
}
if (error.status === 401) {
if (authenticated) {
queryClient.setQueryData([KEY_AUTH], null);
}
setAuthUrl();
let replacePath = `/${error.status}`;
if (error.status === 401) {
if (authenticated) {
queryClient.setQueryData([KEY_AUTH], null);
}
setAuthUrl();
}
void replace(replacePath);
void replace(replacePath);
}, [isError, error?.status, replace, authenticated, queryClient]);
if (isError && error?.status) {
if ([404, 401].includes(error.status)) {
return <Loading />;
}

View File

@@ -3,7 +3,7 @@
@import url('@fontsource/material-icons-outlined');
@import url('@fontsource-variable/material-symbols-outlined');
@import url('@fontsource-variable/inter');
@import url('/assets/fonts/Marianne/Marianne-font.css');
@import url('@gouvfr-lasuite/ui-kit/fonts/Marianne');
body {
margin: 0;

View File

@@ -3,10 +3,11 @@
*/
import { Crisp } from 'crisp-sdk-web';
import { PropsWithChildren, useEffect, useState } from 'react';
import { JSX, PropsWithChildren, ReactNode, useEffect, useState } from 'react';
import { createGlobalStyle } from 'styled-components';
import { User } from '@/features/auth';
import { AbstractAnalytic, AnalyticEvent } from '@/libs';
const CrispStyle = createGlobalStyle`
#crisp-chatbox a{
@@ -70,3 +71,34 @@ export const CrispProvider = ({
</>
);
};
export class CrispAnalytic extends AbstractAnalytic {
private conf?: CrispProviderProps = undefined;
private EVENT = {
PUBLIC_DOC_NOT_CONNECTED: 'public-doc-not-connected',
};
public constructor(conf?: CrispProviderProps) {
super();
this.conf = conf;
}
public Provider(children?: ReactNode): JSX.Element {
return (
<CrispProvider websiteId={this.conf?.websiteId}>{children}</CrispProvider>
);
}
public trackEvent(evt: AnalyticEvent): void {
if (evt.eventName === 'doc') {
if (evt.isPublic && !evt.authenticated) {
Crisp.trigger.run(this.EVENT.PUBLIC_DOC_NOT_CONNECTED);
}
}
}
public isFeatureFlagActivated(): boolean {
return true;
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "impress",
"version": "4.4.0",
"version": "4.5.0",
"private": true,
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
@@ -32,17 +32,21 @@
},
"resolutions": {
"@tiptap/extensions": "3.14.0",
"@types/node": "24.10.4",
"@types/react": "19.2.7",
"@types/node": "24.10.9",
"@types/react": "19.2.8",
"@types/react-dom": "19.2.3",
"docx": "9.5.1",
"eslint": "9.39.2",
"prosemirror-view": "1.41.4",
"react": "19.2.3",
"react-dom": "19.2.3",
"typescript": "5.9.3",
"wrap-ansi": "9.0.2",
"yjs": "13.6.28"
"yjs": "13.6.29"
},
"packageManager": "yarn@1.22.22"
"packageManager": "pnpm@10.28.2",
"dependencies": {
"@socialgouv/e2esdk-client": "link:../../../../../../Library/pnpm/global/5/node_modules/@socialgouv/e2esdk-client",
"@socialgouv/e2esdk-devtools": "link:../../../../../../Library/pnpm/global/5/node_modules/@socialgouv/e2esdk-devtools",
"@socialgouv/e2esdk-react": "link:../../../../../../Library/pnpm/global/5/node_modules/@socialgouv/e2esdk-react"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "eslint-plugin-docs",
"version": "4.4.0",
"version": "4.5.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -19,19 +19,19 @@
"dependencies": {
"@next/eslint-plugin-next": "15.5.9",
"@tanstack/eslint-plugin-query": "5.91.2",
"@typescript-eslint/eslint-plugin": "8.51.0",
"@typescript-eslint/parser": "8.51.0",
"@vitest/eslint-plugin": "1.6.4",
"@typescript-eslint/eslint-plugin": "8.53.0",
"@typescript-eslint/parser": "8.53.0",
"@vitest/eslint-plugin": "1.6.6",
"eslint-config-next": "15.5.9",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-jest": "29.12.0",
"eslint-plugin-jest": "29.12.1",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-playwright": "2.4.0",
"eslint-plugin-prettier": "5.5.4",
"eslint-plugin-playwright": "2.5.0",
"eslint-plugin-prettier": "5.5.5",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-testing-library": "7.15.4",
"prettier": "3.7.4"
"prettier": "3.8.0"
},
"packageManager": "yarn@1.22.22"
"packageManager": "pnpm@10.28.2"
}

View File

@@ -1,6 +1,6 @@
{
"name": "packages-i18n",
"version": "4.4.0",
"version": "4.5.0",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -17,7 +17,7 @@
"dependencies": {
"@types/jest": "30.0.0",
"@types/node": "*",
"eslint-plugin-docs": "*",
"eslint-plugin-docs": "workspace:^",
"eslint-plugin-import": "2.32.0",
"i18next-parser": "9.3.0",
"jest": "30.2.0",

20832
src/frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
packages:
- apps/*
- packages/*
- servers/*
overrides:
'@socialgouv/e2esdk-client': link:../../../../../../Library/pnpm/global/5/node_modules/@socialgouv/e2esdk-client
'@socialgouv/e2esdk-devtools': link:../../../../../../Library/pnpm/global/5/node_modules/@socialgouv/e2esdk-devtools
'@socialgouv/e2esdk-react': link:../../../../../../Library/pnpm/global/5/node_modules/@socialgouv/e2esdk-react
'@tiptap/extensions': 3.14.0
'@types/node': 24.10.9
'@types/react': 19.2.8
'@types/react-dom': 19.2.3
eslint: 9.39.2
prosemirror-view: 1.41.4
react: 19.2.3
react-dom: 19.2.3
typescript: 5.9.3
wrap-ansi: 9.0.2
yjs: 13.6.29

View File

@@ -1,6 +1,6 @@
{
"name": "server-y-provider",
"version": "4.4.0",
"version": "4.5.0",
"description": "Y.js provider for docs",
"repository": "https://github.com/suitenumerique/docs",
"license": "MIT",
@@ -16,10 +16,10 @@
"node": ">=22"
},
"dependencies": {
"@blocknote/server-util": "0.46.1",
"@blocknote/server-util": "0.46.2",
"@hocuspocus/server": "3.4.3",
"@sentry/node": "10.32.1",
"@sentry/profiling-node": "10.32.1",
"@sentry/node": "10.34.0",
"@sentry/profiling-node": "10.34.0",
"@tiptap/extensions": "*",
"axios": "1.13.2",
"cors": "2.8.5",
@@ -30,7 +30,7 @@
"yjs": "*"
},
"devDependencies": {
"@blocknote/core": "0.46.1",
"@blocknote/core": "0.46.2",
"@hocuspocus/provider": "3.4.3",
"@types/cors": "2.8.19",
"@types/express": "5.0.6",
@@ -39,15 +39,15 @@
"@types/supertest": "6.0.3",
"@types/ws": "8.18.1",
"cross-env": "10.1.0",
"eslint-plugin-docs": "*",
"eslint-plugin-docs": "workspace:^",
"nodemon": "3.1.11",
"supertest": "7.1.4",
"supertest": "7.2.2",
"ts-node": "10.9.2",
"tsc-alias": "1.8.16",
"typescript": "*",
"vitest": "4.0.16",
"vitest": "4.0.17",
"vitest-mock-extended": "3.1.0",
"ws": "8.18.3"
"ws": "8.19.0"
},
"packageManager": "yarn@1.22.22"
}

View File

@@ -29,7 +29,8 @@ interface Doc {
abilities: {
accesses_manage: boolean;
accesses_view: boolean;
ai_proxy: boolean;
ai_transform: boolean;
ai_translate: boolean;
attachment_upload: boolean;
children_create: boolean;
children_list: boolean;

View File

@@ -1,13 +1,12 @@
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';
import packageJson from '../../package.json';
import { SENTRY_DSN } from '../env';
Sentry.init({
dsn: SENTRY_DSN,
integrations: [nodeProfilingIntegration()],
profilesSampleRate: 1.0,
release: packageJson.version,
tracesSampleRate: 0.1,
profilesSampleRate: 1.0,
});
Sentry.setTag('application', 'y-provider');

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,7 @@ backend:
DJANGO_EMAIL_PORT: 1025
DJANGO_EMAIL_URL_APP: https://docs.127.0.0.1.nip.io
DJANGO_EMAIL_USE_SSL: False
FRONTEND_SILENT_LOGIN_ENABLED: True
LOGGING_LEVEL_HANDLERS_CONSOLE: ERROR
LOGGING_LEVEL_LOGGERS_ROOT: INFO
LOGGING_LEVEL_LOGGERS_APP: INFO

View File

@@ -30,6 +30,7 @@ backend:
DJANGO_EMAIL_PORT: 1025
DJANGO_EMAIL_URL_APP: https://{{ .Values.feature }}-docs.{{ .Values.domain }}
DJANGO_EMAIL_USE_SSL: False
FRONTEND_SILENT_LOGIN_ENABLED: True
LOGGING_LEVEL_HANDLERS_CONSOLE: ERROR
LOGGING_LEVEL_LOGGERS_ROOT: INFO
LOGGING_LEVEL_LOGGERS_APP: INFO

View File

@@ -1,10 +1,10 @@
environments:
dev:
values:
- version: 4.4.0
- version: 4.5.0
feature:
values:
- version: 4.4.0
- version: 4.5.0
feature: ci
domain: example.com
imageTag: demo

View File

@@ -1,5 +1,5 @@
apiVersion: v2
type: application
name: docs
version: 4.4.0
version: 4.5.0
appVersion: latest

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