Compare commits

...

52 Commits

Author SHA1 Message Date
Manuel Raynaud e7cbe24f3d 🔖(patch) release 4.8.4
Added

- 🚸(frontend) hint min char search users #2064

Changed

- 💄(frontend) improve comments highlights #1961
- ️(frontend) improve BoxButton a11y and native button semantics #2103
- ️(frontend) improve language picker accessibility #2069
- ️(frontend) add aria-hidden to decorative icons in dropdown menu #2093

Fixed

- 🐛(y-provider) destroy Y.Doc instances after each convert request #2129
- 🐛(backend) remove deleted sub documents in favorite_list endpoint #2083
2026-03-25 23:19:28 +01:00
Anthony LC acb20a0d26 🌐(i18n) update translated strings
Update translated files with new translations
2026-03-25 23:19:28 +01:00
Anthony LC cbe6a67704 🔧(y-provider) increase Node.js memory limit
By default, Node.js has a memory limit of
around 512MB, which can lead to out-of-memory
errors when processing large documents.
This commit increases the memory limit to
2GB for the y-provider server, allowing
it to handle larger documents without crashing.
2026-03-25 17:22:32 +01:00
Manuel Raynaud f91223fe4a 🔊(backend) add some log to trace conversion made on docs creation
We added logs on the conversion made when a doc is created.
2026-03-25 17:22:32 +01:00
Manuel Raynaud 330096eb47 🐛(backend) move lock table closer to the insert operation targeted
We want to lock the table just before the insert we want to protect is
made. In the case of the perform_create action in the Document viewset,
an http call is made after the lock and can take a very long time,
blocking for nothing the table.
2026-03-25 15:43:49 +01:00
Paul Vernin ff995c6cd9 🚨(backend) fix lint on test file
Signed-off-by: Paul Vernin <paul.vernin@gmail.com>
2026-03-25 15:14:13 +01:00
Paul Vernin 2e4a1b8ff9 📝(changelog) add fix to CHANGELOG.md
Signed-off-by: Paul Vernin <paul.vernin@gmail.com>
2026-03-25 15:14:09 +01:00
Paul Vernin 004d637c8b 🐛(backend) use ancestors_deleted_at to filter out deleted docs
Filter by ancestors_deleted_at__isnull=True instead of deleted_at__isnull=True
to be more accurate

Signed-off-by: Paul Vernin <paul.vernin@gmail.com>
2026-03-25 15:13:59 +01:00
Paul Vernin 8a0330a30f (backend) add favorite list test for sub-doc
Add test_api_document_favorite_list_with_deleted_child to verify favorite_list
endpoint does not include deleted sub documents

Signed-off-by: Paul Vernin <paul.vernin@gmail.com>
2026-03-25 15:13:47 +01:00
Paul Vernin 677392b89b 🐛(backend) Fix favorite_list result for deleted sub docs
filters out deleted documents from the favorite_list query

Signed-off-by: Paul Vernin <paul.vernin@gmail.com>
2026-03-25 15:13:36 +01:00
Cyril b8e1d12aea ️(frontend) add aria-hidden to decorative icons in dropdown menu
Mark decorative SVG icons with aria-hidden.
2026-03-25 14:15:48 +01:00
Anthony LC 525d8c8417 🐛(y-provider) destroy Y.Doc instances after each convert request
The Yjs reader and writer in `convertHandler.ts`
were creating `Y.Doc`instances on every request
without calling `.destroy()`, causing a slow heap
leak that could crash the server.

Fixed by wrapping both sites in `try/finally`
blocks that call `ydoc.destroy()`.
Regression tests added to assert `destroy` is
called the expected number of times per request path.
2026-03-25 12:03:12 +01:00
Cyril c886cbb41d ️(frontend) fix language dropdown ARIA for screen readers
Add missing attributes for language picker.
2026-03-25 11:08:17 +01:00
Cyril 98f3ca2763 ️(frontend) improve BoxButton a11y and native button semantics
Add type="button", aria-disabled, and align refs with HTMLButtonElement.
2026-03-25 10:05:49 +01:00
Anthony LC fb92a43755 🚸(frontend) hint min char search users
We give a hint to the user about the minimum
number of characters required to perform a search
in the quick search input of the doc share modal.
This is to improve the user experience.
2026-03-25 09:33:14 +01:00
Anthony LC 03fd1fe50e (frontend) fix vitest tests
We upgraded vitest recently, we need to adapt
some of our tests to the new version.
We brought some modules improvments as well,
problemes that was highlighted by the new version
of vitest.
2026-03-24 16:48:40 +01:00
Anthony LC fc803226ac 🔒️(js) fix security warning
Force the upgrade of some dependencies to fix
security warnings.
2026-03-24 15:54:34 +01:00
Anthony LC fb725edda3 🚨(frontend) fix eslint errors
Recent upgrade of eslint-plugin-playwright
highlighted some errors.
This commit fixes those errors.
2026-03-24 13:01:52 +01:00
Anthony LC 6838b387a2 (linter) replace eslint-plugin-import by eslint-plugin-import-x
"eslint-plugin-import" is not well maintained anymore
better to use "eslint-plugin-import-x" which is a fork
of "eslint-plugin-import" and is actively maintained.
2026-03-24 13:01:51 +01:00
Anthony LC 87f570582f ⬇️(frontend) downgrade @react-pdf/renderer and pin it
@react-pdf/renderer is not compatible with the
Blocknote version. We need to downgrade it to a
compatible version and pin it to avoid future issues.
When Blocknote updates to a compatible version,
we can upgrade @react-pdf/renderer again.
2026-03-24 13:01:51 +01:00
Anthony LC 37f56fcc22 📌(frontend) blocked upgrade stylelint
stylelint introduces lot of breaking changes
in its latest version, and since
we use it only for linting css files,
so we can block its upgrade for now and upgrade
it later when we will have more time to handle
the breaking changes.
2026-03-24 13:00:46 +01:00
renovate[bot] 19aa3a36bc ⬆️(dependencies) update js dependencies 2026-03-24 13:00:04 +01:00
ZouicheOmar 0d09f761dc 💄(frontend) improve comments highlights
Updated comments styles to respect design proposal,
adding distinguishable highlighting, click and hover
style interactions.
2026-03-24 09:38:31 +01:00
Manuel Raynaud ce5f9a1417 🔖(patch) release 4.8.3
Changed

- 💫(frontend) fix the help button to the bottom in tree #2073
- ️(frontend) improve version history list accessibility #2033
- ️(frontend) fix more options menu feedback for screen readers #2071
- (frontend) focus skip link on headings and skip grid dropzone #1983
- ️(frontend) fix search modal accessibility issues #2054
- ️(frontend) add sr-only format to export download button #2088
- ️(frontend) announce formatting shortcuts for screen readers #2070
- (frontend) add markdown copy icon for Copy as Markdown option #2096
- ♻️(backend) skip saving in database a document when payload is empty #2062

Fixed

- ️(frontend) fix aria-labels for table of contents #2065
- 🐛(backend) allow using search endpoint without refresh token enabled #2097
2026-03-23 17:32:50 +01:00
Anthony LC 83a24c3796 ️(frontend) add debounce WebSocket reconnect
We add a debounce mechanism to the WebSocket
reconnect logic in the `useProviderStore` to
prevent rapid reconnection attempts that can
lead to performance issues and potential server
overload.
2026-03-23 17:01:02 +01:00
Anthony LC 4a269e6b0e 🐛(y-provider) fix loop when no cookies
We observed a huge amount of logs sometimes in
the y-provider server logs, all related to the
same error: "No cookies".
When this happens, the client keeps trying to
reconnect, and the server keeps logging the error,
creating a loop.
We stop the loop by checking if the error is a
"No cookies" error, and if so, we don't
try to reconnect.
2026-03-23 11:53:55 +01:00
Anthony LC d9d7b70b71 ♻️(frontend) refacto Version modal to fit with the design system
We refactored the version modal to fit
the design system. We removed some dead code and
fixed some state issues.
2026-03-23 10:58:50 +01:00
Anthony LC a4326366c2 🐛(frontend) fix leftpanel button in doc version
The left panel button was shown in the doc version page.
This commit removes the button from the doc version
page by moving it to the DocLayout.
By moving it to the DocLayout, we do not have the
flickering when we switch between subpages.
2026-03-23 10:33:05 +01:00
Anthony LC 1d7b57e03d 🐛(frontend) fix close panel when click on subdoc
Recent refacto of left panel components caused
the close panel function to stop working when
clicking on a subdoc.
This commit fixes that issue by ensuring that the
close panel function is properly called when
a subdoc is clicked.
2026-03-23 10:11:19 +01:00
Manuel Raynaud c4c6c22e42 ♻️(backend) skip saving in database a document when payload is empty
The frontend application is making PATCH request with an empty body.
This PATCH request is not making any change but an UPDATE sql query is
made, the `updated_at` field is the only one updated. When can skip this
save in the databse by returning the Document instance in the serializer
update method
2026-03-21 10:33:02 +01:00
Manuel Raynaud 10a8eccc71 (backend) add missing update api test using the PATCH method
No tests were made using the PATCH method to update a Document using the
API. The frontend appllication mostly use the patch method instead of
the PUT method.
2026-03-21 10:15:50 +01:00
Manuel Raynaud 728332f8f7 (backend) assert document path can not change during API update
We want to assert on every succesful update test that the document path
has not change.
2026-03-21 10:15:49 +01:00
Manuel Raynaud 487b95c207 🐛(backend) allow using search endpoint without refresh token enabled
The search endpoint was using the refresh_roken method decorator. This
decorator force having a valid refresh token stored in the session for
the entire viewset. The search endpoint still allow having the legacy
search behavior and for this we don't need to configure at all the OIDC
refrsh mechanism.
2026-03-21 08:22:45 +00:00
Cyril d23b38e478 (frontend) add markdown copy icon for Copy as Markdown option
Replace generic copy icon with dedicated markdown_copy SVG in DocToolBox.
2026-03-20 15:41:03 +01:00
Cyril d6333c9b81 ️(frontend) fix aria-labels for table of contents nav vs buttons
Screen readers announce nav as "Sommaire, navigation" and button as toggle.
2026-03-20 15:04:29 +01:00
renovate[bot] 03b6c6a206 ⬆️(dependencies) update next to v16.1.7 [SECURITY] 2026-03-20 13:12:08 +00:00
Cyril aadabf8d3c ️(frontend) announce formatting shortcuts for screen readers
Announce formatting shortcuts (headings, lists, paragraph, code block).
2026-03-20 12:56:38 +01:00
Cyril 2a708d6e46 ️(frontend) add format to export download btn aria-label
Add format to export button aria-label for a11y. DRY format options.
2026-03-20 11:27:02 +01:00
Cyril b47c730e19 ️(frontend) announce search results through a live region
Announce result count updates while focus stays in input (#2043)
2026-03-20 10:47:18 +01:00
Cyril cef83067e6 ️(frontend) restore focus to input after search filter reset
Move filters out of listbox and refocus the search input (#2044)
2026-03-20 10:47:03 +01:00
Cyril 4cabfcc921 ️(frontend) update aria-expanded dynamically on search combobox
Override cmdk aria-expanded via ref to reflect list state (#2039)
2026-03-20 10:47:02 +01:00
Cyril b8d4b0a044 ️(frontend) add label text to search input field
Pass label prop to QuickSearch to render non-empty label (#2041)
2026-03-20 10:47:01 +01:00
Cyril 71c4d2921b ️(frontend) add explicit aria-label to search reset button
Add aria-label to clarify the reset button resets filters (#2042)
2026-03-20 10:47:01 +01:00
Cyril d1636dee13 ️(frontend) set empty alt on decorative search image
Set alt="" on decorative empty state image in search modal (#2038)
2026-03-20 10:46:49 +01:00
Cyril bf93640af8 ️(frontend) skip link as anchor instead of button
Replace button with anchor link
2026-03-20 10:05:42 +01:00
Cyril da79c310ae ️(frontend) focus skip link on headings and skip grid dropzone
We land keyboard users on page headings and keep the grid dropzone untabbable.
2026-03-20 10:05:33 +01:00
Cyril 99c486571d ️(frontend) fix more options menu feedback for screen readers
Pin/unpin: vocal announce only. Duplicate, copy: toast only.
2026-03-19 18:34:24 +01:00
Cyril cdf3161869 ️(frontend) use aria-label trad for version history modal #2023
Replace hardcoded aria-label with aria-lab trad.
2026-03-19 15:52:28 +01:00
Cyril ef108227b3 ️(frontend) improve version history list accessibility
Dynamic aria-label per version, aria-pressed + live region
2026-03-19 14:04:59 +01:00
Anthony LC 9991820cb1 🔊(CHANGELOG) fix entries changelog
The changelog was not updated correctly.
By not updating correctly, the changelog was not
showing the correct entries for the release,
leading to a patch release instead of a minor
release.
2026-03-19 13:36:48 +01:00
Anthony LC 2801ece358 ️(frontend) change aria-label for help menu button
The help menu button's aria-label was
previously "Open onboarding menu", which was not
accurate and could be confusing for screen reader
users. This commit updates the aria-label to
"Open help menu" to better reflect the button's
purpose and improve accessibility.
2026-03-19 13:31:03 +01:00
Anthony LC 0b37996899 💫(frontend) fix the help button to the bottom in tree
The tree take a bit of time to load, during this
time the help button was not at the bottom of
the left panel. To fix this issue, we addded a
skeleton for the tree in wait for the tree to
load, by doing this, the help button
is always at the bottom.
2026-03-19 13:28:22 +01:00
128 changed files with 7004 additions and 4232 deletions
+59 -6
View File
@@ -6,12 +6,63 @@ and this project adheres to
## [Unreleased]
## [v4.8.4] - 2026-03-25
### Added
- 🚸(frontend) hint min char search users #2064
### Changed
- 💄(frontend) improve comments highlights #1961
- ♿️(frontend) improve BoxButton a11y and native button semantics #2103
- ♿️(frontend) improve language picker accessibility #2069
- ♿️(frontend) add aria-hidden to decorative icons in dropdown menu #2093
- 🐛(backend) move lock table closer to the insert operation targeted
### Fixed
- 🐛(y-provider) destroy Y.Doc instances after each convert request #2129
- 🐛(backend) remove deleted sub documents in favorite_list endpoint #2083
## [v4.8.3] - 2026-03-23
### Changed
- ♿️(frontend) improve version history list accessibility #2033
- ♿(frontend) focus skip link on headings and skip grid dropzone #1983
- ♿️(frontend) add sr-only format to export download button #2088
- ♿️(frontend) announce formatting shortcuts for screen readers #2070
- ✨(frontend) add markdown copy icon for Copy as Markdown option #2096
- ♻️(backend) skip saving in database a document when payload is empty #2062
- ♻️(frontend) refacto Version modal to fit with the design system #2091
- ⚡️(frontend) add debounce WebSocket reconnect #2104
### Fixed
- ♿️(frontend) fix more options menu feedback for screen readers #2071
- ♿️(frontend) fix more options menu feedback for screen readers #2071
- 💫(frontend) fix the help button to the bottom in tree #2073
- ♿️(frontend) fix aria-labels for table of contents #2065
- 🐛(backend) allow using search endpoint without refresh token enabled #2097
- 🐛(frontend) fix close panel when click on subdoc #2094
- 🐛(frontend) fix leftpanel button in doc version #9238
- 🐛(y-provider) fix loop when no cookies #2101
## [v4.8.2] - 2026-03-19
### Added
- ✨(backend) add resource server api #1923
- ✨(frontend) activate Find search #1834
- ✨ handle searching on subdocuments #1834
- ✨(backend) add search feature flags #1897
### Changed
- ♿️(frontend) ensure doc title is h1 for accessibility #2006
- ♿️(frontend) add nb accesses in share button aria-label #2017
- ✨(backend) improve fallback logic on search endpoint #1834
### Fixed
@@ -25,6 +76,11 @@ and this project adheres to
- 🐛(backend) stop using add_sibling method to create sandbox document #2084
- 🐛(backend) duplicate a document as last-sibling #2084
### Removed
- 🔥(api) remove `documents/<document_id>/descendants/` endpoint #1834
- 🔥(api) remove pagination on `documents/search/` endpoint #1834
## [v4.8.1] - 2026-03-17
### Added
@@ -41,7 +97,6 @@ and this project adheres to
- ✨(backend) add a is_first_connection flag to the User model #1938
- ✨(frontend) add onboarding modal with help menu button #1868
- ✨(backend) add resource server api #1923
### Changed
@@ -127,16 +182,12 @@ and this project adheres to
- ✨(frontend) Add stat for Crisp #1824
- ✨(auth) add silent login #1690
- 🔧(project) add DJANGO_EMAIL_URL_APP environment variable #1825
- ✨(frontend) activate Find search #1834
- ✨ handle searching on subdocuments #1834
- ✨(backend) add search feature flags #1897
### Changed
- ♿(frontend) improve accessibility:
- ♿️(frontend) fix subdoc opening and emoji pick focus #1745
- ✨(backend) add field for button label in email template #1817
- ✨(backend) improve fallback logic on search endpoint #1834
### Fixed
@@ -1141,7 +1192,9 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- 🚀 Impress, project to manage your documents easily and collaboratively.
[unreleased]: https://github.com/suitenumerique/docs/compare/v4.8.2...main
[unreleased]: https://github.com/suitenumerique/docs/compare/v4.8.4...main
[v4.8.4]: https://github.com/suitenumerique/docs/releases/v4.8.4
[v4.8.3]: https://github.com/suitenumerique/docs/releases/v4.8.3
[v4.8.2]: https://github.com/suitenumerique/docs/releases/v4.8.2
[v4.8.1]: https://github.com/suitenumerique/docs/releases/v4.8.1
[v4.8.0]: https://github.com/suitenumerique/docs/releases/v4.8.0
+2 -2
View File
@@ -61,8 +61,8 @@ OIDC_RS_AUDIENCE_CLAIM="client_id" # The claim used to identify the audience
OIDC_RS_ALLOWED_AUDIENCES=""
# Store OIDC tokens in the session. Needed by search/ endpoint.
OIDC_STORE_ACCESS_TOKEN=True
OIDC_STORE_REFRESH_TOKEN=True # Store the encrypted refresh token in the session.
# OIDC_STORE_ACCESS_TOKEN=True
# OIDC_STORE_REFRESH_TOKEN=True # Store the encrypted refresh token in the session.
# Must be a valid Fernet key (32 url-safe base64-encoded bytes)
# To create one, use the bin/fernetkey command.
+3
View File
@@ -60,10 +60,13 @@
"groupName": "ignored js dependencies",
"matchManagers": ["npm"],
"matchPackageNames": [
"@react-pdf/renderer",
"fetch-mock",
"node",
"node-fetch",
"react-resizable-panels",
"stylelint",
"stylelint-config-standard",
"workbox-webpack-plugin"
]
}
+9
View File
@@ -300,6 +300,15 @@ class DocumentSerializer(ListDocumentSerializer):
return file
def update(self, instance, validated_data):
"""
When no data is sent on the update, skip making the update in the database and return
directly the instance unchanged.
"""
if not validated_data:
return instance # No data provided, skip the update
return super().update(instance, validated_data)
def save(self, **kwargs):
"""
Process the content field to extract attachment keys and update the document's
+15
View File
@@ -6,8 +6,10 @@ from abc import ABC, abstractmethod
from django.conf import settings
from django.core.cache import cache
from django.core.files.storage import default_storage
from django.utils.decorators import method_decorator
import botocore
from lasuite.oidc_login.decorators import refresh_oidc_access_token
from rest_framework.throttling import BaseThrottle
@@ -91,6 +93,19 @@ def generate_s3_authorization_headers(key):
return request
def conditional_refresh_oidc_token(func):
"""
Conditionally apply refresh_oidc_access_token decorator.
The decorator is only applied if OIDC_STORE_REFRESH_TOKEN is True, meaning
we can actually refresh something. Broader settings checks are done in settings.py.
"""
if settings.OIDC_STORE_REFRESH_TOKEN:
return method_decorator(refresh_oidc_access_token)(func)
return func
class AIBaseRateThrottle(BaseThrottle, ABC):
"""Base throttle class for AI-related rate limiting with backoff."""
+16 -17
View File
@@ -25,7 +25,6 @@ from django.db.models.functions import Greatest, Left, Length
from django.http import Http404, StreamingHttpResponse
from django.urls import reverse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.http import content_disposition_header
from django.utils.text import capfirst, slugify
@@ -38,7 +37,6 @@ from botocore.exceptions import ClientError
from csp.constants import NONE
from csp.decorators import csp_update
from lasuite.malware_detection import malware_detection
from lasuite.oidc_login.decorators import refresh_oidc_access_token
from lasuite.tools.email import get_domain_from_email
from pydantic import ValidationError as PydanticValidationError
from rest_framework import filters, status, viewsets
@@ -676,17 +674,8 @@ class DocumentViewSet(
return drf.response.Response(serializer.data)
@transaction.atomic
def perform_create(self, serializer):
"""Set the current user as creator and owner of the newly created object."""
# locks the table to ensure safe concurrent access
with connection.cursor() as cursor:
cursor.execute(
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
"IN SHARE ROW EXCLUSIVE MODE;"
)
# Remove file from validated_data as it's not a model field
# Process it if present
uploaded_file = serializer.validated_data.pop("file", None)
@@ -704,15 +693,25 @@ class DocumentViewSet(
)
serializer.validated_data["content"] = converted_content
serializer.validated_data["title"] = uploaded_file.name
logger.info("conversion ended successfully")
except ConversionError as err:
logger.error("could not convert file content with error: %s", err)
raise drf.exceptions.ValidationError(
{"file": ["Could not convert file content"]}
) from err
obj = models.Document.add_root(
creator=self.request.user,
**serializer.validated_data,
)
with transaction.atomic():
# locks the table to ensure safe concurrent access
with connection.cursor() as cursor:
cursor.execute(
f'LOCK TABLE "{models.Document._meta.db_table}" ' # noqa: SLF001
"IN SHARE ROW EXCLUSIVE MODE;"
)
obj = models.Document.add_root(
creator=self.request.user,
**serializer.validated_data,
)
serializer.instance = obj
models.DocumentAccess.objects.create(
document=obj,
@@ -829,6 +828,7 @@ class DocumentViewSet(
queryset = self.queryset.filter(path_list)
queryset = queryset.filter(id__in=favorite_documents_ids)
queryset = queryset.filter(ancestors_deleted_at__isnull=True)
queryset = queryset.annotate_user_roles(user)
queryset = queryset.annotate(
is_favorite=db.Value(True, output_field=db.BooleanField())
@@ -1415,7 +1415,7 @@ class DocumentViewSet(
return duplicated_document
@drf.decorators.action(detail=False, methods=["get"], url_path="search")
@method_decorator(refresh_oidc_access_token)
@utils.conditional_refresh_oidc_token
def search(self, request, *args, **kwargs):
"""
Returns an ordered list of documents best matching the search query parameter 'q'.
@@ -1426,7 +1426,6 @@ class DocumentViewSet(
params = serializers.SearchDocumentSerializer(data=request.query_params)
params.is_valid(raise_exception=True)
search_type = self._get_search_type()
if search_type == SearchType.TITLE:
return self._title_search(request, params.validated_data, *args, **kwargs)
+10 -11
View File
@@ -267,6 +267,16 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
if settings.USER_ONBOARDING_SANDBOX_DOCUMENT:
# transaction.atomic is used in a context manager to avoid a transaction if
# the settings USER_ONBOARDING_SANDBOX_DOCUMENT is unused
sandbox_id = settings.USER_ONBOARDING_SANDBOX_DOCUMENT
try:
template_document = Document.objects.get(id=sandbox_id)
except Document.DoesNotExist:
logger.warning(
"Onboarding sandbox document with id %s does not exist. Skipping.",
sandbox_id,
)
return
with transaction.atomic():
# locks the table to ensure safe concurrent access
with connection.cursor() as cursor:
@@ -274,17 +284,6 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
f'LOCK TABLE "{Document._meta.db_table}" ' # noqa: SLF001
"IN SHARE ROW EXCLUSIVE MODE;"
)
sandbox_id = settings.USER_ONBOARDING_SANDBOX_DOCUMENT
try:
template_document = Document.objects.get(id=sandbox_id)
except Document.DoesNotExist:
logger.warning(
"Onboarding sandbox document with id %s does not exist. Skipping.",
sandbox_id,
)
return
sandbox_document = Document.add_root(
title=template_document.title,
content=template_document.content,
@@ -45,6 +45,8 @@ class Converter:
def convert(self, data, content_type, accept):
"""Convert input into other formats using external microservices."""
logger.info("converting content from %s to %s", content_type, accept)
if content_type == mime_types.DOCX and accept == mime_types.YJS:
blocknote_data = self.docspec.convert(
data, mime_types.DOCX, mime_types.BLOCKNOTE
@@ -114,3 +114,29 @@ def test_api_document_favorite_list_with_favorite_children():
assert content[0]["id"] == str(children[0].id)
assert content[1]["id"] == str(children[1].id)
assert content[2]["id"] == str(access.document.id)
def test_api_document_favorite_list_with_deleted_child():
"""
Authenticated users should not see deleted documents in their favorite list.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
root = factories.DocumentFactory(creator=user, users=[user], favorited_by=[user])
child1, child2 = factories.DocumentFactory.create_batch(
2, parent=root, favorited_by=[user]
)
child1.delete()
response = client.get("/api/v1.0/documents/favorite_list/")
assert response.status_code == 200
assert response.json()["count"] == 2
content = response.json()["results"]
assert content[0]["id"] == str(root.id)
assert content[1]["id"] == str(child2.id)
@@ -70,17 +70,20 @@ def test_api_documents_search_anonymous(search_query, indexer_settings):
@mock.patch("core.api.viewsets.DocumentViewSet.list")
def test_api_documents_search_fall_back_on_search_list(mock_list, indexer_settings):
def test_api_documents_search_fall_back_on_search_list(mock_list, settings):
"""
When indexer is not configured and no path is provided,
should fall back on list method
"""
indexer_settings.SEARCH_URL = None
assert get_document_indexer() is None
assert settings.OIDC_STORE_REFRESH_TOKEN is False
assert settings.OIDC_STORE_ACCESS_TOKEN is False
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
client.force_login(
user, backend="core.authentication.backends.OIDCAuthenticationBackend"
)
mocked_response = {
"count": 0,
@@ -93,6 +96,8 @@ def test_api_documents_search_fall_back_on_search_list(mock_list, indexer_settin
q = "alpha"
response = client.get("/api/v1.0/documents/search/", data={"q": q})
assert response.status_code == 200
assert mock_list.call_count == 1
assert mock_list.call_args[0][0].GET.get("q") == q
assert response.json() == mocked_response
@@ -100,18 +105,21 @@ def test_api_documents_search_fall_back_on_search_list(mock_list, indexer_settin
@mock.patch("core.api.viewsets.DocumentViewSet._list_descendants")
def test_api_documents_search_fallback_on_search_list_sub_docs(
mock_list_descendants, indexer_settings
mock_list_descendants, settings
):
"""
When indexer is not configured and path parameter is provided,
should call _list_descendants() method
"""
indexer_settings.SEARCH_URL = "http://find/api/v1.0/search"
assert get_document_indexer() is not None
assert get_document_indexer() is None
assert settings.OIDC_STORE_REFRESH_TOKEN is False
assert settings.OIDC_STORE_ACCESS_TOKEN is False
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
client.force_login(
user, backend="core.authentication.backends.OIDCAuthenticationBackend"
)
parent = factories.DocumentFactory(title="parent", users=[user])
@@ -128,9 +136,9 @@ def test_api_documents_search_fallback_on_search_list_sub_docs(
"/api/v1.0/documents/search/", data={"q": q, "path": parent.path}
)
assert mock_list_descendants.call_count == 1
assert mock_list_descendants.call_args[0][0].GET.get("q") == q
assert mock_list_descendants.call_args[0][0].GET.get("path") == parent.path
mock_list_descendants.assert_called_with(
mock.ANY, {"q": "alpha", "path": parent.path}
)
assert response.json() == mocked_response
@@ -152,7 +160,9 @@ def test_api_documents_search_indexer_crashes(mock_title_search, indexer_setting
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
client.force_login(
user, backend="core.authentication.backends.OIDCAuthenticationBackend"
)
mocked_response = {
"count": 0,
@@ -185,7 +195,9 @@ def test_api_documents_search_invalid_params(indexer_settings):
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
client.force_login(
user, backend="core.authentication.backends.OIDCAuthenticationBackend"
)
response = client.get("/api/v1.0/documents/search/")
@@ -1,8 +1,10 @@
"""
Tests for Documents API endpoint in impress's core app: update
"""
# pylint: disable=too-many-lines
import random
from unittest.mock import patch
from django.contrib.auth.models import AnonymousUser
from django.core.cache import cache
@@ -17,6 +19,25 @@ from core.tests.conftest import TEAM, USER, VIA
pytestmark = pytest.mark.django_db
# A valid Yjs document derived from YDOC_HELLO_WORLD_BASE64 with "Hello" replaced by "World",
# used in PATCH tests to guarantee a real content change distinct from what DocumentFactory
# produces.
YDOC_UPDATED_CONTENT_BASE64 = (
"AR717vLVDgAHAQ5kb2N1bWVudC1zdG9yZQMKYmxvY2tHcm91cAcA9e7y1Q4AAw5ibG9ja0NvbnRh"
"aW5lcgcA9e7y1Q4BAwdoZWFkaW5nBwD17vLVDgIGBgD17vLVDgMGaXRhbGljAnt9hPXu8tUOBAVX"
"b3JsZIb17vLVDgkGaXRhbGljBG51bGwoAPXu8tUOAg10ZXh0QWxpZ25tZW50AXcEbGVmdCgA9e7y"
"1Q4CBWxldmVsAX0BKAD17vLVDgECaWQBdyQwNGQ2MjM0MS04MzI2LTQyMzYtYTA4My00ODdlMjZm"
"YWQyMzAoAPXu8tUOAQl0ZXh0Q29sb3IBdwdkZWZhdWx0KAD17vLVDgEPYmFja2dyb3VuZENvbG9y"
"AXcHZGVmYXVsdIf17vLVDgEDDmJsb2NrQ29udGFpbmVyBwD17vLVDhADDmJ1bGxldExpc3RJdGVt"
"BwD17vLVDhEGBAD17vLVDhIBd4b17vLVDhMEYm9sZAJ7fYT17vLVDhQCb3KG9e7y1Q4WBGJvbGQE"
"bnVsbIT17vLVDhcCbGQoAPXu8tUOEQ10ZXh0QWxpZ25tZW50AXcEbGVmdCgA9e7y1Q4QAmlkAXck"
"ZDM1MWUwNjgtM2U1NS00MjI2LThlYTUtYWJiMjYzMTk4ZTJhKAD17vLVDhAJdGV4dENvbG9yAXcH"
"ZGVmYXVsdCgA9e7y1Q4QD2JhY2tncm91bmRDb2xvcgF3B2RlZmF1bHSH9e7y1Q4QAw5ibG9ja0Nv"
"bnRhaW5lcgcA9e7y1Q4eAwlwYXJhZ3JhcGgoAPXu8tUOHw10ZXh0QWxpZ25tZW50AXcEbGVmdCgA"
"9e7y1Q4eAmlkAXckODk3MDBjMDctZTBlMS00ZmUwLWFjYTItODQ5MzIwOWE3ZTQyKAD17vLVDh4J"
"dGV4dENvbG9yAXcHZGVmYXVsdCgA9e7y1Q4eD2JhY2tncm91bmRDb2xvcgF3B2RlZmF1bHQA"
)
@pytest.mark.parametrize("via_parent", [True, False])
@pytest.mark.parametrize(
@@ -330,6 +351,7 @@ def test_api_documents_update_authenticated_no_websocket(settings):
ws_resp = responses.get(endpoint_url, json={"count": 0, "exists": False})
assert cache.get(f"docs:no-websocket:{document.id}") is None
old_path = document.path
response = client.put(
f"/api/v1.0/documents/{document.id!s}/",
@@ -338,6 +360,8 @@ def test_api_documents_update_authenticated_no_websocket(settings):
)
assert response.status_code == 200
document.refresh_from_db()
assert document.path == old_path
assert cache.get(f"docs:no-websocket:{document.id}") == session_key
assert ws_resp.call_count == 1
@@ -446,6 +470,7 @@ def test_api_documents_update_user_connected_to_websocket(settings):
ws_resp = responses.get(endpoint_url, json={"count": 3, "exists": True})
assert cache.get(f"docs:no-websocket:{document.id}") is None
old_path = document.path
response = client.put(
f"/api/v1.0/documents/{document.id!s}/",
@@ -453,6 +478,9 @@ def test_api_documents_update_user_connected_to_websocket(settings):
format="json",
)
assert response.status_code == 200
document.refresh_from_db()
assert document.path == old_path
assert cache.get(f"docs:no-websocket:{document.id}") is None
assert ws_resp.call_count == 1
@@ -486,6 +514,7 @@ def test_api_documents_update_websocket_server_unreachable_fallback_to_no_websoc
ws_resp = responses.get(endpoint_url, status=500)
assert cache.get(f"docs:no-websocket:{document.id}") is None
old_path = document.path
response = client.put(
f"/api/v1.0/documents/{document.id!s}/",
@@ -494,6 +523,8 @@ def test_api_documents_update_websocket_server_unreachable_fallback_to_no_websoc
)
assert response.status_code == 200
document.refresh_from_db()
assert document.path == old_path
assert cache.get(f"docs:no-websocket:{document.id}") == session_key
assert ws_resp.call_count == 1
@@ -605,6 +636,7 @@ def test_api_documents_update_force_websocket_param_to_true(settings):
ws_resp = responses.get(endpoint_url, status=500)
assert cache.get(f"docs:no-websocket:{document.id}") is None
old_path = document.path
response = client.put(
f"/api/v1.0/documents/{document.id!s}/",
@@ -613,6 +645,8 @@ def test_api_documents_update_force_websocket_param_to_true(settings):
)
assert response.status_code == 200
document.refresh_from_db()
assert document.path == old_path
assert cache.get(f"docs:no-websocket:{document.id}") is None
assert ws_resp.call_count == 0
@@ -643,6 +677,7 @@ def test_api_documents_update_feature_flag_disabled(settings):
ws_resp = responses.get(endpoint_url, status=500)
assert cache.get(f"docs:no-websocket:{document.id}") is None
old_path = document.path
response = client.put(
f"/api/v1.0/documents/{document.id!s}/",
@@ -651,6 +686,8 @@ def test_api_documents_update_feature_flag_disabled(settings):
)
assert response.status_code == 200
document.refresh_from_db()
assert document.path == old_path
assert cache.get(f"docs:no-websocket:{document.id}") is None
assert ws_resp.call_count == 0
@@ -716,3 +753,724 @@ def test_api_documents_update_invalid_content():
)
assert response.status_code == 400
assert response.json() == {"content": ["Invalid base64 content."]}
# =============================================================================
# PATCH tests
# =============================================================================
@pytest.mark.parametrize("via_parent", [True, False])
@pytest.mark.parametrize(
"reach, role",
[
("restricted", "reader"),
("restricted", "editor"),
("authenticated", "reader"),
("authenticated", "editor"),
("public", "reader"),
],
)
def test_api_documents_patch_anonymous_forbidden(reach, role, via_parent):
"""
Anonymous users should not be allowed to patch a document when link
configuration does not allow it.
"""
if via_parent:
grand_parent = factories.DocumentFactory(link_reach=reach, link_role=role)
parent = factories.DocumentFactory(parent=grand_parent, link_reach="restricted")
document = factories.DocumentFactory(parent=parent, link_reach="restricted")
else:
document = factories.DocumentFactory(link_reach=reach, link_role=role)
old_document_values = serializers.DocumentSerializer(instance=document).data
new_content = YDOC_UPDATED_CONTENT_BASE64
response = APIClient().patch(
f"/api/v1.0/documents/{document.id!s}/",
{"content": new_content},
format="json",
)
assert response.status_code == 401
assert response.json() == {
"detail": "Authentication credentials were not provided."
}
document.refresh_from_db()
assert serializers.DocumentSerializer(instance=document).data == old_document_values
@pytest.mark.parametrize("via_parent", [True, False])
@pytest.mark.parametrize(
"reach,role",
[
("public", "reader"),
("authenticated", "reader"),
("restricted", "reader"),
("restricted", "editor"),
],
)
def test_api_documents_patch_authenticated_unrelated_forbidden(reach, role, via_parent):
"""
Authenticated users should not be allowed to patch a document to which
they are not related if the link configuration does not allow it.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
if via_parent:
grand_parent = factories.DocumentFactory(link_reach=reach, link_role=role)
parent = factories.DocumentFactory(parent=grand_parent, link_reach="restricted")
document = factories.DocumentFactory(parent=parent, link_reach="restricted")
else:
document = factories.DocumentFactory(link_reach=reach, link_role=role)
old_document_values = serializers.DocumentSerializer(instance=document).data
new_content = YDOC_UPDATED_CONTENT_BASE64
response = client.patch(
f"/api/v1.0/documents/{document.id!s}/",
{"content": new_content},
format="json",
)
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
document.refresh_from_db()
assert serializers.DocumentSerializer(instance=document).data == old_document_values
@pytest.mark.parametrize("via_parent", [True, False])
@pytest.mark.parametrize(
"is_authenticated,reach,role",
[
(False, "public", "editor"),
(True, "public", "editor"),
(True, "authenticated", "editor"),
],
)
def test_api_documents_patch_anonymous_or_authenticated_unrelated(
is_authenticated, reach, role, via_parent
):
"""
Anonymous and authenticated users should be able to patch a document to which
they are not related if the link configuration allows it.
"""
client = APIClient()
if is_authenticated:
user = factories.UserFactory(with_owned_document=True)
client.force_login(user)
if via_parent:
grand_parent = factories.DocumentFactory(link_reach=reach, link_role=role)
parent = factories.DocumentFactory(parent=grand_parent, link_reach="restricted")
document = factories.DocumentFactory(parent=parent, link_reach="restricted")
else:
document = factories.DocumentFactory(link_reach=reach, link_role=role)
old_document_values = serializers.DocumentSerializer(instance=document).data
old_path = document.path
new_content = YDOC_UPDATED_CONTENT_BASE64
response = client.patch(
f"/api/v1.0/documents/{document.id!s}/",
{"content": new_content, "websocket": True},
format="json",
)
assert response.status_code == 200
# Using document.refresh_from_db does not wirk because the content is in cache.
# Force reloading it by fetching the document in the database.
document = models.Document.objects.get(id=document.id)
assert document.path == old_path
assert document.content == new_content
document_values = serializers.DocumentSerializer(instance=document).data
for key in [
"id",
"title",
"link_reach",
"link_role",
"creator",
"depth",
"numchild",
"path",
]:
assert document_values[key] == old_document_values[key]
@pytest.mark.parametrize("via_parent", [True, False])
@pytest.mark.parametrize("via", VIA)
def test_api_documents_patch_authenticated_reader(via, via_parent, mock_user_teams):
"""Users who are reader of a document should not be allowed to patch it."""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
if via_parent:
grand_parent = factories.DocumentFactory(link_reach="restricted")
parent = factories.DocumentFactory(parent=grand_parent, link_reach="restricted")
document = factories.DocumentFactory(parent=parent, link_reach="restricted")
access_document = grand_parent
else:
document = factories.DocumentFactory(link_reach="restricted")
access_document = document
if via == USER:
factories.UserDocumentAccessFactory(
document=access_document, user=user, role="reader"
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=access_document, team="lasuite", role="reader"
)
old_document_values = serializers.DocumentSerializer(instance=document).data
new_content = YDOC_UPDATED_CONTENT_BASE64
response = client.patch(
f"/api/v1.0/documents/{document.id!s}/",
{"content": new_content},
format="json",
)
assert response.status_code == 403
assert response.json() == {
"detail": "You do not have permission to perform this action."
}
document.refresh_from_db()
assert serializers.DocumentSerializer(instance=document).data == old_document_values
@pytest.mark.parametrize("via_parent", [True, False])
@pytest.mark.parametrize("role", ["editor", "administrator", "owner"])
@pytest.mark.parametrize("via", VIA)
def test_api_documents_patch_authenticated_editor_administrator_or_owner(
via, role, via_parent, mock_user_teams
):
"""A user who is editor, administrator or owner of a document should be allowed to patch it."""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
if via_parent:
grand_parent = factories.DocumentFactory(link_reach="restricted")
parent = factories.DocumentFactory(parent=grand_parent, link_reach="restricted")
document = factories.DocumentFactory(parent=parent, link_reach="restricted")
access_document = grand_parent
else:
document = factories.DocumentFactory(link_reach="restricted")
access_document = document
if via == USER:
factories.UserDocumentAccessFactory(
document=access_document, user=user, role=role
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=access_document, team="lasuite", role=role
)
old_document_values = serializers.DocumentSerializer(instance=document).data
old_path = document.path
new_content = YDOC_UPDATED_CONTENT_BASE64
response = client.patch(
f"/api/v1.0/documents/{document.id!s}/",
{"content": new_content, "websocket": True},
format="json",
)
assert response.status_code == 200
# Using document.refresh_from_db does not wirk because the content is in cache.
# Force reloading it by fetching the document in the database.
document = models.Document.objects.get(id=document.id)
assert document.path == old_path
assert document.content == new_content
document_values = serializers.DocumentSerializer(instance=document).data
for key in [
"id",
"title",
"link_reach",
"link_role",
"creator",
"depth",
"numchild",
"path",
"nb_accesses_ancestors",
"nb_accesses_direct",
]:
assert document_values[key] == old_document_values[key]
@responses.activate
def test_api_documents_patch_authenticated_no_websocket(settings):
"""
When a user patches the document, not connected to the websocket and is the first to update,
the document should be updated.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "editor")])
new_content = YDOC_UPDATED_CONTENT_BASE64
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, json={"count": 0, "exists": False})
assert cache.get(f"docs:no-websocket:{document.id}") is None
old_path = document.path
response = client.patch(
f"/api/v1.0/documents/{document.id!s}/",
{"content": new_content},
format="json",
)
assert response.status_code == 200
# Using document.refresh_from_db does not work because the content is cached.
# Force reloading it by fetching the document from the database.
document = models.Document.objects.get(id=document.id)
assert document.path == old_path
assert document.content == new_content
assert cache.get(f"docs:no-websocket:{document.id}") == session_key
assert ws_resp.call_count == 1
@responses.activate
def test_api_documents_patch_authenticated_no_websocket_user_already_editing(settings):
"""
When a user patches the document, not connected to the websocket and is not the first to
update, the document should not be updated.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "editor")])
new_content = YDOC_UPDATED_CONTENT_BASE64
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, json={"count": 0, "exists": False})
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
response = client.patch(
f"/api/v1.0/documents/{document.id!s}/",
{"content": new_content},
format="json",
)
assert response.status_code == 403
assert response.json() == {"detail": "You are not allowed to edit this document."}
assert ws_resp.call_count == 1
@responses.activate
def test_api_documents_patch_no_websocket_other_user_connected_to_websocket(settings):
"""
When a user patches the document, not connected to the websocket and another user is connected
to the websocket, the document should not be updated.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "editor")])
new_content = YDOC_UPDATED_CONTENT_BASE64
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, json={"count": 3, "exists": False})
assert cache.get(f"docs:no-websocket:{document.id}") is None
response = client.patch(
f"/api/v1.0/documents/{document.id!s}/",
{"content": new_content},
format="json",
)
assert response.status_code == 403
assert response.json() == {"detail": "You are not allowed to edit this document."}
assert cache.get(f"docs:no-websocket:{document.id}") is None
assert ws_resp.call_count == 1
@responses.activate
def test_api_documents_patch_user_connected_to_websocket(settings):
"""
When a user patches the document while connected to the websocket, the document should be
updated.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "editor")])
new_content = YDOC_UPDATED_CONTENT_BASE64
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, json={"count": 3, "exists": True})
assert cache.get(f"docs:no-websocket:{document.id}") is None
old_path = document.path
response = client.patch(
f"/api/v1.0/documents/{document.id!s}/",
{"content": new_content},
format="json",
)
assert response.status_code == 200
# Using document.refresh_from_db does not wirk because the content is in cache.
# Force reloading it by fetching the document in the database.
document = models.Document.objects.get(id=document.id)
assert document.path == old_path
assert document.content == new_content
assert cache.get(f"docs:no-websocket:{document.id}") is None
assert ws_resp.call_count == 1
@responses.activate
def test_api_documents_patch_websocket_server_unreachable_fallback_to_no_websocket(
settings,
):
"""
When the websocket server is unreachable, the patch should be applied like if the user was
not connected to the websocket.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "editor")])
new_content = YDOC_UPDATED_CONTENT_BASE64
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, status=500)
assert cache.get(f"docs:no-websocket:{document.id}") is None
old_path = document.path
response = client.patch(
f"/api/v1.0/documents/{document.id!s}/",
{"content": new_content},
format="json",
)
assert response.status_code == 200
# Using document.refresh_from_db does not work because the content is cached.
# Force reloading it by fetching the document from the database.
document = models.Document.objects.get(id=document.id)
assert document.path == old_path
assert document.content == new_content
assert cache.get(f"docs:no-websocket:{document.id}") == session_key
assert ws_resp.call_count == 1
@responses.activate
def test_api_documents_patch_websocket_server_unreachable_fallback_to_no_websocket_other_users(
settings,
):
"""
When the websocket server is unreachable, the behavior falls back to no-websocket.
If another user is already editing, the patch must be denied.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "editor")])
new_content = YDOC_UPDATED_CONTENT_BASE64
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, status=500)
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
response = client.patch(
f"/api/v1.0/documents/{document.id!s}/",
{"content": new_content},
format="json",
)
assert response.status_code == 403
assert cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
assert ws_resp.call_count == 1
@responses.activate
def test_api_documents_patch_websocket_server_room_not_found_fallback_to_no_websocket_other_users(
settings,
):
"""
When the WebSocket server does not have the room created, the logic should fallback to
no-WebSocket. If another user is already editing, the patch must be denied.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "editor")])
new_content = YDOC_UPDATED_CONTENT_BASE64
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, status=404)
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
response = client.patch(
f"/api/v1.0/documents/{document.id!s}/",
{"content": new_content},
format="json",
)
assert response.status_code == 403
assert cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
assert ws_resp.call_count == 1
@responses.activate
def test_api_documents_patch_force_websocket_param_to_true(settings):
"""
When the websocket parameter is set to true, the patch should be applied without any check.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "editor")])
new_content = YDOC_UPDATED_CONTENT_BASE64
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, status=500)
assert cache.get(f"docs:no-websocket:{document.id}") is None
old_path = document.path
response = client.patch(
f"/api/v1.0/documents/{document.id!s}/",
{"content": new_content, "websocket": True},
format="json",
)
assert response.status_code == 200
# Using document.refresh_from_db does not work because the content is cached.
# Force reloading it by fetching the document from the database.
document = models.Document.objects.get(id=document.id)
assert document.path == old_path
assert document.content == new_content
assert cache.get(f"docs:no-websocket:{document.id}") is None
assert ws_resp.call_count == 0
@responses.activate
def test_api_documents_patch_feature_flag_disabled(settings):
"""
When the feature flag is disabled, the patch should be applied without any check.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "editor")])
new_content = YDOC_UPDATED_CONTENT_BASE64
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = False
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, status=500)
assert cache.get(f"docs:no-websocket:{document.id}") is None
old_path = document.path
response = client.patch(
f"/api/v1.0/documents/{document.id!s}/",
{"content": new_content},
format="json",
)
assert response.status_code == 200
# Using document.refresh_from_db does not work because the content is cached.
# Force reloading it by fetching the document from the database.
document = models.Document.objects.get(id=document.id)
assert document.path == old_path
assert document.content == new_content
assert cache.get(f"docs:no-websocket:{document.id}") is None
assert ws_resp.call_count == 0
@pytest.mark.parametrize("via", VIA)
def test_api_documents_patch_administrator_or_owner_of_another(via, mock_user_teams):
"""
Being administrator or owner of a document should not grant authorization to patch
another document.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory()
if via == USER:
factories.UserDocumentAccessFactory(
document=document, user=user, role=random.choice(["administrator", "owner"])
)
elif via == TEAM:
mock_user_teams.return_value = ["lasuite", "unknown"]
factories.TeamDocumentAccessFactory(
document=document,
team="lasuite",
role=random.choice(["administrator", "owner"]),
)
other_document = factories.DocumentFactory(title="Old title", link_role="reader")
old_document_values = serializers.DocumentSerializer(instance=other_document).data
new_content = YDOC_UPDATED_CONTENT_BASE64
response = client.patch(
f"/api/v1.0/documents/{other_document.id!s}/",
{"content": new_content},
format="json",
)
assert response.status_code == 403
other_document.refresh_from_db()
assert (
serializers.DocumentSerializer(instance=other_document).data
== old_document_values
)
def test_api_documents_patch_invalid_content():
"""
Patching a document with a non base64 encoded content should raise a validation error.
"""
user = factories.UserFactory(with_owned_document=True)
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(users=[[user, "owner"]])
response = client.patch(
f"/api/v1.0/documents/{document.id!s}/",
{"content": "invalid content"},
format="json",
)
assert response.status_code == 400
assert response.json() == {"content": ["Invalid base64 content."]}
@responses.activate
def test_api_documents_patch_empty_body(settings):
"""
Test when data is empty the document should not be updated.
The `updated_at` property should not change asserting that no update in the database is made.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
session_key = client.session.session_key
document = factories.DocumentFactory(users=[(user, "owner")], creator=user)
document_updated_at = document.updated_at
settings.COLLABORATION_API_URL = "http://example.com/"
settings.COLLABORATION_SERVER_SECRET = "secret-token"
settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True
endpoint_url = (
f"{settings.COLLABORATION_API_URL}get-connections/"
f"?room={document.id}&sessionKey={session_key}"
)
ws_resp = responses.get(endpoint_url, json={"count": 3, "exists": True})
assert cache.get(f"docs:no-websocket:{document.id}") is None
old_document_values = serializers.DocumentSerializer(instance=document).data
with patch("core.models.Document.save") as mock_document_save:
response = client.patch(
f"/api/v1.0/documents/{document.id!s}/",
content_type="application/json",
)
mock_document_save.assert_not_called()
assert response.status_code == 200
document = models.Document.objects.get(id=document.id)
new_document_values = serializers.DocumentSerializer(instance=document).data
assert new_document_values == old_document_values
assert document_updated_at == document.updated_at
assert cache.get(f"docs:no-websocket:{document.id}") is None
assert ws_resp.call_count == 1
@@ -0,0 +1,32 @@
"""module testing the conditional_refresh_oidc_token utils."""
from unittest import mock
from core.api import utils
def test_refresh_oidc_access_token_storing_refresh_token_disabled(settings):
"""The method_decorator must not be called when OIDC_STORE_REFRESH_TOKEN is False."""
settings.OIDC_STORE_REFRESH_TOKEN = False
callback = mock.MagicMock()
with mock.patch.object(utils, "method_decorator") as mock_method_decorator:
result = utils.conditional_refresh_oidc_token(callback)
mock_method_decorator.assert_not_called()
assert result == callback
def test_refresh_oidc_access_token_storing_refresh_token_enabled(settings):
"""The method_decorator must not be called when OIDC_STORE_REFRESH_TOKEN is False."""
settings.OIDC_STORE_REFRESH_TOKEN = True
callback = mock.MagicMock()
with mock.patch.object(utils, "method_decorator") as mock_method_decorator:
utils.conditional_refresh_oidc_token(callback)
mock_method_decorator.assert_called_with(utils.refresh_oidc_access_token)
+87 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
"PO-Revision-Date: 2026-03-25 16:55\n"
"Last-Translator: \n"
"Language-Team: Breton\n"
"Language: br_FR\n"
@@ -46,36 +46,40 @@ msgstr "Gwezennadur"
msgid "Title"
msgstr "Titl"
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
msgid "Search"
msgstr ""
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Creator is me"
msgstr "Me eo an aozer"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Masked"
msgstr "Kuzhet"
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
msgid "Favorite"
msgstr "Sinedoù"
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
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:530 core/api/serializers.py:530
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
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:566 core/api/serializers.py:566
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
msgid "This field is required."
msgstr "Ar vaezienn-mañ a zo rekis."
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
#, python-brace-format
msgid "copy of {title}"
msgstr "eilenn {title}"
@@ -247,98 +251,98 @@ msgstr "implijer"
msgid "users"
msgstr "implijerien"
#: build/lib/core/models.py:378 core/models.py:378
#: build/lib/core/models.py:376 core/models.py:376
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:379 core/models.py:379
#: build/lib/core/models.py:377 core/models.py:377
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:404 core/models.py:404
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
#: core/models.py:708
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
#: core/models.py:711
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
#: build/lib/core/models.py:421 core/models.py:421
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
#: build/lib/core/models.py:422 core/models.py:422
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
#: build/lib/core/models.py:660 core/models.py:660
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
#: build/lib/core/models.py:666 core/models.py:666
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
#: core/models.py:777
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
#: build/lib/core/models.py:672 core/models.py:672
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
#: build/lib/core/models.py:683 core/models.py:683
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
#: build/lib/core/models.py:688 core/models.py:688
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
#: build/lib/core/models.py:693 core/models.py:693
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
#: build/lib/core/models.py:694 core/models.py:694
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
#: build/lib/core/models.py:704 core/models.py:704
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
#: build/lib/core/models.py:709 core/models.py:709
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
#: build/lib/core/models.py:719 core/models.py:719
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:720 core/models.py:720
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#: build/lib/core/models.py:764 core/models.py:764
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
#: build/lib/core/models.py:772 core/models.py:772
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
#: build/lib/core/models.py:778 core/models.py:778
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:877 core/models.py:877
msgid "title"
msgstr "titl"
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:878 core/models.py:878
msgid "excerpt"
msgstr "bomm"
#: build/lib/core/models.py:929 core/models.py:929
#: build/lib/core/models.py:927 core/models.py:927
msgid "Document"
msgstr "Restr"
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:928 core/models.py:928
msgid "Documents"
msgstr "Restroù"
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Restr hep titl"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Digeriñ"
#: build/lib/core/models.py:1382 core/models.py:1382
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} en deus rannet ur restr ganeoc'h!"
#: build/lib/core/models.py:1386 core/models.py:1386
#: build/lib/core/models.py:1385 core/models.py:1385
#, 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:1392 core/models.py:1392
#: build/lib/core/models.py:1391 core/models.py:1391
#, 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:1493 core/models.py:1493
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Roud liamm ar restr/an implijer"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Roudoù liamm ar restr/an implijer"
#: build/lib/core/models.py:1500 core/models.py:1500
#: build/lib/core/models.py:1499 core/models.py:1499
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:1523 core/models.py:1523
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Restr muiañ-karet"
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Restroù muiañ-karet"
#: build/lib/core/models.py:1530 core/models.py:1530
#: build/lib/core/models.py:1529 core/models.py:1529
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:1552 core/models.py:1552
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Liamm restr/implijer"
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Liammoù restr/implijer"
#: build/lib/core/models.py:1559 core/models.py:1559
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "An implijer-mañ a zo dija er restr-mañ."
#: build/lib/core/models.py:1565 core/models.py:1565
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Ar skipailh-mañ a zo dija en restr-mañ."
#: build/lib/core/models.py:1571 core/models.py:1571
#: build/lib/core/models.py:1570 core/models.py:1570
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:1722 core/models.py:1722
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Goulenn tizhout ar restr"
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Goulennoù tizhout ar restr"
#: build/lib/core/models.py:1729 core/models.py:1729
#: build/lib/core/models.py:1728 core/models.py:1728
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:1786 core/models.py:1786
#: build/lib/core/models.py:1785 core/models.py:1785
#, 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:1790 core/models.py:1790
#: build/lib/core/models.py:1789 core/models.py:1789
#, 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:1796 core/models.py:1796
#: build/lib/core/models.py:1795 core/models.py:1795
#, 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:1838 core/models.py:1838
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1889 core/models.py:1889
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1939 core/models.py:1939
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1943 core/models.py:1943
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1954 core/models.py:1954
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "postel"
#: build/lib/core/models.py:1973 core/models.py:1973
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Pedadenn d'ur restr"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Pedadennoù d'ur restr"
#: build/lib/core/models.py:1994 core/models.py:1994
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Ar postel-mañ a zo liammet ouzh un implijer enskrivet."
#: build/lib/impress/settings.py:702 impress/settings.py:702
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""
+126 -122
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
"PO-Revision-Date: 2026-03-25 16:55\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -28,11 +28,11 @@ msgstr "Berechtigungen"
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Wichtige Daten"
msgstr "Wichtige Termine"
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr ""
msgstr "Import-Job erstellt und in der Warteschlange."
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
@@ -46,36 +46,40 @@ msgstr "Baumstruktur"
msgid "Title"
msgstr "Titel"
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
msgid "Search"
msgstr "Suchen"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Creator is me"
msgstr "Ersteller bin ich"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Masked"
msgstr "Maskiert"
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
msgid "Favorite"
msgstr "Favorit"
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
msgid "A new document was created on your behalf!"
msgstr "Ein neues Dokument wurde in Ihrem Namen erstellt!"
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
msgid "You have been granted ownership of a new document:"
msgstr "Sie sind Besitzer eines neuen Dokuments:"
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
msgid "This field is required."
msgstr ""
msgstr "Dies ist ein Pflichtfeld."
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
msgstr "Der Zugriff auf den Link '%(link_reach)s' ist aufgrund der Konfiguration übergeordneter Dokumente nicht erlaubt."
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
#, python-brace-format
msgid "copy of {title}"
msgstr "Kopie von {title}"
@@ -92,7 +96,7 @@ msgstr "Lesen"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr ""
msgstr "Kommentieren"
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
@@ -173,11 +177,11 @@ msgstr "Wir konnten keinen Benutzer mit diesem Abo finden, aber die E-Mail-Adres
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "unter"
msgstr "sub"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr ""
msgstr "Pflichtfeld. 255 Zeichen oder weniger. Buchstaben (nur ASCII), Ziffern und die Zeichen @/-/_/."
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
@@ -233,11 +237,11 @@ msgstr "Ob dieser Benutzer als aktiviert behandelt werden soll. Deaktivieren Sie
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr ""
msgstr "Status der ersten Verbindung"
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr ""
msgstr "Gibt an, ob der Benutzer die Prozedur der ersten Verbindung abgeschlossen hat."
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
@@ -247,98 +251,98 @@ msgstr "Benutzer"
msgid "users"
msgstr "Benutzer"
#: build/lib/core/models.py:378 core/models.py:378
#: build/lib/core/models.py:376 core/models.py:376
msgid "Active email address"
msgstr ""
msgstr "Aktive E-Mail-Adresse"
#: build/lib/core/models.py:379 core/models.py:379
#: build/lib/core/models.py:377 core/models.py:377
msgid "Email address to deactivate"
msgstr ""
msgstr "Zu deaktivierende E-Mail-Adresse"
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:404 core/models.py:404
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
#: core/models.py:708
msgid "Pending"
msgstr "Ausstehend"
#: build/lib/core/models.py:411 core/models.py:411
msgid "Ready"
msgstr "Bereit"
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
msgstr "Fertig"
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
#: core/models.py:711
msgid "Error"
msgstr ""
msgstr "Fehler"
#: build/lib/core/models.py:423 core/models.py:423
#: build/lib/core/models.py:421 core/models.py:421
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
#: build/lib/core/models.py:422 core/models.py:422
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
#: build/lib/core/models.py:660 core/models.py:660
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
#: build/lib/core/models.py:666 core/models.py:666
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
#: core/models.py:777
msgid "Click here"
msgstr ""
msgstr "Klicken Sie hier"
#: build/lib/core/models.py:674 core/models.py:674
#: build/lib/core/models.py:672 core/models.py:672
msgid "Confirm"
msgstr ""
msgstr "Bestätigen Sie"
#: build/lib/core/models.py:685 core/models.py:685
#: build/lib/core/models.py:683 core/models.py:683
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
#: build/lib/core/models.py:688 core/models.py:688
msgid "Your accounts have been merged"
msgstr ""
msgstr "Ihre Konten wurden zusammengelegt"
#: build/lib/core/models.py:695 core/models.py:695
#: build/lib/core/models.py:693 core/models.py:693
msgid "Click here to see"
msgstr ""
msgstr "Klicken Sie hier um zu sehen"
#: build/lib/core/models.py:696 core/models.py:696
#: build/lib/core/models.py:694 core/models.py:694
msgid "See my documents"
msgstr ""
msgstr "Meine Dokumente einsehen"
#: build/lib/core/models.py:706 core/models.py:706
#: build/lib/core/models.py:704 core/models.py:704
msgid "CSV file"
msgstr ""
msgstr "CSV-Datei"
#: build/lib/core/models.py:711 core/models.py:711
#: build/lib/core/models.py:709 core/models.py:709
msgid "Running"
msgstr ""
msgstr "Wird ausgeführt"
#: build/lib/core/models.py:721 core/models.py:721
#: build/lib/core/models.py:719 core/models.py:719
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:720 core/models.py:720
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#: build/lib/core/models.py:764 core/models.py:764
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
@@ -347,177 +351,177 @@ msgid "Your request for reconciliation was unsuccessful.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
#: build/lib/core/models.py:772 core/models.py:772
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
#: build/lib/core/models.py:778 core/models.py:778
msgid "Make a new request"
msgstr ""
msgstr "Neue Anfrage erstellen"
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:877 core/models.py:877
msgid "title"
msgstr "Titel"
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:878 core/models.py:878
msgid "excerpt"
msgstr "Auszug"
#: build/lib/core/models.py:929 core/models.py:929
#: build/lib/core/models.py:927 core/models.py:927
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:928 core/models.py:928
msgid "Documents"
msgstr "Dokumente"
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Unbenanntes Dokument"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Öffnen"
#: build/lib/core/models.py:1382 core/models.py:1382
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} hat ein Dokument mit Ihnen geteilt!"
#: build/lib/core/models.py:1386 core/models.py:1386
#: build/lib/core/models.py:1385 core/models.py:1385
#, 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:1392 core/models.py:1392
#: build/lib/core/models.py:1391 core/models.py:1391
#, 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:1493 core/models.py:1493
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Dokument/Benutzer Linkverfolgung"
#: build/lib/core/models.py:1500 core/models.py:1500
#: build/lib/core/models.py:1499 core/models.py:1499
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:1523 core/models.py:1523
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Dokumentenfavorit"
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Dokumentfavoriten"
#: build/lib/core/models.py:1530 core/models.py:1530
#: build/lib/core/models.py:1529 core/models.py:1529
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:1552 core/models.py:1552
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Dokument/Benutzerbeziehung"
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Dokument/Benutzerbeziehungen"
#: build/lib/core/models.py:1559 core/models.py:1559
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Dieser Benutzer befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1565 core/models.py:1565
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Dieses Team befindet sich bereits in diesem Dokument."
#: build/lib/core/models.py:1571 core/models.py:1571
#: build/lib/core/models.py:1570 core/models.py:1570
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:1722 core/models.py:1722
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
msgstr "Dokument um Zugriff bitten"
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
msgstr "Dokumentenabfragen"
#: build/lib/core/models.py:1729 core/models.py:1729
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr ""
msgstr "Dieser Benutzer hat bereits um Zugang zu diesem Dokument gebeten."
#: build/lib/core/models.py:1786 core/models.py:1786
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
msgstr "{name} möchte Zugriff auf ein Dokument erhalten!"
#: build/lib/core/models.py:1790 core/models.py:1790
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
msgstr "{name} möchte auf das folgende Dokument zugreifen:"
#: build/lib/core/models.py:1796 core/models.py:1796
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
msgstr "{name} bittet um Zugang zum Dokument: {title}"
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Thread"
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1839 core/models.py:1839
msgid "Threads"
msgstr ""
msgstr "Threads"
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
msgstr "Gast"
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Kommentar"
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr ""
msgstr "Kommentare"
#: build/lib/core/models.py:1939 core/models.py:1939
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1943 core/models.py:1943
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
msgstr "Reaktion"
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
msgstr "Reaktionen"
#: build/lib/core/models.py:1954 core/models.py:1954
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "E-Mail-Adresse"
#: build/lib/core/models.py:1973 core/models.py:1973
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Einladung zum Dokument"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Dokumenteinladungen"
#: build/lib/core/models.py:1994 core/models.py:1994
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Diese E-Mail ist bereits einem registrierten Benutzer zugeordnet."
#: build/lib/impress/settings.py:702 impress/settings.py:702
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""
msgstr "Docs AI"
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
@@ -0,0 +1,548 @@
msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
"PO-Revision-Date: 2026-03-25 16:55\n"
"Last-Translator: \n"
"Language-Team: Greek\n"
"Language: el_GR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: lasuite-docs\n"
"X-Crowdin-Project-ID: 754523\n"
"X-Crowdin-Language: el\n"
"X-Crowdin-File: backend-impress.pot\n"
"X-Crowdin-File-ID: 18\n"
#: build/lib/core/admin.py:30 core/admin.py:30
msgid "Personal info"
msgstr "Προσωπικές πληροφορίες"
#: build/lib/core/admin.py:43 build/lib/core/admin.py:161 core/admin.py:43
#: core/admin.py:161
msgid "Permissions"
msgstr "Δικαιώματα"
#: build/lib/core/admin.py:55 core/admin.py:55
msgid "Important dates"
msgstr "Σημαντικές ημερομηνίες"
#: build/lib/core/admin.py:112 core/admin.py:112
msgid "Import job created and queued."
msgstr "Η εργασία εισαγωγής δημιουργήθηκε και μπήκε στην ουρά."
#: build/lib/core/admin.py:116 core/admin.py:116
msgid "Process selected user reconciliations"
msgstr "Επεξεργασία επιλεγμένων συμφωνιών χρηστών"
#: build/lib/core/admin.py:171 core/admin.py:171
msgid "Tree structure"
msgstr "Δομή δέντρου"
#: build/lib/core/api/filters.py:48 core/api/filters.py:48
msgid "Title"
msgstr "Τίτλος"
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
msgid "Search"
msgstr "Αναζήτηση"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Creator is me"
msgstr "Δημιουργός είμαι εγώ"
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Masked"
msgstr "Με κάλυψη"
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
msgid "Favorite"
msgstr "Αγαπημένο"
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
msgid "A new document was created on your behalf!"
msgstr "Ένα νέο έγγραφο δημιουργήθηκε εκ μέρους σας!"
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
msgid "You have been granted ownership of a new document:"
msgstr "Σας παραχωρήθηκε η ιδιοκτησία ενός νέου εγγράφου:"
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
msgid "This field is required."
msgstr "Αυτό το πεδίο είναι υποχρεωτικό."
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Η εμβέλεια συνδέσμου '%(link_reach)s' δεν επιτρέπεται βάσει της διαμόρφωσης του γονικού εγγράφου."
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
#, python-brace-format
msgid "copy of {title}"
msgstr "αντίγραφο του {title}"
#: build/lib/core/apps.py:12 core/apps.py:12
msgid "Impress core application"
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 "Αναγνώστης"
#: build/lib/core/choices.py:36 build/lib/core/choices.py:44 core/choices.py:36
#: core/choices.py:44
msgid "Commenter"
msgstr "Σχολιαστής"
#: build/lib/core/choices.py:37 build/lib/core/choices.py:45 core/choices.py:37
#: core/choices.py:45
msgid "Editor"
msgstr "Συντάκτης"
#: build/lib/core/choices.py:46 core/choices.py:46
msgid "Administrator"
msgstr "Διαχειριστής"
#: build/lib/core/choices.py:47 core/choices.py:47
msgid "Owner"
msgstr "Ιδιοκτήτης"
#: build/lib/core/choices.py:58 core/choices.py:58
msgid "Restricted"
msgstr "Περιορισμένο"
#: build/lib/core/choices.py:62 core/choices.py:62
msgid "Authenticated"
msgstr "Πιστοποιημένο"
#: build/lib/core/choices.py:64 core/choices.py:64
msgid "Public"
msgstr "Δημόσιο"
#: build/lib/core/enums.py:36 core/enums.py:36
msgid "First child"
msgstr "Πρώτο θυγατρικό"
#: build/lib/core/enums.py:37 core/enums.py:37
msgid "Last child"
msgstr "Τελευταίο θυγατρικό"
#: build/lib/core/enums.py:38 core/enums.py:38
msgid "First sibling"
msgstr "Πρώτο αδελφό"
#: build/lib/core/enums.py:39 core/enums.py:39
msgid "Last sibling"
msgstr "Τελευταίο αδελφό"
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr "Αριστερά"
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
msgstr "Δεξιά"
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr "αναγνωριστικό"
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
msgstr "πρωτεύον κλειδί για την εγγραφή ως UUID"
#: build/lib/core/models.py:87 core/models.py:87
msgid "created on"
msgstr "δημιουργήθηκε στις"
#: build/lib/core/models.py:88 core/models.py:88
msgid "date and time at which a record was created"
msgstr "ημερομηνία και ώρα δημιουργίας μιας εγγραφής"
#: build/lib/core/models.py:93 core/models.py:93
msgid "updated on"
msgstr "ενημερώθηκε στις"
#: build/lib/core/models.py:94 core/models.py:94
msgid "date and time at which a record was last updated"
msgstr "ημερομηνία και ώρα τελευταίας ενημέρωσης μιας εγγραφής"
#: build/lib/core/models.py:130 core/models.py:130
msgid "We couldn't find a user with this sub but the email is already associated with a registered user."
msgstr "Δεν μπορέσαμε να βρούμε χρήστη με αυτό το sub, αλλά το email σχετίζεται ήδη με έναν εγγεγραμμένο χρήστη."
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr "sub (αναγνωριστικό υποκειμένου)"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
msgstr "Υποχρεωτικό. 255 χαρακτήρες ή λιγότεροι. Μόνο χαρακτήρες ASCII."
#: build/lib/core/models.py:150 core/models.py:150
msgid "full name"
msgstr "πλήρες όνομα"
#: build/lib/core/models.py:152 core/models.py:152
msgid "short name"
msgstr "σύντομο όνομα"
#: build/lib/core/models.py:155 core/models.py:155
msgid "identity email address"
msgstr "διεύθυνση email ταυτότητας"
#: build/lib/core/models.py:160 core/models.py:160
msgid "admin email address"
msgstr "διεύθυνση email διαχειριστή"
#: build/lib/core/models.py:167 core/models.py:167
msgid "language"
msgstr "γλώσσα"
#: build/lib/core/models.py:168 core/models.py:168
msgid "The language in which the user wants to see the interface."
msgstr "Η γλώσσα στην οποία ο χρήστης θέλει να δει τη διεπαφή."
#: build/lib/core/models.py:176 core/models.py:176
msgid "The timezone in which the user wants to see times."
msgstr "Η ζώνη ώρας στην οποία ο χρήστης θέλει να βλέπει την ώρα."
#: build/lib/core/models.py:179 core/models.py:179
msgid "device"
msgstr "συσκευή"
#: build/lib/core/models.py:181 core/models.py:181
msgid "Whether the user is a device or a real user."
msgstr "Εάν ο χρήστης είναι μια συσκευή ή πραγματικός χρήστης."
#: build/lib/core/models.py:184 core/models.py:184
msgid "staff status"
msgstr "κατάσταση προσωπικού"
#: build/lib/core/models.py:186 core/models.py:186
msgid "Whether the user can log into this admin site."
msgstr "Εάν ο χρήστης μπορεί να συνδεθεί σε αυτόν τον ιστότοπο διαχείρισης."
#: build/lib/core/models.py:189 core/models.py:189
msgid "active"
msgstr "ενεργός"
#: build/lib/core/models.py:192 core/models.py:192
msgid "Whether this user should be treated as active. Unselect this instead of deleting accounts."
msgstr "Εάν αυτός ο χρήστης πρέπει να θεωρείται ενεργός. Αποεπιλέξτε το αντί να διαγράψετε λογαριασμούς."
#: build/lib/core/models.py:197 core/models.py:197
msgid "first connection status"
msgstr "πρώτη κατάσταση σύνδεσης"
#: build/lib/core/models.py:199 core/models.py:199
msgid "Whether the user has completed the first connection process."
msgstr "Εάν ο χρήστης έχει ολοκληρώσει τη διαδικασία της πρώτης σύνδεσης."
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr "χρήστης"
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr "χρήστες"
#: build/lib/core/models.py:376 core/models.py:376
msgid "Active email address"
msgstr "Ενεργή διεύθυνση email"
#: build/lib/core/models.py:377 core/models.py:377
msgid "Email address to deactivate"
msgstr "Διεύθυνση email για απενεργοποίηση"
#: build/lib/core/models.py:404 core/models.py:404
msgid "Unique ID in the source file"
msgstr "Μοναδικό αναγνωριστικό στο πηγαίο αρχείο"
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
#: core/models.py:708
msgid "Pending"
msgstr "Σε εκκρεμότητα"
#: build/lib/core/models.py:411 core/models.py:411
msgid "Ready"
msgstr "Έτοιμο"
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Done"
msgstr "Ολοκληρώθηκε"
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
#: core/models.py:711
msgid "Error"
msgstr "Σφάλμα"
#: build/lib/core/models.py:421 core/models.py:421
msgid "user reconciliation"
msgstr "συμφωνία χρήστη"
#: build/lib/core/models.py:422 core/models.py:422
msgid "user reconciliations"
msgstr "συμφωνία χρηστών"
#: build/lib/core/models.py:660 core/models.py:660
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr "Έχετε ζητήσει έναν συνδυασμό των λογαριασμών χρήστη σας στα Έγγραφα.\n"
" Για να επιβεβαιώσετε ότι είστε εκείνος που ξεκίνησε το αίτημα\n"
" και ότι αυτό το email ανήκει σε σας:"
#: build/lib/core/models.py:666 core/models.py:666
msgid "Confirm by clicking the link to start the reconciliation"
msgstr "Επιβεβαιώστε κάνοντας κλικ στο σύνδεσμο για να ξεκινήσει η συμφωνία"
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
#: core/models.py:777
msgid "Click here"
msgstr "Κάντε κλικ εδώ"
#: build/lib/core/models.py:672 core/models.py:672
msgid "Confirm"
msgstr "Επιβεβαίωση"
#: build/lib/core/models.py:683 core/models.py:683
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr "Το αίτημά σας για συμφωνία έχει επεξεργαστεί.\n"
" Νέα έγγραφα είναι πιθανό να σχετίζονται με τον λογαριασμό σας:"
#: build/lib/core/models.py:688 core/models.py:688
msgid "Your accounts have been merged"
msgstr "Οι λογαριασμοί σας έχουν συγχωνευθεί"
#: build/lib/core/models.py:693 core/models.py:693
msgid "Click here to see"
msgstr "Κάντε κλικ εδώ για να δείτε"
#: build/lib/core/models.py:694 core/models.py:694
msgid "See my documents"
msgstr "Δείτε τα έγγραφά μου"
#: build/lib/core/models.py:704 core/models.py:704
msgid "CSV file"
msgstr "Αρχείο CSV"
#: build/lib/core/models.py:709 core/models.py:709
msgid "Running"
msgstr "Εκτελείται"
#: build/lib/core/models.py:719 core/models.py:719
msgid "user reconciliation CSV import"
msgstr "εισαγωγή CSV συμφωνίας χρηστών"
#: build/lib/core/models.py:720 core/models.py:720
msgid "user reconciliation CSV imports"
msgstr "εισαγωγές CSV συμφωνίας χρηστών"
#: build/lib/core/models.py:764 core/models.py:764
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
" {recipient_email}, {other_email}.\n"
" Please check for typos.\n"
" You can submit another request with the valid email addresses."
msgstr "Το αίτημά σας για επαλήθευση δεν ολοκληρώθηκε με επιτυχία.\n"
" Η επαλήθευση απέτυχε για τις ακόλουθες διευθύνσεις email:\n"
" {recipient_email}, {other_email}.\n"
" Παρακαλούμε ελέγξτε αν υπάρχουν τυπογραφικά λάθη.\n"
" Μπορείτε να υποβάλετε ένα νέο αίτημα με τις σωστές διευθύνσεις email."
#: build/lib/core/models.py:772 core/models.py:772
msgid "Reconciliation of your Docs accounts not completed"
msgstr "Η συμφωνία των λογαριασμών σας Docs δεν ολοκληρώθηκε"
#: build/lib/core/models.py:778 core/models.py:778
msgid "Make a new request"
msgstr "Κάντε ένα νέο αίτημα"
#: build/lib/core/models.py:877 core/models.py:877
msgid "title"
msgstr "τίτλος"
#: build/lib/core/models.py:878 core/models.py:878
msgid "excerpt"
msgstr "απόσπασμα"
#: build/lib/core/models.py:927 core/models.py:927
msgid "Document"
msgstr "Έγγραφο"
#: build/lib/core/models.py:928 core/models.py:928
msgid "Documents"
msgstr "Έγγραφα"
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Έγγραφο χωρίς τίτλο"
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Άνοιγμα"
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "Ο/Η {name} μοιράστηκε ένα έγγραφο μαζί σας!"
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "Ο/Η {name} σας προσκάλεσε με τον ρόλο \"{role}\" στο ακόλουθο έγγραφο:"
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "Ο/Η {name} μοιράστηκε ένα έγγραφο μαζί σας: {title}"
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Ίχνος συνδέσμου εγγράφου/χρήστη"
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Ίχνη συνδέσμου εγγράφου/χρήστη"
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr "Ένα ίχνος συνδέσμου υπάρχει ήδη για αυτό το έγγραφο/χρήστη."
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Αγαπημένο έγγραφο"
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Αγαπημένα έγγραφα"
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Αυτό το έγγραφο στοχεύεται ήδη από μια σχέση αγαπημένου για τον ίδιο χρήστη."
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Σχέση εγγράφου/χρήστη"
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Σχέσεις εγγράφου/χρήστη"
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Αυτός ο χρήστης συμμετέχει ήδη σε αυτό το έγγραφο."
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Αυτή η ομάδα συμμετέχει ήδη σε αυτό το έγγραφο."
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr "Πρέπει να οριστεί είτε χρήστης είτε ομάδα, όχι και τα δύο."
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Αίτημα πρόσβασης σε έγγραφο"
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Αιτήματα πρόσβασης σε έγγραφα"
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr "Αυτός ο χρήστης έχει ήδη ζητήσει πρόσβαση σε αυτό το έγγραφο."
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "Ο/Η {name} θα ήθελε πρόσβαση σε ένα έγγραφο!"
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "Ο/Η {name} θα ήθελε πρόσβαση στο ακόλουθο έγγραφο:"
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "Ο/Η {name} ζητά πρόσβαση στο έγγραφο: {title}"
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Νήμα"
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Νήματα"
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "Ανώνυμος"
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Σχόλιο"
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Σχόλια"
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr "Αυτό το emoji έχει χρησιμοποιηθεί ήδη ως αντίδραση σε αυτό το σχόλιο."
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Αντίδραση"
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Αντιδράσεις"
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "διεύθυνση email"
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Πρόσκληση σε έγγραφο"
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Προσκλήσεις εγγράφου"
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Αυτό το email σχετίζεται ήδη με έναν εγγεγραμμένο χρήστη."
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Τεχνητή Νοημοσύνη (AI) Docs"
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
msgid "Logo email"
msgstr "Λογότυπο email"
#: 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:226
#: core/templates/mail/text/template.txt:16
#, python-format
msgid " Brought to you by %(brandname)s "
msgstr " Σας προσφέρεται από την %(brandname)s "
+87 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
"PO-Revision-Date: 2026-03-25 16:55\n"
"Last-Translator: \n"
"Language-Team: English\n"
"Language: en_US\n"
@@ -46,36 +46,40 @@ msgstr ""
msgid "Title"
msgstr ""
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
msgid "Search"
msgstr ""
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgid "Creator is me"
msgstr ""
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -247,98 +251,98 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:378 core/models.py:378
#: build/lib/core/models.py:376 core/models.py:376
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:379 core/models.py:379
#: build/lib/core/models.py:377 core/models.py:377
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:404 core/models.py:404
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
#: core/models.py:708
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
#: core/models.py:711
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
#: build/lib/core/models.py:421 core/models.py:421
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
#: build/lib/core/models.py:422 core/models.py:422
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
#: build/lib/core/models.py:660 core/models.py:660
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
#: build/lib/core/models.py:666 core/models.py:666
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
#: core/models.py:777
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
#: build/lib/core/models.py:672 core/models.py:672
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
#: build/lib/core/models.py:683 core/models.py:683
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
#: build/lib/core/models.py:688 core/models.py:688
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
#: build/lib/core/models.py:693 core/models.py:693
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
#: build/lib/core/models.py:694 core/models.py:694
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
#: build/lib/core/models.py:704 core/models.py:704
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
#: build/lib/core/models.py:709 core/models.py:709
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
#: build/lib/core/models.py:719 core/models.py:719
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:720 core/models.py:720
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#: build/lib/core/models.py:764 core/models.py:764
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
#: build/lib/core/models.py:772 core/models.py:772
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
#: build/lib/core/models.py:778 core/models.py:778
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:877 core/models.py:877
msgid "title"
msgstr ""
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:878 core/models.py:878
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:929 core/models.py:929
#: build/lib/core/models.py:927 core/models.py:927
msgid "Document"
msgstr ""
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:928 core/models.py:928
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr ""
#: build/lib/core/models.py:1382 core/models.py:1382
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:1386 core/models.py:1386
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:1392 core/models.py:1392
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1493 core/models.py:1493
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1500 core/models.py:1500
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1523 core/models.py:1523
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1530 core/models.py:1530
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1552 core/models.py:1552
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1559 core/models.py:1559
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1565 core/models.py:1565
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1571 core/models.py:1571
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1722 core/models.py:1722
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1729 core/models.py:1729
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1786 core/models.py:1786
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1790 core/models.py:1790
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1796 core/models.py:1796
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1838 core/models.py:1838
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1889 core/models.py:1889
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1939 core/models.py:1939
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1943 core/models.py:1943
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1954 core/models.py:1954
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1973 core/models.py:1973
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1994 core/models.py:1994
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr ""
#: build/lib/impress/settings.py:702 impress/settings.py:702
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""
+106 -102
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
"PO-Revision-Date: 2026-03-25 16:55\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -46,36 +46,40 @@ msgstr "Estructura en árbol"
msgid "Title"
msgstr "Título"
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
msgid "Search"
msgstr "Buscar"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Creator is me"
msgstr "Yo soy el creador"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Masked"
msgstr "Enmascarado"
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
msgid "A new document was created on your behalf!"
msgstr "¡Un nuevo documento se ha creado por ti!"
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
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:566 core/api/serializers.py:566
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
#, python-brace-format
msgid "copy of {title}"
msgstr "copia de {title}"
@@ -247,98 +251,98 @@ msgstr "usuario"
msgid "users"
msgstr "usuarios"
#: build/lib/core/models.py:378 core/models.py:378
#: build/lib/core/models.py:376 core/models.py:376
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:379 core/models.py:379
#: build/lib/core/models.py:377 core/models.py:377
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:404 core/models.py:404
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
#: core/models.py:708
msgid "Pending"
msgstr "Pending"
#: build/lib/core/models.py:411 core/models.py:411
msgid "Ready"
msgstr "Listo"
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
msgstr "Terminado"
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
#: core/models.py:711
msgid "Error"
msgstr ""
msgstr "Error"
#: build/lib/core/models.py:423 core/models.py:423
#: build/lib/core/models.py:421 core/models.py:421
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
#: build/lib/core/models.py:422 core/models.py:422
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
#: build/lib/core/models.py:660 core/models.py:660
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
#: build/lib/core/models.py:666 core/models.py:666
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
#: core/models.py:777
msgid "Click here"
msgstr ""
msgstr "Haga click aquí"
#: build/lib/core/models.py:674 core/models.py:674
#: build/lib/core/models.py:672 core/models.py:672
msgid "Confirm"
msgstr ""
msgstr "Confirmar"
#: build/lib/core/models.py:685 core/models.py:685
#: build/lib/core/models.py:683 core/models.py:683
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
#: build/lib/core/models.py:688 core/models.py:688
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
#: build/lib/core/models.py:693 core/models.py:693
msgid "Click here to see"
msgstr ""
msgstr "Haz clic aquí para ver"
#: build/lib/core/models.py:696 core/models.py:696
#: build/lib/core/models.py:694 core/models.py:694
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
#: build/lib/core/models.py:704 core/models.py:704
msgid "CSV file"
msgstr ""
msgstr "Archivo CSV"
#: build/lib/core/models.py:711 core/models.py:711
#: build/lib/core/models.py:709 core/models.py:709
msgid "Running"
msgstr ""
msgstr "En ejecución"
#: build/lib/core/models.py:721 core/models.py:721
#: build/lib/core/models.py:719 core/models.py:719
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:720 core/models.py:720
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#: build/lib/core/models.py:764 core/models.py:764
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
@@ -347,177 +351,177 @@ msgid "Your request for reconciliation was unsuccessful.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
#: build/lib/core/models.py:772 core/models.py:772
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
#: build/lib/core/models.py:778 core/models.py:778
msgid "Make a new request"
msgstr ""
msgstr "Hacer un nuevo pedido"
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:877 core/models.py:877
msgid "title"
msgstr "título"
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:878 core/models.py:878
msgid "excerpt"
msgstr "resumen"
#: build/lib/core/models.py:929 core/models.py:929
#: build/lib/core/models.py:927 core/models.py:927
msgid "Document"
msgstr "Documento"
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:928 core/models.py:928
msgid "Documents"
msgstr "Documentos"
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Documento sin título"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Abrir"
#: build/lib/core/models.py:1382 core/models.py:1382
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "¡{name} ha compartido un documento contigo!"
#: build/lib/core/models.py:1386 core/models.py:1386
#: build/lib/core/models.py:1385 core/models.py:1385
#, 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:1392 core/models.py:1392
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ha compartido un documento contigo: {title}"
#: build/lib/core/models.py:1493 core/models.py:1493
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Traza del enlace de documento/usuario"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Trazas del enlace de documento/usuario"
#: build/lib/core/models.py:1500 core/models.py:1500
#: build/lib/core/models.py:1499 core/models.py:1499
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:1523 core/models.py:1523
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Documento favorito"
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Documentos favoritos"
#: build/lib/core/models.py:1530 core/models.py:1530
#: build/lib/core/models.py:1529 core/models.py:1529
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:1552 core/models.py:1552
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Relación documento/usuario"
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Relaciones documento/usuario"
#: build/lib/core/models.py:1559 core/models.py:1559
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Este usuario ya forma parte del documento."
#: build/lib/core/models.py:1565 core/models.py:1565
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Este equipo ya forma parte del documento."
#: build/lib/core/models.py:1571 core/models.py:1571
#: build/lib/core/models.py:1570 core/models.py:1570
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:1722 core/models.py:1722
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Solicitud de acceso"
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Solicitud de accesos"
#: build/lib/core/models.py:1729 core/models.py:1729
#: build/lib/core/models.py:1728 core/models.py:1728
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:1786 core/models.py:1786
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "¡{name} desea acceder a un documento!"
#: build/lib/core/models.py:1790 core/models.py:1790
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} desea acceso al siguiente documento:"
#: build/lib/core/models.py:1796 core/models.py:1796
#: build/lib/core/models.py:1795 core/models.py:1795
#, 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:1838 core/models.py:1838
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
msgstr "Thread"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
msgstr "Threads"
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
msgstr "Anónimo"
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Comentario"
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
msgid "Comments"
msgstr ""
msgstr "Comentarios"
#: build/lib/core/models.py:1939 core/models.py:1939
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1943 core/models.py:1943
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
msgstr "Reacción"
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
msgstr "Reacciones"
#: build/lib/core/models.py:1954 core/models.py:1954
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "dirección de correo electrónico"
#: build/lib/core/models.py:1973 core/models.py:1973
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Invitación al documento"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Invitaciones a documentos"
#: build/lib/core/models.py:1994 core/models.py:1994
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Este correo electrónico está asociado a un usuario registrado."
#: build/lib/impress/settings.py:702 impress/settings.py:702
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""
msgstr "Docs AI"
#: core/templates/mail/html/template.html:153
#: core/templates/mail/text/template.txt:3
+83 -79
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
"PO-Revision-Date: 2026-03-25 16:55\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -46,36 +46,40 @@ msgstr "Arborescence"
msgid "Title"
msgstr "Titre"
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
msgid "Search"
msgstr "Recherche"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Creator is me"
msgstr "Je suis l'auteur"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Masked"
msgstr "Masqué"
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
msgid "Favorite"
msgstr "Favoris"
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
msgid "A new document was created on your behalf!"
msgstr "Un nouveau document a été créé pour vous !"
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
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:566 core/api/serializers.py:566
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
msgid "This field is required."
msgstr "Ce champ est obligatoire."
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#, 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/viewsets.py:1298 core/api/viewsets.py:1298
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
#, python-brace-format
msgid "copy of {title}"
msgstr "copie de {title}"
@@ -247,46 +251,46 @@ msgstr "utilisateur"
msgid "users"
msgstr "utilisateurs"
#: build/lib/core/models.py:378 core/models.py:378
#: build/lib/core/models.py:376 core/models.py:376
msgid "Active email address"
msgstr "Adresse email active"
#: build/lib/core/models.py:379 core/models.py:379
#: build/lib/core/models.py:377 core/models.py:377
msgid "Email address to deactivate"
msgstr "Adresse email à désactiver"
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:404 core/models.py:404
msgid "Unique ID in the source file"
msgstr "Identifiant unique dans le fichier source"
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
#: core/models.py:708
msgid "Pending"
msgstr "En attente"
#: build/lib/core/models.py:413 core/models.py:413
#: build/lib/core/models.py:411 core/models.py:411
msgid "Ready"
msgstr "Prêt"
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Done"
msgstr "Terminé"
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
#: core/models.py:711
msgid "Error"
msgstr "Erreur"
#: build/lib/core/models.py:423 core/models.py:423
#: build/lib/core/models.py:421 core/models.py:421
msgid "user reconciliation"
msgstr "rapprochement de l'utilisateur"
#: build/lib/core/models.py:424 core/models.py:424
#: build/lib/core/models.py:422 core/models.py:422
msgid "user reconciliations"
msgstr "rapprochements de l'utilisateur"
#: build/lib/core/models.py:662 core/models.py:662
#: build/lib/core/models.py:660 core/models.py:660
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
@@ -294,54 +298,54 @@ msgstr "Vous avez demandé un rapprochement de vos comptes utilisateur sur Docs.
" Pour confirmer que vous êtes bien à l'origine de cette demande\n"
" et que cet e-mail vous appartient :"
#: build/lib/core/models.py:668 core/models.py:668
#: build/lib/core/models.py:666 core/models.py:666
msgid "Confirm by clicking the link to start the reconciliation"
msgstr "Confirmez en cliquant sur le lien pour commencer le rapprochement"
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
#: core/models.py:777
msgid "Click here"
msgstr "Cliquez ici"
#: build/lib/core/models.py:674 core/models.py:674
#: build/lib/core/models.py:672 core/models.py:672
msgid "Confirm"
msgstr "Confirmer"
#: build/lib/core/models.py:685 core/models.py:685
#: build/lib/core/models.py:683 core/models.py:683
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr "Votre demande de rapprochement a été traitée.\n"
" De nouveaux documents sont probablement associés à votre compte :"
#: build/lib/core/models.py:690 core/models.py:690
#: build/lib/core/models.py:688 core/models.py:688
msgid "Your accounts have been merged"
msgstr "Vos comptes ont été fusionnés"
#: build/lib/core/models.py:695 core/models.py:695
#: build/lib/core/models.py:693 core/models.py:693
msgid "Click here to see"
msgstr "Cliquez ici pour voir"
#: build/lib/core/models.py:696 core/models.py:696
#: build/lib/core/models.py:694 core/models.py:694
msgid "See my documents"
msgstr "Voir mes documents"
#: build/lib/core/models.py:706 core/models.py:706
#: build/lib/core/models.py:704 core/models.py:704
msgid "CSV file"
msgstr "Fichier CSV"
#: build/lib/core/models.py:711 core/models.py:711
#: build/lib/core/models.py:709 core/models.py:709
msgid "Running"
msgstr "En cours"
#: build/lib/core/models.py:721 core/models.py:721
#: build/lib/core/models.py:719 core/models.py:719
msgid "user reconciliation CSV import"
msgstr "importation CSV de rapprochement utilisateur"
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:720 core/models.py:720
msgid "user reconciliation CSV imports"
msgstr "importations CSV de rapprochement utilisateur"
#: build/lib/core/models.py:766 core/models.py:766
#: build/lib/core/models.py:764 core/models.py:764
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
@@ -354,175 +358,175 @@ msgstr "Votre demande de rapprochement n'a pas abouti.\n"
" Veuillez vérifier qu'il n'y a pas de fautes de frappe.\n"
" Vous pouvez envoyer une nouvelle demande avec des adresses e-mail valides."
#: build/lib/core/models.py:774 core/models.py:774
#: build/lib/core/models.py:772 core/models.py:772
msgid "Reconciliation of your Docs accounts not completed"
msgstr "Le rapprochement de vos comptes Docs n'est pas terminé"
#: build/lib/core/models.py:780 core/models.py:780
#: build/lib/core/models.py:778 core/models.py:778
msgid "Make a new request"
msgstr "Faire une nouvelle demande"
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:877 core/models.py:877
msgid "title"
msgstr "titre"
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:878 core/models.py:878
msgid "excerpt"
msgstr "extrait"
#: build/lib/core/models.py:929 core/models.py:929
#: build/lib/core/models.py:927 core/models.py:927
msgid "Document"
msgstr "Document"
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:928 core/models.py:928
msgid "Documents"
msgstr "Documents"
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Document sans titre"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Ouvrir"
#: build/lib/core/models.py:1382 core/models.py:1382
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} a partagé un document avec vous!"
#: build/lib/core/models.py:1386 core/models.py:1386
#: build/lib/core/models.py:1385 core/models.py:1385
#, 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:1392 core/models.py:1392
#: build/lib/core/models.py:1391 core/models.py:1391
#, 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:1493 core/models.py:1493
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Trace du lien document/utilisateur"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Traces du lien document/utilisateur"
#: build/lib/core/models.py:1500 core/models.py:1500
#: build/lib/core/models.py:1499 core/models.py:1499
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:1523 core/models.py:1523
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Document favori"
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Documents favoris"
#: build/lib/core/models.py:1530 core/models.py:1530
#: build/lib/core/models.py:1529 core/models.py:1529
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:1552 core/models.py:1552
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Relation document/utilisateur"
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Relations document/utilisateur"
#: build/lib/core/models.py:1559 core/models.py:1559
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Cet utilisateur est déjà dans ce document."
#: build/lib/core/models.py:1565 core/models.py:1565
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Cette équipe est déjà dans ce document."
#: build/lib/core/models.py:1571 core/models.py:1571
#: build/lib/core/models.py:1570 core/models.py:1570
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:1722 core/models.py:1722
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Demande d'accès au document"
#: build/lib/core/models.py:1729 core/models.py:1729
#: build/lib/core/models.py:1728 core/models.py:1728
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:1786 core/models.py:1786
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} souhaiterait accéder au document suivant !"
#: build/lib/core/models.py:1790 core/models.py:1790
#: build/lib/core/models.py:1789 core/models.py:1789
#, 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:1796 core/models.py:1796
#: build/lib/core/models.py:1795 core/models.py:1795
#, 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:1838 core/models.py:1838
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Conversation"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Conversations"
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "Anonyme"
#: build/lib/core/models.py:1889 core/models.py:1889
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Commentaire"
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Commentaires"
#: build/lib/core/models.py:1939 core/models.py:1939
#: build/lib/core/models.py:1938 core/models.py:1938
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:1943 core/models.py:1943
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Réaction"
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Réactions"
#: build/lib/core/models.py:1954 core/models.py:1954
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "adresse e-mail"
#: build/lib/core/models.py:1973 core/models.py:1973
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Invitation à un document"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Invitations à un document"
#: build/lib/core/models.py:1994 core/models.py:1994
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Cette adresse email est déjà associée à un utilisateur inscrit."
#: build/lib/impress/settings.py:702 impress/settings.py:702
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs IA"
+87 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
"PO-Revision-Date: 2026-03-25 16:55\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -46,36 +46,40 @@ msgstr "Struttura ad albero"
msgid "Title"
msgstr "Titolo"
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
msgid "Search"
msgstr ""
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Creator is me"
msgstr "Il creatore sono io"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
msgid "Favorite"
msgstr "Preferiti"
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
msgid "A new document was created on your behalf!"
msgstr "Un nuovo documento è stato creato a tuo nome!"
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
msgid "You have been granted ownership of a new document:"
msgstr "Sei ora proprietario di un nuovo documento:"
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
#, python-brace-format
msgid "copy of {title}"
msgstr "copia di {title}"
@@ -247,98 +251,98 @@ msgstr "utente"
msgid "users"
msgstr "utenti"
#: build/lib/core/models.py:378 core/models.py:378
#: build/lib/core/models.py:376 core/models.py:376
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:379 core/models.py:379
#: build/lib/core/models.py:377 core/models.py:377
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:404 core/models.py:404
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
#: core/models.py:708
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
#: core/models.py:711
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
#: build/lib/core/models.py:421 core/models.py:421
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
#: build/lib/core/models.py:422 core/models.py:422
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
#: build/lib/core/models.py:660 core/models.py:660
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
#: build/lib/core/models.py:666 core/models.py:666
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
#: core/models.py:777
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
#: build/lib/core/models.py:672 core/models.py:672
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
#: build/lib/core/models.py:683 core/models.py:683
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
#: build/lib/core/models.py:688 core/models.py:688
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
#: build/lib/core/models.py:693 core/models.py:693
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
#: build/lib/core/models.py:694 core/models.py:694
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
#: build/lib/core/models.py:704 core/models.py:704
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
#: build/lib/core/models.py:709 core/models.py:709
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
#: build/lib/core/models.py:719 core/models.py:719
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:720 core/models.py:720
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#: build/lib/core/models.py:764 core/models.py:764
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
#: build/lib/core/models.py:772 core/models.py:772
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
#: build/lib/core/models.py:778 core/models.py:778
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:877 core/models.py:877
msgid "title"
msgstr "titolo"
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:878 core/models.py:878
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:929 core/models.py:929
#: build/lib/core/models.py:927 core/models.py:927
msgid "Document"
msgstr "Documento"
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:928 core/models.py:928
msgid "Documents"
msgstr "Documenti"
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Documento senza titolo"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Apri"
#: build/lib/core/models.py:1382 core/models.py:1382
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ha condiviso un documento con te!"
#: build/lib/core/models.py:1386 core/models.py:1386
#: build/lib/core/models.py:1385 core/models.py:1385
#, 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:1392 core/models.py:1392
#: build/lib/core/models.py:1391 core/models.py:1391
#, 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:1493 core/models.py:1493
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1500 core/models.py:1500
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1523 core/models.py:1523
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Documento preferito"
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Documenti preferiti"
#: build/lib/core/models.py:1530 core/models.py:1530
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1552 core/models.py:1552
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1559 core/models.py:1559
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Questo utente è già presente in questo documento."
#: build/lib/core/models.py:1565 core/models.py:1565
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Questo team è già presente in questo documento."
#: build/lib/core/models.py:1571 core/models.py:1571
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1722 core/models.py:1722
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1729 core/models.py:1729
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1786 core/models.py:1786
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1790 core/models.py:1790
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1796 core/models.py:1796
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1838 core/models.py:1838
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1889 core/models.py:1889
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1939 core/models.py:1939
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1943 core/models.py:1943
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1954 core/models.py:1954
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "indirizzo e-mail"
#: build/lib/core/models.py:1973 core/models.py:1973
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Invito al documento"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Inviti al documento"
#: build/lib/core/models.py:1994 core/models.py:1994
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Questa email è già associata a un utente registrato."
#: build/lib/impress/settings.py:702 impress/settings.py:702
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""
+83 -79
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
"PO-Revision-Date: 2026-03-25 16:55\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Language: nl_NL\n"
@@ -46,36 +46,40 @@ msgstr "Boomstructuur"
msgid "Title"
msgstr "Titel"
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
msgid "Search"
msgstr ""
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Creator is me"
msgstr "Ik ben eigenaar"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Masked"
msgstr "Gemaskeerd"
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
msgid "Favorite"
msgstr "Favoriet"
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
msgid "A new document was created on your behalf!"
msgstr "Een nieuw document is namens u gemaakt!"
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
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:566 core/api/serializers.py:566
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
msgid "This field is required."
msgstr "Dit veld is verplicht."
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#, 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/viewsets.py:1298 core/api/viewsets.py:1298
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
#, python-brace-format
msgid "copy of {title}"
msgstr "kopie van {title}"
@@ -247,46 +251,46 @@ msgstr "gebruiker"
msgid "users"
msgstr "gebruikers"
#: build/lib/core/models.py:378 core/models.py:378
#: build/lib/core/models.py:376 core/models.py:376
msgid "Active email address"
msgstr "Actieve e-mail adres"
#: build/lib/core/models.py:379 core/models.py:379
#: build/lib/core/models.py:377 core/models.py:377
msgid "Email address to deactivate"
msgstr "E-mailadres om te deactiveren"
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:404 core/models.py:404
msgid "Unique ID in the source file"
msgstr "Unieke ID in het bronbestand"
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
#: core/models.py:708
msgid "Pending"
msgstr "In behandeling"
#: build/lib/core/models.py:413 core/models.py:413
#: build/lib/core/models.py:411 core/models.py:411
msgid "Ready"
msgstr "Klaar"
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Done"
msgstr "Klaar"
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
#: core/models.py:711
msgid "Error"
msgstr "Fout"
#: build/lib/core/models.py:423 core/models.py:423
#: build/lib/core/models.py:421 core/models.py:421
msgid "user reconciliation"
msgstr "gebruiker samenvoegen"
#: build/lib/core/models.py:424 core/models.py:424
#: build/lib/core/models.py:422 core/models.py:422
msgid "user reconciliations"
msgstr "gebruikers samenvoegen"
#: build/lib/core/models.py:662 core/models.py:662
#: build/lib/core/models.py:660 core/models.py:660
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
@@ -294,54 +298,54 @@ msgstr "Je hebt gevraagd om een samenvoeging van je gebruikersaccounts op Docs.\
" Om te bevestigen dat u degene bent die het verzoek\n"
" heeft geïnitieerd en dat deze e-mail van u is:"
#: build/lib/core/models.py:668 core/models.py:668
#: build/lib/core/models.py:666 core/models.py:666
msgid "Confirm by clicking the link to start the reconciliation"
msgstr "Bevestig door te klikken op de link om de samenvoeging te starten"
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
#: core/models.py:777
msgid "Click here"
msgstr "Klik hier"
#: build/lib/core/models.py:674 core/models.py:674
#: build/lib/core/models.py:672 core/models.py:672
msgid "Confirm"
msgstr "Bevestig"
#: build/lib/core/models.py:685 core/models.py:685
#: build/lib/core/models.py:683 core/models.py:683
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr "Uw samenvoegingsverzoek is verwerkt.\n"
" Nieuwe documenten worden waarschijnlijk geassocieerd met uw account:"
#: build/lib/core/models.py:690 core/models.py:690
#: build/lib/core/models.py:688 core/models.py:688
msgid "Your accounts have been merged"
msgstr "Je accounts zijn samengevoegd"
#: build/lib/core/models.py:695 core/models.py:695
#: build/lib/core/models.py:693 core/models.py:693
msgid "Click here to see"
msgstr "Klik hier om te bekijken"
#: build/lib/core/models.py:696 core/models.py:696
#: build/lib/core/models.py:694 core/models.py:694
msgid "See my documents"
msgstr "Mijn documenten bekijken"
#: build/lib/core/models.py:706 core/models.py:706
#: build/lib/core/models.py:704 core/models.py:704
msgid "CSV file"
msgstr "CSV bestand"
#: build/lib/core/models.py:711 core/models.py:711
#: build/lib/core/models.py:709 core/models.py:709
msgid "Running"
msgstr "Bezig"
#: build/lib/core/models.py:721 core/models.py:721
#: build/lib/core/models.py:719 core/models.py:719
msgid "user reconciliation CSV import"
msgstr "gebruiker samenvoeging CSV import"
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:720 core/models.py:720
msgid "user reconciliation CSV imports"
msgstr "gebruiker reconciliation CSV imports"
#: build/lib/core/models.py:766 core/models.py:766
#: build/lib/core/models.py:764 core/models.py:764
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
@@ -354,175 +358,175 @@ msgstr "Uw verzoek tot verzoening is mislukt.\n"
" Controleer op typefouten.\n"
" U kunt een ander verzoek indienen met de geldige e-mailadressen."
#: build/lib/core/models.py:774 core/models.py:774
#: build/lib/core/models.py:772 core/models.py:772
msgid "Reconciliation of your Docs accounts not completed"
msgstr "Samenvoeging van je Docs accounts is niet voltooid"
#: build/lib/core/models.py:780 core/models.py:780
#: build/lib/core/models.py:778 core/models.py:778
msgid "Make a new request"
msgstr "Maak een nieuw verzoek"
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:877 core/models.py:877
msgid "title"
msgstr "titel"
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:878 core/models.py:878
msgid "excerpt"
msgstr "uittreksel"
#: build/lib/core/models.py:929 core/models.py:929
#: build/lib/core/models.py:927 core/models.py:927
msgid "Document"
msgstr "Document"
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:928 core/models.py:928
msgid "Documents"
msgstr "Documenten"
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Naamloos Document"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Open"
#: build/lib/core/models.py:1382 core/models.py:1382
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} heeft een document met u gedeeld!"
#: build/lib/core/models.py:1386 core/models.py:1386
#: build/lib/core/models.py:1385 core/models.py:1385
#, 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:1392 core/models.py:1392
#: build/lib/core/models.py:1391 core/models.py:1391
#, 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:1493 core/models.py:1493
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Document/gebruiker link"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Document/gebruiker link"
#: build/lib/core/models.py:1500 core/models.py:1500
#: build/lib/core/models.py:1499 core/models.py:1499
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:1523 core/models.py:1523
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Document favoriet"
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Document favorieten"
#: build/lib/core/models.py:1530 core/models.py:1530
#: build/lib/core/models.py:1529 core/models.py:1529
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:1552 core/models.py:1552
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Document/gebruiker relatie"
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Document/gebruiker relaties"
#: build/lib/core/models.py:1559 core/models.py:1559
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "De gebruiker bestaat al in dit document."
#: build/lib/core/models.py:1565 core/models.py:1565
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Dit team bestaat al in dit document."
#: build/lib/core/models.py:1571 core/models.py:1571
#: build/lib/core/models.py:1570 core/models.py:1570
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:1722 core/models.py:1722
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Document verzoekt om toegang"
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Document verzoekt om toegangen"
#: build/lib/core/models.py:1729 core/models.py:1729
#: build/lib/core/models.py:1728 core/models.py:1728
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:1786 core/models.py:1786
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} verzoekt toegang tot een document!"
#: build/lib/core/models.py:1790 core/models.py:1790
#: build/lib/core/models.py:1789 core/models.py:1789
#, 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:1796 core/models.py:1796
#: build/lib/core/models.py:1795 core/models.py:1795
#, 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:1838 core/models.py:1838
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Kanaal"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Kanalen"
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "Anoniem"
#: build/lib/core/models.py:1889 core/models.py:1889
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Reactie"
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Reacties"
#: build/lib/core/models.py:1939 core/models.py:1939
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr "Deze emoji is al op deze opmerking gereageerd."
#: build/lib/core/models.py:1943 core/models.py:1943
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Reactie"
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Reacties"
#: build/lib/core/models.py:1954 core/models.py:1954
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "e-mailadres"
#: build/lib/core/models.py:1973 core/models.py:1973
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Document uitnodiging"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Document uitnodigingen"
#: build/lib/core/models.py:1994 core/models.py:1994
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Deze email is al geassocieerd met een geregistreerde gebruiker."
#: build/lib/impress/settings.py:702 impress/settings.py:702
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs AI"
+93 -89
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
"PO-Revision-Date: 2026-03-25 16:55\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Language: pt_PT\n"
@@ -46,36 +46,40 @@ msgstr "Estrutura de árvore"
msgid "Title"
msgstr "Título"
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
msgid "Search"
msgstr ""
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Creator is me"
msgstr "Eu sou o criador"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
msgid "Favorite"
msgstr "Favorito"
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
msgid "A new document was created on your behalf!"
msgstr "Um novo documento foi criado em seu nome!"
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
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:566 core/api/serializers.py:566
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
#, python-brace-format
msgid "copy of {title}"
msgstr "cópia de {title}"
@@ -137,7 +141,7 @@ msgstr ""
#: build/lib/core/enums.py:40 core/enums.py:40
msgid "Left"
msgstr ""
msgstr "Esquerda"
#: build/lib/core/enums.py:41 core/enums.py:41
msgid "Right"
@@ -145,7 +149,7 @@ msgstr ""
#: build/lib/core/models.py:80 core/models.py:80
msgid "id"
msgstr ""
msgstr "id"
#: build/lib/core/models.py:81 core/models.py:81
msgid "primary key for the record as UUID"
@@ -173,7 +177,7 @@ msgstr ""
#: build/lib/core/models.py:141 core/models.py:141
msgid "sub"
msgstr ""
msgstr "sub"
#: build/lib/core/models.py:142 core/models.py:142
msgid "Required. 255 characters or fewer. ASCII characters only."
@@ -241,104 +245,104 @@ msgstr ""
#: build/lib/core/models.py:209 core/models.py:209
msgid "user"
msgstr ""
msgstr "utilizador"
#: build/lib/core/models.py:210 core/models.py:210
msgid "users"
msgstr ""
#: build/lib/core/models.py:378 core/models.py:378
#: build/lib/core/models.py:376 core/models.py:376
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:379 core/models.py:379
#: build/lib/core/models.py:377 core/models.py:377
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:404 core/models.py:404
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
#: core/models.py:708
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
msgstr "Concluído"
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
#: core/models.py:711
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
#: build/lib/core/models.py:421 core/models.py:421
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
#: build/lib/core/models.py:422 core/models.py:422
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
#: build/lib/core/models.py:660 core/models.py:660
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
#: build/lib/core/models.py:666 core/models.py:666
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
#: core/models.py:777
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
#: build/lib/core/models.py:672 core/models.py:672
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
#: build/lib/core/models.py:683 core/models.py:683
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
#: build/lib/core/models.py:688 core/models.py:688
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
#: build/lib/core/models.py:693 core/models.py:693
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
#: build/lib/core/models.py:694 core/models.py:694
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
#: build/lib/core/models.py:704 core/models.py:704
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
#: build/lib/core/models.py:709 core/models.py:709
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
#: build/lib/core/models.py:719 core/models.py:719
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:720 core/models.py:720
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#: build/lib/core/models.py:764 core/models.py:764
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
#: build/lib/core/models.py:772 core/models.py:772
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
#: build/lib/core/models.py:778 core/models.py:778
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:877 core/models.py:877
msgid "title"
msgstr ""
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:878 core/models.py:878
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:929 core/models.py:929
#: build/lib/core/models.py:927 core/models.py:927
msgid "Document"
msgstr ""
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:928 core/models.py:928
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr ""
msgstr "Abrir"
#: build/lib/core/models.py:1382 core/models.py:1382
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:1386 core/models.py:1386
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:1392 core/models.py:1392
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1493 core/models.py:1493
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1500 core/models.py:1500
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1523 core/models.py:1523
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1530 core/models.py:1530
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1552 core/models.py:1552
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1559 core/models.py:1559
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1565 core/models.py:1565
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1571 core/models.py:1571
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1722 core/models.py:1722
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1729 core/models.py:1729
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1786 core/models.py:1786
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1790 core/models.py:1790
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1796 core/models.py:1796
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1838 core/models.py:1838
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1889 core/models.py:1889
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1939 core/models.py:1939
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1943 core/models.py:1943
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1954 core/models.py:1954
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1973 core/models.py:1973
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1994 core/models.py:1994
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr ""
#: build/lib/impress/settings.py:702 impress/settings.py:702
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""
+83 -79
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
"PO-Revision-Date: 2026-03-25 16:55\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -46,36 +46,40 @@ msgstr "Древовидная структура"
msgid "Title"
msgstr "Заголовок"
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
msgid "Search"
msgstr "Поиск"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Creator is me"
msgstr "Создатель - я"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Masked"
msgstr "Скрытый"
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
msgid "Favorite"
msgstr "Избранное"
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
msgid "A new document was created on your behalf!"
msgstr "Новый документ был создан от вашего имени!"
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
msgid "You have been granted ownership of a new document:"
msgstr "Вы назначены владельцем для нового документа:"
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
msgid "This field is required."
msgstr "Это поле обязательное."
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Доступ по ссылке '%(link_reach)s' запрещён в соответствии с настройками родительского документа."
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
#, python-brace-format
msgid "copy of {title}"
msgstr "копия {title}"
@@ -247,46 +251,46 @@ msgstr "пользователь"
msgid "users"
msgstr "пользователи"
#: build/lib/core/models.py:378 core/models.py:378
#: build/lib/core/models.py:376 core/models.py:376
msgid "Active email address"
msgstr "Активный адрес электронной почты"
#: build/lib/core/models.py:379 core/models.py:379
#: build/lib/core/models.py:377 core/models.py:377
msgid "Email address to deactivate"
msgstr "Адрес электронной почты для деактивации"
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:404 core/models.py:404
msgid "Unique ID in the source file"
msgstr "Уникальный идентификатор в исходном файле"
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
#: core/models.py:708
msgid "Pending"
msgstr "В обработке"
#: build/lib/core/models.py:413 core/models.py:413
#: build/lib/core/models.py:411 core/models.py:411
msgid "Ready"
msgstr "Готово"
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Done"
msgstr "Выполнено"
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
#: core/models.py:711
msgid "Error"
msgstr "Ошибка"
#: build/lib/core/models.py:423 core/models.py:423
#: build/lib/core/models.py:421 core/models.py:421
msgid "user reconciliation"
msgstr "сверка данных пользователя"
#: build/lib/core/models.py:424 core/models.py:424
#: build/lib/core/models.py:422 core/models.py:422
msgid "user reconciliations"
msgstr "сверки данных пользователя"
#: build/lib/core/models.py:662 core/models.py:662
#: build/lib/core/models.py:660 core/models.py:660
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
@@ -294,54 +298,54 @@ msgstr "Вы запросили сверку учётных записей по
" Чтобы подтвердить факт того, что вы являетесь инициатором запроса\n"
" и что этот адрес принадлежит вам:"
#: build/lib/core/models.py:668 core/models.py:668
#: build/lib/core/models.py:666 core/models.py:666
msgid "Confirm by clicking the link to start the reconciliation"
msgstr "Чтобы начать сверку, подтвердите это, нажав на ссылку"
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
#: core/models.py:777
msgid "Click here"
msgstr "Нажмите здесь"
#: build/lib/core/models.py:674 core/models.py:674
#: build/lib/core/models.py:672 core/models.py:672
msgid "Confirm"
msgstr "Подтверждение"
#: build/lib/core/models.py:685 core/models.py:685
#: build/lib/core/models.py:683 core/models.py:683
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr "Ваш запрос на сверку был обработан.\n"
" Новые документы, вероятно, связаны с вашей учётной записью:"
#: build/lib/core/models.py:690 core/models.py:690
#: build/lib/core/models.py:688 core/models.py:688
msgid "Your accounts have been merged"
msgstr "Ваши учётные записи были объединены"
#: build/lib/core/models.py:695 core/models.py:695
#: build/lib/core/models.py:693 core/models.py:693
msgid "Click here to see"
msgstr "Нажмите здесь, чтобы просмотреть"
#: build/lib/core/models.py:696 core/models.py:696
#: build/lib/core/models.py:694 core/models.py:694
msgid "See my documents"
msgstr "Просмотреть мои документы"
#: build/lib/core/models.py:706 core/models.py:706
#: build/lib/core/models.py:704 core/models.py:704
msgid "CSV file"
msgstr "CSV-файл"
#: build/lib/core/models.py:711 core/models.py:711
#: build/lib/core/models.py:709 core/models.py:709
msgid "Running"
msgstr "Выполнение"
#: build/lib/core/models.py:721 core/models.py:721
#: build/lib/core/models.py:719 core/models.py:719
msgid "user reconciliation CSV import"
msgstr "импорт из CSV сверки пользователей"
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:720 core/models.py:720
msgid "user reconciliation CSV imports"
msgstr "импорты из CSV сверки пользователями"
#: build/lib/core/models.py:766 core/models.py:766
#: build/lib/core/models.py:764 core/models.py:764
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
@@ -354,175 +358,175 @@ msgstr "Ваш запрос на сверку не удался.\n"
" Пожалуйста, проверьте, нет ли в них опечаток.\n"
" Вы можете отправить ещё один запрос с действительными адресами электронной почты."
#: build/lib/core/models.py:774 core/models.py:774
#: build/lib/core/models.py:772 core/models.py:772
msgid "Reconciliation of your Docs accounts not completed"
msgstr "Сверка ваших учётных записей Docs не завершена"
#: build/lib/core/models.py:780 core/models.py:780
#: build/lib/core/models.py:778 core/models.py:778
msgid "Make a new request"
msgstr "Создать новый запрос"
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:877 core/models.py:877
msgid "title"
msgstr "заголовок"
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:878 core/models.py:878
msgid "excerpt"
msgstr "отрывок"
#: build/lib/core/models.py:929 core/models.py:929
#: build/lib/core/models.py:927 core/models.py:927
msgid "Document"
msgstr "Документ"
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:928 core/models.py:928
msgid "Documents"
msgstr "Документы"
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Безымянный документ"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Открыть"
#: build/lib/core/models.py:1382 core/models.py:1382
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} делится с вами документом!"
#: build/lib/core/models.py:1386 core/models.py:1386
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} приглашает вас присоединиться к следующему документу с ролью \"{role}\":"
#: build/lib/core/models.py:1392 core/models.py:1392
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} делится с вами документом: {title}"
#: build/lib/core/models.py:1493 core/models.py:1493
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Трассировка связи документ/пользователь"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Трассировка связей документ/пользователь"
#: build/lib/core/models.py:1500 core/models.py:1500
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr "Для этого документа/пользователя уже существует трассировка ссылки."
#: build/lib/core/models.py:1523 core/models.py:1523
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Избранный документ"
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Избранные документы"
#: build/lib/core/models.py:1530 core/models.py:1530
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Этот документ уже помечен как избранный для этого пользователя."
#: build/lib/core/models.py:1552 core/models.py:1552
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Отношение документ/пользователь"
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Отношения документ/пользователь"
#: build/lib/core/models.py:1559 core/models.py:1559
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Этот пользователь уже имеет доступ к этому документу."
#: build/lib/core/models.py:1565 core/models.py:1565
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Эта команда уже имеет доступ к этому документу."
#: build/lib/core/models.py:1571 core/models.py:1571
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr "Может быть выбран либо пользователь, либо команда, но не оба варианта сразу."
#: build/lib/core/models.py:1722 core/models.py:1722
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Документ запрашивает доступ"
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Документ запрашивает доступы"
#: build/lib/core/models.py:1729 core/models.py:1729
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr "Этот пользователь уже запросил доступ к этому документу."
#: build/lib/core/models.py:1786 core/models.py:1786
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хочет получить доступ к документу!"
#: build/lib/core/models.py:1790 core/models.py:1790
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} хочет получить доступ к следующему документу:"
#: build/lib/core/models.py:1796 core/models.py:1796
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запрашивает доступ к документу: {title}"
#: build/lib/core/models.py:1838 core/models.py:1838
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Обсуждение"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Обсуждения"
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "Аноним"
#: build/lib/core/models.py:1889 core/models.py:1889
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Комментарий"
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Комментарии"
#: build/lib/core/models.py:1939 core/models.py:1939
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr "Этот эмодзи уже использован в этом комментарии."
#: build/lib/core/models.py:1943 core/models.py:1943
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Реакция"
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Реакции"
#: build/lib/core/models.py:1954 core/models.py:1954
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "адрес электронной почты"
#: build/lib/core/models.py:1973 core/models.py:1973
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Приглашение для документа"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Приглашения для документов"
#: build/lib/core/models.py:1994 core/models.py:1994
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Этот адрес уже связан с зарегистрированным пользователем."
#: build/lib/impress/settings.py:702 impress/settings.py:702
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs ИИ"
+87 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
"PO-Revision-Date: 2026-03-25 16:55\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Language: sl_SI\n"
@@ -46,36 +46,40 @@ msgstr "Drevesna struktura"
msgid "Title"
msgstr "Naslov"
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
msgid "Search"
msgstr ""
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Creator is me"
msgstr "Ustvaril sem jaz"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
msgid "Favorite"
msgstr "Priljubljena"
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
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:530 core/api/serializers.py:530
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
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:566 core/api/serializers.py:566
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -247,98 +251,98 @@ msgstr "uporabnik"
msgid "users"
msgstr "uporabniki"
#: build/lib/core/models.py:378 core/models.py:378
#: build/lib/core/models.py:376 core/models.py:376
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:379 core/models.py:379
#: build/lib/core/models.py:377 core/models.py:377
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:404 core/models.py:404
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
#: core/models.py:708
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
#: core/models.py:711
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
#: build/lib/core/models.py:421 core/models.py:421
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
#: build/lib/core/models.py:422 core/models.py:422
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
#: build/lib/core/models.py:660 core/models.py:660
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
#: build/lib/core/models.py:666 core/models.py:666
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
#: core/models.py:777
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
#: build/lib/core/models.py:672 core/models.py:672
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
#: build/lib/core/models.py:683 core/models.py:683
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
#: build/lib/core/models.py:688 core/models.py:688
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
#: build/lib/core/models.py:693 core/models.py:693
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
#: build/lib/core/models.py:694 core/models.py:694
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
#: build/lib/core/models.py:704 core/models.py:704
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
#: build/lib/core/models.py:709 core/models.py:709
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
#: build/lib/core/models.py:719 core/models.py:719
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:720 core/models.py:720
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#: build/lib/core/models.py:764 core/models.py:764
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
#: build/lib/core/models.py:772 core/models.py:772
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
#: build/lib/core/models.py:778 core/models.py:778
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:877 core/models.py:877
msgid "title"
msgstr "naslov"
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:878 core/models.py:878
msgid "excerpt"
msgstr "odlomek"
#: build/lib/core/models.py:929 core/models.py:929
#: build/lib/core/models.py:927 core/models.py:927
msgid "Document"
msgstr "Dokument"
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:928 core/models.py:928
msgid "Documents"
msgstr "Dokumenti"
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Dokument brez naslova"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Odpri"
#: build/lib/core/models.py:1382 core/models.py:1382
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} je delil dokument z vami!"
#: build/lib/core/models.py:1386 core/models.py:1386
#: build/lib/core/models.py:1385 core/models.py:1385
#, 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:1392 core/models.py:1392
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} je delil dokument z vami: {title}"
#: build/lib/core/models.py:1493 core/models.py:1493
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Dokument/sled povezave uporabnika"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Sledi povezav dokumenta/uporabnika"
#: build/lib/core/models.py:1500 core/models.py:1500
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr "Za ta dokument/uporabnika že obstaja sled povezave."
#: build/lib/core/models.py:1523 core/models.py:1523
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Priljubljeni dokument"
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Priljubljeni dokumenti"
#: build/lib/core/models.py:1530 core/models.py:1530
#: build/lib/core/models.py:1529 core/models.py:1529
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:1552 core/models.py:1552
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Odnos dokument/uporabnik"
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Odnosi dokument/uporabnik"
#: build/lib/core/models.py:1559 core/models.py:1559
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Ta uporabnik je že v tem dokumentu."
#: build/lib/core/models.py:1565 core/models.py:1565
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Ta ekipa je že v tem dokumentu."
#: build/lib/core/models.py:1571 core/models.py:1571
#: build/lib/core/models.py:1570 core/models.py:1570
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:1722 core/models.py:1722
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1729 core/models.py:1729
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1786 core/models.py:1786
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1790 core/models.py:1790
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1796 core/models.py:1796
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1838 core/models.py:1838
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1889 core/models.py:1889
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1939 core/models.py:1939
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1943 core/models.py:1943
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1954 core/models.py:1954
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "elektronski naslov"
#: build/lib/core/models.py:1973 core/models.py:1973
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Vabilo na dokument"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Vabila na dokument"
#: build/lib/core/models.py:1994 core/models.py:1994
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Ta e-poštni naslov je že povezan z registriranim uporabnikom."
#: build/lib/impress/settings.py:702 impress/settings.py:702
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""
+87 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
"PO-Revision-Date: 2026-03-25 16:55\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Language: sv_SE\n"
@@ -46,36 +46,40 @@ msgstr ""
msgid "Title"
msgstr "Titel"
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
msgid "Search"
msgstr ""
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Creator is me"
msgstr "Skaparen är jag"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
msgid "Favorite"
msgstr "Favoriter"
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
msgid "A new document was created on your behalf!"
msgstr "Ett nytt dokument skapades åt dig!"
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
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:566 core/api/serializers.py:566
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -247,98 +251,98 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:378 core/models.py:378
#: build/lib/core/models.py:376 core/models.py:376
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:379 core/models.py:379
#: build/lib/core/models.py:377 core/models.py:377
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:404 core/models.py:404
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
#: core/models.py:708
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
#: core/models.py:711
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
#: build/lib/core/models.py:421 core/models.py:421
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
#: build/lib/core/models.py:422 core/models.py:422
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
#: build/lib/core/models.py:660 core/models.py:660
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
#: build/lib/core/models.py:666 core/models.py:666
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
#: core/models.py:777
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
#: build/lib/core/models.py:672 core/models.py:672
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
#: build/lib/core/models.py:683 core/models.py:683
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
#: build/lib/core/models.py:688 core/models.py:688
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
#: build/lib/core/models.py:693 core/models.py:693
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
#: build/lib/core/models.py:694 core/models.py:694
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
#: build/lib/core/models.py:704 core/models.py:704
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
#: build/lib/core/models.py:709 core/models.py:709
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
#: build/lib/core/models.py:719 core/models.py:719
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:720 core/models.py:720
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#: build/lib/core/models.py:764 core/models.py:764
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
#: build/lib/core/models.py:772 core/models.py:772
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
#: build/lib/core/models.py:778 core/models.py:778
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:877 core/models.py:877
msgid "title"
msgstr ""
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:878 core/models.py:878
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:929 core/models.py:929
#: build/lib/core/models.py:927 core/models.py:927
msgid "Document"
msgstr ""
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:928 core/models.py:928
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Öppna"
#: build/lib/core/models.py:1382 core/models.py:1382
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:1386 core/models.py:1386
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:1392 core/models.py:1392
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1493 core/models.py:1493
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1500 core/models.py:1500
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1523 core/models.py:1523
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1530 core/models.py:1530
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1552 core/models.py:1552
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1559 core/models.py:1559
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1565 core/models.py:1565
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1571 core/models.py:1571
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1722 core/models.py:1722
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1729 core/models.py:1729
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1786 core/models.py:1786
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1790 core/models.py:1790
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1796 core/models.py:1796
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1838 core/models.py:1838
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1889 core/models.py:1889
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1939 core/models.py:1939
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1943 core/models.py:1943
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1954 core/models.py:1954
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "e-postadress"
#: build/lib/core/models.py:1973 core/models.py:1973
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Bjud in dokument"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Inbjudningar dokument"
#: build/lib/core/models.py:1994 core/models.py:1994
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Denna e-postadress är redan associerad med en registrerad användare."
#: build/lib/impress/settings.py:702 impress/settings.py:702
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""
+87 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
"PO-Revision-Date: 2026-03-25 16:55\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Language: tr_TR\n"
@@ -46,36 +46,40 @@ msgstr ""
msgid "Title"
msgstr ""
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
msgid "Creator is me"
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
msgid "Search"
msgstr ""
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Masked"
msgid "Creator is me"
msgstr ""
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Masked"
msgstr ""
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
msgid "Favorite"
msgstr ""
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
msgid "A new document was created on your behalf!"
msgstr ""
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
msgid "You have been granted ownership of a new document:"
msgstr ""
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
msgid "This field is required."
msgstr ""
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr ""
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
#, python-brace-format
msgid "copy of {title}"
msgstr ""
@@ -247,98 +251,98 @@ msgstr ""
msgid "users"
msgstr ""
#: build/lib/core/models.py:378 core/models.py:378
#: build/lib/core/models.py:376 core/models.py:376
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:379 core/models.py:379
#: build/lib/core/models.py:377 core/models.py:377
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:404 core/models.py:404
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
#: core/models.py:708
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
#: core/models.py:711
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
#: build/lib/core/models.py:421 core/models.py:421
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
#: build/lib/core/models.py:422 core/models.py:422
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
#: build/lib/core/models.py:660 core/models.py:660
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
#: build/lib/core/models.py:666 core/models.py:666
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
#: core/models.py:777
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
#: build/lib/core/models.py:672 core/models.py:672
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
#: build/lib/core/models.py:683 core/models.py:683
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
#: build/lib/core/models.py:688 core/models.py:688
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
#: build/lib/core/models.py:693 core/models.py:693
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
#: build/lib/core/models.py:694 core/models.py:694
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
#: build/lib/core/models.py:704 core/models.py:704
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
#: build/lib/core/models.py:709 core/models.py:709
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
#: build/lib/core/models.py:719 core/models.py:719
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:720 core/models.py:720
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#: build/lib/core/models.py:764 core/models.py:764
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
#: build/lib/core/models.py:772 core/models.py:772
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
#: build/lib/core/models.py:778 core/models.py:778
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:877 core/models.py:877
msgid "title"
msgstr ""
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:878 core/models.py:878
msgid "excerpt"
msgstr ""
#: build/lib/core/models.py:929 core/models.py:929
#: build/lib/core/models.py:927 core/models.py:927
msgid "Document"
msgstr ""
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:928 core/models.py:928
msgid "Documents"
msgstr ""
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr ""
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr ""
#: build/lib/core/models.py:1382 core/models.py:1382
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr ""
#: build/lib/core/models.py:1386 core/models.py:1386
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr ""
#: build/lib/core/models.py:1392 core/models.py:1392
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr ""
#: build/lib/core/models.py:1493 core/models.py:1493
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr ""
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr ""
#: build/lib/core/models.py:1500 core/models.py:1500
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr ""
#: build/lib/core/models.py:1523 core/models.py:1523
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr ""
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr ""
#: build/lib/core/models.py:1530 core/models.py:1530
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr ""
#: build/lib/core/models.py:1552 core/models.py:1552
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr ""
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr ""
#: build/lib/core/models.py:1559 core/models.py:1559
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr ""
#: build/lib/core/models.py:1565 core/models.py:1565
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr ""
#: build/lib/core/models.py:1571 core/models.py:1571
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr ""
#: build/lib/core/models.py:1722 core/models.py:1722
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr ""
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr ""
#: build/lib/core/models.py:1729 core/models.py:1729
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr ""
#: build/lib/core/models.py:1786 core/models.py:1786
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr ""
#: build/lib/core/models.py:1790 core/models.py:1790
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr ""
#: build/lib/core/models.py:1796 core/models.py:1796
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr ""
#: build/lib/core/models.py:1838 core/models.py:1838
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr ""
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr ""
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr ""
#: build/lib/core/models.py:1889 core/models.py:1889
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr ""
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr ""
#: build/lib/core/models.py:1939 core/models.py:1939
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr ""
#: build/lib/core/models.py:1943 core/models.py:1943
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr ""
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr ""
#: build/lib/core/models.py:1954 core/models.py:1954
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr ""
#: build/lib/core/models.py:1973 core/models.py:1973
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr ""
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr ""
#: build/lib/core/models.py:1994 core/models.py:1994
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr ""
#: build/lib/impress/settings.py:702 impress/settings.py:702
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""
+83 -79
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
"PO-Revision-Date: 2026-03-25 16:55\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -46,36 +46,40 @@ msgstr "Ієрархічна структура"
msgid "Title"
msgstr "Заголовок"
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
msgid "Search"
msgstr "Пошук"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Creator is me"
msgstr "Творець — я"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Masked"
msgstr "Приховано"
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
msgid "Favorite"
msgstr "Обране"
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
msgid "A new document was created on your behalf!"
msgstr "Новий документ був створений від вашого імені!"
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
msgid "You have been granted ownership of a new document:"
msgstr "Ви тепер є власником нового документа:"
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
msgid "This field is required."
msgstr "Це поле є обов’язковим."
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "Доступ до посилання '%(link_reach)s' заборонено на основі конфігурації батьківського документа."
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
#, python-brace-format
msgid "copy of {title}"
msgstr "копія {title}"
@@ -247,46 +251,46 @@ msgstr "користувач"
msgid "users"
msgstr "користувачі"
#: build/lib/core/models.py:378 core/models.py:378
#: build/lib/core/models.py:376 core/models.py:376
msgid "Active email address"
msgstr "Активна електронна адреса"
#: build/lib/core/models.py:379 core/models.py:379
#: build/lib/core/models.py:377 core/models.py:377
msgid "Email address to deactivate"
msgstr "Електронна адреса, що буде деактивована"
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:404 core/models.py:404
msgid "Unique ID in the source file"
msgstr "Унікальний ідентифікатор у вихідному файлі"
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
#: core/models.py:708
msgid "Pending"
msgstr "В очікуванні"
#: build/lib/core/models.py:413 core/models.py:413
#: build/lib/core/models.py:411 core/models.py:411
msgid "Ready"
msgstr "Готово"
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Done"
msgstr "Виконано"
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
#: core/models.py:711
msgid "Error"
msgstr "Помилка"
#: build/lib/core/models.py:423 core/models.py:423
#: build/lib/core/models.py:421 core/models.py:421
msgid "user reconciliation"
msgstr "узгодження користувачів"
#: build/lib/core/models.py:424 core/models.py:424
#: build/lib/core/models.py:422 core/models.py:422
msgid "user reconciliations"
msgstr "узгодження користувачів"
#: build/lib/core/models.py:662 core/models.py:662
#: build/lib/core/models.py:660 core/models.py:660
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
@@ -294,54 +298,54 @@ msgstr "Ви запросили узгодження своїх облікови
" Щоб підтвердити, що саме ви ініціювали запит\n"
" і що ця електронна адреса належить вам:"
#: build/lib/core/models.py:668 core/models.py:668
#: build/lib/core/models.py:666 core/models.py:666
msgid "Confirm by clicking the link to start the reconciliation"
msgstr "Підтвердіть, натиснувши на посилання, щоб почати узгодження"
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
#: core/models.py:777
msgid "Click here"
msgstr "Натисніть тут"
#: build/lib/core/models.py:674 core/models.py:674
#: build/lib/core/models.py:672 core/models.py:672
msgid "Confirm"
msgstr "Підтвердження"
#: build/lib/core/models.py:685 core/models.py:685
#: build/lib/core/models.py:683 core/models.py:683
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr "Ваш запит на узгодження оброблено.\n"
" Нові документи, ймовірно, пов'язані з вашим обліковим записом:"
#: build/lib/core/models.py:690 core/models.py:690
#: build/lib/core/models.py:688 core/models.py:688
msgid "Your accounts have been merged"
msgstr "Ваші облікові записи були об'єднані"
#: build/lib/core/models.py:695 core/models.py:695
#: build/lib/core/models.py:693 core/models.py:693
msgid "Click here to see"
msgstr "Натисніть тут, щоб переглянути"
#: build/lib/core/models.py:696 core/models.py:696
#: build/lib/core/models.py:694 core/models.py:694
msgid "See my documents"
msgstr "Переглянути мої документи"
#: build/lib/core/models.py:706 core/models.py:706
#: build/lib/core/models.py:704 core/models.py:704
msgid "CSV file"
msgstr "CSV-файл"
#: build/lib/core/models.py:711 core/models.py:711
#: build/lib/core/models.py:709 core/models.py:709
msgid "Running"
msgstr "Виконується"
#: build/lib/core/models.py:721 core/models.py:721
#: build/lib/core/models.py:719 core/models.py:719
msgid "user reconciliation CSV import"
msgstr "імпорт CSV для узгодження користувачів"
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:720 core/models.py:720
msgid "user reconciliation CSV imports"
msgstr "імпорт CSV для узгодження користувачів"
#: build/lib/core/models.py:766 core/models.py:766
#: build/lib/core/models.py:764 core/models.py:764
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
@@ -354,175 +358,175 @@ msgstr "Ваш запит на узгодження не був виконани
" Перевірте, чи немає помилок.\n"
" Ви можете надіслати інший запит із дійсними адресами електронної пошти."
#: build/lib/core/models.py:774 core/models.py:774
#: build/lib/core/models.py:772 core/models.py:772
msgid "Reconciliation of your Docs accounts not completed"
msgstr "Узгодження ваших облікових записів не завершено"
#: build/lib/core/models.py:780 core/models.py:780
#: build/lib/core/models.py:778 core/models.py:778
msgid "Make a new request"
msgstr "Зробити новий запит"
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:877 core/models.py:877
msgid "title"
msgstr "заголовок"
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:878 core/models.py:878
msgid "excerpt"
msgstr "уривок"
#: build/lib/core/models.py:929 core/models.py:929
#: build/lib/core/models.py:927 core/models.py:927
msgid "Document"
msgstr "Документ"
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:928 core/models.py:928
msgid "Documents"
msgstr "Документи"
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "Документ без назви"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "Відкрити"
#: build/lib/core/models.py:1382 core/models.py:1382
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} ділиться з вами документом!"
#: build/lib/core/models.py:1386 core/models.py:1386
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} запрошує вас для роботи з документом із роллю \"{role}\":"
#: build/lib/core/models.py:1392 core/models.py:1392
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} ділиться з вами документом: {title}"
#: build/lib/core/models.py:1493 core/models.py:1493
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "Трасування посилання Документ/користувач"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "Трасування посилань Документ/користувач"
#: build/lib/core/models.py:1500 core/models.py:1500
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr "Відстеження вже існуючих посилань для цього документа/користувача."
#: build/lib/core/models.py:1523 core/models.py:1523
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "Обраний документ"
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "Обрані документи"
#: build/lib/core/models.py:1530 core/models.py:1530
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "Цей документ вже вказаний як обраний для одного користувача."
#: build/lib/core/models.py:1552 core/models.py:1552
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "Відносини документ/користувач"
#: build/lib/core/models.py:1559 core/models.py:1559
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "Цей користувач вже має доступ до цього документу."
#: build/lib/core/models.py:1565 core/models.py:1565
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "Ця команда вже має доступ до цього документа."
#: build/lib/core/models.py:1571 core/models.py:1571
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr "Вкажіть користувача або команду, а не обох."
#: build/lib/core/models.py:1722 core/models.py:1722
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "Запит доступу до документа"
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "Запит доступу для документа"
#: build/lib/core/models.py:1729 core/models.py:1729
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr "Цей користувач вже попросив доступ до цього документа."
#: build/lib/core/models.py:1786 core/models.py:1786
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} хоче отримати доступ до документа!"
#: build/lib/core/models.py:1790 core/models.py:1790
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} бажає отримати доступ до наступного документа:"
#: build/lib/core/models.py:1796 core/models.py:1796
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} запитує доступ до документа: {title}"
#: build/lib/core/models.py:1838 core/models.py:1838
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "Обговорення"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "Обговорення"
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "Анонім"
#: build/lib/core/models.py:1889 core/models.py:1889
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "Коментар"
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "Коментарі"
#: build/lib/core/models.py:1939 core/models.py:1939
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr "Цим емодзі вже відреагували на цей коментар."
#: build/lib/core/models.py:1943 core/models.py:1943
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "Реакція"
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "Реакції"
#: build/lib/core/models.py:1954 core/models.py:1954
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "електронна адреса"
#: build/lib/core/models.py:1973 core/models.py:1973
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "Запрошення до редагування документа"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "Запрошення до редагування документів"
#: build/lib/core/models.py:1994 core/models.py:1994
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "Ця електронна пошта вже пов'язана з зареєстрованим користувачем."
#: build/lib/impress/settings.py:702 impress/settings.py:702
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr "Docs ШІ"
+87 -83
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: lasuite-docs\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-12 13:31+0000\n"
"PO-Revision-Date: 2026-03-13 16:53\n"
"POT-Creation-Date: 2026-03-25 16:42+0000\n"
"PO-Revision-Date: 2026-03-25 16:55\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -46,36 +46,40 @@ msgstr "樹狀結構"
msgid "Title"
msgstr "標題"
#: build/lib/core/api/filters.py:62 core/api/filters.py:62
#: build/lib/core/api/filters.py:51 core/api/filters.py:51
msgid "Search"
msgstr ""
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
msgid "Creator is me"
msgstr "建立者是我"
#: build/lib/core/api/filters.py:65 core/api/filters.py:65
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
msgid "Masked"
msgstr "已隱藏"
#: build/lib/core/api/filters.py:68 core/api/filters.py:68
#: build/lib/core/api/filters.py:71 core/api/filters.py:71
msgid "Favorite"
msgstr "我的最愛"
#: build/lib/core/api/serializers.py:526 core/api/serializers.py:526
#: build/lib/core/api/serializers.py:535 core/api/serializers.py:535
msgid "A new document was created on your behalf!"
msgstr "已代表您建立新文件!"
#: build/lib/core/api/serializers.py:530 core/api/serializers.py:530
#: build/lib/core/api/serializers.py:539 core/api/serializers.py:539
msgid "You have been granted ownership of a new document:"
msgstr "您已獲得新文件的所有權:"
#: build/lib/core/api/serializers.py:566 core/api/serializers.py:566
#: build/lib/core/api/serializers.py:575 core/api/serializers.py:575
msgid "This field is required."
msgstr "此欄位為必填。"
#: build/lib/core/api/serializers.py:577 core/api/serializers.py:577
#: build/lib/core/api/serializers.py:586 core/api/serializers.py:586
#, python-format
msgid "Link reach '%(link_reach)s' is not allowed based on parent document configuration."
msgstr "根據父文件設定,不允許連結範圍「%(link_reach)s」。"
#: build/lib/core/api/viewsets.py:1298 core/api/viewsets.py:1298
#: build/lib/core/api/viewsets.py:1315 core/api/viewsets.py:1315
#, python-brace-format
msgid "copy of {title}"
msgstr "{title} 的副本"
@@ -247,98 +251,98 @@ msgstr "使用者"
msgid "users"
msgstr "使用者"
#: build/lib/core/models.py:378 core/models.py:378
#: build/lib/core/models.py:376 core/models.py:376
msgid "Active email address"
msgstr ""
#: build/lib/core/models.py:379 core/models.py:379
#: build/lib/core/models.py:377 core/models.py:377
msgid "Email address to deactivate"
msgstr ""
#: build/lib/core/models.py:406 core/models.py:406
#: build/lib/core/models.py:404 core/models.py:404
msgid "Unique ID in the source file"
msgstr ""
#: build/lib/core/models.py:410 build/lib/core/models.py:708 core/models.py:410
#: core/models.py:708
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:411 core/models.py:411
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:412 build/lib/core/models.py:710 core/models.py:412
#: core/models.py:710
msgid "Pending"
msgstr ""
#: build/lib/core/models.py:413 core/models.py:413
msgid "Ready"
msgstr ""
#: build/lib/core/models.py:414 build/lib/core/models.py:712 core/models.py:414
#: core/models.py:712
msgid "Done"
msgstr ""
#: build/lib/core/models.py:415 build/lib/core/models.py:713 core/models.py:415
#: core/models.py:713
#: build/lib/core/models.py:413 build/lib/core/models.py:711 core/models.py:413
#: core/models.py:711
msgid "Error"
msgstr ""
#: build/lib/core/models.py:423 core/models.py:423
#: build/lib/core/models.py:421 core/models.py:421
msgid "user reconciliation"
msgstr ""
#: build/lib/core/models.py:424 core/models.py:424
#: build/lib/core/models.py:422 core/models.py:422
msgid "user reconciliations"
msgstr ""
#: build/lib/core/models.py:662 core/models.py:662
#: build/lib/core/models.py:660 core/models.py:660
msgid "You have requested a reconciliation of your user accounts on Docs.\n"
" To confirm that you are the one who initiated the request\n"
" and that this email belongs to you:"
msgstr ""
#: build/lib/core/models.py:668 core/models.py:668
#: build/lib/core/models.py:666 core/models.py:666
msgid "Confirm by clicking the link to start the reconciliation"
msgstr ""
#: build/lib/core/models.py:673 build/lib/core/models.py:779 core/models.py:673
#: core/models.py:779
#: build/lib/core/models.py:671 build/lib/core/models.py:777 core/models.py:671
#: core/models.py:777
msgid "Click here"
msgstr ""
#: build/lib/core/models.py:674 core/models.py:674
#: build/lib/core/models.py:672 core/models.py:672
msgid "Confirm"
msgstr ""
#: build/lib/core/models.py:685 core/models.py:685
#: build/lib/core/models.py:683 core/models.py:683
msgid "Your reconciliation request has been processed.\n"
" New documents are likely associated with your account:"
msgstr ""
#: build/lib/core/models.py:690 core/models.py:690
#: build/lib/core/models.py:688 core/models.py:688
msgid "Your accounts have been merged"
msgstr ""
#: build/lib/core/models.py:695 core/models.py:695
#: build/lib/core/models.py:693 core/models.py:693
msgid "Click here to see"
msgstr ""
#: build/lib/core/models.py:696 core/models.py:696
#: build/lib/core/models.py:694 core/models.py:694
msgid "See my documents"
msgstr ""
#: build/lib/core/models.py:706 core/models.py:706
#: build/lib/core/models.py:704 core/models.py:704
msgid "CSV file"
msgstr ""
#: build/lib/core/models.py:711 core/models.py:711
#: build/lib/core/models.py:709 core/models.py:709
msgid "Running"
msgstr ""
#: build/lib/core/models.py:721 core/models.py:721
#: build/lib/core/models.py:719 core/models.py:719
msgid "user reconciliation CSV import"
msgstr ""
#: build/lib/core/models.py:722 core/models.py:722
#: build/lib/core/models.py:720 core/models.py:720
msgid "user reconciliation CSV imports"
msgstr ""
#: build/lib/core/models.py:766 core/models.py:766
#: build/lib/core/models.py:764 core/models.py:764
#, python-brace-format
msgid "Your request for reconciliation was unsuccessful.\n"
" Reconciliation failed for the following email addresses:\n"
@@ -347,175 +351,175 @@ msgid "Your request for reconciliation was unsuccessful.\n"
" You can submit another request with the valid email addresses."
msgstr ""
#: build/lib/core/models.py:774 core/models.py:774
#: build/lib/core/models.py:772 core/models.py:772
msgid "Reconciliation of your Docs accounts not completed"
msgstr ""
#: build/lib/core/models.py:780 core/models.py:780
#: build/lib/core/models.py:778 core/models.py:778
msgid "Make a new request"
msgstr ""
#: build/lib/core/models.py:879 core/models.py:879
#: build/lib/core/models.py:877 core/models.py:877
msgid "title"
msgstr "標題"
#: build/lib/core/models.py:880 core/models.py:880
#: build/lib/core/models.py:878 core/models.py:878
msgid "excerpt"
msgstr "摘要"
#: build/lib/core/models.py:929 core/models.py:929
#: build/lib/core/models.py:927 core/models.py:927
msgid "Document"
msgstr "文件"
#: build/lib/core/models.py:930 core/models.py:930
#: build/lib/core/models.py:928 core/models.py:928
msgid "Documents"
msgstr "文件"
#: build/lib/core/models.py:942 build/lib/core/models.py:1346
#: core/models.py:942 core/models.py:1346
#: build/lib/core/models.py:940 build/lib/core/models.py:1345
#: core/models.py:940 core/models.py:1345
msgid "Untitled Document"
msgstr "未命名文件"
#: build/lib/core/models.py:1347 core/models.py:1347
#: build/lib/core/models.py:1346 core/models.py:1346
msgid "Open"
msgstr "開啟"
#: build/lib/core/models.py:1382 core/models.py:1382
#: build/lib/core/models.py:1381 core/models.py:1381
#, python-brace-format
msgid "{name} shared a document with you!"
msgstr "{name} 與您分享了一份文件!"
#: build/lib/core/models.py:1386 core/models.py:1386
#: build/lib/core/models.py:1385 core/models.py:1385
#, python-brace-format
msgid "{name} invited you with the role \"{role}\" on the following document:"
msgstr "{name} 邀請您以「{role}」角色參與以下文件:"
#: build/lib/core/models.py:1392 core/models.py:1392
#: build/lib/core/models.py:1391 core/models.py:1391
#, python-brace-format
msgid "{name} shared a document with you: {title}"
msgstr "{name} 與您分享了一份文件:{title}"
#: build/lib/core/models.py:1493 core/models.py:1493
#: build/lib/core/models.py:1492 core/models.py:1492
msgid "Document/user link trace"
msgstr "文件/使用者連結追蹤"
#: build/lib/core/models.py:1494 core/models.py:1494
#: build/lib/core/models.py:1493 core/models.py:1493
msgid "Document/user link traces"
msgstr "文件/使用者連結追蹤"
#: build/lib/core/models.py:1500 core/models.py:1500
#: build/lib/core/models.py:1499 core/models.py:1499
msgid "A link trace already exists for this document/user."
msgstr "此文件/使用者已存在連結追蹤。"
#: build/lib/core/models.py:1523 core/models.py:1523
#: build/lib/core/models.py:1522 core/models.py:1522
msgid "Document favorite"
msgstr "文件收藏"
#: build/lib/core/models.py:1524 core/models.py:1524
#: build/lib/core/models.py:1523 core/models.py:1523
msgid "Document favorites"
msgstr "文件收藏"
#: build/lib/core/models.py:1530 core/models.py:1530
#: build/lib/core/models.py:1529 core/models.py:1529
msgid "This document is already targeted by a favorite relation instance for the same user."
msgstr "此使用者已將此文件加入收藏。"
#: build/lib/core/models.py:1552 core/models.py:1552
#: build/lib/core/models.py:1551 core/models.py:1551
msgid "Document/user relation"
msgstr "文件/使用者關聯"
#: build/lib/core/models.py:1553 core/models.py:1553
#: build/lib/core/models.py:1552 core/models.py:1552
msgid "Document/user relations"
msgstr "文件/使用者關聯"
#: build/lib/core/models.py:1559 core/models.py:1559
#: build/lib/core/models.py:1558 core/models.py:1558
msgid "This user is already in this document."
msgstr "此使用者已在此文件中。"
#: build/lib/core/models.py:1565 core/models.py:1565
#: build/lib/core/models.py:1564 core/models.py:1564
msgid "This team is already in this document."
msgstr "此團隊已在此文件中。"
#: build/lib/core/models.py:1571 core/models.py:1571
#: build/lib/core/models.py:1570 core/models.py:1570
msgid "Either user or team must be set, not both."
msgstr "必須設定使用者或團隊其中之一,不能同時設定兩者。"
#: build/lib/core/models.py:1722 core/models.py:1722
#: build/lib/core/models.py:1721 core/models.py:1721
msgid "Document ask for access"
msgstr "要求文件存取權"
#: build/lib/core/models.py:1723 core/models.py:1723
#: build/lib/core/models.py:1722 core/models.py:1722
msgid "Document ask for accesses"
msgstr "要求文件存取權"
#: build/lib/core/models.py:1729 core/models.py:1729
#: build/lib/core/models.py:1728 core/models.py:1728
msgid "This user has already asked for access to this document."
msgstr "此使用者已要求過存取此文件的權限。"
#: build/lib/core/models.py:1786 core/models.py:1786
#: build/lib/core/models.py:1785 core/models.py:1785
#, python-brace-format
msgid "{name} would like access to a document!"
msgstr "{name} 想要存取文件!"
#: build/lib/core/models.py:1790 core/models.py:1790
#: build/lib/core/models.py:1789 core/models.py:1789
#, python-brace-format
msgid "{name} would like access to the following document:"
msgstr "{name} 想要存取以下文件:"
#: build/lib/core/models.py:1796 core/models.py:1796
#: build/lib/core/models.py:1795 core/models.py:1795
#, python-brace-format
msgid "{name} is asking for access to the document: {title}"
msgstr "{name} 正要求存取文件:{title}"
#: build/lib/core/models.py:1838 core/models.py:1838
#: build/lib/core/models.py:1837 core/models.py:1837
msgid "Thread"
msgstr "對話串"
#: build/lib/core/models.py:1839 core/models.py:1839
#: build/lib/core/models.py:1838 core/models.py:1838
msgid "Threads"
msgstr "對話串"
#: build/lib/core/models.py:1842 build/lib/core/models.py:1894
#: core/models.py:1842 core/models.py:1894
#: build/lib/core/models.py:1841 build/lib/core/models.py:1893
#: core/models.py:1841 core/models.py:1893
msgid "Anonymous"
msgstr "匿名"
#: build/lib/core/models.py:1889 core/models.py:1889
#: build/lib/core/models.py:1888 core/models.py:1888
msgid "Comment"
msgstr "評論"
#: build/lib/core/models.py:1890 core/models.py:1890
#: build/lib/core/models.py:1889 core/models.py:1889
msgid "Comments"
msgstr "評論"
#: build/lib/core/models.py:1939 core/models.py:1939
#: build/lib/core/models.py:1938 core/models.py:1938
msgid "This emoji has already been reacted to this comment."
msgstr "此評論已標記過此表情符號。"
#: build/lib/core/models.py:1943 core/models.py:1943
#: build/lib/core/models.py:1942 core/models.py:1942
msgid "Reaction"
msgstr "回應"
#: build/lib/core/models.py:1944 core/models.py:1944
#: build/lib/core/models.py:1943 core/models.py:1943
msgid "Reactions"
msgstr "回應"
#: build/lib/core/models.py:1954 core/models.py:1954
#: build/lib/core/models.py:1953 core/models.py:1953
msgid "email address"
msgstr "電子郵件地址"
#: build/lib/core/models.py:1973 core/models.py:1973
#: build/lib/core/models.py:1972 core/models.py:1972
msgid "Document invitation"
msgstr "文件邀請"
#: build/lib/core/models.py:1974 core/models.py:1974
#: build/lib/core/models.py:1973 core/models.py:1973
msgid "Document invitations"
msgstr "文件邀請"
#: build/lib/core/models.py:1994 core/models.py:1994
#: build/lib/core/models.py:1993 core/models.py:1993
msgid "This email is already associated to a registered user."
msgstr "此電子郵件地址已與已註冊使用者關聯。"
#: build/lib/impress/settings.py:702 impress/settings.py:702
#: build/lib/impress/settings.py:808 impress/settings.py:808
msgid "Docs AI"
msgstr ""
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "impress"
version = "4.8.2"
version = "4.8.4"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test';
import {
createDoc,
getMenuItem,
mockedDocument,
overrideConfig,
verifyDocName,
@@ -47,9 +46,9 @@ test.describe('Doc AI feature', () => {
await page.locator('.bn-block-outer').last().fill('Anything');
await page.getByText('Anything').selectText();
expect(
await page.locator('button[data-test="convertMarkdown"]').count(),
).toBe(1);
await expect(
page.locator('button[data-test="convertMarkdown"]'),
).toHaveCount(1);
await expect(
page.getByRole('button', { name: config.selector, exact: true }),
).toBeHidden();
@@ -179,18 +178,32 @@ test.describe('Doc AI feature', () => {
await page.getByRole('button', { name: 'AI', exact: true }).click();
await expect(getMenuItem(page, 'Use as prompt')).toBeVisible();
await expect(getMenuItem(page, 'Rephrase')).toBeVisible();
await expect(getMenuItem(page, 'Summarize')).toBeVisible();
await expect(getMenuItem(page, 'Correct')).toBeVisible();
await expect(getMenuItem(page, 'Language')).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
await expect(
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 getMenuItem(page, 'Language').hover();
await expect(getMenuItem(page, 'English', { exact: true })).toBeVisible();
await expect(getMenuItem(page, 'French', { exact: true })).toBeVisible();
await expect(getMenuItem(page, 'German', { exact: true })).toBeVisible();
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 getMenuItem(page, 'German', { exact: true }).click();
await page.getByRole('menuitem', { name: 'German', exact: true }).click();
await expect(editor.getByText('Hallo Welt')).toBeVisible();
});
@@ -256,15 +269,23 @@ test.describe('Doc AI feature', () => {
await page.getByRole('button', { name: 'AI', exact: true }).click();
if (ai_transform) {
await expect(getMenuItem(page, 'Use as prompt')).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeVisible();
} else {
await expect(getMenuItem(page, 'Use as prompt')).toBeHidden();
await expect(
page.getByRole('menuitem', { name: 'Use as prompt' }),
).toBeHidden();
}
if (ai_translate) {
await expect(getMenuItem(page, 'Language')).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeVisible();
} else {
await expect(getMenuItem(page, 'Language')).toBeHidden();
await expect(
page.getByRole('menuitem', { name: 'Language' }),
).toBeHidden();
}
});
});
@@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test';
import {
closeHeaderMenu,
createDoc,
getMenuItem,
getOtherBrowserName,
verifyDocName,
} from './utils-common';
@@ -131,12 +130,13 @@ test.describe('Doc Comments', () => {
await thread.getByRole('paragraph').first().fill('This is a comment');
await thread.locator('[data-test="save"]').click();
await expect(thread.getByText('This is a comment').first()).toBeHidden();
await expect(editor.getByText('Hello')).toHaveClass('bn-thread-mark');
// Check background color changed
await expect(editor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(237, 180, 0, 0.4)',
'color(srgb 0.882353 0.831373 0.717647 / 0.4)',
);
await editor.first().click();
await editor.getByText('Hello').click();
@@ -151,7 +151,7 @@ test.describe('Doc Comments', () => {
// Edit Comment
await thread.getByText('This is a comment').first().hover();
await thread.locator('[data-test="moreactions"]').first().click();
await getMenuItem(thread, 'Edit comment').click();
await thread.getByRole('menuitem', { name: 'Edit comment' }).click();
const commentEditor = thread.getByText('This is a comment').first();
await commentEditor.fill('This is an edited comment');
const saveBtn = thread.locator('button[data-test="save"]').first();
@@ -176,7 +176,7 @@ test.describe('Doc Comments', () => {
// Delete second comment
await thread.getByText('This is a second comment').first().hover();
await thread.locator('[data-test="moreactions"]').first().click();
await getMenuItem(thread, 'Delete comment').click();
await thread.getByRole('menuitem', { name: 'Delete comment' }).click();
await expect(
thread.getByText('This is a second comment').first(),
).toBeHidden();
@@ -185,6 +185,7 @@ test.describe('Doc Comments', () => {
await thread.getByText('This is an edited comment').first().hover();
await thread.locator('[data-test="resolve"]').click();
await expect(thread).toBeHidden();
await expect(editor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
@@ -196,18 +197,21 @@ test.describe('Doc Comments', () => {
await thread.getByRole('paragraph').first().fill('This is a new comment');
await thread.locator('[data-test="save"]').click();
await expect(editor.getByText('Hello')).toHaveClass('bn-thread-mark');
await expect(editor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(237, 180, 0, 0.4)',
'color(srgb 0.882353 0.831373 0.717647 / 0.4)',
);
await editor.first().click();
await editor.getByText('Hello').click();
await thread.getByText('This is a new comment').first().hover();
await thread.locator('[data-test="moreactions"]').first().click();
await getMenuItem(thread, 'Delete comment').click();
await thread.getByRole('menuitem', { name: 'Delete comment' }).click();
await expect(editor.getByText('Hello')).not.toHaveClass('bn-thread-mark');
await expect(editor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)',
@@ -263,7 +267,7 @@ test.describe('Doc Comments', () => {
await expect(otherEditor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(237, 180, 0, 0.4)',
'color(srgb 0.882353 0.831373 0.717647 / 0.4)',
);
// We change the role of the second user to reader
@@ -298,7 +302,7 @@ test.describe('Doc Comments', () => {
await expect(otherEditor.getByText('Hello')).toHaveCSS(
'background-color',
'rgba(237, 180, 0, 0.4)',
'color(srgb 0.882353 0.831373 0.717647 / 0.4)',
);
await otherEditor.getByText('Hello').click();
await expect(
@@ -344,7 +348,7 @@ test.describe('Doc Comments', () => {
await expect(editor1.getByText('Document One')).toHaveCSS(
'background-color',
'rgba(237, 180, 0, 0.4)',
'color(srgb 0.882353 0.831373 0.717647 / 0.4)',
);
await editor1.getByText('Document One').click();
@@ -5,7 +5,6 @@ import cs from 'convert-stream';
import {
createDoc,
getMenuItem,
goToGridDoc,
overrideConfig,
verifyDocName,
@@ -148,20 +147,18 @@ test.describe('Doc Editor', () => {
const wsClosePromise = webSocket.waitForEvent('close');
await selectVisibility.click();
await getMenuItem(page, 'Connected').click();
await page.getByRole('menuitemradio', { name: 'Connected' }).click();
// Assert that the doc reconnects to the ws
const wsClose = await wsClosePromise;
expect(wsClose.isClosed()).toBeTruthy();
// Check the ws is connected again
webSocketPromise = page.waitForEvent('websocket', (webSocket) => {
webSocket = await page.waitForEvent('websocket', (webSocket) => {
return webSocket
.url()
.includes('ws://localhost:4444/collaboration/ws/?room=');
});
webSocket = await webSocketPromise;
framesentPromise = webSocket.waitForEvent('framesent');
framesent = await framesentPromise;
expect(framesent.payload).not.toBeNull();
@@ -578,12 +575,10 @@ test.describe('Doc Editor', () => {
await page.reload();
responseCanEditPromise = page.waitForResponse(
responseCanEdit = await page.waitForResponse(
(response) =>
response.url().includes(`/can-edit/`) && response.status() === 200,
);
responseCanEdit = await responseCanEditPromise;
expect(responseCanEdit.ok()).toBeTruthy();
jsonCanEdit = (await responseCanEdit.json()) as { can_edit: boolean };
@@ -609,7 +604,7 @@ test.describe('Doc Editor', () => {
await page.getByRole('button', { name: 'Share' }).click();
await page.getByTestId('doc-access-mode').click();
await getMenuItem(page, 'Reading').click();
await page.getByRole('menuitemradio', { name: 'Reading' }).click();
// Close the modal
await page.getByRole('button', { name: 'close' }).first().click();
@@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test';
import {
createDoc,
getGridRow,
getMenuItem,
getOtherBrowserName,
mockedListDocs,
toggleHeaderMenu,
@@ -207,7 +206,7 @@ test.describe('Doc grid move', () => {
const row = await getGridRow(page, titleDoc1);
await row.getByText(`more_horiz`).click();
await getMenuItem(page, 'Move into a doc').click();
await page.getByRole('menuitem', { name: 'Move into a doc' }).click();
await expect(
page.getByRole('dialog').getByRole('heading', { name: 'Move' }),
@@ -295,7 +294,7 @@ test.describe('Doc grid move', () => {
const row = await getGridRow(page, titleDoc1);
await row.getByText(`more_horiz`).click();
await getMenuItem(page, 'Move into a doc').click();
await page.getByRole('menuitem', { name: 'Move into a doc' }).click();
await expect(
page.getByRole('dialog').getByRole('heading', { name: 'Move' }),
@@ -342,7 +341,9 @@ test.describe('Doc grid move', () => {
`doc-share-access-request-row-${emailRequest}`,
);
await container.getByTestId('doc-role-dropdown').click();
await getMenuItem(otherPage, 'Administrator').click();
await otherPage
.getByRole('menuitemradio', { name: 'Administrator' })
.click();
await container.getByRole('button', { name: 'Approve' }).click();
await expect(otherPage.getByText('Access Requests')).toBeHidden();
@@ -353,7 +354,7 @@ test.describe('Doc grid move', () => {
await page.reload();
await row.getByText(`more_horiz`).click();
await getMenuItem(page, 'Move into a doc').click();
await page.getByRole('menuitem', { name: 'Move into a doc' }).click();
await expect(
page.getByRole('dialog').getByRole('heading', { name: 'Move' }),
@@ -1,11 +1,6 @@
import { expect, test } from '@playwright/test';
import {
createDoc,
getGridRow,
getMenuItem,
verifyDocName,
} from './utils-common';
import { createDoc, getGridRow, verifyDocName } from './utils-common';
import { addNewMember, connectOtherUserToDoc } from './utils-share';
type SmallDoc = {
@@ -104,7 +99,7 @@ test.describe('Document grid item options', () => {
const row = await getGridRow(page, docTitle);
await row.getByText(`more_horiz`).click();
await getMenuItem(page, 'Share').click();
await page.getByRole('menuitem', { name: 'Share' }).click();
await expect(
page.getByRole('dialog').getByText('Share the document'),
@@ -120,7 +115,7 @@ test.describe('Document grid item options', () => {
// Pin
await row.getByText(`more_horiz`).click();
await getMenuItem(page, 'Pin').click();
await page.getByRole('menuitem', { name: 'Pin' }).click();
// Check is pinned
await expect(row.getByTestId('doc-pinned-icon')).toBeVisible();
@@ -147,7 +142,7 @@ test.describe('Document grid item options', () => {
const row = await getGridRow(page, docTitle);
await row.getByText(`more_horiz`).click();
await getMenuItem(page, 'Delete').click();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await expect(
page.getByRole('heading', { name: 'Delete a doc' }),
@@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test';
import {
createDoc,
getGridRow,
getMenuItem,
goToGridDoc,
mockedDocument,
verifyDocName,
@@ -79,7 +78,7 @@ test.describe('Doc Header', () => {
await page.getByTestId('doc-visibility').click();
await getMenuItem(page, 'Public').click();
await page.getByRole('menuitemradio', { name: 'Public' }).click();
await page.getByRole('button', { name: 'close' }).first().click();
@@ -153,8 +152,10 @@ test.describe('Doc Header', () => {
const emojiPicker = page.locator('.--docs--doc-title').getByRole('button');
const optionMenu = page.getByLabel('Open the document options');
const addEmojiMenuItem = getMenuItem(page, 'Add emoji');
const removeEmojiMenuItem = getMenuItem(page, 'Remove emoji');
const addEmojiMenuItem = page.getByRole('menuitem', { name: 'Add emoji' });
const removeEmojiMenuItem = page.getByRole('menuitem', {
name: 'Remove emoji',
});
// Top parent should not have emoji picker
await expect(emojiPicker).toBeHidden();
@@ -208,7 +209,7 @@ test.describe('Doc Header', () => {
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
await page.getByLabel('Open the document options').click();
await getMenuItem(page, 'Delete document').click();
await page.getByRole('menuitem', { name: 'Delete document' }).click();
await expect(
page.getByRole('heading', { name: 'Delete a doc' }),
@@ -236,7 +237,7 @@ test.describe('Doc Header', () => {
hasText: randomDoc,
});
expect(await row.count()).toBe(0);
await expect(row).toHaveCount(0);
});
test('it checks the options available if administrator', async ({ page }) => {
@@ -270,10 +271,12 @@ test.describe('Doc Header', () => {
await page.getByLabel('Open the document options').click();
await expect(getMenuItem(page, 'Delete document')).toBeDisabled();
await expect(
page.getByRole('menuitem', { name: 'Delete document' }),
).toBeDisabled();
// Click somewhere else to close the options
await page.click('body', { position: { x: 0, y: 0 } });
await page.locator('body').click({ position: { x: 0, y: 0 } });
await page.getByRole('button', { name: 'Share' }).click();
@@ -293,7 +296,7 @@ test.describe('Doc Header', () => {
await invitationRole.click();
await getMenuItem(page, 'Remove access').click();
await page.getByRole('menuitemradio', { name: 'Remove access' }).click();
await expect(invitationCard).toBeHidden();
const memberCard = shareModal.getByLabel('List members card');
@@ -305,7 +308,9 @@ test.describe('Doc Header', () => {
await expect(roles).toBeVisible();
await roles.click();
await expect(getMenuItem(page, 'Remove access')).toBeEnabled();
await expect(
page.getByRole('menuitemradio', { name: 'Remove access' }),
).toBeEnabled();
});
test('it checks the options available if editor', async ({ page }) => {
@@ -345,10 +350,12 @@ test.describe('Doc Header', () => {
).toBeVisible();
await page.getByLabel('Open the document options').click();
await expect(getMenuItem(page, 'Delete document')).toBeDisabled();
await expect(
page.getByRole('menuitem', { name: 'Delete document' }),
).toBeDisabled();
// Click somewhere else to close the options
await page.click('body', { position: { x: 0, y: 0 } });
await page.locator('body').click({ position: { x: 0, y: 0 } });
await page.getByRole('button', { name: 'Share' }).click();
@@ -415,10 +422,12 @@ test.describe('Doc Header', () => {
).toBeVisible();
await page.getByLabel('Open the document options').click();
await expect(getMenuItem(page, 'Delete document')).toBeDisabled();
await expect(
page.getByRole('menuitem', { name: 'Delete document' }),
).toBeDisabled();
// Click somewhere else to close the options
await page.click('body', { position: { x: 0, y: 0 } });
await page.locator('body').click({ position: { x: 0, y: 0 } });
await page.getByRole('button', { name: 'Share' }).click();
@@ -473,8 +482,10 @@ test.describe('Doc Header', () => {
// Copy content to clipboard
await page.getByLabel('Open the document options').click();
await getMenuItem(page, 'Copy as Markdown').click();
await expect(page.getByText('Copied to clipboard')).toBeVisible();
await page.getByRole('menuitem', { name: 'Copy as Markdown' }).click();
await expect(
page.getByText('Copied as Markdown to clipboard'),
).toBeVisible();
// Test that clipboard is in Markdown format
const handle = await page.evaluateHandle(() =>
@@ -535,7 +546,7 @@ test.describe('Doc Header', () => {
.click();
// Pin
await getMenuItem(page, 'Pin').click();
await page.getByRole('menuitem', { name: 'Pin' }).click();
await page
.getByRole('button', { name: 'Open the document options' })
.click();
@@ -556,11 +567,11 @@ test.describe('Doc Header', () => {
.click();
// Unpin
await getMenuItem(page, 'Unpin').click();
await page.getByRole('menuitem', { name: 'Unpin' }).click();
await page
.getByRole('button', { name: 'Open the document options' })
.click();
await expect(getMenuItem(page, 'Pin')).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Pin' })).toBeVisible();
await page.goto('/');
@@ -578,7 +589,7 @@ test.describe('Doc Header', () => {
await page.getByLabel('Open the document options').click();
await getMenuItem(page, 'Duplicate').click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
await expect(
page.getByText('Document duplicated successfully!'),
).toBeVisible();
@@ -593,7 +604,7 @@ test.describe('Doc Header', () => {
await expect(row.getByText(duplicateTitle)).toBeVisible();
await row.getByText(`more_horiz`).click();
await getMenuItem(page, 'Duplicate').click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
const duplicateDuplicateTitle = 'Copy of ' + duplicateTitle;
await page.getByText(duplicateDuplicateTitle).click();
await expect(page.getByText('Hello Duplicated World')).toBeVisible();
@@ -626,7 +637,7 @@ test.describe('Doc Header', () => {
const currentUrl = page.url();
await getMenuItem(page, 'Duplicate').click();
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
await expect(page).not.toHaveURL(new RegExp(currentUrl));
@@ -665,8 +676,10 @@ test.describe('Documents Header mobile', () => {
await expect(page.getByRole('button', { name: 'Copy link' })).toBeHidden();
await page.getByLabel('Open the document options').click();
await expect(getMenuItem(page, 'Copy link')).toBeVisible();
await getMenuItem(page, 'Share').click();
await expect(
page.getByRole('menuitem', { name: 'Copy link' }),
).toBeVisible();
await page.getByRole('menuitem', { name: 'Share' }).click();
await expect(page.getByRole('button', { name: 'Copy link' })).toBeVisible();
});
@@ -689,7 +702,7 @@ test.describe('Documents Header mobile', () => {
await goToGridDoc(page);
await page.getByLabel('Open the document options').click();
await getMenuItem(page, 'Share').click();
await page.getByRole('menuitem', { name: 'Share' }).click();
const shareModal = page.getByRole('dialog', {
name: 'Share the document',
@@ -177,5 +177,5 @@ const dragAndDropFiles = async (
return dt;
}, filesData);
await page.dispatchEvent(selector, 'drop', { dataTransfer });
await page.locator(selector).dispatchEvent('drop', { dataTransfer });
};
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { createDoc, getMenuItem, verifyDocName } from './utils-common';
import { createDoc, verifyDocName } from './utils-common';
import { updateShareLink } from './utils-share';
import { createRootSubPage } from './utils-sub-pages';
@@ -53,17 +53,19 @@ test.describe('Inherited share accesses', () => {
await expect(docVisibilityCard.getByText('Reading')).toBeVisible();
await docVisibilityCard.getByText('Reading').click();
await getMenuItem(page, 'Editing').click();
await page.getByRole('menuitemradio', { name: 'Editing' }).click();
await expect(docVisibilityCard.getByText('Reading')).toBeHidden();
await expect(docVisibilityCard.getByText('Editing')).toBeVisible();
// Verify inherited link
await docVisibilityCard.getByText('Connected').click();
await expect(getMenuItem(page, 'Private')).toBeDisabled();
await expect(
page.getByRole('menuitemradio', { name: 'Private' }),
).toBeDisabled();
// Update child link
await getMenuItem(page, 'Public').click();
await page.getByRole('menuitemradio', { name: 'Public' }).click();
await expect(docVisibilityCard.getByText('Connected')).toBeHidden();
await expect(
@@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test';
import {
BROWSERS,
createDoc,
getMenuItem,
keyCloakSignIn,
randomName,
verifyDocName,
@@ -17,6 +16,41 @@ test.describe('Document create member', () => {
await page.goto('/');
});
test('it checks search hints', async ({ page, browserName }) => {
await createDoc(page, 'select-multi-users', browserName, 1);
await page.getByRole('button', { name: 'Share' }).click();
const shareModal = page.getByLabel('Share the document');
await expect(shareModal.getByText('Document owner')).toBeVisible();
const inputSearch = page.getByTestId('quick-search-input');
await inputSearch.fill('u');
await expect(shareModal.getByText('Document owner')).toBeHidden();
await expect(
shareModal.getByText('Type at least 3 characters to display user names'),
).toBeVisible();
await inputSearch.fill('user');
await expect(
shareModal.getByText('Type at least 3 characters to display user names'),
).toBeHidden();
await expect(shareModal.getByText('Choose a user')).toBeVisible();
await inputSearch.fill('anything');
await expect(shareModal.getByText('Choose a user')).toBeHidden();
await expect(
shareModal.getByText(
'No results. Type a full email address to invite someone.',
),
).toBeVisible();
await inputSearch.fill('anything@test.com');
await expect(
shareModal.getByText(
'No results. Type a full email address to invite someone.',
),
).toBeHidden();
await expect(shareModal.getByText('Choose the email')).toBeVisible();
});
test('it selects 2 users and 1 invitation', async ({ page, browserName }) => {
const inputFill = 'user.test';
const responsePromise = page.waitForResponse(
@@ -76,13 +110,21 @@ test.describe('Document create member', () => {
// Check roles are displayed
await list.getByTestId('doc-role-dropdown').click();
await expect(getMenuItem(page, 'Reader')).toBeVisible();
await expect(getMenuItem(page, 'Editor')).toBeVisible();
await expect(getMenuItem(page, 'Owner')).toBeVisible();
await expect(getMenuItem(page, 'Administrator')).toBeVisible();
await expect(
page.getByRole('menuitemradio', { name: 'Reader' }),
).toBeVisible();
await expect(
page.getByRole('menuitemradio', { name: 'Editor' }),
).toBeVisible();
await expect(
page.getByRole('menuitemradio', { name: 'Owner' }),
).toBeVisible();
await expect(
page.getByRole('menuitemradio', { name: 'Administrator' }),
).toBeVisible();
// Validate
await getMenuItem(page, 'Administrator').click();
await page.getByRole('menuitemradio', { name: 'Administrator' }).click();
await page.getByTestId('doc-share-invite-button').click();
// Check invitation added
@@ -128,7 +170,7 @@ test.describe('Document create member', () => {
// Choose a role
const container = page.getByTestId('doc-share-add-member-list');
await container.getByTestId('doc-role-dropdown').click();
await getMenuItem(page, 'Owner').click();
await page.getByRole('menuitemradio', { name: 'Owner' }).click();
const responsePromiseCreateInvitation = page.waitForResponse(
(response) =>
@@ -146,7 +188,7 @@ test.describe('Document create member', () => {
// Choose a role
await container.getByTestId('doc-role-dropdown').click();
await getMenuItem(page, 'Owner').click();
await page.getByRole('menuitemradio', { name: 'Owner' }).click();
const responsePromiseCreateInvitationFail = page.waitForResponse(
(response) =>
@@ -183,7 +225,7 @@ test.describe('Document create member', () => {
// Choose a role
const container = page.getByTestId('doc-share-add-member-list');
await container.getByTestId('doc-role-dropdown').click();
await getMenuItem(page, 'Administrator').click();
await page.getByRole('menuitemradio', { name: 'Administrator' }).click();
const responsePromiseCreateInvitation = page.waitForResponse(
(response) =>
@@ -210,13 +252,13 @@ test.describe('Document create member', () => {
);
await userInvitation.getByTestId('doc-role-dropdown').click();
await getMenuItem(page, 'Reader').click();
await page.getByRole('menuitemradio', { name: 'Reader' }).click();
const responsePatchInvitation = await responsePromisePatchInvitation;
expect(responsePatchInvitation.ok()).toBeTruthy();
await userInvitation.getByTestId('doc-role-dropdown').click();
await getMenuItem(page, 'Remove access').click();
await page.getByRole('menuitemradio', { name: 'Remove access' }).click();
await expect(userInvitation).toBeHidden();
});
@@ -268,7 +310,7 @@ test.describe('Document create member', () => {
`doc-share-access-request-row-${emailRequest}`,
);
await container.getByTestId('doc-role-dropdown').click();
await getMenuItem(page, 'Administrator').click();
await page.getByRole('menuitemradio', { name: 'Administrator' }).click();
await container.getByRole('button', { name: 'Approve' }).click();
await expect(page.getByText('Access Requests')).toBeHidden();
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { createDoc, getMenuItem, verifyDocName } from './utils-common';
import { createDoc, verifyDocName } from './utils-common';
import { addNewMember } from './utils-share';
test.beforeEach(async ({ page }) => {
@@ -160,7 +160,9 @@ test.describe('Document list members', () => {
`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.`,
);
await expect(soloOwner).toBeVisible();
await expect(getMenuItem(page, 'Administrator')).toBeDisabled();
await expect(
page.getByRole('menuitemradio', { name: 'Administrator' }),
).toBeDisabled();
await list.click({
force: true, // Force click to close the dropdown
@@ -183,18 +185,20 @@ test.describe('Document list members', () => {
});
await currentUserRole.click();
await getMenuItem(page, 'Administrator').click();
await page.getByRole('menuitemradio', { name: 'Administrator' }).click();
await list.click();
await expect(currentUserRole).toBeVisible();
await newUserRoles.click();
await expect(getMenuItem(page, 'Owner')).toBeDisabled();
await expect(
page.getByRole('menuitemradio', { name: 'Owner' }),
).toBeDisabled();
await list.click({
force: true, // Force click to close the dropdown
});
await currentUserRole.click();
await getMenuItem(page, 'Reader').click();
await page.getByRole('menuitemradio', { name: 'Reader' }).click();
await list.click({
force: true, // Force click to close the dropdown
});
@@ -234,11 +238,11 @@ test.describe('Document list members', () => {
await expect(userReader).toBeVisible();
await userReaderRole.click();
await getMenuItem(page, 'Remove access').click();
await page.getByRole('menuitemradio', { name: 'Remove access' }).click();
await expect(userReader).toBeHidden();
await mySelfRole.click();
await getMenuItem(page, 'Remove access').click();
await page.getByRole('menuitemradio', { name: 'Remove access' }).click();
await expect(
page.getByText('Insufficient access rights to view the document.'),
).toBeVisible();
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { createDoc, getMenuItem, verifyDocName } from './utils-common';
import { createDoc, verifyDocName } from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
test.beforeEach(async ({ page }) => {
@@ -29,7 +29,7 @@ test.describe('Document search', () => {
await page.getByTestId('search-docs-button').click();
await expect(
page.getByRole('img', { name: 'No active search' }),
page.getByLabel('Search modal').locator('img[alt=""]'),
).toBeVisible();
await expect(
@@ -107,7 +107,7 @@ test.describe('Document search', () => {
await searchButton.click();
await expect(
page.getByRole('combobox', { name: 'Quick search input' }),
page.getByRole('combobox', { name: 'Search documents' }),
).toBeVisible();
await expect(filters).toBeHidden();
@@ -120,7 +120,7 @@ test.describe('Document search', () => {
await searchButton.click();
await expect(
page.getByRole('combobox', { name: 'Quick search input' }),
page.getByRole('combobox', { name: 'Search documents' }),
).toBeVisible();
await expect(filters).toBeHidden();
@@ -136,9 +136,13 @@ test.describe('Document search', () => {
await filters.click();
await filters.getByRole('button', { name: 'Current doc' }).click();
await expect(getMenuItem(page, 'All docs')).toBeVisible();
await expect(getMenuItem(page, 'Current doc')).toBeVisible();
await getMenuItem(page, 'All docs').click();
await expect(
page.getByRole('menuitemcheckbox', { name: 'All docs' }),
).toBeVisible();
await expect(
page.getByRole('menuitemcheckbox', { name: 'Current doc' }),
).toBeVisible();
await page.getByRole('menuitemcheckbox', { name: 'All docs' }).click();
await expect(page.getByRole('button', { name: 'Reset' })).toBeVisible();
});
@@ -164,9 +168,9 @@ test.describe('Document search', () => {
const searchButton = page.getByTestId('search-docs-button');
await searchButton.click();
await page.getByRole('combobox', { name: 'Quick search input' }).click();
await page.getByRole('combobox', { name: 'Search documents' }).click();
await page
.getByRole('combobox', { name: 'Quick search input' })
.getByRole('combobox', { name: 'Search documents' })
.fill('sub page search');
// Expect to find the first and second docs in the results list
@@ -188,7 +192,7 @@ test.describe('Document search', () => {
);
await searchButton.click();
await page
.getByRole('combobox', { name: 'Quick search input' })
.getByRole('combobox', { name: 'Search documents' })
.fill('second');
// Now there is a sub page - expect to have the focus on the current doc
@@ -19,7 +19,9 @@ test.describe('Doc Table Content', () => {
await page.locator('.ProseMirror').click();
await expect(page.getByRole('button', { name: 'Summary' })).toBeHidden();
await expect(
page.getByRole('button', { name: 'Show the table of contents' }),
).toBeHidden();
await page.keyboard.type('# Level 1\n## Level 2\n### Level 3');
@@ -3,7 +3,6 @@ import { expect, test } from '@playwright/test';
import {
createDoc,
expectLoginPage,
getMenuItem,
keyCloakSignIn,
updateDocTitle,
verifyDocName,
@@ -43,15 +42,12 @@ test.describe('Doc Tree', () => {
await expect(secondSubPageItem).toBeVisible();
// Check the position of the sub pages
const allSubPageItems = await docTree
.getByTestId(/^doc-sub-page-item/)
.all();
expect(allSubPageItems.length).toBe(2);
const allSubPageItems = docTree.getByTestId(/^doc-sub-page-item/);
await expect(allSubPageItems).toHaveCount(2);
// Check that elements are in the correct order
await expect(allSubPageItems[0].getByText('first move')).toBeVisible();
await expect(allSubPageItems[1].getByText('second move')).toBeVisible();
await expect(allSubPageItems.nth(0).getByText('first move')).toBeVisible();
await expect(allSubPageItems.nth(1).getByText('second move')).toBeVisible();
// Will move the first sub page to the second position
const firstSubPageBoundingBox = await firstSubPageItem.boundingBox();
@@ -91,17 +87,15 @@ test.describe('Doc Tree', () => {
await expect(secondSubPageItem).toBeVisible();
// Check that elements are in the correct order
const allSubPageItemsAfterReload = await docTree
.getByTestId(/^doc-sub-page-item/)
.all();
expect(allSubPageItemsAfterReload.length).toBe(2);
const allSubPageItemsAfterReload =
docTree.getByTestId(/^doc-sub-page-item/);
await expect(allSubPageItemsAfterReload).toHaveCount(2);
await expect(
allSubPageItemsAfterReload[0].getByText('second move'),
allSubPageItemsAfterReload.nth(0).getByText('second move'),
).toBeVisible();
await expect(
allSubPageItemsAfterReload[1].getByText('first move'),
allSubPageItemsAfterReload.nth(1).getByText('first move'),
).toBeVisible();
});
@@ -163,7 +157,7 @@ test.describe('Doc Tree', () => {
);
const currentUserRole = currentUser.getByTestId('doc-role-dropdown');
await currentUserRole.click();
await getMenuItem(page, 'Administrator').click();
await page.getByRole('menuitemradio', { name: 'Administrator' }).click();
await list.click();
await page.getByRole('button', { name: 'Ok' }).click();
@@ -193,10 +187,9 @@ test.describe('Doc Tree', () => {
const menu = child.getByText(`more_horiz`);
await menu.click();
await expect(getMenuItem(page, 'Move to my docs')).toHaveAttribute(
'aria-disabled',
'true',
);
await expect(
page.getByRole('menuitem', { name: 'Move to my docs' }),
).toHaveAttribute('aria-disabled', 'true');
});
test('keyboard navigation with Enter key opens documents', async ({
@@ -299,7 +292,7 @@ test.describe('Doc Tree', () => {
await page.keyboard.press('Tab');
await expect(page.getByLabel('Open onboarding menu')).toBeFocused();
await expect(page.getByLabel('Open help menu')).toBeFocused();
await page.keyboard.press('Tab');
@@ -309,7 +302,7 @@ test.describe('Doc Tree', () => {
await page.keyboard.press('Shift+Tab');
await expect(page.getByLabel('Open onboarding menu')).toBeFocused();
await expect(page.getByLabel('Open help menu')).toBeFocused();
await page.keyboard.press('Shift+Tab');
@@ -340,7 +333,9 @@ test.describe('Doc Tree', () => {
await row.hover();
const menu = row.getByText(`more_horiz`);
await menu.click();
await expect(getMenuItem(page, 'Remove emoji')).toBeHidden();
await expect(
page.getByRole('menuitem', { name: 'Remove emoji' }),
).toBeHidden();
// Close the menu
await page.keyboard.press('Escape');
@@ -360,7 +355,7 @@ test.describe('Doc Tree', () => {
// Now remove the emoji using the new action
await row.hover();
await menu.click();
await getMenuItem(page, 'Remove emoji').click();
await page.getByRole('menuitem', { name: 'Remove emoji' }).click();
await expect(row.getByText('😀')).toBeHidden();
await expect(titleEmojiPicker).toBeHidden();
@@ -390,7 +385,7 @@ test.describe('Doc Tree: Inheritance', () => {
const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click();
await getMenuItem(page, 'Public').click();
await page.getByRole('menuitemradio', { name: 'Public' }).click();
await expect(
page.getByText('The document visibility has been updated.'),
@@ -2,7 +2,6 @@ import { expect, test } from '@playwright/test';
import {
createDoc,
getMenuItem,
goToGridDoc,
mockedDocument,
verifyDocName,
@@ -21,11 +20,11 @@ test.describe('Doc Version', () => {
// Initially, there is no version
await page.getByLabel('Open the document options').click();
await getMenuItem(page, 'Version history').click();
await page.getByRole('menuitem', { name: 'Version history' }).click();
await expect(page.getByText('History', { exact: true })).toBeVisible();
const modal = page.getByRole('dialog', { name: 'Version history' });
const panel = modal.getByLabel('version list');
const panel = modal.getByLabel('Version list');
await expect(panel).toBeVisible();
await expect(modal.getByText('No versions')).toBeVisible();
@@ -75,14 +74,14 @@ test.describe('Doc Version', () => {
).toBeVisible();
await page.getByLabel('Open the document options').click();
await getMenuItem(page, 'Version history').click();
await page.getByRole('menuitem', { name: 'Version history' }).click();
await expect(panel).toBeVisible();
await expect(page.getByText('History', { exact: true })).toBeVisible();
await expect(page.getByRole('status')).toBeHidden();
const items = await panel.locator('.version-item').all();
expect(items.length).toBe(2);
await items[1].click();
const items = panel.locator('.version-item');
await expect(items).toHaveCount(2);
await items.nth(1).click();
await expect(modal.getByText('Hello World')).toBeVisible();
await expect(modal.getByText('It will create a version')).toBeHidden();
@@ -90,7 +89,7 @@ test.describe('Doc Version', () => {
modal.locator('div[data-content-type="callout"]').first(),
).toBeHidden();
await items[0].click();
await items.nth(0).click();
await expect(modal.getByText('Hello World')).toBeVisible();
await expect(modal.getByText('It will create a version')).toBeVisible();
@@ -101,7 +100,7 @@ test.describe('Doc Version', () => {
modal.getByText('It will create a second version'),
).toBeHidden();
await items[1].click();
await items.nth(1).click();
await expect(modal.getByText('Hello World')).toBeVisible();
await expect(modal.getByText('It will create a version')).toBeHidden();
@@ -125,7 +124,9 @@ test.describe('Doc Version', () => {
await verifyDocName(page, 'Mocked document');
await page.getByLabel('Open the document options').click();
await expect(getMenuItem(page, 'Version history')).toBeDisabled();
await expect(
page.getByRole('menuitem', { name: 'Version history' }),
).toBeDisabled();
});
test('it restores the doc version', async ({ page, browserName }) => {
@@ -152,23 +153,28 @@ test.describe('Doc Version', () => {
await expect(page.getByText('World')).toBeVisible();
await page.getByLabel('Open the document options').click();
await getMenuItem(page, 'Version history').click();
await page.getByRole('menuitem', { name: 'Version history' }).click();
const modal = page.getByRole('dialog', { name: 'Version history' });
const panel = modal.getByLabel('version list');
const panel = modal.getByLabel('Version list');
await expect(panel).toBeVisible();
await expect(page.getByText('History', { exact: true })).toBeVisible();
await panel.getByRole('button', { name: 'version item' }).click();
await panel.locator('.version-item').first().click();
await expect(modal.getByText('World')).toBeHidden();
await page.getByRole('button', { name: 'Restore' }).click();
await expect(page.getByText('Your current document will')).toBeVisible();
await page.getByText('If a member is editing, his').click();
await page.getByRole('button', { name: 'Restore', exact: true }).click();
await expect(
page.getByText(
"The current document will be replaced, but you'll still find it in the version history.",
),
).toBeVisible();
await page.getByLabel('Restore', { exact: true }).click();
await page.waitForTimeout(500);
await expect(page.getByText('Hello')).toBeVisible();
await expect(page.getByText('World')).toBeHidden();
});
@@ -4,7 +4,6 @@ import {
BROWSERS,
createDoc,
expectLoginPage,
getMenuItem,
keyCloakSignIn,
verifyDocName,
} from './utils-common';
@@ -47,17 +46,21 @@ test.describe('Doc Visibility', () => {
await expect(selectVisibility.getByText('Private')).toBeVisible();
await expect(getMenuItem(page, 'Read only')).toBeHidden();
await expect(getMenuItem(page, 'Can read and edit')).toBeHidden();
await expect(
page.getByRole('menuitemradio', { name: 'Read only' }),
).toBeHidden();
await expect(
page.getByRole('menuitemradio', { name: 'Can read and edit' }),
).toBeHidden();
await selectVisibility.click();
await getMenuItem(page, 'Connected').click();
await page.getByRole('menuitemradio', { name: 'Connected' }).click();
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
await selectVisibility.click();
await getMenuItem(page, 'Public').click();
await page.getByRole('menuitemradio', { name: 'Public' }).click();
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
});
@@ -202,7 +205,7 @@ test.describe('Doc Visibility: Public', () => {
const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click();
await getMenuItem(page, 'Public').click();
await page.getByRole('menuitemradio', { name: 'Public' }).click();
await expect(
page.getByText('The document visibility has been updated.'),
@@ -210,7 +213,7 @@ test.describe('Doc Visibility: Public', () => {
await expect(page.getByTestId('doc-access-mode')).toBeVisible();
await page.getByTestId('doc-access-mode').click();
await getMenuItem(page, 'Reading').click();
await page.getByRole('menuitemradio', { name: 'Reading' }).click();
await expect(
page.getByText('The document visibility has been updated.').first(),
@@ -296,14 +299,14 @@ test.describe('Doc Visibility: Public', () => {
const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click();
await getMenuItem(page, 'Public').click();
await page.getByRole('menuitemradio', { name: 'Public' }).click();
await expect(
page.getByText('The document visibility has been updated.'),
).toBeVisible();
await page.getByTestId('doc-access-mode').click();
await getMenuItem(page, 'Editing').click();
await page.getByRole('menuitemradio', { name: 'Editing' }).click();
await expect(
page.getByText('The document visibility has been updated.').first(),
@@ -387,7 +390,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click();
await getMenuItem(page, 'Connected').click();
await page.getByRole('menuitemradio', { name: 'Connected' }).click();
await expect(
page.getByText('The document visibility has been updated.'),
@@ -435,7 +438,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click();
await getMenuItem(page, 'Connected').click();
await page.getByRole('menuitemradio', { name: 'Connected' }).click();
await expect(
page.getByText('The document visibility has been updated.'),
@@ -533,7 +536,7 @@ test.describe('Doc Visibility: Authenticated', () => {
await page.getByRole('button', { name: 'Share' }).click();
const selectVisibility = page.getByTestId('doc-visibility');
await selectVisibility.click();
await getMenuItem(page, 'Connected').click();
await page.getByRole('menuitemradio', { name: 'Connected' }).click();
await expect(
page.getByText('The document visibility has been updated.'),
@@ -541,7 +544,7 @@ test.describe('Doc Visibility: Authenticated', () => {
const urlDoc = page.url();
await page.getByTestId('doc-access-mode').click();
await getMenuItem(page, 'Editing').click();
await page.getByRole('menuitemradio', { name: 'Editing' }).click();
await expect(
page.getByText('The document visibility has been updated.').first(),
@@ -1,6 +1,6 @@
import { expect, test } from '@playwright/test';
import { getMenuItem, overrideConfig } from './utils-common';
import { overrideConfig } from './utils-common';
test.describe('Footer', () => {
test.use({ storageState: { cookies: [], origins: [] } });
@@ -47,7 +47,7 @@ test.describe('Footer', () => {
// Check the translation
const header = page.locator('header').first();
await header.getByRole('button').getByText('English').click();
await getMenuItem(page, 'Français').click();
await page.getByRole('menuitemradio', { name: 'Français' }).click();
await expect(
page.locator('footer').getByText('Mentions légales'),
@@ -131,7 +131,7 @@ test.describe('Footer', () => {
// Check the translation
const header = page.locator('header').first();
await header.getByRole('button').getByText('English').click();
await getMenuItem(page, 'Français').click();
await page.getByRole('menuitemradio', { name: 'Français' }).click();
await expect(
page
@@ -191,25 +191,27 @@ test.describe('Header: Override configuration', () => {
});
test.describe('Header: Skip to Content', () => {
test('it displays skip link on first TAB and focuses main content on click', async ({
test('it displays skip link on first TAB and focuses page heading on click', async ({
page,
}) => {
await page.goto('/');
// Wait for skip button to be mounted (client-side only component)
const skipButton = page.getByRole('button', { name: 'Go to content' });
await skipButton.waitFor({ state: 'attached' });
// Wait for skip link to be mounted (client-side only component)
const skipLink = page.getByRole('link', { name: 'Go to content' });
await skipLink.waitFor({ state: 'attached' });
// First TAB shows the skip button
// First TAB shows the skip link
await page.keyboard.press('Tab');
// The skip button should be visible and focused
await expect(skipButton).toBeFocused();
await expect(skipButton).toBeVisible();
// Clicking moves focus to the main content
await skipButton.click();
const mainContent = page.locator('main#mainContent');
await expect(mainContent).toBeFocused();
// The skip link should be visible and focused
await expect(skipLink).toBeFocused();
await expect(skipLink).toBeVisible();
// Clicking moves focus to the page heading
await skipLink.click();
const pageHeading = page.getByRole('heading', {
name: 'All docs',
level: 2,
});
await expect(pageHeading).toBeFocused();
});
});
@@ -2,7 +2,6 @@ import { expect, test } from '@playwright/test';
import {
TestLanguage,
getMenuItem,
overrideConfig,
waitForLanguageSwitch,
} from './utils-common';
@@ -27,7 +26,7 @@ test.describe('Help feature', () => {
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Open onboarding menu' }),
page.getByRole('button', { name: 'Open help menu' }),
).toBeHidden();
});
@@ -43,9 +42,9 @@ test.describe('Help feature', () => {
},
});
await page.getByRole('button', { name: 'Open onboarding menu' }).click();
await page.getByRole('button', { name: 'Open help menu' }).click();
await getMenuItem(page, 'Onboarding').click();
await page.getByRole('menuitem', { name: 'Onboarding' }).click();
const modal = page.getByTestId('onboarding-modal');
await expect(modal).toBeVisible();
@@ -87,8 +86,8 @@ test.describe('Help feature', () => {
});
test('closes modal with Skip button', async ({ page }) => {
await page.getByRole('button', { name: 'Open onboarding menu' }).click();
await getMenuItem(page, 'Onboarding').click();
await page.getByRole('button', { name: 'Open help menu' }).click();
await page.getByRole('menuitem', { name: 'Onboarding' }).click();
const modal = page.getByTestId('onboarding-modal');
await expect(modal).toBeVisible();
@@ -107,11 +106,9 @@ test.describe('Help feature', () => {
// switch to french
await waitForLanguageSwitch(page, TestLanguage.French);
await page
.getByRole('button', { name: "Ouvrir le menu d'embarquement" })
.click();
await page.getByRole('button', { name: "Ouvrir le menu d'aide" }).click();
await getMenuItem(page, 'Premiers pas').click();
await page.getByRole('menuitem', { name: 'Premiers pas' }).click();
const modal = page.getByLabel('Apprenez les principes fondamentaux');
@@ -75,7 +75,7 @@ test.describe('Language', () => {
await expect(page.locator('[role="menu"]')).toBeVisible();
const menuItems = page.locator('[role="menuitem"], [role="menuitemradio"]');
const menuItems = page.locator('[role="menuitemradio"]');
await expect(menuItems.first()).toBeVisible();
await menuItems.first().click();
@@ -1,6 +1,7 @@
import { expect, test } from '@playwright/test';
import { createDoc, goToGridDoc, verifyDocName } from './utils-common';
import { createRootSubPage } from './utils-sub-pages';
test.describe('Left panel desktop', () => {
test.beforeEach(async ({ page }) => {
@@ -18,7 +19,7 @@ test.describe('Left panel desktop', () => {
await expect(page.getByTestId('home-button')).toBeVisible();
});
test('focuses main content after switching the docs filter', async ({
test('focuses page heading after switching the docs filter', async ({
page,
}) => {
await page.goto('/');
@@ -28,8 +29,11 @@ test.describe('Left panel desktop', () => {
await page.keyboard.press('Enter');
await expect(page).toHaveURL(/target=my_docs/);
const mainContent = page.locator('main#mainContent');
await expect(mainContent).toBeFocused();
const pageHeading = page.getByRole('heading', {
name: 'My docs',
level: 2,
});
await expect(pageHeading).toBeFocused();
});
test('checks resize handle is present and functional on document page', async ({
@@ -118,6 +122,47 @@ test.describe('Left panel mobile', () => {
await expect(logoutButton).toBeInViewport();
});
test('checks panel closes when clicking on a subdoc', async ({
page,
browserName,
}) => {
const [docTitle] = await createDoc(
page,
'mobile-doc-test',
browserName,
1,
true,
);
const { name: docChild } = await createRootSubPage(
page,
browserName,
'mobile-doc-test-child',
true,
);
const { name: docChild2 } = await createRootSubPage(
page,
browserName,
'mobile-doc-test-child-2',
true,
);
const header = page.locator('header').first();
await header.getByLabel('Open the header menu').click();
await expect(page.getByTestId('left-panel-mobile')).toBeInViewport();
const docTree = page.getByTestId('doc-tree');
await expect(docTree.getByText(docTitle)).toBeVisible();
await expect(docTree.getByText(docChild)).toBeVisible();
await expect(docTree.getByText(docChild2)).toBeVisible();
await docTree.getByText(docChild).click();
await verifyDocName(page, docChild);
await expect(page.getByTestId('left-panel-mobile')).not.toBeInViewport();
});
test('checks resize handle is not present on mobile', async ({ page }) => {
await page.goto('/');
@@ -3,16 +3,6 @@ import path from 'path';
import { Locator, Page, TestInfo, expect } from '@playwright/test';
/** Returns a locator for a menu item (handles both menuitem and menuitemradio roles) */
export const getMenuItem = (
context: Page | Locator,
name: string,
options?: { exact?: boolean },
): Locator =>
context
.getByRole('menuitem', { name, exact: options?.exact })
.or(context.getByRole('menuitemradio', { name, exact: options?.exact }));
import theme_customization from '../../../../../backend/impress/configuration/theme/default.json';
export type BrowserName = 'chromium' | 'firefox' | 'webkit';
@@ -392,12 +382,12 @@ export async function waitForLanguageSwitch(
await languagePicker.click();
await getMenuItem(page, lang.label).click();
await page.getByRole('menuitemradio', { name: lang.label }).click();
}
export const clickInEditorMenu = async (page: Page, textButton: string) => {
await page.getByRole('button', { name: 'Open the document options' }).click();
await getMenuItem(page, textButton).click();
await page.getByRole('menuitem', { name: textButton }).click();
};
export const clickInGridMenu = async (
@@ -408,7 +398,7 @@ export const clickInGridMenu = async (
await row
.getByRole('button', { name: /Open the menu of actions for the document/ })
.click();
await getMenuItem(page, textButton).click();
await page.getByRole('menuitem', { name: textButton }).click();
};
export const writeReport = async (
@@ -2,7 +2,6 @@ import { Page, chromium, expect } from '@playwright/test';
import {
BrowserName,
getMenuItem,
getOtherBrowserName,
keyCloakSignIn,
verifyDocName,
@@ -40,7 +39,7 @@ export const addNewMember = async (
// Choose a role
await page.getByTestId('doc-role-dropdown').click();
await getMenuItem(page, role).click();
await page.getByRole('menuitemradio', { name: role }).click();
await page.getByTestId('doc-share-invite-button').click();
return users[index].email;
@@ -52,7 +51,7 @@ export const updateShareLink = async (
linkRole?: LinkRole | null,
) => {
await page.getByTestId('doc-visibility').click();
await getMenuItem(page, linkReach).click();
await page.getByRole('menuitemradio', { name: linkReach }).click();
const visibilityUpdatedText = page
.getByText('The document visibility has been updated')
@@ -62,7 +61,7 @@ export const updateShareLink = async (
if (linkRole) {
await page.getByTestId('doc-access-mode').click();
await getMenuItem(page, linkRole).click();
await page.getByRole('menuitemradio', { name: linkRole }).click();
await expect(visibilityUpdatedText).toBeVisible();
}
};
@@ -77,7 +76,7 @@ export const updateRoleUser = async (
const currentUser = list.getByTestId(`doc-share-member-row-${email}`);
const currentUserRole = currentUser.getByTestId('doc-role-dropdown');
await currentUserRole.click();
await getMenuItem(page, role).click();
await page.getByRole('menuitemradio', { name: role }).click();
await list.click();
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "app-e2e",
"version": "4.8.2",
"version": "4.8.4",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
+22 -23
View File
@@ -1,6 +1,6 @@
{
"name": "app-impress",
"version": "4.8.2",
"version": "4.8.4",
"repository": "https://github.com/suitenumerique/docs",
"author": "DINUM",
"license": "MIT",
@@ -23,7 +23,7 @@
},
"dependencies": {
"@ag-media/react-pdf-table": "2.0.3",
"@ai-sdk/openai": "3.0.19",
"@ai-sdk/openai": "3.0.45",
"@blocknote/code-block": "0.47.1",
"@blocknote/core": "0.47.1",
"@blocknote/mantine": "0.47.1",
@@ -38,20 +38,20 @@
"@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.35",
"@fontsource-variable/material-symbols-outlined": "5.2.38",
"@fontsource/material-icons": "5.2.7",
"@gouvfr-lasuite/cunningham-react": "4.2.0",
"@gouvfr-lasuite/integration": "1.0.3",
"@gouvfr-lasuite/ui-kit": "0.19.6",
"@gouvfr-lasuite/ui-kit": "0.19.10",
"@hocuspocus/provider": "3.4.4",
"@mantine/core": "8.3.14",
"@mantine/hooks": "8.3.14",
"@mantine/core": "8.3.17",
"@mantine/hooks": "8.3.17",
"@react-aria/live-announcer": "3.4.4",
"@react-pdf/renderer": "4.3.1",
"@sentry/nextjs": "10.38.0",
"@sentry/nextjs": "10.43.0",
"@tanstack/react-query": "5.90.21",
"@tiptap/extensions": "*",
"ai": "6.0.49",
"ai": "6.0.128",
"canvg": "4.0.3",
"clsx": "2.1.1",
"cmdk": "1.1.1",
@@ -59,28 +59,28 @@
"emoji-datasource-apple": "16.0.0",
"emoji-mart": "5.6.0",
"emoji-regex": "10.6.0",
"i18next": "25.8.12",
"i18next": "25.8.18",
"i18next-browser-languagedetector": "8.2.1",
"idb": "8.0.3",
"lodash": "4.17.23",
"luxon": "3.7.2",
"next": "16.1.6",
"posthog-js": "1.347.2",
"next": "16.1.7",
"posthog-js": "1.360.2",
"react": "*",
"react-aria-components": "1.15.1",
"react-aria-components": "1.16.0",
"react-dom": "*",
"react-dropzone": "15.0.0",
"react-i18next": "16.5.4",
"react-intersection-observer": "10.0.2",
"react-i18next": "16.5.8",
"react-intersection-observer": "10.0.3",
"react-resizable-panels": "3.0.6",
"react-select": "5.10.2",
"styled-components": "6.3.9",
"styled-components": "6.3.11",
"use-debounce": "10.1.0",
"uuid": "13.0.0",
"y-protocols": "1.0.7",
"yjs": "*",
"zod": "3.25.28",
"zustand": "5.0.11"
"zod": "4.3.6",
"zustand": "5.0.12"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
@@ -89,26 +89,25 @@
"@testing-library/jest-dom": "6.9.1",
"@testing-library/react": "16.3.2",
"@testing-library/user-event": "14.6.1",
"@types/lodash": "4.17.23",
"@types/lodash": "4.17.24",
"@types/luxon": "3.7.1",
"@types/node": "*",
"@types/react": "*",
"@types/react-dom": "*",
"@vitejs/plugin-react": "5.1.4",
"@vitejs/plugin-react": "6.0.1",
"cross-env": "10.1.0",
"dotenv": "17.3.1",
"eslint-plugin-docs": "*",
"fetch-mock": "9.11.0",
"jsdom": "28.1.0",
"jsdom": "29.0.0",
"node-fetch": "2.7.0",
"prettier": "3.8.1",
"stylelint": "16.26.1",
"stylelint-config-standard": "39.0.1",
"stylelint-prettier": "5.0.3",
"typescript": "*",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.0.18",
"webpack": "5.105.2",
"vitest": "4.1.0",
"webpack": "5.105.4",
"workbox-webpack-plugin": "7.1.0"
},
"packageManager": "yarn@1.22.22"
@@ -0,0 +1,9 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 -960 960 960"
fill="currentColor"
>
<path
d="M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360Zm0-80h360v-480H360v480ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Zm210-360h60v-180h40v120h60v-120h40v180h60v-200q0-17-11.5-28.5T630-680H450q-17 0-28.5 11.5T410-640v200Zm-50 120v-480 480Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 439 B

@@ -1,17 +1,17 @@
import { forwardRef } from 'react';
import { Ref, forwardRef } from 'react';
import { css } from 'styled-components';
import { Box, BoxType } from './Box';
export type BoxButtonType = BoxType & {
export type BoxButtonType = Omit<BoxType, 'ref'> & {
disabled?: boolean;
ref?: Ref<HTMLButtonElement>;
};
/**
/**
* Styleless button that extends the Box component.
* Good to wrap around SVGs or other elements that need to be clickable.
* Uses aria-disabled instead of native disabled to preserve keyboard focusability.
* @param props - @see BoxType props
* @param ref
* @see Box
@@ -22,8 +22,8 @@ export type BoxButtonType = BoxType & {
* </BoxButton>
* ```
*/
const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
({ $css, ...props }, ref) => {
const BoxButton = forwardRef<HTMLButtonElement, BoxButtonType>(
({ $css, disabled, ...props }, ref) => {
const theme = props.$theme || 'gray';
const variation = props.$variation || 'primary';
@@ -31,16 +31,18 @@ const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
<Box
ref={ref}
as="button"
type="button"
$background="none"
$margin="none"
$padding="none"
$hasTransition
aria-disabled={disabled || undefined}
$css={css`
cursor: ${props.disabled ? 'not-allowed' : 'pointer'};
cursor: ${disabled ? 'not-allowed' : 'pointer'};
border: none;
outline: none;
font-family: inherit;
color: ${props.disabled &&
color: ${disabled &&
`var(--c--contextuals--content--semantic--disabled--primary)`};
&:focus-visible {
transition: none;
@@ -53,11 +55,11 @@ const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
`}
{...props}
className={`--docs--box-button ${props.className || ''}`}
onClick={(event: React.MouseEvent<HTMLDivElement>) => {
if (props.disabled) {
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
if (disabled) {
return;
}
props.onClick?.(event);
props.onClick?.(event as unknown as React.MouseEvent<HTMLDivElement>);
}}
/>
);
@@ -1,63 +1,33 @@
import { Button } from '@gouvfr-lasuite/cunningham-react';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
import { focusMainContentStart } from '@/layouts/utils';
export const SkipToContent = () => {
const { t } = useTranslation();
const router = useRouter();
const { spacingsTokens } = useCunninghamTheme();
const [isVisible, setIsVisible] = useState(false);
// Reset focus after route change so first TAB goes to skip link
useEffect(() => {
const handleRouteChange = () => {
(document.activeElement as HTMLElement)?.blur();
document.body.setAttribute('tabindex', '-1');
document.body.focus({ preventScroll: true });
setTimeout(() => {
document.body.removeAttribute('tabindex');
}, 100);
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router.events]);
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
const mainContent = document.getElementById(MAIN_LAYOUT_ID);
if (mainContent) {
mainContent.focus();
mainContent.scrollIntoView({ behavior: 'smooth', block: 'start' });
const focusTarget = focusMainContentStart();
if (focusTarget instanceof HTMLElement) {
focusTarget.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
return (
<Box
$css={css`
.c__button--brand--primary.--docs--skip-to-content:focus-visible {
box-shadow:
0 0 0 1px var(--c--globals--colors--white-000),
0 0 0 4px var(--c--contextuals--border--semantic--brand--primary);
border-radius: var(--c--globals--spacings--st);
}
`}
>
<Box>
<Button
onClick={handleClick}
type="button"
href={`#${MAIN_LAYOUT_ID}`}
color="brand"
className="--docs--skip-to-content"
onClick={handleClick}
onFocus={() => setIsVisible(true)}
onBlur={() => setIsVisible(false)}
style={{
@@ -65,7 +35,6 @@ export const SkipToContent = () => {
pointerEvents: isVisible ? 'auto' : 'none',
position: 'fixed',
top: spacingsTokens['2xs'],
// padding header + logo(32px) + gap(3xs≈4px) + text "Docs"(≈70px) + 12px
left: `calc(${spacingsTokens['base']} + 32px + ${spacingsTokens['3xs']} + 70px + 12px)`,
zIndex: 9999,
whiteSpace: 'nowrap',
@@ -26,6 +26,7 @@ import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
export type DropdownMenuOption = {
icon?: ReactNode;
label: string;
lang?: string;
testId?: string;
value?: string;
callback?: () => void | Promise<unknown>;
@@ -69,7 +70,10 @@ export const DropdownMenu = ({
const [isOpen, setIsOpen] = useState(opened ?? false);
const [focusedIndex, setFocusedIndex] = useState(-1);
const blockButtonRef = useRef<HTMLDivElement>(null);
const menuItemRefs = useRef<(HTMLDivElement | null)[]>([]);
const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]);
const isSingleSelectable = options.some(
(option) => option.isSelected !== undefined,
);
const onOpenChange = useCallback(
(isOpen: boolean) => {
@@ -110,10 +114,6 @@ export const DropdownMenu = ({
[onOpenChange],
);
const hasSelectable =
selectedValues !== undefined ||
options.some((option) => option.isSelected !== undefined);
if (disabled) {
return children;
}
@@ -176,20 +176,25 @@ export const DropdownMenu = ({
}
const isDisabled = option.disabled !== undefined && option.disabled;
const isFocused = index === focusedIndex;
const ariaChecked = hasSelectable
? option.isSelected ||
selectedValues?.includes(option.value ?? '') ||
false
: undefined;
const isSelected =
option.isSelected === true ||
(selectedValues?.includes(option.value ?? '') ?? false);
const itemRole =
selectedValues !== undefined
? 'menuitemcheckbox'
: isSingleSelectable
? 'menuitemradio'
: 'menuitem';
const optionKey = option.value ?? option.testId ?? `option-${index}`;
return (
<Fragment key={option.label}>
<Fragment key={optionKey}>
<BoxButton
ref={(el) => {
menuItemRefs.current[index] = el;
}}
role={hasSelectable ? 'menuitemradio' : 'menuitem'}
aria-checked={ariaChecked}
role={itemRole}
aria-checked={itemRole === 'menuitem' ? undefined : isSelected}
data-testid={option.testId}
$direction="row"
disabled={isDisabled}
@@ -200,7 +205,6 @@ export const DropdownMenu = ({
triggerOption(option);
}}
onKeyDown={keyboardAction(() => triggerOption(option))}
key={option.label}
$align="center"
$justify="space-between"
$background="var(--c--contextuals--background--surface--primary)"
@@ -276,11 +280,10 @@ export const DropdownMenu = ({
</Box>
)}
<Text $variation={isDisabled ? 'tertiary' : 'primary'}>
{option.label}
<span lang={option.lang}>{option.label}</span>
</Text>
</Box>
{(option.isSelected ||
selectedValues?.includes(option.value ?? '')) && (
{isSelected && (
<Icon
iconName="check"
$size="20px"
@@ -58,7 +58,7 @@ describe('<DropdownMenu />', () => {
expect(radios[2]).toHaveAttribute('aria-checked', 'false');
});
test('renders menuitemradio role with aria-checked when selectedValues is provided', async () => {
test('renders menuitemcheckbox role with aria-checked when selectedValues is provided', async () => {
const optionsWithValues: DropdownMenuOption[] = [
{ label: 'English', value: 'en', callback: vi.fn() },
{ label: 'Français', value: 'fr', callback: vi.fn() },
@@ -77,12 +77,12 @@ describe('<DropdownMenu />', () => {
{ wrapper: AppWrapper },
);
const radios = screen.getAllByRole('menuitemradio');
expect(radios).toHaveLength(3);
const checkboxes = screen.getAllByRole('menuitemcheckbox');
expect(checkboxes).toHaveLength(3);
expect(radios[0]).toHaveAttribute('aria-checked', 'false');
expect(radios[1]).toHaveAttribute('aria-checked', 'true');
expect(radios[2]).toHaveAttribute('aria-checked', 'false');
expect(checkboxes[0]).toHaveAttribute('aria-checked', 'false');
expect(checkboxes[1]).toHaveAttribute('aria-checked', 'true');
expect(checkboxes[2]).toHaveAttribute('aria-checked', 'false');
});
test('trigger button has aria-haspopup and aria-expanded', async () => {
@@ -6,7 +6,7 @@ type UseDropdownKeyboardNavProps = {
isOpen: boolean;
focusedIndex: number;
options: DropdownMenuOption[];
menuItemRefs: RefObject<(HTMLDivElement | null)[]>;
menuItemRefs: RefObject<(HTMLButtonElement | null)[]>;
setFocusedIndex: (index: number) => void;
onOpenChange: (isOpen: boolean) => void;
};
@@ -32,6 +32,7 @@ export type QuickSearchProps = {
label?: string;
placeholder?: string;
groupKey?: string;
beforeList?: ReactNode;
};
export const QuickSearch = ({
@@ -41,6 +42,7 @@ export const QuickSearch = ({
showInput = true,
label,
placeholder,
beforeList,
children,
}: PropsWithChildren<QuickSearchProps>) => {
const ref = useRef<HTMLDivElement | null>(null);
@@ -76,6 +78,7 @@ export const QuickSearch = ({
{inputContent}
</QuickSearchInput>
)}
{beforeList}
<Command.List id={listId} aria-label={label} role="listbox">
<Box>{children}</Box>
</Command.List>
@@ -1,9 +1,10 @@
import { Command } from 'cmdk';
import { PropsWithChildren } from 'react';
import { PropsWithChildren, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { HorizontalSeparator } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useFocusStore } from '@/stores';
import { Box } from '../Box';
import { Icon } from '../Icon';
@@ -14,7 +15,6 @@ type QuickSearchInputProps = {
placeholder?: string;
withSeparator?: boolean;
listId?: string;
isExpanded?: boolean;
};
export const QuickSearchInput = ({
inputValue,
@@ -26,6 +26,12 @@ export const QuickSearchInput = ({
}: PropsWithChildren<QuickSearchInputProps>) => {
const { t } = useTranslation();
const { spacingsTokens } = useCunninghamTheme();
const inputRef = useRef<HTMLInputElement>(null);
const addLastFocus = useFocusStore((state) => state.addLastFocus);
useEffect(() => {
addLastFocus(inputRef.current);
}, [addLastFocus]);
if (children) {
return (
@@ -42,11 +48,12 @@ export const QuickSearchInput = ({
$direction="row"
$align="center"
className="quick-search-input"
$gap={spacingsTokens['2xs']}
$gap={spacingsTokens['xxs']}
$padding={{ horizontal: 'base', vertical: 'xxs' }}
>
<Icon iconName="search" $variation="secondary" aria-hidden="true" />
<Command.Input
ref={inputRef}
autoFocus={true}
aria-label={t('Quick search input')}
aria-controls={listId}
@@ -55,6 +62,7 @@ export const QuickSearchInput = ({
placeholder={placeholder ?? t('Search')}
onValueChange={onFilter}
maxLength={254}
minLength={6}
data-testid="quick-search-input"
/>
</Box>
@@ -18,14 +18,15 @@ export const QuickSearchStyle = createGlobalStyle`
[cmdk-input] {
border: none;
width: 100%;
font-size: 17px;
font-size: 16px;
background: white;
outline: none;
color: var(--c--contextuals--content--semantic--neutral--primary);
border-radius: var(--c--globals--spacings--0);
font-family: var(--c--globals--font--families--base);
&::placeholder {
color: var(--c--globals--colors--gray-500);
color: var(--c--contextuals--content--semantic--neutral--tertiary);
}
}
@@ -2,7 +2,7 @@ import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { Box, Loading } from '@/components';
import { DocHeader, FloatingBar } from '@/docs/doc-header/';
import { DocHeader } from '@/docs/doc-header/';
import {
Doc,
LinkReach,
@@ -35,7 +35,6 @@ export const DocEditorContainer = ({
return (
<>
{isDesktop && <FloatingBar />}
<Box
$maxWidth="868px"
$width="100%"
@@ -8,12 +8,37 @@ export const cssComments = (
& .--docs--main-editor .ProseMirror {
// Comments marks in the editor
.bn-editor {
.bn-thread-mark:not([data-orphan='true']),
.bn-thread-mark-selected:not([data-orphan='true']) {
background: ${canSeeComment ? '#EDB40066' : 'transparent'};
color: var(--c--globals--colors--gray-700);
// Resets blocknote comments styles
.bn-thread-mark,
.bn-thread-mark-selected {
background-color: transparent;
}
${canSeeComment &&
css`
.bn-thread-mark:not([data-orphan='true']) {
background-color: color-mix(
in srgb,
var(--c--contextuals--background--palette--yellow--tertiary) 40%,
transparent
);
border-bottom: 2px solid
var(--c--contextuals--background--palette--yellow--secondary);
mix-blend-mode: multiply;
transition:
background-color var(--c--globals--transitions--duration),
border-bottom-color var(--c--globals--transitions--duration);
&:has(.bn-thread-mark-selected) {
background-color: var(
--c--contextuals--background--palette--yellow--tertiary
);
}
}
`}
[data-show-selection] {
color: HighlightText;
}
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useState } from 'react';
import * as Y from 'yjs';
import { useUpdateDoc } from '@/docs/doc-management/';
import { KEY_LIST_DOC_VERSIONS } from '@/docs/doc-versioning';
import { KEY_LIST_DOC_VERSIONS } from '@/docs/doc-versioning/api/useDocVersions';
import { toBase64 } from '@/utils/string';
import { isFirefox } from '@/utils/userAgent';
@@ -1,11 +1,83 @@
import { useEffect } from 'react';
import { announce } from '@react-aria/live-announcer';
import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { DocsBlockNoteEditor } from '../types';
const getFormattingShortcutLabel = (
event: KeyboardEvent,
t: (key: string) => string,
): string | null => {
const isMod = event.ctrlKey || event.metaKey;
if (!isMod) {
return null;
}
if (event.altKey) {
switch (event.code) {
case 'Digit1':
return t('Heading 1 applied');
case 'Digit2':
return t('Heading 2 applied');
case 'Digit3':
return t('Heading 3 applied');
default:
return null;
}
}
if (event.shiftKey) {
switch (event.code) {
case 'Digit0':
return t('Paragraph applied');
case 'Digit6':
return t('Toggle list applied');
case 'Digit7':
return t('Numbered list applied');
case 'Digit8':
return t('Bulleted list applied');
case 'Digit9':
return t('Checklist applied');
case 'KeyC':
return t('Code block applied');
default:
return null;
}
}
return null;
};
export const useShortcuts = (
editor: DocsBlockNoteEditor,
el: HTMLDivElement | null,
) => {
const { t } = useTranslation();
const handleFormattingShortcut = useCallback(
(event: KeyboardEvent) => {
if (!editor?.isFocused()) {
return;
}
const label = getFormattingShortcutLabel(event, t);
if (label) {
setTimeout(() => {
announce(label, 'assertive');
}, 150);
}
},
[editor, t],
);
useEffect(() => {
el?.addEventListener('keydown', handleFormattingShortcut, true);
return () => {
el?.removeEventListener('keydown', handleFormattingShortcut, true);
};
}, [el, handleFormattingShortcut]);
useEffect(() => {
// Check if editor and its view are mounted
if (!editor || !el) {
@@ -1,4 +1,9 @@
import { afterAll, afterEach, describe, expect, it, vi } from 'vitest';
vi.mock('@/docs/doc-export/components/ModalExport', () => ({
ModalExport: vi.fn(),
}));
const originalEnv = process.env.NEXT_PUBLIC_PUBLISH_AS_MIT;
describe('useModuleExport', () => {
@@ -16,12 +21,12 @@ describe('useModuleExport', () => {
const Export = await import('@/features/docs/doc-export/');
expect(Export.default).toBeUndefined();
}, 15000);
});
it('should load modules when NEXT_PUBLIC_PUBLISH_AS_MIT is false', async () => {
process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'false';
const Export = await import('@/features/docs/doc-export/');
expect(Export.default).toHaveProperty('ModalExport');
}, 15000);
});
});
@@ -60,6 +60,23 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
const { untitledDocument } = useTrans();
const mediaUrl = useMediaUrl();
const formatOptions = [
{ label: t('PDF'), value: DocDownloadFormat.PDF },
{ label: t('Docx'), value: DocDownloadFormat.DOCX },
{ label: t('ODT'), value: DocDownloadFormat.ODT },
{ label: t('HTML'), value: DocDownloadFormat.HTML },
{ label: t('Print'), value: DocDownloadFormat.PRINT },
];
const formatLabels = Object.fromEntries(
formatOptions.map((opt) => [opt.value, opt.label]),
);
const downloadButtonAriaLabel =
format === DocDownloadFormat.PRINT
? t('Print')
: t('Download {{format}}', { format: formatLabels[format] });
async function onSubmit() {
if (!editor) {
toast(t('The export failed'), VariantType.ERROR);
@@ -211,9 +228,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
</Button>
<Button
data-testid="doc-export-download-button"
aria-label={
format === DocDownloadFormat.PRINT ? t('Print') : t('Download')
}
aria-label={downloadButtonAriaLabel}
variant="primary"
fullWidth
onClick={() => void onSubmit()}
@@ -260,13 +275,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
clearable={false}
fullWidth
label={t('Format')}
options={[
{ label: t('PDF'), value: DocDownloadFormat.PDF },
{ label: t('Docx'), value: DocDownloadFormat.DOCX },
{ label: t('ODT'), value: DocDownloadFormat.ODT },
{ label: t('HTML'), value: DocDownloadFormat.HTML },
{ label: t('Print'), value: DocDownloadFormat.PRINT },
]}
options={formatOptions}
value={format}
onChange={(options) =>
setFormat(options.target.value as DocDownloadFormat)
@@ -1,6 +1,4 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { afterAll, beforeEach, describe, expect, vi } from 'vitest';
import { AppWrapper } from '@/tests/utils';
@@ -40,17 +38,11 @@ describe('DocToolBox - Licence', () => {
render(<DocToolBox doc={doc as any} />, {
wrapper: AppWrapper,
});
const optionsButton = await screen.findByLabelText('Export the document');
await userEvent.click(optionsButton);
// Wait for the export modal to be visible, then assert on its content text.
await screen.findByTestId('modal-export-title');
expect(
screen.getByText(
'Export your document to print or download in .docx, .odt, .pdf or .html(zip) format.',
),
await screen.findByLabelText('Export the document'),
).toBeInTheDocument();
}, 10000);
}, 15000);
test('The export button is not rendered when MIT version is activated', async () => {
process.env.NEXT_PUBLIC_PUBLISH_AS_MIT = 'true';
@@ -68,5 +60,5 @@ describe('DocToolBox - Licence', () => {
expect(
screen.queryByLabelText('Export the document'),
).not.toBeInTheDocument();
});
}, 15000);
});
@@ -1,9 +1,8 @@
import { Button, useModal } from '@gouvfr-lasuite/cunningham-react';
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
import { useQueryClient } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -17,6 +16,7 @@ import GroupSVG from '@/assets/icons/ui-kit/group.svg';
import HistorySVG from '@/assets/icons/ui-kit/history.svg';
import KeepSVG from '@/assets/icons/ui-kit/keep.svg';
import KeepOffSVG from '@/assets/icons/ui-kit/keep_off.svg';
import MarkdownCopySVG from '@/assets/icons/ui-kit/markdown_copy.svg';
import {
Box,
DropdownMenu,
@@ -38,7 +38,6 @@ import {
useDocUtils,
useDuplicateDoc,
} from '@/docs/doc-management';
import { KEY_LIST_DOC_VERSIONS } from '@/docs/doc-versioning';
import { useFocusStore, useResponsiveStore } from '@/stores';
import { useCopyCurrentEditorToClipboard } from '../hooks/useCopyCurrentEditorToClipboard';
@@ -87,7 +86,6 @@ interface DocToolBoxProps {
export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const { t } = useTranslation();
const treeContext = useTreeContext<Doc>();
const queryClient = useQueryClient();
const router = useRouter();
const { isChild, isTopRoot } = useDocUtils(doc);
@@ -113,16 +111,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC, KEY_LIST_FAVORITE_DOC],
});
useEffect(() => {
if (selectHistoryModal.isOpen) {
return;
}
void queryClient.resetQueries({
queryKey: [KEY_LIST_DOC_VERSIONS],
});
}, [selectHistoryModal.isOpen, queryClient]);
// Emoji Management
const { emoji } = getEmojiAndTitle(doc.title ?? '');
const { updateDocEmoji } = useDocTitleUpdate();
@@ -130,13 +118,13 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const options: DropdownMenuOption[] = [
{
label: t('Share'),
icon: <GroupSVG width={24} height={24} />,
icon: <GroupSVG width={24} height={24} aria-hidden="true" />,
callback: modalShare.open,
show: isSmallMobile,
},
{
label: t('Export'),
icon: <DownloadSVG width={24} height={24} />,
icon: <DownloadSVG width={24} height={24} aria-hidden="true" />,
callback: () => {
setIsModalExportOpen(true);
},
@@ -145,9 +133,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
{
label: doc.is_favorite ? t('Unpin') : t('Pin'),
icon: doc.is_favorite ? (
<KeepOffSVG width={24} height={24} />
<KeepOffSVG width={24} height={24} aria-hidden="true" />
) : (
<KeepSVG width={24} height={24} />
<KeepSVG width={24} height={24} aria-hidden="true" />
),
callback: () => {
if (doc.is_favorite) {
@@ -160,7 +148,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
{
label: t('Version history'),
icon: <HistorySVG width={24} height={24} />,
icon: <HistorySVG width={24} height={24} aria-hidden="true" />,
disabled: !doc.abilities.versions_list,
callback: () => {
selectHistoryModal.open();
@@ -170,7 +158,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
{
label: t('Remove emoji'),
icon: <RemoveEmojiSVG width={24} height={24} />,
icon: <RemoveEmojiSVG width={24} height={24} aria-hidden="true" />,
callback: () => {
updateDocEmoji(doc.id, doc.title ?? '', '');
},
@@ -179,7 +167,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
{
label: t('Add emoji'),
icon: <AddEmojiSVG width={24} height={24} />,
icon: <AddEmojiSVG width={24} height={24} aria-hidden="true" />,
callback: () => {
updateDocEmoji(doc.id, doc.title ?? '', '📄');
},
@@ -188,12 +176,12 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
{
label: t('Copy link'),
icon: <AddLinkSVG width={24} height={24} />,
icon: <AddLinkSVG width={24} height={24} aria-hidden="true" />,
callback: copyDocLink,
},
{
label: t('Copy as {{format}}', { format: 'Markdown' }),
icon: <ContentCopySVG width={24} height={24} />,
icon: <MarkdownCopySVG width={24} height={24} aria-hidden="true" />,
callback: () => {
void copyCurrentEditorToClipboard('markdown');
},
@@ -201,7 +189,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
{
label: t('Duplicate'),
icon: <ContentCopySVG width={24} height={24} />,
icon: <ContentCopySVG width={24} height={24} aria-hidden="true" />,
disabled: !doc.abilities.duplicate,
callback: () => {
duplicateDoc({
@@ -214,7 +202,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
},
{
label: isChild ? t('Delete sub-document') : t('Delete document'),
icon: <DeleteSVG width={24} height={24} />,
icon: <DeleteSVG width={24} height={24} aria-hidden="true" />,
disabled: !doc.abilities.destroy,
callback: () => {
setIsModalRemoveOpen(true);
@@ -4,7 +4,7 @@ import {
} from '@gouvfr-lasuite/cunningham-react';
import { useTranslation } from 'react-i18next';
import { useEditorStore } from '../../doc-editor';
import { useEditorStore } from '@/docs/doc-editor/stores/useEditorStore';
export const useCopyCurrentEditorToClipboard = () => {
const { editor } = useEditorStore();
@@ -13,20 +13,31 @@ export const useCopyCurrentEditorToClipboard = () => {
return async (asFormat: 'html' | 'markdown') => {
if (!editor) {
toast(t('Editor unavailable'), VariantType.ERROR, { duration: 3000 });
const message = t('Editor unavailable');
toast(message, VariantType.ERROR, { duration: 3000 });
return;
}
try {
const editorContentFormatted =
asFormat === 'html'
? await editor.blocksToHTMLLossy()
: await editor.blocksToMarkdownLossy();
? editor.blocksToHTMLLossy()
: editor.blocksToMarkdownLossy();
await navigator.clipboard.writeText(editorContentFormatted);
toast(t('Copied to clipboard'), VariantType.SUCCESS, { duration: 3000 });
const successMessage =
asFormat === 'markdown'
? t('Copied as Markdown to clipboard')
: t('Copied to clipboard');
toast(successMessage, VariantType.SUCCESS, { duration: 3000 });
} catch (error) {
console.error(error);
toast(t('Failed to copy to clipboard'), VariantType.ERROR, {
const errorMessage =
asFormat === 'markdown'
? t('Failed to copy as Markdown to clipboard')
: t('Failed to copy to clipboard');
toast(errorMessage, VariantType.ERROR, {
duration: 3000,
});
}
@@ -1,4 +1,6 @@
import { announce } from '@react-aria/live-announcer';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { APIError, errorCauses, fetchAPI } from '@/api';
@@ -29,6 +31,8 @@ export function useCreateFavoriteDoc({
listInvalidQueries,
}: CreateFavoriteDocProps) {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation<void, APIError, CreateFavoriteDocParams>({
mutationFn: createFavoriteDoc,
onSuccess: () => {
@@ -37,7 +41,15 @@ export function useCreateFavoriteDoc({
queryKey: [queryKey],
});
});
const message = t('Document pinned successfully!');
announce(message, 'polite');
onSuccess?.();
},
onError: () => {
const message = t('Failed to pin the document.');
announce(message, 'assertive');
},
});
}
@@ -1,4 +1,6 @@
import { announce } from '@react-aria/live-announcer';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { APIError, errorCauses, fetchAPI } from '@/api';
@@ -29,6 +31,8 @@ export function useDeleteFavoriteDoc({
listInvalidQueries,
}: DeleteFavoriteDocProps) {
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation<void, APIError, DeleteFavoriteDocParams>({
mutationFn: deleteFavoriteDoc,
onSuccess: () => {
@@ -37,7 +41,15 @@ export function useDeleteFavoriteDoc({
queryKey: [queryKey],
});
});
const message = t('Document unpinned successfully!');
announce(message, 'polite');
onSuccess?.();
},
onError: () => {
const message = t('Failed to unpin the document.');
announce(message, 'assertive');
},
});
}
@@ -88,14 +88,16 @@ export function useDuplicateDoc(options?: DuplicateDocOptions) {
queryKey: [KEY_LIST_DOC],
});
toast(t('Document duplicated successfully!'), VariantType.SUCCESS, {
const message = t('Document duplicated successfully!');
toast(message, VariantType.SUCCESS, {
duration: 3000,
});
void options?.onSuccess?.(data, variables, onMutateResult, context);
},
onError: (error, variables, onMutateResult, context) => {
toast(t('Failed to duplicate the document...'), VariantType.ERROR, {
const message = t('Failed to duplicate the document...');
toast(message, VariantType.ERROR, {
duration: 3000,
});
@@ -44,7 +44,7 @@ export function useUpdateDoc(queryConfig?: UseUpdateDoc) {
...queryConfig,
onSuccess: (data, variables, onMutateResult, context) => {
queryConfig?.listInvalidQueries?.forEach((queryKey) => {
void queryClient.invalidateQueries({
void queryClient.resetQueries({
queryKey: [queryKey],
});
});
@@ -44,7 +44,7 @@ export const DocIcon = ({
const { t } = useTranslation();
const { addLastFocus, restoreFocus } = useFocusStore();
const iconRef = useRef<HTMLDivElement>(null);
const iconRef = useRef<HTMLButtonElement>(null);
const [openEmojiPicker, setOpenEmojiPicker] = useState<boolean>(false);
const [pickerPosition, setPickerPosition] = useState<{
@@ -14,6 +14,11 @@ vi.mock('@/stores', () => ({
}),
}));
vi.mock('@gouvfr-lasuite/ui-kit', async () => ({
...(await vi.importActual('@gouvfr-lasuite/ui-kit')),
useTreeContext: () => null,
}));
describe('useDocTitleUpdate', () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -30,6 +30,8 @@ const defaultValues = {
type ExtendedCloseEvent = CloseEvent & { wasClean: boolean };
let reconnectTimeout: ReturnType<typeof setTimeout> | undefined;
export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
...defaultValues,
createProvider: (wsUrl, storeId, initialDoc) => {
@@ -48,7 +50,20 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
onDisconnect(data) {
// Attempt to reconnect if the disconnection was clean (initiated by the client or server)
if ((data.event as ExtendedCloseEvent).wasClean) {
void provider.connect();
if (data.event.reason === 'No cookies' && data.event.code === 4001) {
console.error(
'Disconnection due to missing cookies. Not attempting to reconnect.',
);
void provider.disconnect();
set({
isReady: true,
isConnected: false,
});
return;
}
clearTimeout(reconnectTimeout);
reconnectTimeout = setTimeout(() => void provider.connect(), 1000);
}
},
onAuthenticationFailed() {
@@ -107,6 +122,7 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
return provider;
},
destroyProvider: () => {
clearTimeout(reconnectTimeout);
const provider = get().provider;
if (provider) {
provider.destroy();
@@ -1,3 +1,4 @@
import { announce } from '@react-aria/live-announcer';
import { t } from 'i18next';
import { useEffect, useState } from 'react';
import { InView } from 'react-intersection-observer';
@@ -73,10 +74,12 @@ export const DocSearchContent = ({
docs = docs.filter(filterResults);
}
const elements = search || isSearchNotMandatory ? docs : [];
setDocsData({
groupName: docs.length > 0 ? groupName : '',
groupKey: 'docs',
elements: search || isSearchNotMandatory ? docs : [],
elements,
emptyString: t('No document found'),
endActions: hasNextPage
? [
@@ -90,6 +93,13 @@ export const DocSearchContent = ({
]
: [],
});
if (search) {
announce(
t('{{count}} result(s) available', { count: elements.length }),
'polite',
);
}
}, [
search,
data?.pages,
@@ -58,7 +58,13 @@ export const DocSearchFilters = ({
/>
</Box>
{hasFilters && (
<Button color="brand" variant="tertiary" size="small" onClick={onReset}>
<Button
color="brand"
variant="tertiary"
size="small"
onClick={onReset}
aria-label={t('Reset search filters')}
>
{t('Reset')}
</Button>
)}
@@ -14,7 +14,7 @@ import {
DocSearchFiltersValues,
DocSearchTarget,
} from '@/docs/doc-search';
import { useResponsiveStore } from '@/stores';
import { useFocusStore, useResponsiveStore } from '@/stores';
import EmptySearchIcon from '../assets/illustration-docs-empty.png';
@@ -36,6 +36,7 @@ const DocSearchModalGlobal = ({
}: DocSearchModalGlobalProps) => {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const restoreFocus = useFocusStore((state) => state.restoreFocus);
const router = useRouter();
const [search, setSearch] = useState('');
const [filters, setFilters] = useState<DocSearchFiltersValues>(
@@ -51,6 +52,7 @@ const DocSearchModalGlobal = ({
const handleResetFilters = () => {
setFilters({});
restoreFocus();
};
return (
@@ -62,6 +64,7 @@ const DocSearchModalGlobal = ({
aria-describedby="doc-search-modal-title"
>
<Box
aria-label={t('Search modal')}
$direction="column"
$justify="space-between"
className="--docs--doc-search-modal"
@@ -85,21 +88,26 @@ const DocSearchModalGlobal = ({
/>
</Box>
<QuickSearch
label={t('Search documents')}
placeholder={t('Type the name of a document')}
loading={loading}
onFilter={handleInputSearch}
beforeList={
showFilters ? (
<Box $padding={{ horizontal: '10px' }}>
<DocSearchFilters
values={filters}
onValuesChange={setFilters}
onReset={handleResetFilters}
/>
</Box>
) : undefined
}
>
<Box
$padding={{ horizontal: '10px', vertical: 'base' }}
$height={isDesktop ? '500px' : 'calc(100vh - 68px - 1rem)'}
>
{showFilters && (
<DocSearchFilters
values={filters}
onValuesChange={setFilters}
onReset={handleResetFilters}
/>
)}
{search.length === 0 && (
<Box
$direction="column"
@@ -107,11 +115,7 @@ const DocSearchModalGlobal = ({
$align="center"
$justify="center"
>
<Image
width={320}
src={EmptySearchIcon}
alt={t('No active search')}
/>
<Image width={320} src={EmptySearchIcon} alt="" />
</Box>
)}
{search && (
@@ -124,7 +124,8 @@ export const DocShareAddMemberList = ({
$scope="surface"
$theme="tertiary"
$variation=""
$border="1px solid var(--c--contextuals--border--semantic--contextual--primary)"
$border="1px solid var(--c--contextuals--border--surface--primary)"
$margin={{ bottom: 'sm' }}
>
<Box
$direction="row"
@@ -289,7 +289,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
/>
)}
{showMemberSection && isRootDoc && (
<Box $padding={{ horizontal: 'base' }}>
<Box $padding={{ horizontal: 'base', top: 'base' }}>
<QuickSearchGroupAccessRequest doc={doc} />
<QuickSearchGroupInvitation doc={doc} />
<QuickSearchGroupMember doc={doc} />
@@ -301,6 +301,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
searchUsersRawData={searchUsersQuery.data}
onSelect={onSelect}
userQuery={userQuery}
minLength={API_USERS_SEARCH_QUERY_MIN_LENGTH}
/>
)}
</QuickSearch>
@@ -321,14 +322,35 @@ interface QuickSearchInviteInputSectionProps {
onSelect: (usr: User) => void;
searchUsersRawData: User[] | undefined;
userQuery: string;
minLength: number;
}
const QuickSearchInviteInputSection = ({
onSelect,
searchUsersRawData,
userQuery,
minLength,
}: QuickSearchInviteInputSectionProps) => {
const { t } = useTranslation();
const hint = useMemo(() => {
if (userQuery.length < minLength) {
return t('Type at least {{minLength}} characters to display user names', {
minLength,
});
}
if (isValidEmail(userQuery)) {
return t('Choose the email');
}
if (!searchUsersRawData?.length) {
return t('No results. Type a full email address to invite someone.');
}
return t('Choose a user');
}, [minLength, searchUsersRawData?.length, t, userQuery]);
useEffect(() => {
announce(hint, 'polite');
}, [hint]);
const searchUserData: QuickSearchData<User> = useMemo(() => {
const users = searchUsersRawData || [];
@@ -347,7 +369,7 @@ const QuickSearchInviteInputSection = ({
);
return {
groupName: t('Search user result'),
groupName: hint,
elements: users,
endActions:
isEmail && !hasEmailInUsers
@@ -359,12 +381,12 @@ const QuickSearchInviteInputSection = ({
]
: undefined,
};
}, [onSelect, searchUsersRawData, t, userQuery]);
}, [searchUsersRawData, userQuery, hint, onSelect]);
return (
<Box
aria-label={t('List search user result card')}
$padding={{ horizontal: 'base', bottom: '3xs' }}
$padding={{ horizontal: 'base', bottom: '3xs', top: 'base' }}
>
<QuickSearchGroup
group={searchUserData}
@@ -51,11 +51,13 @@ export const Heading = ({
editor.setTextCursorPosition(headingId, 'end');
document.querySelector(`[data-id="${headingId}"]`)?.scrollIntoView({
behavior: 'smooth',
inline: 'start',
block: 'start',
});
document
.querySelector<HTMLElement>(`[data-id="${headingId}"]`)
?.scrollIntoView({
behavior: 'smooth',
inline: 'start',
block: 'start',
});
}}
$radius="var(--c--globals--spacings--st)"
$background={
@@ -91,7 +91,7 @@ export const TableContent = () => {
$height="100%"
$justify="center"
$align="center"
aria-label={t('Summary')}
aria-label={t('Show the table of contents')}
aria-expanded={isOpen}
aria-controls="toc-list"
$css={css`
@@ -218,8 +218,8 @@ const TableContentOpened = ({
onClick={onClose}
$justify="center"
$align="center"
aria-label={t('Summary')}
aria-expanded="true"
aria-label={t('Hide the table of contents')}
aria-expanded={true}
aria-controls="toc-list"
$css={css`
transition: none !important;
@@ -96,7 +96,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
const ariaLabel = docTitle;
const isDisabled = !!doc.deleted_at;
const actionsRef = useRef<HTMLDivElement>(null);
const buttonOptionRef = useRef<HTMLDivElement | null>(null);
const buttonOptionRef = useRef<HTMLButtonElement | null>(null);
const handleKeyDown = (e: React.KeyboardEvent) => {
const target = e.target as HTMLElement | null;
@@ -18,6 +18,7 @@ import {
useMoveDoc,
useTrans,
} from '@/docs/doc-management';
import { TreeSkeleton } from '@/features/skeletons/components/TreeSkeleton';
import { CLASS_DOC_TITLE } from '../../doc-header';
import { KEY_DOC_TREE, useDocTree } from '../api/useDocTree';
@@ -43,7 +44,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
treeContext?.treeData.selectedNode?.id === treeContext.root.id;
const rootItemRef = useRef<HTMLDivElement>(null);
const rootActionsRef = useRef<HTMLDivElement>(null);
const rootButtonOptionRef = useRef<HTMLDivElement | null>(null);
const rootButtonOptionRef = useRef<HTMLButtonElement | null>(null);
const { t } = useTranslation();
@@ -257,7 +258,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
}, [currentDoc, treeContext]);
if (!treeContext || !treeContext.root) {
return null;
return <TreeSkeleton />;
}
return (
@@ -33,7 +33,7 @@ type DocTreeItemActionsProps = {
onOpenChange?: (isOpen: boolean) => void;
parentId?: string | null;
actionsRef?: React.RefObject<HTMLDivElement | null>;
buttonOptionRef?: React.RefObject<HTMLDivElement | null>;
buttonOptionRef?: React.RefObject<HTMLButtonElement | null>;
};
export const DocTreeItemActions = ({
@@ -48,7 +48,7 @@ export const DocTreeItemActions = ({
}: DocTreeItemActionsProps) => {
const internalActionsRef = useRef<HTMLDivElement | null>(null);
const targetActionsRef = actionsRef ?? internalActionsRef;
const internalButtonRef = useRef<HTMLDivElement | null>(null);
const internalButtonRef = useRef<HTMLButtonElement | null>(null);
const targetButtonRef = buttonOptionRef ?? internalButtonRef;
const router = useRouter();
const { t } = useTranslation();
@@ -4,9 +4,12 @@ import { useEffect, useState } from 'react';
import * as Y from 'yjs';
import { Box, Text, TextErrors } from '@/components';
import { BlockNoteReader, DocEditorContainer } from '@/docs/doc-editor/';
import { BlockNoteReader } from '@/docs/doc-editor/components/BlockNoteEditor';
import { DocEditorContainer } from '@/docs/doc-editor/components/DocEditor';
import { Doc, base64ToBlocknoteXmlFragment } from '@/docs/doc-management';
import { Versions, useDocVersion } from '@/docs/doc-versioning/';
import { useDocVersion } from '../api/useDocVersion';
import { Versions } from '../types';
import { DocVersionHeader } from './DocVersionHeader';
@@ -5,8 +5,8 @@ import {
VariantType,
useToastProvider,
} from '@gouvfr-lasuite/cunningham-react';
import { useRouter } from 'next/router';
import { useTranslation } from 'react-i18next';
import { createGlobalStyle } from 'styled-components';
import { Box, Text } from '@/components';
import {
@@ -21,15 +21,22 @@ import { KEY_LIST_DOC_VERSIONS } from '../api/useDocVersions';
import { Versions } from '../types';
import { revertUpdate } from '../utils';
interface ModalConfirmationVersionProps {
onClose: () => void;
docId: Doc['id'];
const ModalStyle = createGlobalStyle`
.c__modal__title {
margin-bottom: var(--c--globals--spacings--sm);
}
`;
interface ModalConfirmationVersionProps {
docId: Doc['id'];
onClose: () => void;
onSuccess: () => void;
versionId: Versions['version_id'];
}
export const ModalConfirmationVersion = ({
onClose,
onSuccess,
docId,
versionId,
}: ModalConfirmationVersionProps) => {
@@ -39,14 +46,13 @@ export const ModalConfirmationVersion = ({
});
const { t } = useTranslation();
const { toast } = useToastProvider();
const { push } = useRouter();
const { provider } = useProviderStore();
const { mutate: updateDoc } = useUpdateDoc({
listInvalidQueries: [KEY_LIST_DOC_VERSIONS],
onSuccess: () => {
const onDisplaySuccess = () => {
toast(t('Version restored successfully'), VariantType.SUCCESS);
void push(`/docs/${docId}`);
onSuccess();
};
if (!provider || !version?.content) {
@@ -64,6 +70,10 @@ export const ModalConfirmationVersion = ({
},
});
if (!version) {
return null;
}
return (
<Modal
isOpen
@@ -102,7 +112,7 @@ export const ModalConfirmationVersion = ({
</Button>
</>
}
size={ModalSize.SMALL}
size={ModalSize.MEDIUM}
title={
<Text
as="h1"
@@ -111,17 +121,17 @@ export const ModalConfirmationVersion = ({
$size="h6"
$align="flex-start"
>
{t('Warning')}
{t('Restoring an older version')}
</Text>
}
>
<ModalStyle />
<Box className="--docs--modal-confirmation-version">
<Box>
<Text $variation="secondary" as="p">
{t('Your current document will revert to this version.')}
</Text>
<Text $variation="secondary" as="p">
{t('If a member is editing, his works can be lost.')}
<Text $variation="secondary" as="p" $margin="none">
{t(
"The current document will be replaced, but you'll still find it in the version history.",
)}
</Text>
</Box>
</Box>
@@ -114,11 +114,12 @@ export const ModalSelectVersion = ({
$height="calc(100vh - 2em - 12px)"
$css={css`
overflow-y: hidden;
border-left: 1px solid var(--c--globals--colors--gray-200);
border-left: 1px solid
var(--c--contextuals--border--surface--primary);
`}
>
<Box
aria-label="version list"
aria-label={t('Version list')}
$css={css`
overflow-y: auto;
flex: 1;
@@ -130,7 +131,8 @@ export const ModalSelectVersion = ({
$direction="row"
$align="center"
$css={css`
border-bottom: 1px solid var(--c--globals--colors--gray-200);
border-bottom: 1px solid
var(--c--contextuals--border--surface--primary);
`}
$padding="sm"
>
@@ -155,7 +157,8 @@ export const ModalSelectVersion = ({
<Box
$padding="xs"
$css={css`
border-top: 1px solid var(--c--globals--colors--gray-200);
border-top: 1px solid
var(--c--contextuals--border--surface--primary);
`}
>
<Button
@@ -174,6 +177,9 @@ export const ModalSelectVersion = ({
<ModalConfirmationVersion
onClose={() => {
restoreModal.close();
}}
onSuccess={() => {
restoreModal.close();
onClose();
setSelectedVersionId(undefined);
}}
@@ -1,78 +1,38 @@
import dynamic from 'next/dynamic';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Text } from '@/components';
import { BoxButton, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Doc } from '@/docs/doc-management';
import { Versions } from '../types';
const ModalConfirmationVersion = dynamic(
() =>
import('./ModalConfirmationVersion').then((mod) => ({
default: mod.ModalConfirmationVersion,
})),
{ ssr: false },
);
interface VersionItemProps {
docId: Doc['id'];
text: string;
versionId?: Versions['version_id'];
isActive: boolean;
onSelect?: () => void;
}
export const VersionItem = ({
docId,
versionId,
text,
isActive,
}: VersionItemProps) => {
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
export const VersionItem = ({ text, isActive, onSelect }: VersionItemProps) => {
const { t } = useTranslation();
const { spacingsTokens } = useCunninghamTheme();
return (
<>
<Box
$width="100%"
as="li"
$background={isActive ? colorsTokens['gray-100'] : 'transparent'}
$radius={spacingsTokens['3xs']}
$css={`
cursor: pointer;
&:hover {
background: ${colorsTokens['gray-100']};
}
`}
$hasTransition
$minWidth="13rem"
className="--docs--version-item"
>
<Box
$padding={{ vertical: '0.7rem', horizontal: 'small' }}
$align="center"
$direction="row"
$justify="space-between"
$width="100%"
>
<Box $direction="row" $gap="0.5rem" $align="center">
<Text $weight="bold" $size="sm">
{text}
</Text>
</Box>
</Box>
</Box>
{isModalVersionOpen && versionId && (
<ModalConfirmationVersion
onClose={() => setIsModalVersionOpen(false)}
docId={docId}
versionId={versionId}
/>
)}
</>
<BoxButton
aria-label={t('Restore version of {{date}}', { date: text })}
aria-pressed={isActive}
$width="100%"
$css={`
background: ${isActive ? 'var(--c--contextuals--background--semantic--overlay--primary)' : 'transparent'};
&:focus-visible, &:hover {
background: var(--c--contextuals--background--semantic--overlay--primary);
}
`}
className="version-item --docs--version-item"
onClick={onSelect}
$radius={spacingsTokens['3xs']}
$padding={{ vertical: 'm', horizontal: 'xs' }}
$hasTransition
>
<Text $weight="bold" $size="sm" $textAlign="left">
{text}
</Text>
</BoxButton>
);
};
@@ -3,14 +3,7 @@ import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
import { APIError } from '@/api';
import {
Box,
BoxButton,
Icon,
InfiniteScroll,
Text,
TextErrors,
} from '@/components';
import { Box, Icon, InfiniteScroll, Text, TextErrors } from '@/components';
import { Doc } from '@/docs/doc-management';
import { useDate } from '@/hooks';
@@ -23,7 +16,6 @@ interface VersionListStateProps {
isLoading: boolean;
error: APIError<unknown> | null;
versions?: Versions[];
doc: Doc;
selectedVersionId?: Versions['version_id'];
onSelectVersion?: (versionId: Versions['version_id']) => void;
}
@@ -31,13 +23,11 @@ interface VersionListStateProps {
const VersionListState = ({
onSelectVersion,
selectedVersionId,
isLoading,
error,
versions,
doc,
}: VersionListStateProps) => {
const { formatDate } = useDate();
const { formatDateSpecial } = useDate();
if (isLoading) {
return (
@@ -48,24 +38,23 @@ const VersionListState = ({
}
return (
<Box $gap="10px" $padding="xs">
{versions?.map((version) => (
<BoxButton
aria-label="version item"
className="version-item"
key={version.version_id}
onClick={() => {
onSelectVersion?.(version.version_id);
}}
>
<VersionItem
versionId={version.version_id}
text={formatDate(version.last_modified, DateTime.DATETIME_MED)}
docId={doc.id}
isActive={version.version_id === selectedVersionId}
/>
</BoxButton>
))}
<Box $gap="xxs" $padding="xs">
{versions?.map((version) => {
const formattedDate = formatDateSpecial(
version.last_modified,
'dd MMMM · HH:mm',
);
const isSelected = version.version_id === selectedVersionId;
return (
<Box as="li" key={version.version_id} $css="list-style: none;">
<VersionItem
text={formattedDate}
isActive={isSelected}
onSelect={() => onSelectVersion?.(version.version_id)}
/>
</Box>
);
})}
{error && (
<Box
$justify="center"
@@ -97,6 +86,7 @@ export const VersionList = ({
selectedVersionId,
}: VersionListProps) => {
const { t } = useTranslation();
const { formatDate } = useDate();
const {
data,
@@ -112,6 +102,12 @@ export const VersionList = ({
const versions = data?.pages.reduce((acc, page) => {
return acc.concat(page.versions);
}, [] as Versions[]);
const selectedVersion = versions?.find(
(version) => version.version_id === selectedVersionId,
);
const selectedVersionDate = selectedVersion
? formatDate(selectedVersion.last_modified, DateTime.DATETIME_MED)
: null;
return (
<Box
@@ -127,7 +123,7 @@ export const VersionList = ({
as="ul"
$padding="none"
$margin={{ top: 'none' }}
role="listbox"
role="list"
>
{versions?.length === 0 && (
<Box $align="center" $margin="large">
@@ -141,10 +137,14 @@ export const VersionList = ({
isLoading={isLoading}
error={error}
versions={versions}
doc={doc}
selectedVersionId={selectedVersionId}
/>
</InfiniteScroll>
<Text className="sr-only" aria-live="polite">
{selectedVersionDate
? t('Selected version {{date}}', { date: selectedVersionDate })
: ''}
</Text>
</Box>
);
};
@@ -1,4 +1,4 @@
import { Doc } from '../doc-management';
import { Doc } from '../doc-management/types';
export interface APIListVersions {
count: number;
@@ -116,7 +116,9 @@ export const DocsGrid = ({
$padding={{
bottom: 'md',
}}
{...(withUpload ? getRootProps({ className: 'dropzone' }) : {})}
{...(withUpload
? getRootProps({ className: 'dropzone', tabIndex: -1 })
: {})}
>
{withUpload && <input {...getInputProps()} />}
<DocGridTitleBar
@@ -19,7 +19,7 @@ import {
useDuplicateDoc,
useTrans,
} from '@/docs/doc-management';
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
import { focusMainContentStart } from '@/layouts/utils';
import { useFocusStore } from '@/stores';
import { DocMoveModal } from './DocMoveModal';
@@ -55,10 +55,9 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
const { mutate: duplicateDoc } = useDuplicateDoc({
onSuccess: () => {
const mainContent = document.getElementById(MAIN_LAYOUT_ID);
if (mainContent) {
requestAnimationFrame(() => mainContent.focus());
}
requestAnimationFrame(() => {
focusMainContentStart({ preventScroll: true });
});
},
});
@@ -73,9 +72,9 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
{
label: doc.is_favorite ? t('Unpin') : t('Pin'),
icon: doc.is_favorite ? (
<KeepOffSVG width={24} height={24} />
<KeepOffSVG width={24} height={24} aria-hidden="true" />
) : (
<KeepSVG width={24} height={24} />
<KeepSVG width={24} height={24} aria-hidden="true" />
),
callback: () => {
if (doc.is_favorite) {
@@ -89,7 +88,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
},
{
label: t('Share'),
icon: <GroupSVG width={24} height={24} />,
icon: <GroupSVG width={24} height={24} aria-hidden="true" />,
callback: () => {
shareModal.open();
},
@@ -98,7 +97,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
},
{
label: t('Move into a doc'),
icon: <DocMoveInSVG width={24} height={24} />,
icon: <DocMoveInSVG width={24} height={24} aria-hidden="true" />,
callback: () => {
importModal.open();
},
@@ -107,7 +106,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
},
{
label: t('Duplicate'),
icon: <ContentCopySVG width={24} height={24} />,
icon: <ContentCopySVG width={24} height={24} aria-hidden="true" />,
disabled: !doc.abilities.duplicate,
callback: () => {
duplicateDoc({
@@ -120,7 +119,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
},
{
label: t('Delete'),
icon: <DeleteSVG width={24} height={24} />,
icon: <DeleteSVG width={24} height={24} aria-hidden="true" />,
callback: () => deleteModal.open(),
disabled: !doc.abilities.destroy,
testId: `docs-grid-actions-remove-${doc.id}`,
@@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react';
import { act, render, screen, waitFor } from '@testing-library/react';
import fetchMock from 'fetch-mock';
import i18next from 'i18next';
import { DateTime } from 'luxon';
@@ -73,7 +73,9 @@ describe('DocsGridItemDate', () => {
});
it(`should render rendered the updated_at field in the correct language`, async () => {
await i18next.changeLanguage('fr');
await act(async () => {
await i18next.changeLanguage('fr');
});
render(
<DocsGridItemDate
@@ -90,7 +92,9 @@ describe('DocsGridItemDate', () => {
expect(screen.getByText('il y a 5 jours')).toBeInTheDocument();
await i18next.changeLanguage('en');
await act(async () => {
await i18next.changeLanguage('en');
});
});
[
@@ -60,7 +60,7 @@ export const HelpMenu = ({
>
<Box $direction="row" $align="center">
<Button
aria-label={t('Open onboarding menu')}
aria-label={t('Open help menu')}
color={colorButton || 'neutral'}
variant="tertiary"
iconPosition="left"
@@ -1,3 +1,4 @@
import { announce } from '@react-aria/live-announcer';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -17,23 +18,35 @@ export const LanguagePicker = () => {
const { changeLanguageSynchronized } = useSynchronizedLanguage();
const language = i18n.language;
const toLangTag = (locale: string) => locale.replace('_', '-');
// Compute options for dropdown
const optionsPicker = useMemo(() => {
const backendOptions = conf?.LANGUAGES ?? [[language, language]];
return backendOptions.map(([backendLocale, backendLabel]) => {
return {
label: backendLabel,
lang: toLangTag(backendLocale),
value: backendLocale,
isSelected: getMatchingLocales([backendLocale], [language]).length > 0,
callback: () => changeLanguageSynchronized(backendLocale, user),
callback: async () => {
await changeLanguageSynchronized(backendLocale, user);
announce(
t('Language changed to {{language}}', {
language: backendLabel,
defaultValue: `Language changed to ${backendLabel}`,
}),
'polite',
);
},
};
});
}, [changeLanguageSynchronized, conf?.LANGUAGES, language, user]);
}, [changeLanguageSynchronized, conf?.LANGUAGES, language, t, user]);
// Extract current language label for display
const currentLanguageLabel =
conf?.LANGUAGES.find(
([code]) => getMatchingLocales([code], [language]).length > 0,
)?.[1] || language;
const [currentLanguageCode, currentLanguageLabel] = conf?.LANGUAGES.find(
([code]) => getMatchingLocales([code], [language]).length > 0,
) ?? [language, language];
return (
<DropdownMenu
@@ -65,7 +78,9 @@ export const LanguagePicker = () => {
$align="center"
>
<Icon iconName="translate" $color="inherit" $size="xl" />
{currentLanguageLabel}
<span lang={toLangTag(currentLanguageCode)}>
{currentLanguageLabel}
</span>
</Box>
</DropdownMenu>
);
@@ -1,3 +1,5 @@
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { createGlobalStyle, css } from 'styled-components';
@@ -23,11 +25,15 @@ const MobileLeftPanelStyle = createGlobalStyle`
export const LeftPanel = () => {
const { isDesktop } = useResponsiveStore();
const { t } = useTranslation();
if (isDesktop) {
return <LeftPanelDesktop />;
}
const { spacingsTokens } = useCunninghamTheme();
const { isPanelOpen, isPanelOpenMobile } = useLeftPanelStore();
const isPanelOpenState = isDesktop ? isPanelOpen : isPanelOpenMobile;
return <LeftPanelMobile />;
};
export const LeftPanelDesktop = () => {
const { t } = useTranslation();
const { data: config } = useConfig();
/**
* The onboarding can be disable, so we need to check if it's enabled before displaying the help menu.
@@ -36,42 +42,51 @@ export const LeftPanel = () => {
*/
const showHelpMenu = config?.theme_customization?.onboarding?.enabled;
if (isDesktop) {
return (
return (
<Box
data-testid="left-panel-desktop"
$css={css`
height: calc(100vh - ${HEADER_HEIGHT}px);
width: 100%;
overflow: hidden;
background-color: var(--c--contextuals--background--surface--primary);
`}
className="--docs--left-panel-desktop"
as="nav"
aria-label={t('Document sections')}
>
<Box
data-testid="left-panel-desktop"
$css={css`
height: calc(100vh - ${HEADER_HEIGHT}px);
width: 100%;
overflow: hidden;
background-color: var(--c--contextuals--background--surface--primary);
flex: 0 0 auto;
`}
className="--docs--left-panel-desktop"
as="nav"
aria-label={t('Document sections')}
>
<Box
$css={css`
flex: 0 0 auto;
`}
>
<LeftPanelHeader />
</Box>
<LeftPanelContent />
{showHelpMenu && (
<SeparatedSection showSeparator={false}>
<Box $padding={{ horizontal: 'sm' }} $justify="flex-start">
<HelpMenu />
</Box>
</SeparatedSection>
)}
<LeftPanelHeader />
</Box>
);
}
<LeftPanelContent />
{showHelpMenu && (
<SeparatedSection showSeparator={false}>
<Box $padding={{ horizontal: 'sm' }} $justify="flex-start">
<HelpMenu />
</Box>
</SeparatedSection>
)}
</Box>
);
};
const LeftPanelMobile = () => {
const { t } = useTranslation();
const { spacingsTokens } = useCunninghamTheme();
const { closePanel, isPanelOpenMobile } = useLeftPanelStore();
const pathname = usePathname();
useEffect(() => {
closePanel({ type: 'mobile' });
}, [pathname, closePanel]);
return (
<>
{isPanelOpenState && <MobileLeftPanelStyle />}
{isPanelOpenMobile && <MobileLeftPanelStyle />}
<Box
$hasTransition
$height="100vh"
@@ -81,7 +96,7 @@ export const LeftPanel = () => {
height: calc(100dvh - 52px);
border-right: 1px solid var(--c--globals--colors--gray-200);
position: fixed;
transform: translateX(${isPanelOpenState ? '0' : '-100dvw'});
transform: translateX(${isPanelOpenMobile ? '0' : '-100dvw'});
background-color: var(--c--contextuals--background--surface--primary);
overflow-y: auto;
overflow-x: hidden;
@@ -18,8 +18,46 @@ export const LeftPanelCollapseButton = () => {
const { isPanelOpen, togglePanel } = useLeftPanelStore();
const { currentDoc } = useDocStore();
const [isDocTitleVisible, setIsDocTitleVisible] = useState(true);
const [isDocTitleInDom, setIsDocTitleInDom] = useState(true);
/**
* CLASS_DOC_TITLE is not every time in the DOM when
* this component is rendered, we need to observe the DOM
* to know when it is added, then we can observe
* its visibility.
*/
useEffect(() => {
setIsDocTitleInDom(false);
const docTitleEl = document.querySelector(`.${CLASS_DOC_TITLE}`);
if (docTitleEl) {
setIsDocTitleInDom(true);
return;
}
const mutationObserver = new MutationObserver(() => {
if (document.querySelector(`.${CLASS_DOC_TITLE}`)) {
mutationObserver.disconnect();
setIsDocTitleInDom(true);
}
});
mutationObserver.observe(document.body, { childList: true, subtree: true });
return () => {
mutationObserver.disconnect();
};
}, [currentDoc?.id]);
/**
* When the doc title is in the DOM, we observe its
* visibility to show or hide the collapse button accordingly
*/
useEffect(() => {
if (!isDocTitleInDom) {
return;
}
const mainContent = document.getElementById(MAIN_LAYOUT_ID);
const docTitleEl = document.querySelector(`.${CLASS_DOC_TITLE}`);
@@ -43,7 +81,7 @@ export const LeftPanelCollapseButton = () => {
observer.disconnect();
setIsDocTitleVisible(true);
};
}, [currentDoc?.id]);
}, [isDocTitleInDom]);
const { untitledDocument } = useTrans();

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